Added initial message interface, moved demo/example code, initial portaudio support

pull/37/head
Karlis Goba 2022-06-16 12:28:07 +03:00
rodzic 72754d02f0
commit a05dbae957
149 zmienionych plików z 2058 dodań i 602 usunięć

Wyświetl plik

@ -1,27 +1,46 @@
BUILD_DIR = .build
FT8_SRC = $(wildcard ft8/*.c)
FT8_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FT8_SRC))
COMMON_SRC = $(wildcard common/*.c)
COMMON_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(COMMON_SRC))
FFT_SRC = $(wildcard fft/*.c)
FFT_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FFT_SRC))
TARGETS = gen_ft8 decode_ft8 test_ft8
CFLAGS = -O3 -ggdb3 -fsanitize=address
CPPFLAGS = -std=c11 -I.
LDFLAGS = -lm -fsanitize=address
CPPFLAGS = -std=c11 -I. -I/opt/local/include
LDFLAGS = -lm -fsanitize=address -lportaudio -L/opt/local/lib
TARGETS = gen_ft8 decode_ft8 test
CPPFLAGS += -DUSE_PORTAUDIO -I/opt/local/include
LDFLAGS += -lportaudio -L/opt/local/lib
.PHONY: run_tests all clean
.PHONY: all clean run_tests install
all: $(TARGETS)
run_tests: test
@./test
gen_ft8: gen_ft8.o ft8/constants.o ft8/text.o ft8/pack.o ft8/encode.o ft8/crc.o common/wave.o
$(CXX) $(LDFLAGS) -o $@ $^
test: test.o ft8/pack.o ft8/encode.o ft8/crc.o ft8/text.o ft8/constants.o fft/kiss_fftr.o fft/kiss_fft.o
$(CXX) $(LDFLAGS) -o $@ $^
decode_ft8: decode_ft8.o fft/kiss_fftr.o fft/kiss_fft.o ft8/decode.o ft8/encode.o ft8/crc.o ft8/ldpc.o ft8/unpack.o ft8/text.o ft8/constants.o common/wave.o
$(CXX) $(LDFLAGS) -o $@ $^
clean:
rm -f *.o ft8/*.o common/*.o fft/*.o $(TARGETS)
rm -rf $(BUILD_DIR) $(TARGETS)
run_tests: test_ft8
@./test_ft8
install:
$(AR) rc libft8.a ft8/constants.o ft8/encode.o ft8/pack.o ft8/text.o common/wave.o
$(AR) rc libft8.a $(FT8_OBJ) $(COMMON_OBJ)
install libft8.a /usr/lib/libft8.a
gen_ft8: $(BUILD_DIR)/demo/gen_ft8.o $(FT8_OBJ) $(COMMON_OBJ) $(FFT_OBJ)
$(CC) $(LDFLAGS) -o $@ $^
decode_ft8: $(BUILD_DIR)/demo/decode_ft8.o $(FT8_OBJ) $(COMMON_OBJ) $(FFT_OBJ)
$(CC) $(LDFLAGS) -o $@ $^
test_ft8: $(BUILD_DIR)/test/test.o $(FT8_OBJ)
$(CC) $(LDFLAGS) -o $@ $^
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $^

Wyświetl plik

@ -1,12 +0,0 @@
#pragma once
#include <stdio.h>
#define LOG_DEBUG 0
#define LOG_INFO 1
#define LOG_WARN 2
#define LOG_ERROR 3
#define LOG_FATAL 4
#define LOG(level, ...) if (level >= LOG_LEVEL) fprintf(stderr, __VA_ARGS__)

173
common/monitor.c 100644
Wyświetl plik

@ -0,0 +1,173 @@
#include "monitor.h"
#define LOG_LEVEL LOG_INFO
#include <ft8/debug.h>
#include <stdlib.h>
static float hann_i(int i, int N)
{
float x = sinf((float)M_PI * i / N);
return x * x;
}
// static float hamming_i(int i, int N)
// {
// const float a0 = (float)25 / 46;
// const float a1 = 1 - a0;
// float x1 = cosf(2 * (float)M_PI * i / N);
// return a0 - a1 * x1;
// }
// static float blackman_i(int i, int N)
// {
// const float alpha = 0.16f; // or 2860/18608
// const float a0 = (1 - alpha) / 2;
// const float a1 = 1.0f / 2;
// const float a2 = alpha / 2;
// float x1 = cosf(2 * (float)M_PI * i / N);
// float x2 = 2 * x1 * x1 - 1; // Use double angle formula
// return a0 - a1 * x1 + a2 * x2;
// }
static void waterfall_init(waterfall_t* me, int max_blocks, int num_bins, int time_osr, int freq_osr)
{
size_t mag_size = max_blocks * time_osr * freq_osr * num_bins * sizeof(me->mag[0]);
me->max_blocks = max_blocks;
me->num_blocks = 0;
me->num_bins = num_bins;
me->time_osr = time_osr;
me->freq_osr = freq_osr;
me->block_stride = (time_osr * freq_osr * num_bins);
me->mag = (uint8_t*)malloc(mag_size);
LOG(LOG_DEBUG, "Waterfall size = %zu\n", mag_size);
}
static void waterfall_free(waterfall_t* me)
{
free(me->mag);
}
void monitor_init(monitor_t* me, const monitor_config_t* cfg)
{
float slot_time = (cfg->protocol == PROTO_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
float symbol_period = (cfg->protocol == PROTO_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD;
// Compute DSP parameters that depend on the sample rate
me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol
me->subblock_size = me->block_size / cfg->time_osr;
me->nfft = me->block_size * cfg->freq_osr;
me->fft_norm = 2.0f / me->nfft;
// const int len_window = 1.8f * me->block_size; // hand-picked and optimized
me->window = (float*)malloc(me->nfft * sizeof(me->window[0]));
for (int i = 0; i < me->nfft; ++i)
{
// window[i] = 1;
me->window[i] = hann_i(i, me->nfft);
// me->window[i] = blackman_i(i, me->nfft);
// me->window[i] = hamming_i(i, me->nfft);
// me->window[i] = (i < len_window) ? hann_i(i, len_window) : 0;
}
me->last_frame = (float*)calloc(me->nfft, sizeof(me->last_frame[0]));
size_t fft_work_size;
kiss_fftr_alloc(me->nfft, 0, 0, &fft_work_size);
LOG(LOG_INFO, "Block size = %d\n", me->block_size);
LOG(LOG_INFO, "Subblock size = %d\n", me->subblock_size);
LOG(LOG_INFO, "N_FFT = %d\n", me->nfft);
LOG(LOG_DEBUG, "FFT work area = %zu\n", fft_work_size);
me->fft_work = malloc(fft_work_size);
me->fft_cfg = kiss_fftr_alloc(me->nfft, 0, me->fft_work, &fft_work_size);
// Allocate enough blocks to fit the entire FT8/FT4 slot in memory
const int max_blocks = (int)(slot_time / symbol_period);
// Keep only FFT bins in the specified frequency range (f_min/f_max)
me->min_bin = (int)(cfg->f_min * symbol_period);
me->max_bin = (int)(cfg->f_max * symbol_period) + 1;
const int num_bins = me->max_bin - me->min_bin;
waterfall_init(&me->wf, max_blocks, num_bins, cfg->time_osr, cfg->freq_osr);
me->wf.protocol = cfg->protocol;
me->symbol_period = symbol_period;
me->max_mag = -120.0f;
}
void monitor_free(monitor_t* me)
{
waterfall_free(&me->wf);
free(me->fft_work);
free(me->last_frame);
free(me->window);
}
void monitor_reset(monitor_t* me)
{
me->wf.num_blocks = 0;
me->max_mag = 0;
}
// Compute FFT magnitudes (log wf) for a frame in the signal and update waterfall data
void monitor_process(monitor_t* me, const float* frame)
{
// Check if we can still store more waterfall data
if (me->wf.num_blocks >= me->wf.max_blocks)
return;
int offset = me->wf.num_blocks * me->wf.block_stride;
int frame_pos = 0;
// Loop over block subdivisions
for (int time_sub = 0; time_sub < me->wf.time_osr; ++time_sub)
{
kiss_fft_scalar timedata[me->nfft];
kiss_fft_cpx freqdata[me->nfft / 2 + 1];
// Shift the new data into analysis frame
for (int pos = 0; pos < me->nfft - me->subblock_size; ++pos)
{
me->last_frame[pos] = me->last_frame[pos + me->subblock_size];
}
for (int pos = me->nfft - me->subblock_size; pos < me->nfft; ++pos)
{
me->last_frame[pos] = frame[frame_pos];
++frame_pos;
}
// Compute windowed analysis frame
for (int pos = 0; pos < me->nfft; ++pos)
{
timedata[pos] = me->fft_norm * me->window[pos] * me->last_frame[pos];
}
kiss_fftr(me->fft_cfg, timedata, freqdata);
// Loop over two possible frequency bin offsets (for averaging)
for (int freq_sub = 0; freq_sub < me->wf.freq_osr; ++freq_sub)
{
for (int bin = me->min_bin; bin < me->max_bin; ++bin)
{
int src_bin = (bin * me->wf.freq_osr) + freq_sub;
float mag2 = (freqdata[src_bin].i * freqdata[src_bin].i) + (freqdata[src_bin].r * freqdata[src_bin].r);
float db = 10.0f * log10f(1E-12f + mag2);
// Scale decibels to unsigned 8-bit range and clamp the value
// Range 0-240 covers -120..0 dB in 0.5 dB steps
int scaled = (int)(2 * db + 240);
me->wf.mag[offset] = (scaled < 0) ? 0 : ((scaled > 255) ? 255 : scaled);
++offset;
if (db > me->max_mag)
me->max_mag = db;
}
}
}
++me->wf.num_blocks;
}

53
common/monitor.h 100644
Wyświetl plik

@ -0,0 +1,53 @@
#ifndef _INCLUDE_MONITOR_H_
#define _INCLUDE_MONITOR_H_
#ifdef __cplusplus
extern "C"
{
#endif
#include <ft8/decode.h>
#include <fft/kiss_fftr.h>
/// Configuration options for FT4/FT8 monitor
typedef struct
{
float f_min; ///< Lower frequency bound for analysis
float f_max; ///< Upper frequency bound for analysis
int sample_rate; ///< Sample rate in Hertz
int time_osr; ///< Number of time subdivisions
int freq_osr; ///< Number of frequency subdivisions
ftx_protocol_t protocol; ///< Protocol: FT4 or FT8
} monitor_config_t;
/// FT4/FT8 monitor object that manages DSP processing of incoming audio data
/// and prepares a waterfall object
typedef struct
{
float symbol_period; ///< FT4/FT8 symbol period in seconds
int min_bin;
int max_bin;
int block_size; ///< Number of samples per symbol (block)
int subblock_size; ///< Analysis shift size (number of samples)
int nfft; ///< FFT size
float fft_norm; ///< FFT normalization factor
float* window; ///< Window function for STFT analysis (nfft samples)
float* last_frame; ///< Current STFT analysis frame (nfft samples)
waterfall_t wf; ///< Waterfall object
float max_mag; ///< Maximum detected magnitude (debug stats)
// KISS FFT housekeeping variables
void* fft_work; ///< Work area required by Kiss FFT
kiss_fftr_cfg fft_cfg; ///< Kiss FFT housekeeping object
} monitor_t;
void monitor_init(monitor_t* me, const monitor_config_t* cfg);
void monitor_reset(monitor_t* me);
void monitor_process(monitor_t* me, const float* frame);
void monitor_free(monitor_t* me);
#ifdef __cplusplus
}
#endif
#endif // _INCLUDE_MONITOR_H_

Wyświetl plik

@ -1,397 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include "ft8/unpack.h"
#include "ft8/ldpc.h"
#include "ft8/decode.h"
#include "ft8/constants.h"
#include "ft8/encode.h"
#include "ft8/crc.h"
#include "common/common.h"
#include "common/wave.h"
#include "common/debug.h"
#include "fft/kiss_fftr.h"
#define LOG_LEVEL LOG_INFO
const int kMin_score = 10; // Minimum sync score threshold for candidates
const int kMax_candidates = 120;
const int kLDPC_iterations = 20;
const int kMax_decoded_messages = 50;
const int kFreq_osr = 2; // Frequency oversampling rate (bin subdivision)
const int kTime_osr = 2; // Time oversampling rate (symbol subdivision)
void usage()
{
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
}
static float hann_i(int i, int N)
{
float x = sinf((float)M_PI * i / N);
return x * x;
}
static float hamming_i(int i, int N)
{
const float a0 = (float)25 / 46;
const float a1 = 1 - a0;
float x1 = cosf(2 * (float)M_PI * i / N);
return a0 - a1 * x1;
}
static float blackman_i(int i, int N)
{
const float alpha = 0.16f; // or 2860/18608
const float a0 = (1 - alpha) / 2;
const float a1 = 1.0f / 2;
const float a2 = alpha / 2;
float x1 = cosf(2 * (float)M_PI * i / N);
float x2 = 2 * x1 * x1 - 1; // Use double angle formula
return a0 - a1 * x1 + a2 * x2;
}
void waterfall_init(waterfall_t* me, int max_blocks, int num_bins, int time_osr, int freq_osr)
{
size_t mag_size = max_blocks * time_osr * freq_osr * num_bins * sizeof(me->mag[0]);
me->max_blocks = max_blocks;
me->num_blocks = 0;
me->num_bins = num_bins;
me->time_osr = time_osr;
me->freq_osr = freq_osr;
me->block_stride = (time_osr * freq_osr * num_bins);
me->mag = (uint8_t*)malloc(mag_size);
LOG(LOG_DEBUG, "Waterfall size = %zu\n", mag_size);
}
void waterfall_free(waterfall_t* me)
{
free(me->mag);
}
/// Configuration options for FT4/FT8 monitor
typedef struct
{
float f_min; ///< Lower frequency bound for analysis
float f_max; ///< Upper frequency bound for analysis
int sample_rate; ///< Sample rate in Hertz
int time_osr; ///< Number of time subdivisions
int freq_osr; ///< Number of frequency subdivisions
ftx_protocol_t protocol; ///< Protocol: FT4 or FT8
} monitor_config_t;
/// FT4/FT8 monitor object that manages DSP processing of incoming audio data
/// and prepares a waterfall object
typedef struct
{
float symbol_period; ///< FT4/FT8 symbol period in seconds
int min_bin;
int max_bin;
int block_size; ///< Number of samples per symbol (block)
int subblock_size; ///< Analysis shift size (number of samples)
int nfft; ///< FFT size
float fft_norm; ///< FFT normalization factor
float* window; ///< Window function for STFT analysis (nfft samples)
float* last_frame; ///< Current STFT analysis frame (nfft samples)
waterfall_t wf; ///< Waterfall object
float max_mag; ///< Maximum detected magnitude (debug stats)
// KISS FFT housekeeping variables
void* fft_work; ///< Work area required by Kiss FFT
kiss_fftr_cfg fft_cfg; ///< Kiss FFT housekeeping object
} monitor_t;
void monitor_init(monitor_t* me, const monitor_config_t* cfg)
{
float slot_time = (cfg->protocol == PROTO_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
float symbol_period = (cfg->protocol == PROTO_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD;
// Compute DSP parameters that depend on the sample rate
me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol
me->subblock_size = me->block_size / cfg->time_osr;
me->nfft = me->block_size * cfg->freq_osr;
me->fft_norm = 2.0f / me->nfft;
// const int len_window = 1.8f * me->block_size; // hand-picked and optimized
me->window = (float*)malloc(me->nfft * sizeof(me->window[0]));
for (int i = 0; i < me->nfft; ++i)
{
// window[i] = 1;
me->window[i] = hann_i(i, me->nfft);
// me->window[i] = blackman_i(i, me->nfft);
// me->window[i] = hamming_i(i, me->nfft);
// me->window[i] = (i < len_window) ? hann_i(i, len_window) : 0;
}
me->last_frame = (float*)malloc(me->nfft * sizeof(me->last_frame[0]));
size_t fft_work_size;
kiss_fftr_alloc(me->nfft, 0, 0, &fft_work_size);
LOG(LOG_INFO, "Block size = %d\n", me->block_size);
LOG(LOG_INFO, "Subblock size = %d\n", me->subblock_size);
LOG(LOG_INFO, "N_FFT = %d\n", me->nfft);
LOG(LOG_DEBUG, "FFT work area = %zu\n", fft_work_size);
me->fft_work = malloc(fft_work_size);
me->fft_cfg = kiss_fftr_alloc(me->nfft, 0, me->fft_work, &fft_work_size);
// Allocate enough blocks to fit the entire FT8/FT4 slot in memory
const int max_blocks = (int)(slot_time / symbol_period);
// Keep only FFT bins in the specified frequency range (f_min/f_max)
me->min_bin = (int)(cfg->f_min * symbol_period);
me->max_bin = (int)(cfg->f_max * symbol_period) + 1;
const int num_bins = me->max_bin - me->min_bin;
waterfall_init(&me->wf, max_blocks, num_bins, cfg->time_osr, cfg->freq_osr);
me->wf.protocol = cfg->protocol;
me->symbol_period = symbol_period;
me->max_mag = -120.0f;
}
void monitor_free(monitor_t* me)
{
waterfall_free(&me->wf);
free(me->fft_work);
free(me->last_frame);
free(me->window);
}
// Compute FFT magnitudes (log wf) for a frame in the signal and update waterfall data
void monitor_process(monitor_t* me, const float* frame)
{
// Check if we can still store more waterfall data
if (me->wf.num_blocks >= me->wf.max_blocks)
return;
int offset = me->wf.num_blocks * me->wf.block_stride;
int frame_pos = 0;
// Loop over block subdivisions
for (int time_sub = 0; time_sub < me->wf.time_osr; ++time_sub)
{
kiss_fft_scalar timedata[me->nfft];
kiss_fft_cpx freqdata[me->nfft / 2 + 1];
// Shift the new data into analysis frame
for (int pos = 0; pos < me->nfft - me->subblock_size; ++pos)
{
me->last_frame[pos] = me->last_frame[pos + me->subblock_size];
}
for (int pos = me->nfft - me->subblock_size; pos < me->nfft; ++pos)
{
me->last_frame[pos] = frame[frame_pos];
++frame_pos;
}
// Compute windowed analysis frame
for (int pos = 0; pos < me->nfft; ++pos)
{
timedata[pos] = me->fft_norm * me->window[pos] * me->last_frame[pos];
}
kiss_fftr(me->fft_cfg, timedata, freqdata);
// Loop over two possible frequency bin offsets (for averaging)
for (int freq_sub = 0; freq_sub < me->wf.freq_osr; ++freq_sub)
{
for (int bin = me->min_bin; bin < me->max_bin; ++bin)
{
int src_bin = (bin * me->wf.freq_osr) + freq_sub;
float mag2 = (freqdata[src_bin].i * freqdata[src_bin].i) + (freqdata[src_bin].r * freqdata[src_bin].r);
float db = 10.0f * log10f(1E-12f + mag2);
// Scale decibels to unsigned 8-bit range and clamp the value
// Range 0-240 covers -120..0 dB in 0.5 dB steps
int scaled = (int)(2 * db + 240);
me->wf.mag[offset] = (scaled < 0) ? 0 : ((scaled > 255) ? 255 : scaled);
++offset;
if (db > me->max_mag)
me->max_mag = db;
}
}
}
++me->wf.num_blocks;
}
void monitor_reset(monitor_t* me)
{
me->wf.num_blocks = 0;
me->max_mag = 0;
}
int main(int argc, char** argv)
{
// Accepted arguments
const char* wav_path = NULL;
bool is_ft8 = true;
// Parse arguments one by one
int arg_idx = 1;
while (arg_idx < argc)
{
// Check if the current argument is an option (-xxx)
if (argv[arg_idx][0] == '-')
{
// Check agaist valid options
if (0 == strcmp(argv[arg_idx], "-ft4"))
{
is_ft8 = false;
}
else
{
usage();
return -1;
}
}
else
{
if (wav_path == NULL)
{
wav_path = argv[arg_idx];
}
else
{
usage();
return -1;
}
}
++arg_idx;
}
// Check if all mandatory arguments have been received
if (wav_path == NULL)
{
usage();
return -1;
}
int sample_rate = 12000;
int num_samples = 15 * sample_rate;
float signal[num_samples];
int rc = load_wav(signal, &num_samples, &sample_rate, wav_path);
if (rc < 0)
{
return -1;
}
LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate);
// Compute FFT over the whole signal and store it
monitor_t mon;
monitor_config_t mon_cfg = {
.f_min = 100,
.f_max = 3000,
.sample_rate = sample_rate,
.time_osr = kTime_osr,
.freq_osr = kFreq_osr,
.protocol = is_ft8 ? PROTO_FT8 : PROTO_FT4
};
monitor_init(&mon, &mon_cfg);
LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks);
for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size)
{
// Process the waveform data frame by frame - you could have a live loop here with data from an audio device
monitor_process(&mon, signal + frame_pos);
}
LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks);
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag);
// Find top candidates by Costas sync score and localize them in time and frequency
candidate_t candidate_list[kMax_candidates];
int num_candidates = ft8_find_sync(&mon.wf, kMax_candidates, candidate_list, kMin_score);
// Hash table for decoded messages (to check for duplicates)
int num_decoded = 0;
message_t decoded[kMax_decoded_messages];
message_t* decoded_hashtable[kMax_decoded_messages];
// Initialize hash table pointers
for (int i = 0; i < kMax_decoded_messages; ++i)
{
decoded_hashtable[i] = NULL;
}
// Go over candidates and attempt to decode messages
for (int idx = 0; idx < num_candidates; ++idx)
{
const candidate_t* cand = &candidate_list[idx];
if (cand->score < kMin_score)
continue;
float freq_hz = (mon.min_bin + cand->freq_offset + (float)cand->freq_sub / mon.wf.freq_osr) / mon.symbol_period;
float time_sec = (cand->time_offset + (float)cand->time_sub / mon.wf.time_osr) * mon.symbol_period;
message_t message;
decode_status_t status;
if (!ft8_decode(&mon.wf, cand, &message, kLDPC_iterations, NULL, &status))
{
// printf("000000 %3d %+4.2f %4.0f ~ ---\n", cand->score, time_sec, freq_hz);
if (status.ldpc_errors > 0)
{
LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors);
}
else if (status.crc_calculated != status.crc_extracted)
{
LOG(LOG_DEBUG, "CRC mismatch!\n");
}
else if (status.unpack_status != 0)
{
LOG(LOG_DEBUG, "Error while unpacking!\n");
}
continue;
}
LOG(LOG_DEBUG, "Checking hash table for %4.1fs / %4.1fHz [%d]...\n", time_sec, freq_hz, cand->score);
int idx_hash = message.hash % kMax_decoded_messages;
bool found_empty_slot = false;
bool found_duplicate = false;
do
{
if (decoded_hashtable[idx_hash] == NULL)
{
LOG(LOG_DEBUG, "Found an empty slot\n");
found_empty_slot = true;
}
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == strcmp(decoded_hashtable[idx_hash]->text, message.text)))
{
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", message.text);
found_duplicate = true;
}
else
{
LOG(LOG_DEBUG, "Hash table clash!\n");
// Move on to check the next entry in hash table
idx_hash = (idx_hash + 1) % kMax_decoded_messages;
}
} while (!found_empty_slot && !found_duplicate);
if (found_empty_slot)
{
// Fill the empty hashtable slot
memcpy(&decoded[idx_hash], &message, sizeof(message));
decoded_hashtable[idx_hash] = &decoded[idx_hash];
++num_decoded;
// Fake WSJT-X-like output for now
float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR
printf("000000 %2.1f %+4.2f %4.0f ~ %s\n", snr, time_sec, freq_hz, message.text);
}
}
LOG(LOG_INFO, "Decoded %d messages\n", num_decoded);
monitor_free(&mon);
return 0;
}

344
demo/decode_ft8.c 100644
Wyświetl plik

@ -0,0 +1,344 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <ft8/decode.h>
#include <ft8/encode.h>
#include <common/common.h>
#include <common/wave.h>
#include <common/monitor.h>
#define LOG_LEVEL LOG_INFO
#include <ft8/debug.h>
const int kMin_score = 10; // Minimum sync score threshold for candidates
const int kMax_candidates = 120;
const int kLDPC_iterations = 20;
const int kMax_decoded_messages = 50;
const int kFreq_osr = 2; // Frequency oversampling rate (bin subdivision)
const int kTime_osr = 2; // Time oversampling rate (symbol subdivision)
void usage(void)
{
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
}
#ifdef USE_PORTAUDIO
#include "portaudio.h"
typedef struct
{
PaTime startTime;
} audio_cb_context_t;
static audio_cb_context_t audio_cb_context;
static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData)
{
audio_cb_context_t* context = (audio_cb_context_t*)userData;
int16_t* samples_in = (int16_t*)inputBuffer;
// PaTime time = data->startTime + timeInfo->inputBufferAdcTime;
return 0;
}
void audio_list(void)
{
PaError pa_rc;
pa_rc = Pa_Initialize(); // Initialize PortAudio
if (pa_rc != paNoError)
{
printf("Error initializing PortAudio.\n");
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
return;
}
int numDevices;
numDevices = Pa_GetDeviceCount();
if (numDevices < 0)
{
printf("ERROR: Pa_CountDevices returned 0x%x\n", numDevices);
return;
}
printf("%d audio devices found:\n", numDevices);
for (int i = 0; i < numDevices; i++)
{
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
PaStreamParameters inputParameters = {
.device = i,
.channelCount = 1, // 1 = mono, 2 = stereo
.sampleFormat = paInt16,
.suggestedLatency = 0.2,
.hostApiSpecificStreamInfo = NULL
};
double sample_rate = 12000; // sample rate (frames per second)
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
printf("%d: [%s] [%s]\n", (i + 1), deviceInfo->name, (pa_rc == paNoError) ? "OK" : "NOT SUPPORTED");
}
}
int audio_open(const char* name)
{
PaError pa_rc;
pa_rc = Pa_Initialize(); // Initialize PortAudio
if (pa_rc != paNoError)
{
printf("Error initializing PortAudio.\n");
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
Pa_Terminate(); // I don't think we need this but...
return -1;
}
PaDeviceIndex ndevice_in = -1;
int numDevices = Pa_GetDeviceCount();
for (int i = 0; i < numDevices; i++)
{
const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
if (0 == strcmp(deviceInfo->name, name))
{
ndevice_in = i;
break;
}
}
if (ndevice_in < 0)
{
printf("Could not find device [%s].\n", name);
audio_list();
return -1;
}
PaStream* instream;
unsigned long nfpb = 1920 / 4; // frames per buffer
double sample_rate = 12000; // sample rate (frames per second)
PaStreamParameters inputParameters = {
.device = ndevice_in,
.channelCount = 1, // 1 = mono, 2 = stereo
.sampleFormat = paInt16,
.suggestedLatency = 0.2,
.hostApiSpecificStreamInfo = NULL
};
// Test if this configuration actually works, so we do not run into an ugly assertion
pa_rc = Pa_IsFormatSupported(&inputParameters, NULL, sample_rate);
if (pa_rc != paNoError)
{
printf("Error opening input audio stream.\n");
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
return -2;
}
pa_rc = Pa_OpenStream(
&instream, // address of stream
&inputParameters,
NULL,
sample_rate, // Sample rate
nfpb, // Frames per buffer
paNoFlag,
(PaStreamCallback*)audio_cb, // Callback routine
(void*)&audio_cb_context); // address of data structure
if (pa_rc != paNoError)
{ // We should have no error here usually
printf("Error opening input audio stream:\n");
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
return -3;
}
// printf("Successfully opened audio input.\n");
pa_rc = Pa_StartStream(instream); // Start input stream
if (pa_rc != paNoError)
{
printf("Error starting input audio stream!\n");
printf("\tErrortext: %s\n\tNumber: %d\n", Pa_GetErrorText(pa_rc), pa_rc);
return -4;
}
// while (Pa_IsStreamActive(instream))
// {
// Pa_Sleep(100);
// }
// Pa_AbortStream(instream); // Abort stream
// Pa_CloseStream(instream); // Close stream, we're done.
return 0;
}
#endif
int main(int argc, char** argv)
{
// Accepted arguments
const char* wav_path = NULL;
bool is_ft8 = true;
// Parse arguments one by one
int arg_idx = 1;
while (arg_idx < argc)
{
// Check if the current argument is an option (-xxx)
if (argv[arg_idx][0] == '-')
{
// Check agaist valid options
if (0 == strcmp(argv[arg_idx], "-ft4"))
{
is_ft8 = false;
}
else
{
usage();
return -1;
}
}
else
{
if (wav_path == NULL)
{
wav_path = argv[arg_idx];
}
else
{
usage();
return -1;
}
}
++arg_idx;
}
// Check if all mandatory arguments have been received
if (wav_path == NULL)
{
usage();
return -1;
}
audio_list();
int sample_rate = 12000;
int num_samples = 15 * sample_rate;
float signal[num_samples];
int rc = load_wav(signal, &num_samples, &sample_rate, wav_path);
if (rc < 0)
{
return -1;
}
LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate);
// Compute FFT over the whole signal and store it
monitor_t mon;
monitor_config_t mon_cfg = {
.f_min = 200,
.f_max = 3000,
.sample_rate = sample_rate,
.time_osr = kTime_osr,
.freq_osr = kFreq_osr,
.protocol = is_ft8 ? PROTO_FT8 : PROTO_FT4
};
monitor_init(&mon, &mon_cfg);
LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks);
for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size)
{
// Process the waveform data frame by frame - you could have a live loop here with data from an audio device
monitor_process(&mon, signal + frame_pos);
}
LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks);
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag);
// Find top candidates by Costas sync score and localize them in time and frequency
candidate_t candidate_list[kMax_candidates];
int num_candidates = ft8_find_sync(&mon.wf, kMax_candidates, candidate_list, kMin_score);
// Hash table for decoded messages (to check for duplicates)
int num_decoded = 0;
message_t decoded[kMax_decoded_messages];
message_t* decoded_hashtable[kMax_decoded_messages];
// Initialize hash table pointers
for (int i = 0; i < kMax_decoded_messages; ++i)
{
decoded_hashtable[i] = NULL;
}
// Go over candidates and attempt to decode messages
for (int idx = 0; idx < num_candidates; ++idx)
{
const candidate_t* cand = &candidate_list[idx];
if (cand->score < kMin_score)
continue;
float freq_hz = (mon.min_bin + cand->freq_offset + (float)cand->freq_sub / mon.wf.freq_osr) / mon.symbol_period;
float time_sec = (cand->time_offset + (float)cand->time_sub / mon.wf.time_osr) * mon.symbol_period;
message_t message;
decode_status_t status;
if (!ft8_decode(&mon.wf, cand, &message, kLDPC_iterations, NULL, &status))
{
// printf("000000 %3d %+4.2f %4.0f ~ ---\n", cand->score, time_sec, freq_hz);
if (status.ldpc_errors > 0)
{
LOG(LOG_DEBUG, "LDPC decode: %d errors\n", status.ldpc_errors);
}
else if (status.crc_calculated != status.crc_extracted)
{
LOG(LOG_DEBUG, "CRC mismatch!\n");
}
else if (status.unpack_status != 0)
{
LOG(LOG_DEBUG, "Error while unpacking!\n");
}
continue;
}
LOG(LOG_DEBUG, "Checking hash table for %4.1fs / %4.1fHz [%d]...\n", time_sec, freq_hz, cand->score);
int idx_hash = message.hash % kMax_decoded_messages;
bool found_empty_slot = false;
bool found_duplicate = false;
do
{
if (decoded_hashtable[idx_hash] == NULL)
{
LOG(LOG_DEBUG, "Found an empty slot\n");
found_empty_slot = true;
}
else if ((decoded_hashtable[idx_hash]->hash == message.hash) && (0 == strcmp(decoded_hashtable[idx_hash]->text, message.text)))
{
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", message.text);
found_duplicate = true;
}
else
{
LOG(LOG_DEBUG, "Hash table clash!\n");
// Move on to check the next entry in hash table
idx_hash = (idx_hash + 1) % kMax_decoded_messages;
}
} while (!found_empty_slot && !found_duplicate);
if (found_empty_slot)
{
// Fill the empty hashtable slot
memcpy(&decoded[idx_hash], &message, sizeof(message));
decoded_hashtable[idx_hash] = &decoded[idx_hash];
++num_decoded;
// Fake WSJT-X-like output for now
float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR
printf("000000 %2.1f %+4.2f %4.0f ~ %s\n", snr, time_sec, freq_hz, message.text);
}
}
LOG(LOG_INFO, "Decoded %d messages\n", num_decoded);
monitor_free(&mon);
return 0;
}

Wyświetl plik

@ -6,7 +6,7 @@
#include "common/common.h"
#include "common/wave.h"
#include "common/debug.h"
#include "ft8/debug.h"
#include "ft8/pack.h"
#include "ft8/encode.h"
#include "ft8/constants.h"

22
ft8/debug.h 100644
Wyświetl plik

@ -0,0 +1,22 @@
#ifndef _DEBUG_H_INCLUDED_
#define _DEBUG_H_INCLUDED_
#define LOG_DEBUG 0
#define LOG_INFO 1
#define LOG_WARN 2
#define LOG_ERROR 3
#define LOG_FATAL 4
#ifdef LOG_LEVEL
#ifndef LOG_PRINTF
#include <stdio.h>
#define LOG_PRINTF(...) fprintf(stderr, __VA_ARGS__)
#endif
#define LOG(level, ...) \
if (level >= LOG_LEVEL) \
LOG_PRINTF(__VA_ARGS__)
#else // ifdef LOG_LEVEL
#define LOG(level, ...)
#endif
#endif // _DEBUG_H_INCLUDED_

Wyświetl plik

@ -7,6 +7,9 @@
#include <stdbool.h>
#include <math.h>
// #define LOG_LEVEL LOG_DEBUG
// #include "debug.h"
/// Compute log likelihood log(p(1) / p(0)) of 174 message bits for later use in soft-decision LDPC decoding
/// @param[in] wf Waterfall data collected during message slot
/// @param[in] cand Candidate to extract the message from
@ -420,6 +423,7 @@ bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, message_t* messa
}
}
// LOG(LOG_DEBUG, "Decoded message (CRC %04x), trying to unpack...\n", status->crc_extracted);
status->unpack_status = unpack77(a91, message->text, hash_if);
if (status->unpack_status < 0)

Wyświetl plik

@ -12,6 +12,8 @@ extern "C"
{
#endif
#define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator
/// Input structure to ft8_find_sync() function. This structure describes stored waterfall data over the whole message slot.
/// Fields time_osr and freq_osr specify additional oversampling rate for time and frequency resolution.
/// If time_osr=1, FFT magnitude data is collected once for every symbol transmitted, i.e. every 1/6.25 = 0.16 seconds.
@ -45,9 +47,8 @@ typedef struct
/// Structure that holds the decoded message
typedef struct
{
// TODO: check again that this size is enough
char text[25]; ///< Plain text
uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates
char text[FTX_MAX_MESSAGE_LENGTH]; ///< Plain text
uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates
} message_t;
/// Structure that contains the status of various steps during decoding of a message

965
ft8/message.c 100644
Wyświetl plik

@ -0,0 +1,965 @@
#include "message.h"
#include "text.h"
#include <stdlib.h>
#include <string.h>
#define LOG_LEVEL LOG_DEBUG
#include "debug.h"
#define MAX22 ((uint32_t)4194304ul)
#define NTOKENS ((uint32_t)2063592ul)
#define MAXGRID4 ((uint16_t)32400ul)
////////////////////////////////////////////////////// Static function prototypes //////////////////////////////////////////////////////////////
static bool trim_brackets(char* result, const char* original, int length);
static void add_brackets(char* result, const char* original, int length);
/// Compute hash value for a callsign and save it in a hash table via the provided callsign hash interface.
/// @param[in] hash_if Callsign hash interface
/// @param[in] callsign Callsign (up to 11 characters, trimmed)
/// @param[out] n22_out Pointer to store 22-bit hash value (can be NULL)
/// @param[out] n12_out Pointer to store 12-bit hash value (can be NULL)
/// @param[out] n10_out Pointer to store 10-bit hash value (can be NULL)
/// @return True on success
static bool save_callsign(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint32_t* n22_out, uint16_t* n12_out, uint16_t* n10_out);
static bool lookup_callsign(const ftx_callsign_hash_interface_t* hash_if, ftx_callsign_hash_type_e hash_type, uint32_t hash, char* callsign);
static int32_t pack_basecall(const char* callsign, int length);
/// Pack a special token, a 22-bit hash code, or a valid base call into a 29-bit integer.
static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* hash_if, uint8_t* ip);
/// Unpack a callsign from 28+1 bit field in the payload of the standard message (type 1 or type 2).
/// @param[in] n29 29-bit integer, e.g. n29a or n29b, containing encoded callsign, plus suffix flag (1 bit) as LSB
/// @param[in] i3 Payload type (3 bits), 1 or 2
/// @param[in] hash_if Callsign hash table interface (can be NULL)
/// @param[out] result Unpacked callsign (max size: 13 characters including the terminating \0)
static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result);
/// Pack a non-standard base call into a 28-bit integer.
static bool pack58(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint64_t* n58);
/// Unpack a non-standard base call from a 58-bit integer.
static bool unpack58(uint64_t n58, const ftx_callsign_hash_interface_t* hash_if, char* callsign);
static uint16_t packgrid(const char* grid4);
static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra);
/////////////////////////////////////////////////////////// Exported functions /////////////////////////////////////////////////////////////////
void ftx_message_init(ftx_message_t* msg)
{
memset((void*)msg, 0, sizeof(ftx_message_t));
}
// bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign)
// {
// return false;
// }
ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg)
{
// Extract i3 (bits 74..76)
uint8_t i3 = (msg->payload[9] >> 3) & 0x07u;
switch (i3)
{
case 0: {
// Extract n3 (bits 71..73)
uint8_t n3 = ((msg->payload[8] << 2) & 0x04u) | ((msg->payload[9] >> 6) & 0x03u);
switch (n3)
{
case 0:
return FTX_MESSAGE_TYPE_FREE_TEXT;
case 1:
return FTX_MESSAGE_TYPE_DXPEDITION;
case 2:
return FTX_MESSAGE_TYPE_EU_VHF;
case 3:
case 4:
return FTX_MESSAGE_TYPE_ARRL_FD;
case 5:
return FTX_MESSAGE_TYPE_TELEMETRY;
default:
return FTX_MESSAGE_TYPE_UNKNOWN;
}
break;
}
case 1:
case 2:
return FTX_MESSAGE_TYPE_STANDARD;
break;
case 3:
return FTX_MESSAGE_TYPE_ARRL_RTTY;
break;
case 4:
return FTX_MESSAGE_TYPE_NONSTD_CALL;
break;
case 5:
return FTX_MESSAGE_TYPE_WWROF;
default:
return FTX_MESSAGE_TYPE_UNKNOWN;
}
}
ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* message_text)
{
char call_to[12];
char call_de[12];
char extra[20];
const char* parse_position = message_text;
parse_position = copy_token(call_to, 12, parse_position);
parse_position = copy_token(call_de, 12, parse_position);
parse_position = copy_token(extra, 20, parse_position);
if (call_to[11] != '\0')
{
// token too long
return FTX_MESSAGE_RC_ERROR_CALLSIGN1;
}
if (call_de[11] != '\0')
{
// token too long
return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
}
if (extra[19] != '\0')
{
// token too long
return FTX_MESSAGE_RC_ERROR_GRID;
}
ftx_message_rc_t rc;
rc = ftx_message_encode_std(msg, hash_if, call_to, call_de, extra);
if (rc == FTX_MESSAGE_RC_OK)
return rc;
rc = ftx_message_encode_nonstd(msg, hash_if, call_to, call_de, extra);
if (rc == FTX_MESSAGE_RC_OK)
return rc;
// rc = ftx_message_encode_telemetry_hex(msg, hash_if, call_to, call_de, extra);
return rc;
}
ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra)
{
uint8_t ipa, ipb;
int32_t n28a = pack28(call_to, hash_if, &ipa);
int32_t n28b = pack28(call_de, hash_if, &ipb);
LOG(LOG_DEBUG, "n29a = %d, n29b = %d\n", n28a, n28b);
if (n28a < 0)
return FTX_MESSAGE_RC_ERROR_CALLSIGN1;
if (n28b < 0)
return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
uint8_t i3 = 1; // No suffix or /R
if (ends_with(call_to, "/P") || ends_with(call_de, "/P"))
{
i3 = 2; // Suffix /P for EU VHF contest
if (ends_with(call_to, "/R") || ends_with(call_de, "/R"))
{
return FTX_MESSAGE_RC_ERROR_SUFFIX;
}
}
uint16_t igrid4 = packgrid(extra);
LOG(LOG_DEBUG, "igrid4 = %d\n", igrid4);
// Shift in ipa and ipb bits into n28a and n28b
uint32_t n29a = ((uint32_t)n28a << 1) | ipa;
uint32_t n29b = ((uint32_t)n28b << 1) | ipb;
// TODO: check for suffixes
if (ends_with(call_to, "/R"))
n29a |= 1; // ipa = 1
else if (ends_with(call_to, "/P"))
{
n29a |= 1; // ipa = 1
i3 = 2;
}
// Pack into (28 + 1) + (28 + 1) + (1 + 15) + 3 bits
msg->payload[0] = (uint8_t)(n29a >> 21);
msg->payload[1] = (uint8_t)(n29a >> 13);
msg->payload[2] = (uint8_t)(n29a >> 5);
msg->payload[3] = (uint8_t)(n29a << 3) | (uint8_t)(n29b >> 26);
msg->payload[4] = (uint8_t)(n29b >> 18);
msg->payload[5] = (uint8_t)(n29b >> 10);
msg->payload[6] = (uint8_t)(n29b >> 2);
msg->payload[7] = (uint8_t)(n29b << 6) | (uint8_t)(igrid4 >> 10);
msg->payload[8] = (uint8_t)(igrid4 >> 2);
msg->payload[9] = (uint8_t)(igrid4 << 6) | (uint8_t)(i3 << 3);
return FTX_MESSAGE_RC_OK;
}
ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra)
{
uint8_t i3 = 4;
uint8_t icq = (uint8_t)equals(call_to, "CQ");
int len_call_to = strlen(call_to);
int len_call_de = strlen(call_de);
// if ((icq != 0) || (pack_basecall(call_to, len_call_to) >= 0))
// {
// if (pack_basecall(call_de, len_call_de) >= 0)
// {
// // no need for encode_nonstd, should use encode_std
// return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
// }
// }
if ((icq == 0) && ((len_call_to < 3)))
return FTX_MESSAGE_RC_ERROR_CALLSIGN1;
if ((len_call_de < 3))
return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
uint8_t iflip;
uint16_t n12;
uint64_t n58;
uint8_t nrpt;
const char* call58;
if (icq == 0)
{
// choose which of the callsigns to encode as plain-text (58 bits) or hash (12 bits)
iflip = 0; // call_de will be sent plain-text
if (call_de[0] == '<' && call_de[len_call_to - 1] == '>')
{
iflip = 1;
}
const char* call12;
call12 = (iflip == 0) ? call_to : call_de;
call58 = (iflip == 0) ? call_de : call_to;
if (!save_callsign(hash_if, call12, NULL, &n12, NULL))
{
return FTX_MESSAGE_RC_ERROR_CALLSIGN1;
}
}
else
{
iflip = 0;
n12 = 0;
call58 = call_de;
}
if (!pack58(hash_if, call58, &n58))
{
return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
}
if (icq != 0)
nrpt = 0;
else if (equals(extra, "RRR"))
nrpt = 1;
else if (equals(extra, "RR73"))
nrpt = 2;
else if (equals(extra, "73"))
nrpt = 3;
else
nrpt = 0;
// Pack into 12 + 58 + 1 + 2 + 1 + 3 == 77 bits
// write(c77,1010) n12,n58,iflip,nrpt,icq,i3
// format(b12.12,b58.58,b1,b2.2,b1,b3.3)
msg->payload[0] = (uint8_t)(n12 >> 4);
msg->payload[1] = (uint8_t)(n12 << 4) | (uint8_t)(n58 >> 54);
msg->payload[2] = (uint8_t)(n58 >> 46);
msg->payload[3] = (uint8_t)(n58 >> 38);
msg->payload[4] = (uint8_t)(n58 >> 30);
msg->payload[5] = (uint8_t)(n58 >> 22);
msg->payload[6] = (uint8_t)(n58 >> 14);
msg->payload[7] = (uint8_t)(n58 >> 6);
msg->payload[8] = (uint8_t)(n58 << 2) | (uint8_t)(iflip << 1) | (uint8_t)(nrpt >> 1);
msg->payload[9] = (uint8_t)(nrpt << 7) | (uint8_t)(icq << 6) | (uint8_t)(i3 << 3);
return FTX_MESSAGE_RC_OK;
}
ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message)
{
ftx_message_rc_t rc;
char buf[31]; // 12 + 12 + 7 (std/nonstd) / 14 (free text) / 19 (telemetry)
char* field1 = buf;
char* field2 = buf + 12;
char* field3 = buf + 12 + 12;
message[0] = '\0';
ftx_message_type_t msg_type = ftx_message_get_type(msg);
switch (msg_type)
{
case FTX_MESSAGE_TYPE_STANDARD:
rc = ftx_message_decode_std(msg, hash_if, field1, field2, field3);
break;
case FTX_MESSAGE_TYPE_NONSTD_CALL:
rc = ftx_message_decode_nonstd(msg, hash_if, field1, field2, field3);
break;
case FTX_MESSAGE_TYPE_FREE_TEXT:
ftx_message_decode_free(msg, field1);
rc = FTX_MESSAGE_RC_OK;
break;
case FTX_MESSAGE_TYPE_TELEMETRY:
ftx_message_decode_telemetry_hex(msg, field1);
rc = FTX_MESSAGE_RC_OK;
break;
default:
// not handled yet
rc = FTX_MESSAGE_RC_ERROR_TYPE;
break;
}
// TODO join fields via whitespace
return rc;
}
ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra)
{
uint32_t n29a, n29b;
uint16_t igrid4;
uint8_t ir;
// Extract packed fields
n29a = (msg->payload[0] << 21);
n29a |= (msg->payload[1] << 13);
n29a |= (msg->payload[2] << 5);
n29a |= (msg->payload[3] >> 3);
n29b = ((msg->payload[3] & 0x07u) << 26);
n29b |= (msg->payload[4] << 18);
n29b |= (msg->payload[5] << 10);
n29b |= (msg->payload[6] << 2);
n29b |= (msg->payload[7] >> 6);
ir = ((msg->payload[7] & 0x20u) >> 5);
igrid4 = ((msg->payload[7] & 0x1Fu) << 10);
igrid4 |= (msg->payload[8] << 2);
igrid4 |= (msg->payload[9] >> 6);
// Extract i3 (bits 74..76)
uint8_t i3 = (msg->payload[9] >> 3) & 0x07u;
LOG(LOG_DEBUG, "decode_std() n28a=%d ipa=%d n28b=%d ipb=%d ir=%d igrid4=%d i3=%d\n", n29a >> 1, n29a & 1u, n29b >> 1, n29b & 1u, ir, igrid4, i3);
call_to[0] = call_de[0] = extra[0] = '\0';
// Unpack both callsigns
if (unpack28(n29a >> 1, n29a & 1u, i3, hash_if, call_to) < 0)
{
return FTX_MESSAGE_RC_ERROR_CALLSIGN1;
}
if (unpack28(n29b >> 1, n29b & 1u, i3, hash_if, call_de) < 0)
{
return FTX_MESSAGE_RC_ERROR_CALLSIGN2;
}
if (unpackgrid(igrid4, ir, extra) < 0)
{
return FTX_MESSAGE_RC_ERROR_GRID;
}
LOG(LOG_INFO, "Decoded standard (type %d) message [%s] [%s] [%s]\n", i3, call_to, call_de, extra);
return FTX_MESSAGE_RC_OK;
}
// non-standard messages, code originally by KD8CEC
ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra)
{
uint16_t n12, iflip, nrpt, icq;
uint64_t n58;
n12 = (msg->payload[0] << 4); // 11 ~ 4 : 8
n12 |= (msg->payload[1] >> 4); // 3 ~ 0 : 12
n58 = ((uint64_t)(msg->payload[1] & 0x0Fu) << 54); // 57 ~ 54 : 4
n58 |= ((uint64_t)msg->payload[2] << 46); // 53 ~ 46 : 12
n58 |= ((uint64_t)msg->payload[3] << 38); // 45 ~ 38 : 12
n58 |= ((uint64_t)msg->payload[4] << 30); // 37 ~ 30 : 12
n58 |= ((uint64_t)msg->payload[5] << 22); // 29 ~ 22 : 12
n58 |= ((uint64_t)msg->payload[6] << 14); // 21 ~ 14 : 12
n58 |= ((uint64_t)msg->payload[7] << 6); // 13 ~ 6 : 12
n58 |= ((uint64_t)msg->payload[8] >> 2); // 5 ~ 0 : 765432 10
iflip = (msg->payload[8] >> 1) & 0x01u; // 76543210
nrpt = ((msg->payload[8] & 0x01u) << 1);
nrpt |= (msg->payload[9] >> 7); // 76543210
icq = ((msg->payload[9] >> 6) & 0x01u);
// Extract i3 (bits 74..76)
uint8_t i3 = (msg->payload[9] >> 3) & 0x07u;
LOG(LOG_DEBUG, "decode_nonstd() n12=%04x n58=%08llx iflip=%d nrpt=%d icq=%d i3=%d\n", n12, n58, iflip, nrpt, icq, i3);
// Decode one of the calls from 58 bit encoded string
char call_decoded[14];
unpack58(n58, hash_if, call_decoded);
// Decode the other call from hash lookup table
char call_3[12];
lookup_callsign(hash_if, FTX_CALLSIGN_HASH_12_BITS, n12, call_3);
// Possibly flip them around
char* call_1 = (iflip) ? call_decoded : call_3;
char* call_2 = (iflip) ? call_3 : call_decoded;
if (icq == 0)
{
strcpy(call_to, call_1);
if (nrpt == 1)
strcpy(extra, "RRR");
else if (nrpt == 2)
strcpy(extra, "RR73");
else if (nrpt == 3)
strcpy(extra, "73");
else
extra[0] = '\0';
}
else
{
strcpy(call_to, "CQ");
extra[0] = '\0';
}
strcpy(call_de, call_2);
LOG(LOG_INFO, "Decoded non-standard (type %d) message [%s] [%s] [%s]\n", i3, call_to, call_de, extra);
return FTX_MESSAGE_RC_OK;
}
void ftx_message_decode_free(const ftx_message_t* msg, char* text)
{
uint8_t b71[9];
ftx_message_decode_telemetry(msg, b71);
char c14[14];
c14[13] = 0;
for (int idx = 12; idx >= 0; --idx)
{
// Divide the long integer in b71 by 42
uint16_t rem = 0;
for (int i = 0; i < 9; ++i)
{
rem = (rem << 8) | b71[i];
b71[i] = rem / 42;
rem = rem % 42;
}
c14[idx] = charn(rem, FT8_CHAR_TABLE_FULL);
}
strcpy(text, trim(c14));
}
void ftx_message_decode_telemetry_hex(const ftx_message_t* msg, char* telemetry_hex)
{
uint8_t b71[9];
ftx_message_decode_telemetry(msg, b71);
// Convert b71 to hexadecimal string
for (int i = 0; i < 9; ++i)
{
uint8_t nibble1 = (b71[i] >> 4);
uint8_t nibble2 = (b71[i] & 0x0F);
char c1 = (nibble1 > 9) ? (nibble1 - 10 + 'A') : nibble1 + '0';
char c2 = (nibble2 > 9) ? (nibble2 - 10 + 'A') : nibble2 + '0';
telemetry_hex[i * 2] = c1;
telemetry_hex[i * 2 + 1] = c2;
}
telemetry_hex[18] = '\0';
}
void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry)
{
// Shift bits in payload right by 1 bit to right-align the data
uint8_t carry = 0;
for (int i = 0; i < 9; ++i)
{
telemetry[i] = (carry << 7) | (msg->payload[i] >> 1);
carry = (msg->payload[i] & 0x01);
}
}
#ifdef FTX_DEBUG_PRINT
#include <stdio.h>
void ftx_message_print(ftx_message_t* msg)
{
printf("[");
for (int i = 0; i < PAYLOAD_LENGTH_BYTES; ++i)
{
if (i > 0)
printf(" ");
printf("%02x", msg->payload[i]));
}
printf("]");
}
#endif
/////////////////////////////////////////////////////////// Static functions /////////////////////////////////////////////////////////////////
static bool trim_brackets(char* result, const char* original, int length)
{
if (original[0] == '<' && original[length - 1] == '>')
{
memcpy(result, original + 1, length - 2);
result[length - 2] = '\0';
return true;
}
else
{
memcpy(result, original, length);
result[length] = '\0';
return false;
}
}
static void add_brackets(char* result, const char* original, int length)
{
result[0] = '<';
memcpy(result + 1, original, length);
result[length + 1] = '>';
result[length + 2] = '\0';
}
static bool save_callsign(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint32_t* n22_out, uint16_t* n12_out, uint16_t* n10_out)
{
uint64_t n58 = 0;
int i = 0;
while (callsign[i] != '\0' && i < 11)
{
int j = nchar(callsign[i], FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH);
if (j < 0)
return false; // hash error (wrong character set)
n58 = (38 * n58) + j;
i++;
}
// pretend to have trailing whitespace (with j=0, index of ' ')
while (i < 11)
{
n58 = (38 * n58);
i++;
}
uint32_t n22 = (47055833459ull * n58) >> (64 - 22);
uint32_t n12 = n22 >> 10;
uint32_t n10 = n22 >> 12;
LOG(LOG_DEBUG, "save_callsign('%s') = [n22=%d, n12=%d, n10=%d]\n", callsign, n22, n12, n10);
if (n22_out != NULL)
*n22_out = n22;
if (n12_out != NULL)
*n12_out = n12;
if (n10_out != NULL)
*n10_out = n10;
if (hash_if != NULL)
hash_if->save_hash(callsign, n22);
return true;
}
static bool lookup_callsign(const ftx_callsign_hash_interface_t* hash_if, ftx_callsign_hash_type_e hash_type, uint32_t hash, char* callsign)
{
char c11[12];
bool found;
if (hash_if != NULL)
found = hash_if->lookup_hash(hash_type, hash, c11);
else
found = false;
if (!found)
{
strcpy(callsign, "<...>");
}
else
{
add_brackets(callsign, c11, strlen(c11));
}
LOG(LOG_DEBUG, "lookup_callsign(n%s=%d) = '%s'\n", (hash_type == FTX_CALLSIGN_HASH_22_BITS ? "22" : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? "12" : "10")), hash, callsign);
return found;
}
static int32_t pack_basecall(const char* callsign, int length)
{
if (length > 2)
{
// Attempt to pack a standard callsign, if fail, revert to hashed callsign
char c6[6] = { ' ', ' ', ' ', ' ', ' ', ' ' };
// Copy callsign to 6 character buffer
if (starts_with(callsign, "3DA0") && (length > 4) && (length <= 7))
{
// Work-around for Swaziland prefix: 3DA0XYZ -> 3D0XYZ
memcpy(c6, "3D0", 3);
memcpy(c6 + 3, callsign + 4, length - 4);
}
else if (starts_with(callsign, "3X") && is_letter(callsign[2]) && length <= 7)
{
// Work-around for Guinea prefixes: 3XA0XYZ -> QA0XYZ
memcpy(c6, "Q", 1);
memcpy(c6 + 1, callsign + 2, length - 2);
}
else
{
// Check the position of callsign digit and make a right-aligned copy into c6
if (is_digit(callsign[2]) && length <= 6)
{
// AB0XYZ
memcpy(c6, callsign, length);
}
else if (is_digit(callsign[1]) && length <= 5)
{
// A0XYZ -> " A0XYZ"
memcpy(c6 + 1, callsign, length);
}
}
// Check for standard callsign
int i0 = nchar(c6[0], FT8_CHAR_TABLE_ALPHANUM_SPACE);
int i1 = nchar(c6[1], FT8_CHAR_TABLE_ALPHANUM);
int i2 = nchar(c6[2], FT8_CHAR_TABLE_NUMERIC);
int i3 = nchar(c6[3], FT8_CHAR_TABLE_LETTERS_SPACE);
int i4 = nchar(c6[4], FT8_CHAR_TABLE_LETTERS_SPACE);
int i5 = nchar(c6[5], FT8_CHAR_TABLE_LETTERS_SPACE);
if ((i0 >= 0) && (i1 >= 0) && (i2 >= 0) && (i3 >= 0) && (i4 >= 0) && (i5 >= 0))
{
// This is a standard callsign
LOG(LOG_DEBUG, "Encoding basecall [%.6s]\n", c6);
int32_t n = i0;
n = n * 36 + i1;
n = n * 10 + i2;
n = n * 27 + i3;
n = n * 27 + i4;
n = n * 27 + i5;
return n; // Standard callsign
}
}
return -1;
}
static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* hash_if, uint8_t* ip)
{
LOG(LOG_DEBUG, "pack28() callsign [%s]\n", callsign);
*ip = 0;
// Check for special tokens first
if (equals(callsign, "DE"))
return 0;
if (equals(callsign, "QRZ"))
return 1;
if (equals(callsign, "CQ"))
return 2;
int length = strlen(callsign);
LOG(LOG_DEBUG, "Callsign length = %d\n", length);
if (starts_with(callsign, "CQ_") && length < 8)
{
int nnum = 0, nlet = 0;
// TODO: decode CQ_nnn or CQ_abcd
LOG(LOG_WARN, "CQ_nnn/CQ_abcd detected, not implemented\n");
return -1;
}
// Detect /R and /P suffix for basecall check
int length_base = length;
if (ends_with(callsign, "/P") || ends_with(callsign, "/R"))
{
LOG(LOG_DEBUG, "Suffix /P or /R detected\n");
*ip = 1;
length_base = length - 2;
}
int32_t n28 = pack_basecall(callsign, length_base);
if (n28 >= 0)
{
// Callsign can be encoded as a standard basecall with optional /P or /R suffix
if (!save_callsign(hash_if, callsign, NULL, NULL, NULL))
return -1; // Error (some problem with callsign contents)
return NTOKENS + MAX22 + (uint32_t)n28; // Standard callsign
}
if ((length >= 3) && (length <= 11))
{
// Treat this as a nonstandard callsign: compute its 22-bit hash
LOG(LOG_DEBUG, "Encoding as non-standard callsign\n");
uint32_t n22;
if (!save_callsign(hash_if, callsign, &n22, NULL, NULL))
return -1; // Error (some problem with callsign contents)
*ip = 0;
return NTOKENS + n22; // 22-bit hashed callsign
}
return -1; // Error
}
static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result)
{
LOG(LOG_DEBUG, "unpack28() n28=%d i3=%d\n", n28, i3);
// Check for special tokens DE, QRZ, CQ, CQ_nnn, CQ_aaaa
if (n28 < NTOKENS)
{
if (n28 <= 2u)
{
if (n28 == 0)
strcpy(result, "DE");
else if (n28 == 1)
strcpy(result, "QRZ");
else /* if (n28 == 2) */
strcpy(result, "CQ");
return 0; // Success
}
if (n28 <= 1002u)
{
// CQ nnn with 3 digits
strcpy(result, "CQ ");
int_to_dd(result + 3, n28 - 3, 3, false);
return 0; // Success
}
if (n28 <= 532443ul)
{
// CQ ABCD with 4 alphanumeric symbols
uint32_t n = n28 - 1003u;
char aaaa[5];
aaaa[4] = '\0';
for (int i = 3; /* no condition */; --i)
{
aaaa[i] = charn(n % 27u, FT8_CHAR_TABLE_LETTERS_SPACE);
if (i == 0)
break;
n /= 27u;
}
strcpy(result, "CQ ");
strcat(result, trim_front(aaaa));
return 0; // Success
}
// unspecified
return -1;
}
n28 = n28 - NTOKENS;
if (n28 < MAX22)
{
// This is a 22-bit hash of a result
lookup_callsign(hash_if, FTX_CALLSIGN_HASH_22_BITS, n28, result);
return 0; // Success
}
// Standard callsign
uint32_t n = n28 - MAX22;
char callsign[7];
callsign[6] = '\0';
callsign[5] = charn(n % 27, FT8_CHAR_TABLE_LETTERS_SPACE);
n /= 27;
callsign[4] = charn(n % 27, FT8_CHAR_TABLE_LETTERS_SPACE);
n /= 27;
callsign[3] = charn(n % 27, FT8_CHAR_TABLE_LETTERS_SPACE);
n /= 27;
callsign[2] = charn(n % 10, FT8_CHAR_TABLE_NUMERIC);
n /= 10;
callsign[1] = charn(n % 36, FT8_CHAR_TABLE_ALPHANUM);
n /= 36;
callsign[0] = charn(n % 37, FT8_CHAR_TABLE_ALPHANUM_SPACE);
// Copy callsign to 6 character buffer
if (starts_with(callsign, "3D0") && !is_space(callsign[3]))
{
// Work-around for Swaziland prefix: 3D0XYZ -> 3DA0XYZ
memcpy(result, "3DA0", 4);
trim_copy(result + 4, callsign + 3);
}
else if ((callsign[0] == 'Q') && is_letter(callsign[1]))
{
// Work-around for Guinea prefixes: QA0XYZ -> 3XA0XYZ
memcpy(result, "3X", 2);
trim_copy(result + 2, callsign + 1);
}
else
{
// Skip trailing and leading whitespace in case of a short callsign
trim_copy(result, callsign);
}
int length = strlen(result);
if (length < 3)
return -1; // Callsign too short
// Check if we should append /R or /P suffix
if (ip != 0)
{
if (i3 == 1)
strcat(result, "/R");
else if (i3 == 2)
strcat(result, "/P");
else
return -2;
}
// Save the result to hash table
save_callsign(hash_if, result, NULL, NULL, NULL);
return 0; // Success
}
static bool pack58(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint64_t* n58)
{
// Decode one of the calls from 58 bit encoded string
const char* src = callsign;
if (*src == '<')
src++;
int length = 0;
uint64_t result = 0;
char c11[12];
while (*src != '\0' && *src != '<' && (length < 11))
{
c11[length] = *src;
int j = nchar(*src, FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH);
if (j < 0)
return false;
result = (result * 38) + j;
src++;
length++;
}
c11[length] = '\0';
if (!save_callsign(hash_if, c11, NULL, NULL, NULL))
return false;
*n58 = result;
LOG(LOG_DEBUG, "pack58('%s')=%016llx\n", callsign, *n58);
return true;
}
static bool unpack58(uint64_t n58, const ftx_callsign_hash_interface_t* hash_if, char* callsign)
{
// Decode one of the calls from 58 bit encoded string
char c11[12];
c11[11] = '\0';
uint64_t n58_backup = n58;
for (int i = 10; /* no condition */; --i)
{
c11[i] = charn(n58 % 38, FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH);
if (i == 0)
break;
n58 /= 38;
}
// The decoded string will be right-aligned, so trim all whitespace (also from back just in case)
trim_copy(callsign, c11);
LOG(LOG_DEBUG, "unpack58(%016llx)=%s\n", n58_backup, callsign);
// Save the decoded call in a hash table for later
if (strlen(callsign) >= 3)
return save_callsign(hash_if, callsign, NULL, NULL, NULL);
return false;
}
static uint16_t packgrid(const char* grid4)
{
if (grid4 == 0 || grid4[0] == '\0')
{
// Two callsigns only, no report/grid
return MAXGRID4 + 1;
}
// Take care of special cases
if (equals(grid4, "RRR"))
return MAXGRID4 + 2;
if (equals(grid4, "RR73"))
return MAXGRID4 + 3;
if (equals(grid4, "73"))
return MAXGRID4 + 4;
// TODO: Check for "R " prefix before a 4 letter grid
// Check for standard 4 letter grid
if (in_range(grid4[0], 'A', 'R') && in_range(grid4[1], 'A', 'R') && is_digit(grid4[2]) && is_digit(grid4[3]))
{
uint16_t igrid4 = (grid4[0] - 'A');
igrid4 = igrid4 * 18 + (grid4[1] - 'A');
igrid4 = igrid4 * 10 + (grid4[2] - '0');
igrid4 = igrid4 * 10 + (grid4[3] - '0');
return igrid4;
}
// Parse report: +dd / -dd / R+dd / R-dd
// TODO: check the range of dd
if (grid4[0] == 'R')
{
int dd = dd_to_int(grid4 + 1, 3);
uint16_t irpt = 35 + dd;
return (MAXGRID4 + irpt) | 0x8000; // ir = 1
}
else
{
int dd = dd_to_int(grid4, 3);
uint16_t irpt = 35 + dd;
return (MAXGRID4 + irpt); // ir = 0
}
return MAXGRID4 + 1;
}
static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra)
{
char* dst = extra;
if (igrid4 <= MAXGRID4)
{
// Extract 4 symbol grid locator
if (ir > 0)
{
// In case of ir=1 add an "R " before grid
dst = stpcpy(dst, "R ");
}
uint16_t n = igrid4;
dst[4] = '\0';
dst[3] = '0' + (n % 10); // 0..9
n /= 10;
dst[2] = '0' + (n % 10); // 0..9
n /= 10;
dst[1] = 'A' + (n % 18); // A..R
n /= 18;
dst[0] = 'A' + (n % 18); // A..R
// if (ir > 0 && strncmp(call_to, "CQ", 2) == 0) return -1;
}
else
{
// Extract report
int irpt = igrid4 - MAXGRID4;
// Check special cases first (irpt > 0 always)
if (irpt == 1)
dst[0] = '\0';
else if (irpt == 2)
strcpy(dst, "RRR");
else if (irpt == 3)
strcpy(dst, "RR73");
else if (irpt == 4)
strcpy(dst, "73");
else
{
// Extract signal report as a two digit number with a + or - sign
if (ir > 0)
{
*dst++ = 'R'; // Add "R" before report
}
int_to_dd(dst, irpt - 35, 2, true);
}
// if (irpt >= 2 && strncmp(call_to, "CQ", 2) == 0) return -1;
}
return 0;
}

119
ft8/message.h 100644
Wyświetl plik

@ -0,0 +1,119 @@
#ifndef _INCLUDE_MESSAGE_H_
#define _INCLUDE_MESSAGE_H_
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PAYLOAD_LENGTH 77
#define PAYLOAD_LENGTH_BYTES 10
/// Structure that holds the decoded message
typedef struct
{
uint8_t payload[PAYLOAD_LENGTH_BYTES];
uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates
} ftx_message_t;
// ----------------------------------------------------------------------------------
// i3.n3 Example message Bits Total Purpose
// ----------------------------------------------------------------------------------
// 0.0 FREE TEXT MSG 71 71 Free text
// 0.1 K1ABC RR73; W9XYZ <KH1/KH7Z> -12 28 28 10 5 71 DXpedition Mode
// 0.2 PA3XYZ/P R 590003 IO91NP 28 1 1 3 12 25 70 EU VHF contest
// 0.3 WA9XYZ KA1ABC R 16A EMA 28 28 1 4 3 7 71 ARRL Field Day
// 0.4 WA9XYZ KA1ABC R 32A EMA 28 28 1 4 3 7 71 ARRL Field Day
// 0.5 123456789ABCDEF012 71 71 Telemetry (18 hex)
// 0.6 K1ABC RR73; CQ W9XYZ EN37 28 28 15 71 Contesting
// 0.7 ... tbd
// 1 WA9XYZ/R KA1ABC/R R FN42 28 1 28 1 1 15 74 Standard msg
// 2 PA3XYZ/P GM4ABC/P R JO22 28 1 28 1 1 15 74 EU VHF contest
// 3 TU; W9XYZ K1ABC R 579 MA 1 28 28 1 3 13 74 ARRL RTTY Roundup
// 4 <WA9XYZ> PJ4/KA1ABC RR73 12 58 1 2 1 74 Nonstandard calls
// 5 TU; W9XYZ K1ABC R-07 FN 1 28 28 1 7 9 74 WWROF contest ?
typedef enum
{
FTX_MESSAGE_TYPE_FREE_TEXT, // 0.0 FREE TEXT MSG 71 71 Free text
FTX_MESSAGE_TYPE_DXPEDITION, // 0.1 K1ABC RR73; W9XYZ <KH1/KH7Z> -12 28 28 10 5 71 DXpedition Mode
FTX_MESSAGE_TYPE_EU_VHF, // 0.2 PA3XYZ/P R 590003 IO91NP 28 1 1 3 12 25 70 EU VHF contest
FTX_MESSAGE_TYPE_ARRL_FD, // 0.3 WA9XYZ KA1ABC R 16A EMA 28 28 1 4 3 7 71 ARRL Field Day
// 0.4 WA9XYZ KA1ABC R 32A EMA 28 28 1 4 3 7 71 ARRL Field Day
FTX_MESSAGE_TYPE_TELEMETRY, // 0.5 0123456789abcdef01 71 71 Telemetry (18 hex)
FTX_MESSAGE_TYPE_CONTESTING, // 0.6 K1ABC RR73; CQ W9XYZ EN37 28 28 15 71 Contesting
FTX_MESSAGE_TYPE_STANDARD, // 1 WA9XYZ/R KA1ABC/R R FN42 28 1 28 1 1 15 74 Standard msg
// 2 PA3XYZ/P GM4ABC/P R JO22 28 1 28 1 1 15 74 EU VHF contest
FTX_MESSAGE_TYPE_ARRL_RTTY, // 3 TU; W9XYZ K1ABC R 579 MA 1 28 28 1 3 13 74 ARRL RTTY Roundup
FTX_MESSAGE_TYPE_NONSTD_CALL, // 4 <WA9XYZ> PJ4/KA1ABC RR73 12 58 1 2 1 74 Nonstandard calls
FTX_MESSAGE_TYPE_WWROF, // 5 TU; W9XYZ K1ABC R-07 FN 1 28 28 1 7 9 74 WWROF contest ?
FTX_MESSAGE_TYPE_UNKNOWN // Unknown or invalid type
} ftx_message_type_t;
typedef enum
{
FTX_CALLSIGN_HASH_22_BITS,
FTX_CALLSIGN_HASH_12_BITS,
FTX_CALLSIGN_HASH_10_BITS
} ftx_callsign_hash_type_e;
typedef struct
{
/// Called when a callsign is looked up by its 22/12/10 bit hash code
bool (*lookup_hash)(ftx_callsign_hash_type_e hash_type, uint32_t hash, char* callsign);
/// Called when a callsign should hashed and stored (by its 22, 12 and 10 bit hash codes)
void (*save_hash)(const char* callsign, uint32_t n22);
} ftx_callsign_hash_interface_t;
typedef enum
{
FTX_MESSAGE_RC_OK,
FTX_MESSAGE_RC_ERROR_CALLSIGN1,
FTX_MESSAGE_RC_ERROR_CALLSIGN2,
FTX_MESSAGE_RC_ERROR_SUFFIX,
FTX_MESSAGE_RC_ERROR_GRID,
FTX_MESSAGE_RC_ERROR_TYPE
} ftx_message_rc_t;
// Basecall - 1-2 letter/digit prefix (at least one letter), 1 digit area code, 1-3 letter suffix, total 3-6 chars (except for 7 char 3DA0- and 3X- calls)
// Ext. basecall - basecall followed by /R or /P
// Nonstd. call - all the rest, limited to 3-11 characters either alphanumeric or stroke (/)
void ftx_message_init(ftx_message_t* msg);
bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign);
ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg);
/// Pack (encode) a text message
ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* message_text);
/// Pack Type 1 (Standard 77-bit message) or Type 2 (ditto, with a "/P" call) message
/// Rules of callsign validity:
/// - call_to can be 'DE', 'CQ', 'QRZ', 'CQ_nnn' (three digits), or 'CQ_abcd' (four letters)
/// - nonstandard calls within <> brackets are allowed, if they don't contain '/'
ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra);
/// Pack Type 4 (One nonstandard call and one hashed call) message
ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra);
void ftx_message_encode_free(const char* text);
void ftx_message_encode_telemetry_hex(const char* telemetry_hex);
void ftx_message_encode_telemetry(const uint8_t* telemetry);
ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message);
ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra);
ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra);
void ftx_message_decode_free(const ftx_message_t* msg, char* text);
void ftx_message_decode_telemetry_hex(const ftx_message_t* msg, char* telemetry_hex);
void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry);
#ifdef FTX_DEBUG_PRINT
void ftx_message_print(ftx_message_t* msg);
#endif
#ifdef __cplusplus
}
#endif
#endif // _INCLUDE_MESSAGE_H_

Wyświetl plik

@ -54,6 +54,7 @@ static int32_t pack28(const char* callsign)
}
else
{
// Check the position of callsign digit and make a right-aligned copy into c6
if (is_digit(callsign[2]) && length <= 6)
{
// AB0XYZ

Wyświetl plik

@ -4,8 +4,7 @@
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
extern "C" {
#endif
/// Parse and pack FT8/FT4 text message into 77 bit binary payload

Wyświetl plik

@ -22,8 +22,6 @@ void trim_back(char* str)
}
}
// 1) trims a string from the back by changing whitespaces to '\0'
// 2) trims a string from the front by skipping whitespaces
char* trim(char* str)
{
str = (char*)trim_front(str);
@ -32,6 +30,18 @@ char* trim(char* str)
return str;
}
void trim_copy(char* trimmed, const char* str)
{
str = (char*)trim_front(str);
int len = strlen(str) - 1;
while (len >= 0 && str[len] == ' ')
{
len--;
}
strncpy(trimmed, str, len + 1);
trimmed[len + 1] = '\0';
}
char to_upper(char c)
{
return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
@ -62,6 +72,16 @@ bool starts_with(const char* string, const char* prefix)
return 0 == memcmp(string, prefix, strlen(prefix));
}
bool ends_with(const char* string, const char* suffix)
{
int pos = strlen(string) - strlen(suffix);
if (pos >= 0)
{
return 0 == memcmp(string + pos, suffix, strlen(suffix));
}
return false;
}
bool equals(const char* string1, const char* string2)
{
return 0 == strcmp(string1, string2);
@ -87,6 +107,34 @@ void fmtmsg(char* msg_out, const char* msg_in)
*msg_out = 0; // Add zero termination
}
const char* copy_token(char* token, int length, const char* string)
{
// Copy characters until a whitespace character or the end of string
while (*string != ' ' && *string != '\0')
{
if (length > 0)
{
*token = *string;
token++;
length--;
}
string++;
}
// Fill up the rest of token with \0 terminators
while (length > 0)
{
*token = '\0';
token++;
length--;
}
// Skip whitespace characters
while (*string == ' ')
{
string++;
}
return string;
}
// Parse a 2 digit integer from string
int dd_to_int(const char* str, int length)
{

Wyświetl plik

@ -5,22 +5,30 @@
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
extern "C" {
#endif
// Utility functions for characters and strings
const char* trim_front(const char* str);
void trim_back(char* str);
/// In-place whitespace trim from front and back:
/// 1) trims the back by changing whitespaces to '\0'
/// 2) trims the front by skipping whitespaces
/// @return trimmed string (pointer to first non-whitespace character)
char* trim(char* str);
/// Trim whitespace from start and end of string
void trim_copy(char* trimmed, const char* str);
char to_upper(char c);
bool is_digit(char c);
bool is_letter(char c);
bool is_space(char c);
bool in_range(char c, char min, char max);
bool starts_with(const char* string, const char* prefix);
bool ends_with(const char* string, const char* suffix);
bool equals(const char* string1, const char* string2);
// Text message formatting:
@ -28,6 +36,14 @@ bool equals(const char* string1, const char* string2);
// - merges consecutive spaces into single space
void fmtmsg(char* msg_out, const char* msg_in);
/// Extract and copy a space-delimited token from a string.
/// When the last token has been extracted, the return value points to the terminating zero character.
/// @param[out] token Buffer to receive the extracted token
/// @param[in] length Length of the token buffer (number of characters)
/// @param[in] string Pointer to the string
/// @return Pointer to the next token (can be passed to copy_token to extract the next token)
const char* copy_token(char* token, int length, const char* string);
// Parse a 2 digit integer from string
int dd_to_int(const char* str, int length);

Wyświetl plik

@ -214,7 +214,6 @@ static int unpack_type1(const uint8_t* a77, uint8_t i3, char* call_to, char* cal
static int unpack_text(const uint8_t* a71, char* text)
{
// TODO: test
uint8_t b71[9];
// Shift 71 bits right by 1 bit, so that it's right-aligned in the byte array
@ -389,9 +388,9 @@ int unpack77_fields(const uint8_t* a77, char* call_to, char* call_de, char* extr
// }
else if (i3 == 4)
{
// // Type 4: Nonstandard calls, e.g. <WA9XYZ> PJ4/KA1ABC RR73
// // One hashed call or "CQ"; one compound or nonstandard call with up
// // to 11 characters; and (if not "CQ") an optional RRR, RR73, or 73.
// Type 4: Nonstandard calls, e.g. <WA9XYZ> PJ4/KA1ABC RR73
// One hashed call or "CQ"; one compound or nonstandard call with up
// to 11 characters; and (if not "CQ") an optional RRR, RR73, or 73.
return unpack_nonstandard(a77, call_to, call_de, extra, hash_if);
}
// else if (i3 == 5) {

Wyświetl plik

@ -4,8 +4,7 @@
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
extern "C" {
#endif
typedef struct

159
test.c
Wyświetl plik

@ -1,159 +0,0 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include "ft8/text.h"
#include "ft8/pack.h"
#include "ft8/encode.h"
#include "ft8/constants.h"
#include "fft/kiss_fftr.h"
#include "common/common.h"
#include "common/debug.h"
#define LOG_LEVEL LOG_INFO
void convert_8bit_to_6bit(uint8_t* dst, const uint8_t* src, int nBits)
{
// Zero-fill the destination array as we will only be setting bits later
for (int j = 0; j < (nBits + 5) / 6; ++j)
{
dst[j] = 0;
}
// Set the relevant bits
uint8_t mask_src = (1 << 7);
uint8_t mask_dst = (1 << 5);
for (int i = 0, j = 0; nBits > 0; --nBits)
{
if (src[i] & mask_src)
{
dst[j] |= mask_dst;
}
mask_src >>= 1;
if (mask_src == 0)
{
mask_src = (1 << 7);
++i;
}
mask_dst >>= 1;
if (mask_dst == 0)
{
mask_dst = (1 << 5);
++j;
}
}
}
/*
bool test1() {
//const char *msg = "CQ DL7ACA JO40"; // 62, 32, 32, 49, 37, 27, 59, 2, 30, 19, 49, 16
const char *msg = "VA3UG F1HMR 73"; // 52, 54, 60, 12, 55, 54, 7, 19, 2, 23, 59, 16
//const char *msg = "RA3Y VE3NLS 73"; // 46, 6, 32, 22, 55, 20, 11, 32, 53, 23, 59, 16
uint8_t a72[9];
int rc = packmsg(msg, a72);
if (rc < 0) return false;
LOG(LOG_INFO, "8-bit packed: ");
for (int i = 0; i < 9; ++i) {
LOG(LOG_INFO, "%02x ", a72[i]);
}
LOG(LOG_INFO, "\n");
uint8_t a72_6bit[12];
convert_8bit_to_6bit(a72_6bit, a72, 72);
LOG(LOG_INFO, "6-bit packed: ");
for (int i = 0; i < 12; ++i) {
LOG(LOG_INFO, "%d ", a72_6bit[i]);
}
LOG(LOG_INFO, "\n");
char msg_out_raw[14];
unpack(a72, msg_out_raw);
char msg_out[14];
fmtmsg(msg_out, msg_out_raw);
LOG(LOG_INFO, "msg_out = [%s]\n", msg_out);
return true;
}
void test2() {
uint8_t test_in[11] = { 0xF1, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xFF };
uint8_t test_out[22];
encode174(test_in, test_out);
for (int j = 0; j < 22; ++j) {
LOG(LOG_INFO, "%02x ", test_out[j]);
}
LOG(LOG_INFO, "\n");
}
void test3() {
uint8_t test_in2[10] = { 0x11, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x04, 0x01, 0x00 };
uint16_t crc1 = ftx_compute_crc(test_in2, 76); // Calculate CRC of 76 bits only
LOG(LOG_INFO, "CRC: %04x\n", crc1); // should be 0x0708
}
*/
void test_tones(float* log174)
{
// Just a test case
for (int i = 0; i < FT8_ND; ++i)
{
const uint8_t inv_map[8] = { 0, 1, 3, 2, 6, 4, 5, 7 };
uint8_t tone = ("0000000011721762454112705354533170166234757420515470163426"[i]) - '0';
uint8_t b3 = inv_map[tone];
log174[3 * i] = (b3 & 4) ? +1.0 : -1.0;
log174[3 * i + 1] = (b3 & 2) ? +1.0 : -1.0;
log174[3 * i + 2] = (b3 & 1) ? +1.0 : -1.0;
}
}
void test4()
{
const int nfft = 128;
const float fft_norm = 2.0 / nfft;
size_t fft_work_size;
kiss_fftr_alloc(nfft, 0, 0, &fft_work_size);
printf("N_FFT = %d\n", nfft);
printf("FFT work area = %lu\n", fft_work_size);
void* fft_work = malloc(fft_work_size);
kiss_fftr_cfg fft_cfg = kiss_fftr_alloc(nfft, 0, fft_work, &fft_work_size);
kiss_fft_scalar window[nfft];
for (int i = 0; i < nfft; ++i)
{
window[i] = sinf(i * 2 * (float)M_PI / nfft);
}
kiss_fft_cpx freqdata[nfft / 2 + 1];
kiss_fftr(fft_cfg, window, freqdata);
float mag_db[nfft];
// Compute log magnitude in decibels
for (int j = 0; j < nfft / 2 + 1; ++j)
{
float mag2 = (freqdata[j].i * freqdata[j].i + freqdata[j].r * freqdata[j].r);
mag_db[j] = 10.0f * log10f(1E-10f + mag2 * fft_norm * fft_norm);
}
printf("F[0] = %.1f dB\n", mag_db[0]);
printf("F[1] = %.3f dB\n", mag_db[1]);
}
int main()
{
//test1();
test4();
return 0;
}

262
test/test.c 100644
Wyświetl plik

@ -0,0 +1,262 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include "ft8/debug.h"
#include "ft8/text.h"
#include "ft8/pack.h"
#include "ft8/encode.h"
#include "ft8/constants.h"
#include "fft/kiss_fftr.h"
#include "common/common.h"
#include "ft8/message.h"
#define LOG_LEVEL LOG_INFO
// void convert_8bit_to_6bit(uint8_t* dst, const uint8_t* src, int nBits)
// {
// // Zero-fill the destination array as we will only be setting bits later
// for (int j = 0; j < (nBits + 5) / 6; ++j)
// {
// dst[j] = 0;
// }
// // Set the relevant bits
// uint8_t mask_src = (1 << 7);
// uint8_t mask_dst = (1 << 5);
// for (int i = 0, j = 0; nBits > 0; --nBits)
// {
// if (src[i] & mask_src)
// {
// dst[j] |= mask_dst;
// }
// mask_src >>= 1;
// if (mask_src == 0)
// {
// mask_src = (1 << 7);
// ++i;
// }
// mask_dst >>= 1;
// if (mask_dst == 0)
// {
// mask_dst = (1 << 5);
// ++j;
// }
// }
// }
/*
bool test1() {
//const char *msg = "CQ DL7ACA JO40"; // 62, 32, 32, 49, 37, 27, 59, 2, 30, 19, 49, 16
const char *msg = "VA3UG F1HMR 73"; // 52, 54, 60, 12, 55, 54, 7, 19, 2, 23, 59, 16
//const char *msg = "RA3Y VE3NLS 73"; // 46, 6, 32, 22, 55, 20, 11, 32, 53, 23, 59, 16
uint8_t a72[9];
int rc = packmsg(msg, a72);
if (rc < 0) return false;
LOG(LOG_INFO, "8-bit packed: ");
for (int i = 0; i < 9; ++i) {
LOG(LOG_INFO, "%02x ", a72[i]);
}
LOG(LOG_INFO, "\n");
uint8_t a72_6bit[12];
convert_8bit_to_6bit(a72_6bit, a72, 72);
LOG(LOG_INFO, "6-bit packed: ");
for (int i = 0; i < 12; ++i) {
LOG(LOG_INFO, "%d ", a72_6bit[i]);
}
LOG(LOG_INFO, "\n");
char msg_out_raw[14];
unpack(a72, msg_out_raw);
char msg_out[14];
fmtmsg(msg_out, msg_out_raw);
LOG(LOG_INFO, "msg_out = [%s]\n", msg_out);
return true;
}
void test2() {
uint8_t test_in[11] = { 0xF1, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xFF };
uint8_t test_out[22];
encode174(test_in, test_out);
for (int j = 0; j < 22; ++j) {
LOG(LOG_INFO, "%02x ", test_out[j]);
}
LOG(LOG_INFO, "\n");
}
void test3() {
uint8_t test_in2[10] = { 0x11, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x04, 0x01, 0x00 };
uint16_t crc1 = ftx_compute_crc(test_in2, 76); // Calculate CRC of 76 bits only
LOG(LOG_INFO, "CRC: %04x\n", crc1); // should be 0x0708
}
*/
#define CHECK(condition) \
if (!(condition)) \
{ \
printf("FAIL: Condition \'" #condition "' failed!\n"); \
return; \
}
#define TEST_END printf("Test OK\n\n")
#define CALLSIGN_HASHTABLE_SIZE 256
struct
{
char callsign[12];
uint32_t hash;
} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE];
void hashtable_init(void)
{
// for (int idx = 0; idx < CALLSIGN_HASHTABLE_SIZE; ++idx)
// {
// callsign_hashtable[idx]->callsign[0] = '\0';
// }
memset(callsign_hashtable, 0, sizeof(callsign_hashtable));
}
void hashtable_add(const char* callsign, uint32_t hash)
{
int idx_hash = (hash * 23) % CALLSIGN_HASHTABLE_SIZE;
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
{
if ((callsign_hashtable[idx_hash].hash == hash) && (0 == strcmp(callsign_hashtable[idx_hash].callsign, callsign)))
{
LOG(LOG_DEBUG, "Found a duplicate [%s]\n", callsign);
return;
}
else
{
LOG(LOG_DEBUG, "Hash table clash!\n");
// Move on to check the next entry in hash table
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
}
}
strncpy(callsign_hashtable[idx_hash].callsign, callsign, 11);
callsign_hashtable[idx_hash].callsign[11] = '\0';
callsign_hashtable[idx_hash].hash = hash;
}
bool hashtable_lookup(ftx_callsign_hash_type_e hash_type, uint32_t hash, char* callsign)
{
uint32_t hash_mask = (hash_type == FTX_CALLSIGN_HASH_10_BITS) ? 0x3FFu : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? 0xFFFu : 0x3FFFFFu);
int idx_hash = (hash * 23) % CALLSIGN_HASHTABLE_SIZE;
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
{
if ((callsign_hashtable[idx_hash].hash & hash_mask) == hash)
{
strcpy(callsign, callsign_hashtable[idx_hash].callsign);
return true;
}
// Move on to check the next entry in hash table
idx_hash = (idx_hash + 1) % CALLSIGN_HASHTABLE_SIZE;
}
callsign[0] = '\0';
return false;
}
ftx_callsign_hash_interface_t hash_if = {
.lookup_hash = hashtable_lookup,
.save_hash = hashtable_add
};
void test_std_msg(const char* call_to_tx, const char* call_de_tx, const char* extra_tx)
{
ftx_message_t msg;
ftx_message_init(&msg);
ftx_message_rc_t rc_encode = ftx_message_encode_std(&msg, &hash_if, call_to_tx, call_de_tx, extra_tx);
CHECK(rc_encode == FTX_MESSAGE_RC_OK);
printf("Encoded [%s] [%s] [%s]\n", call_to_tx, call_de_tx, extra_tx);
char call_to[14];
char call_de[14];
char extra[14];
ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, &hash_if, call_to, call_de, extra);
CHECK(rc_decode == FTX_MESSAGE_RC_OK);
printf("Decoded [%s] [%s] [%s]\n", call_to, call_de, extra);
CHECK(0 == strcmp(call_to, call_to_tx));
CHECK(0 == strcmp(call_de, call_de_tx));
CHECK(0 == strcmp(extra, extra_tx));
// CHECK(1 == 2);
TEST_END;
}
void test_msg(const char* call_to_tx, const char* call_de_tx, const char* extra_tx)
{
char message_text[12 + 12 + 20];
char* copy_ptr = message_text;
copy_ptr = stpcpy(copy_ptr, call_to_tx);
copy_ptr = stpcpy(copy_ptr, " ");
copy_ptr = stpcpy(copy_ptr, call_de_tx);
if (strlen(extra_tx) > 0)
{
copy_ptr = stpcpy(copy_ptr, " ");
copy_ptr = stpcpy(copy_ptr, extra_tx);
}
printf("Testing [%s]\n", message_text);
ftx_message_t msg;
ftx_message_init(&msg);
ftx_message_rc_t rc_encode = ftx_message_encode(&msg, NULL, message_text);
CHECK(rc_encode == FTX_MESSAGE_RC_OK);
char call_to[14];
char call_de[14];
char extra[14];
ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, NULL, call_to, call_de, extra);
CHECK(rc_decode == FTX_MESSAGE_RC_OK);
CHECK(0 == strcmp(call_to, call_to_tx));
CHECK(0 == strcmp(call_de, call_de_tx));
CHECK(0 == strcmp(extra, extra_tx));
// CHECK(1 == 2);
TEST_END;
}
#define SIZEOF_ARRAY(x) (sizeof(x) / sizeof((x)[0]))
int main()
{
// test1();
// test4();
const char* callsigns[] = { "YL3JG", "W1A", "W1A/R", "W5AB", "W8ABC", "DE6ABC", "DE6ABC/R", "DE7AB", "DE9A", "3DA0X", "3DA0XYZ", "3DA0XYZ/R", "3XZ0AB", "3XZ0A" };
const char* tokens[] = { "CQ", "QRZ" };
const char* grids[] = { "KO26", "RR99", "AA00", "RR09", "AA01", "RRR", "RR73", "73", "R+10", "R+05", "R-12", "R-02", "+10", "+05", "-02", "-02", "" };
for (int idx_grid = 0; idx_grid < SIZEOF_ARRAY(grids); ++idx_grid)
{
for (int idx_callsign = 0; idx_callsign < SIZEOF_ARRAY(callsigns); ++idx_callsign)
{
for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2)
{
test_std_msg(callsigns[idx_callsign], callsigns[idx_callsign2], grids[idx_grid]);
}
}
for (int idx_token = 0; idx_token < SIZEOF_ARRAY(tokens); ++idx_token)
{
for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2)
{
test_std_msg(tokens[idx_token], callsigns[idx_callsign2], grids[idx_grid]);
}
}
}
// test_std_msg("YOMAMA", "MYMAMA/QRP", "73");
return 0;
}

Some files were not shown because too many files have changed in this diff Show More