kopia lustrzana https://github.com/kgoba/ft8_lib
commit
50ee0c0636
|
@ -2,6 +2,7 @@ BasedOnStyle: WebKit
|
||||||
# Cpp11BracedListStyle: false
|
# Cpp11BracedListStyle: false
|
||||||
# ColumnLimit: 120
|
# ColumnLimit: 120
|
||||||
IndentCaseLabels: false
|
IndentCaseLabels: false
|
||||||
|
IndentExternBlock: false
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
TabWidth: 8
|
TabWidth: 8
|
||||||
UseTab: Never
|
UseTab: Never
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
*.o
|
|
||||||
gen_ft8
|
gen_ft8
|
||||||
decode_ft8
|
decode_ft8
|
||||||
|
test_ft8
|
||||||
|
libft8.a
|
||||||
|
wsjtx2/
|
||||||
|
.build/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
58
Makefile
58
Makefile
|
@ -1,27 +1,49 @@
|
||||||
CFLAGS = -O3 -ggdb3 -fsanitize=address
|
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 = -fsanitize=address -O3 -ggdb3
|
||||||
CPPFLAGS = -std=c11 -I.
|
CPPFLAGS = -std=c11 -I.
|
||||||
LDFLAGS = -lm -fsanitize=address
|
LDFLAGS = -fsanitize=address -lm
|
||||||
|
|
||||||
TARGETS = gen_ft8 decode_ft8 test
|
# Optionally, use Portaudio for live audio input
|
||||||
|
ifdef PORTAUDIO_PREFIX
|
||||||
|
CPPFLAGS += -DUSE_PORTAUDIO -I$(PORTAUDIO_PREFIX)/include
|
||||||
|
LDFLAGS += -lportaudio -L$(PORTAUDIO_PREFIX)/lib
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: run_tests all clean
|
.PHONY: all clean run_tests install
|
||||||
|
|
||||||
all: $(TARGETS)
|
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:
|
clean:
|
||||||
rm -f *.o ft8/*.o common/*.o fft/*.o $(TARGETS)
|
rm -rf $(BUILD_DIR) $(TARGETS)
|
||||||
|
|
||||||
|
run_tests: test_ft8
|
||||||
|
@./test_ft8
|
||||||
|
|
||||||
install:
|
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
|
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 $^
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
#include "audio.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef USE_PORTAUDIO
|
||||||
|
#include <portaudio.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
PaStream* instream;
|
||||||
|
} audio_context_t;
|
||||||
|
|
||||||
|
static audio_context_t audio_context;
|
||||||
|
|
||||||
|
static int audio_cb(void* inputBuffer, void* outputBuffer, unsigned long framesPerBuffer,
|
||||||
|
const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData)
|
||||||
|
{
|
||||||
|
audio_context_t* context = (audio_context_t*)userData;
|
||||||
|
float* samples_in = (float*)inputBuffer;
|
||||||
|
|
||||||
|
// PaTime time = data->startTime + timeInfo->inputBufferAdcTime;
|
||||||
|
printf("Callback with %ld samples\n", framesPerBuffer);
|
||||||
|
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 = paFloat32,
|
||||||
|
.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_init(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);
|
||||||
|
Pa_Terminate(); // I don't think we need this but...
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_open(const char* name)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
audio_context.instream = NULL;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = paFloat32,
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaStream* instream;
|
||||||
|
pa_rc = Pa_OpenStream(
|
||||||
|
&instream, // address of stream
|
||||||
|
&inputParameters,
|
||||||
|
NULL,
|
||||||
|
sample_rate, // Sample rate
|
||||||
|
nfpb, // Frames per buffer
|
||||||
|
paNoFlag,
|
||||||
|
NULL /*(PaStreamCallback*)audio_cb*/, // Callback routine
|
||||||
|
NULL /*(void*)&audio_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_context.instream = instream;
|
||||||
|
|
||||||
|
// while (Pa_IsStreamActive(instream))
|
||||||
|
// {
|
||||||
|
// Pa_Sleep(100);
|
||||||
|
// }
|
||||||
|
// Pa_AbortStream(instream); // Abort stream
|
||||||
|
// Pa_CloseStream(instream); // Close stream, we're done.
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_read(float* buffer, int num_samples)
|
||||||
|
{
|
||||||
|
PaError pa_rc;
|
||||||
|
pa_rc = Pa_ReadStream(audio_context.instream, (void*)buffer, num_samples);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
int audio_init(void)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_list(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_open(const char* name)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audio_read(float* buffer, int num_samples)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef _INCLUDE_AUDIO_H_
|
||||||
|
#define _INCLUDE_AUDIO_H_
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int audio_init(void);
|
||||||
|
void audio_list(void);
|
||||||
|
int audio_open(const char* name);
|
||||||
|
int audio_read(float* buffer, int num_samples);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_AUDIO_H_
|
|
@ -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__)
|
|
|
@ -0,0 +1,263 @@
|
||||||
|
#include "monitor.h"
|
||||||
|
#include <common/common.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(ftx_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 = (WF_ELEM_T*)malloc(mag_size);
|
||||||
|
LOG(LOG_DEBUG, "Waterfall size = %zu\n", mag_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void waterfall_free(ftx_waterfall_t* me)
|
||||||
|
{
|
||||||
|
free(me->mag);
|
||||||
|
}
|
||||||
|
|
||||||
|
void monitor_init(monitor_t* me, const monitor_config_t* cfg)
|
||||||
|
{
|
||||||
|
float slot_time = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME;
|
||||||
|
float symbol_period = (cfg->protocol == FTX_PROTOCOL_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] = me->fft_norm * 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]));
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "Block size = %d\n", me->block_size);
|
||||||
|
LOG(LOG_INFO, "Subblock size = %d\n", me->subblock_size);
|
||||||
|
|
||||||
|
size_t fft_work_size = 0;
|
||||||
|
kiss_fftr_alloc(me->nfft, 0, 0, &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);
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "N_FFT = %d\n", me->nfft);
|
||||||
|
LOG(LOG_DEBUG, "FFT work area = %zu\n", fft_work_size);
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
me->nifft = 64; // Gives 200 Hz sample rate for FT8 (160ms symbol period)
|
||||||
|
|
||||||
|
size_t ifft_work_size = 0;
|
||||||
|
kiss_fft_alloc(me->nifft, 1, 0, &ifft_work_size);
|
||||||
|
me->ifft_work = malloc(ifft_work_size);
|
||||||
|
me->ifft_cfg = kiss_fft_alloc(me->nifft, 1, me->ifft_work, &ifft_work_size);
|
||||||
|
|
||||||
|
LOG(LOG_INFO, "N_iFFT = %d\n", me->nifft);
|
||||||
|
LOG(LOG_DEBUG, "iFFT work area = %zu\n", ifft_work_size);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 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 = -120.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do DFT of windowed analysis frame
|
||||||
|
for (int pos = 0; pos < me->nfft; ++pos)
|
||||||
|
{
|
||||||
|
timedata[pos] = me->window[pos] * me->last_frame[pos];
|
||||||
|
}
|
||||||
|
kiss_fftr(me->fft_cfg, timedata, freqdata);
|
||||||
|
|
||||||
|
// Loop over possible frequency OSR offsets
|
||||||
|
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);
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
// Save the magnitude in dB and phase in radians
|
||||||
|
float phase = atan2f(freqdata[src_bin].i, freqdata[src_bin].r);
|
||||||
|
me->wf.mag[offset].mag = db;
|
||||||
|
me->wf.mag[offset].phase = phase;
|
||||||
|
#else
|
||||||
|
// 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);
|
||||||
|
#endif
|
||||||
|
++offset;
|
||||||
|
|
||||||
|
if (db > me->max_mag)
|
||||||
|
me->max_mag = db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++me->wf.num_blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal)
|
||||||
|
{
|
||||||
|
const int num_ifft = me->nifft;
|
||||||
|
const int num_shift = num_ifft / 2;
|
||||||
|
const int taper_width = 4;
|
||||||
|
const int num_tones = 8;
|
||||||
|
|
||||||
|
// Starting offset is 3 subblocks due to analysis buffer loading
|
||||||
|
int offset = 1; // candidate->time_offset;
|
||||||
|
offset = (offset * me->wf.time_osr) + 1; // + candidate->time_sub;
|
||||||
|
offset = (offset * me->wf.freq_osr); // + candidate->freq_sub;
|
||||||
|
offset = (offset * me->wf.num_bins); // + candidate->freq_offset;
|
||||||
|
|
||||||
|
WF_ELEM_T* el = me->wf.mag + offset;
|
||||||
|
|
||||||
|
// DFT frequency data - initialize to zero
|
||||||
|
kiss_fft_cpx freqdata[num_ifft];
|
||||||
|
for (int i = 0; i < num_ifft; ++i)
|
||||||
|
{
|
||||||
|
freqdata[i].r = 0;
|
||||||
|
freqdata[i].i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
for (int num_block = 1; num_block < me->wf.num_blocks; ++num_block)
|
||||||
|
{
|
||||||
|
// Extract frequency data around the selected candidate only
|
||||||
|
for (int i = candidate->freq_offset - taper_width - 1; i < candidate->freq_offset + 8 + taper_width - 1; ++i)
|
||||||
|
{
|
||||||
|
if ((i >= 0) && (i < me->wf.num_bins))
|
||||||
|
{
|
||||||
|
int tgt_bin = (me->wf.freq_osr * (i - candidate->freq_offset) + num_ifft) % num_ifft;
|
||||||
|
float weight = 1.0f;
|
||||||
|
if (i < candidate->freq_offset)
|
||||||
|
{
|
||||||
|
weight = ((i - candidate->freq_offset) + taper_width) / (float)taper_width;
|
||||||
|
}
|
||||||
|
else if (i > candidate->freq_offset + 7)
|
||||||
|
{
|
||||||
|
weight = ((candidate->freq_offset + 7 - i) + taper_width) / (float)taper_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert (dB magnitude, phase) to (real, imaginary)
|
||||||
|
float mag = powf(10.0f, el[i].mag / 20) / 2 * weight;
|
||||||
|
freqdata[tgt_bin].r = mag * cosf(el[i].phase);
|
||||||
|
freqdata[tgt_bin].i = mag * sinf(el[i].phase);
|
||||||
|
|
||||||
|
int i2 = i + me->wf.num_bins;
|
||||||
|
tgt_bin = (tgt_bin + 1) % num_ifft;
|
||||||
|
float mag2 = powf(10.0f, el[i2].mag / 20) / 2 * weight;
|
||||||
|
freqdata[tgt_bin].r = mag2 * cosf(el[i2].phase);
|
||||||
|
freqdata[tgt_bin].i = mag2 * sinf(el[i2].phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute inverse DFT and overlap-add the waveform
|
||||||
|
kiss_fft_cpx timedata[num_ifft];
|
||||||
|
kiss_fft(me->ifft_cfg, freqdata, timedata);
|
||||||
|
for (int i = 0; i < num_ifft; ++i)
|
||||||
|
{
|
||||||
|
signal[pos + i] += timedata[i].i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next symbol
|
||||||
|
el += me->wf.block_stride;
|
||||||
|
pos += num_shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,62 @@
|
||||||
|
#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; ///< First FFT bin in the frequency range (begin)
|
||||||
|
int max_bin; ///< First FFT bin outside the frequency range (end)
|
||||||
|
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)
|
||||||
|
ftx_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
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
int nifft; ///< iFFT size
|
||||||
|
void* ifft_work; ///< Work area required by inverse Kiss FFT
|
||||||
|
kiss_fft_cfg ifft_cfg; ///< Inverse Kiss FFT housekeeping object
|
||||||
|
#endif
|
||||||
|
} 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 WATERFALL_USE_PHASE
|
||||||
|
void monitor_resynth(const monitor_t* me, const candidate_t* candidate, float* signal);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _INCLUDE_MONITOR_H_
|
|
@ -7,7 +7,7 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
void save_wav(const float* signal, int num_samples, int sample_rate, const char* path)
|
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path)
|
||||||
{
|
{
|
||||||
char subChunk1ID[4] = { 'f', 'm', 't', ' ' };
|
char subChunk1ID[4] = { 'f', 'm', 't', ' ' };
|
||||||
uint32_t subChunk1Size = 16; // 16 for PCM
|
uint32_t subChunk1Size = 16; // 16 for PCM
|
||||||
|
@ -37,6 +37,8 @@ void save_wav(const float* signal, int num_samples, int sample_rate, const char*
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE* f = fopen(path, "wb");
|
FILE* f = fopen(path, "wb");
|
||||||
|
if (f == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
// NOTE: works only on little-endian architecture
|
// NOTE: works only on little-endian architecture
|
||||||
fwrite(chunkID, sizeof(chunkID), 1, f);
|
fwrite(chunkID, sizeof(chunkID), 1, f);
|
||||||
|
@ -60,6 +62,7 @@ void save_wav(const float* signal, int num_samples, int sample_rate, const char*
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
|
||||||
free(raw_data);
|
free(raw_data);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
|
@ -82,6 +85,8 @@ int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path
|
||||||
char format[4]; // = {'W', 'A', 'V', 'E'};
|
char format[4]; // = {'W', 'A', 'V', 'E'};
|
||||||
|
|
||||||
FILE* f = fopen(path, "rb");
|
FILE* f = fopen(path, "rb");
|
||||||
|
if (f == NULL)
|
||||||
|
return -1;
|
||||||
|
|
||||||
// NOTE: works only on little-endian architecture
|
// NOTE: works only on little-endian architecture
|
||||||
fread((void*)chunkID, sizeof(chunkID), 1, f);
|
fread((void*)chunkID, sizeof(chunkID), 1, f);
|
||||||
|
@ -91,7 +96,7 @@ int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path
|
||||||
fread((void*)subChunk1ID, sizeof(subChunk1ID), 1, f);
|
fread((void*)subChunk1ID, sizeof(subChunk1ID), 1, f);
|
||||||
fread((void*)&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
fread((void*)&subChunk1Size, sizeof(subChunk1Size), 1, f);
|
||||||
if (subChunk1Size != 16)
|
if (subChunk1Size != 16)
|
||||||
return -1;
|
return -2;
|
||||||
|
|
||||||
fread((void*)&audioFormat, sizeof(audioFormat), 1, f);
|
fread((void*)&audioFormat, sizeof(audioFormat), 1, f);
|
||||||
fread((void*)&numChannels, sizeof(numChannels), 1, f);
|
fread((void*)&numChannels, sizeof(numChannels), 1, f);
|
||||||
|
@ -101,13 +106,13 @@ int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path
|
||||||
fread((void*)&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
fread((void*)&bitsPerSample, sizeof(bitsPerSample), 1, f);
|
||||||
|
|
||||||
if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16)
|
if (audioFormat != 1 || numChannels != 1 || bitsPerSample != 16)
|
||||||
return -1;
|
return -3;
|
||||||
|
|
||||||
fread((void*)subChunk2ID, sizeof(subChunk2ID), 1, f);
|
fread((void*)subChunk2ID, sizeof(subChunk2ID), 1, f);
|
||||||
fread((void*)&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
fread((void*)&subChunk2Size, sizeof(subChunk2Size), 1, f);
|
||||||
|
|
||||||
if (subChunk2Size / blockAlign > *num_samples)
|
if (subChunk2Size / blockAlign > *num_samples)
|
||||||
return -2;
|
return -4;
|
||||||
|
|
||||||
*num_samples = subChunk2Size / blockAlign;
|
*num_samples = subChunk2Size / blockAlign;
|
||||||
*sample_rate = sampleRate;
|
*sample_rate = sampleRate;
|
||||||
|
|
|
@ -6,11 +6,11 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
// Save signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
void save_wav(const float* signal, int num_samples, int sample_rate, const char* path);
|
int save_wav(const float* signal, int num_samples, int sample_rate, const char* path);
|
||||||
|
|
||||||
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
// Load signal in floating point format (-1 .. +1) as a WAVE file using 16-bit signed integers.
|
||||||
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path);
|
int load_wav(float* signal, int* num_samples, int* sample_rate, const char* path);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
389
decode_ft8.c
389
decode_ft8.c
|
@ -1,389 +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 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);
|
|
||||||
|
|
||||||
const int max_blocks = (int)(slot_time / symbol_period);
|
|
||||||
const int num_bins = (int)(cfg->sample_rate * symbol_period / 2);
|
|
||||||
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 = 0; bin < me->wf.num_bins; ++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 = (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, &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
|
|
||||||
int snr = 0; // TODO: compute SNR
|
|
||||||
printf("000000 %3d %+4.2f %4.0f ~ %s\n", cand->score, time_sec, freq_hz, message.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG(LOG_INFO, "Decoded %d messages\n", num_decoded);
|
|
||||||
|
|
||||||
monitor_free(&mon);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
|
@ -0,0 +1,391 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include <ft8/decode.h>
|
||||||
|
#include <ft8/encode.h>
|
||||||
|
#include <ft8/message.h>
|
||||||
|
|
||||||
|
#include <common/common.h>
|
||||||
|
#include <common/wave.h>
|
||||||
|
#include <common/monitor.h>
|
||||||
|
#include <common/audio.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 = 140;
|
||||||
|
const int kLDPC_iterations = 25;
|
||||||
|
|
||||||
|
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(const char* error_msg)
|
||||||
|
{
|
||||||
|
if (error_msg != NULL)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "ERROR: %s\n", error_msg);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "Usage: decode_ft8 [-list|([-ft4] [INPUT|-dev DEVICE])]\n\n");
|
||||||
|
fprintf(stderr, "Decode a 15-second (or slighly shorter) WAV file.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CALLSIGN_HASHTABLE_SIZE 256
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
char callsign[12]; ///> Up to 11 symbols of callsign + trailing zeros (always filled)
|
||||||
|
uint32_t hash; ///> 8 MSBs contain the age of callsign; 22 LSBs contain hash value
|
||||||
|
} callsign_hashtable[CALLSIGN_HASHTABLE_SIZE];
|
||||||
|
|
||||||
|
static int callsign_hashtable_size;
|
||||||
|
|
||||||
|
void hashtable_init(void)
|
||||||
|
{
|
||||||
|
callsign_hashtable_size = 0;
|
||||||
|
memset(callsign_hashtable, 0, sizeof(callsign_hashtable));
|
||||||
|
}
|
||||||
|
|
||||||
|
void hashtable_cleanup(uint8_t max_age)
|
||||||
|
{
|
||||||
|
for (int idx_hash = 0; idx_hash < CALLSIGN_HASHTABLE_SIZE; ++idx_hash)
|
||||||
|
{
|
||||||
|
if (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
uint8_t age = (uint8_t)(callsign_hashtable[idx_hash].hash >> 24);
|
||||||
|
if (age > max_age)
|
||||||
|
{
|
||||||
|
LOG(LOG_INFO, "Removing [%s] from hash table, age = %d\n", callsign_hashtable[idx_hash].callsign, age);
|
||||||
|
// free the hash entry
|
||||||
|
callsign_hashtable[idx_hash].callsign[0] = '\0';
|
||||||
|
callsign_hashtable[idx_hash].hash = 0;
|
||||||
|
callsign_hashtable_size--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// increase callsign age
|
||||||
|
callsign_hashtable[idx_hash].hash = (((uint32_t)age + 1u) << 24) | (callsign_hashtable[idx_hash].hash & 0x3FFFFFu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hashtable_add(const char* callsign, uint32_t hash)
|
||||||
|
{
|
||||||
|
uint16_t hash10 = (hash >> 12) & 0x3FFu;
|
||||||
|
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) == hash) && (0 == strcmp(callsign_hashtable[idx_hash].callsign, callsign)))
|
||||||
|
{
|
||||||
|
// reset age
|
||||||
|
callsign_hashtable[idx_hash].hash &= 0x3FFFFFu;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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_t hash_type, uint32_t hash, char* callsign)
|
||||||
|
{
|
||||||
|
uint8_t hash_shift = (hash_type == FTX_CALLSIGN_HASH_10_BITS) ? 12 : (hash_type == FTX_CALLSIGN_HASH_12_BITS ? 10 : 0);
|
||||||
|
uint16_t hash10 = (hash >> (12 - hash_shift)) & 0x3FFu;
|
||||||
|
int idx_hash = (hash10 * 23) % CALLSIGN_HASHTABLE_SIZE;
|
||||||
|
while (callsign_hashtable[idx_hash].callsign[0] != '\0')
|
||||||
|
{
|
||||||
|
if (((callsign_hashtable[idx_hash].hash & 0x3FFFFFu) >> hash_shift) == 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 decode(const monitor_t* mon, struct tm* tm_slot_start)
|
||||||
|
{
|
||||||
|
const ftx_waterfall_t* wf = &mon->wf;
|
||||||
|
// Find top candidates by Costas sync score and localize them in time and frequency
|
||||||
|
ftx_candidate_t candidate_list[kMax_candidates];
|
||||||
|
int num_candidates = ftx_find_candidates(wf, kMax_candidates, candidate_list, kMin_score);
|
||||||
|
|
||||||
|
// Hash table for decoded messages (to check for duplicates)
|
||||||
|
int num_decoded = 0;
|
||||||
|
ftx_message_t decoded[kMax_decoded_messages];
|
||||||
|
ftx_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 ftx_candidate_t* cand = &candidate_list[idx];
|
||||||
|
|
||||||
|
float freq_hz = (mon->min_bin + cand->freq_offset + (float)cand->freq_sub / wf->freq_osr) / mon->symbol_period;
|
||||||
|
float time_sec = (cand->time_offset + (float)cand->time_sub / wf->time_osr) * mon->symbol_period;
|
||||||
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
// int resynth_len = 12000 * 16;
|
||||||
|
// float resynth_signal[resynth_len];
|
||||||
|
// for (int pos = 0; pos < resynth_len; ++pos)
|
||||||
|
// {
|
||||||
|
// resynth_signal[pos] = 0;
|
||||||
|
// }
|
||||||
|
// monitor_resynth(mon, cand, resynth_signal);
|
||||||
|
// char resynth_path[80];
|
||||||
|
// sprintf(resynth_path, "resynth_%04f_%02.1f.wav", freq_hz, time_sec);
|
||||||
|
// save_wav(resynth_signal, resynth_len, 12000, resynth_path);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ftx_message_t message;
|
||||||
|
ftx_decode_status_t status;
|
||||||
|
if (!ftx_decode_candidate(wf, cand, kLDPC_iterations, &message, &status))
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
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 == memcmp(decoded_hashtable[idx_hash]->payload, message.payload, sizeof(message.payload))))
|
||||||
|
{
|
||||||
|
LOG(LOG_DEBUG, "Found a duplicate!\n");
|
||||||
|
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;
|
||||||
|
|
||||||
|
char text[FTX_MAX_MESSAGE_LENGTH];
|
||||||
|
ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text);
|
||||||
|
if (unpack_status != FTX_MESSAGE_RC_OK)
|
||||||
|
{
|
||||||
|
snprintf(text, sizeof(text), "Error [%d] while unpacking!", (int)unpack_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake WSJT-X-like output for now
|
||||||
|
float snr = cand->score * 0.5f; // TODO: compute better approximation of SNR
|
||||||
|
printf("%02d%02d%02d %+05.1f %+4.2f %4.0f ~ %s\n",
|
||||||
|
tm_slot_start->tm_hour, tm_slot_start->tm_min, tm_slot_start->tm_sec,
|
||||||
|
snr, time_sec, freq_hz, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "Decoded %d messages, callsign hashtable size %d\n", num_decoded, callsign_hashtable_size);
|
||||||
|
hashtable_cleanup(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
// Accepted arguments
|
||||||
|
const char* wav_path = NULL;
|
||||||
|
const char* dev_name = NULL;
|
||||||
|
ftx_protocol_t protocol = FTX_PROTOCOL_FT8;
|
||||||
|
float time_shift = 0.8;
|
||||||
|
|
||||||
|
// 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"))
|
||||||
|
{
|
||||||
|
protocol = FTX_PROTOCOL_FT4;
|
||||||
|
}
|
||||||
|
else if (0 == strcmp(argv[arg_idx], "-list"))
|
||||||
|
{
|
||||||
|
audio_init();
|
||||||
|
audio_list();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (0 == strcmp(argv[arg_idx], "-dev"))
|
||||||
|
{
|
||||||
|
if (arg_idx + 1 < argc)
|
||||||
|
{
|
||||||
|
++arg_idx;
|
||||||
|
dev_name = argv[arg_idx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Expected an audio device name after -dev");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Unknown command line option");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (wav_path == NULL)
|
||||||
|
{
|
||||||
|
wav_path = argv[arg_idx];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
usage("Multiple positional arguments");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++arg_idx;
|
||||||
|
}
|
||||||
|
// Check if all mandatory arguments have been received
|
||||||
|
if (wav_path == NULL && dev_name == NULL)
|
||||||
|
{
|
||||||
|
usage("Expected either INPUT file path or DEVICE name");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float slot_period = ((protocol == FTX_PROTOCOL_FT8) ? FT8_SLOT_TIME : FT4_SLOT_TIME);
|
||||||
|
int sample_rate = 12000;
|
||||||
|
int num_samples = slot_period * sample_rate;
|
||||||
|
float signal[num_samples];
|
||||||
|
bool is_live = false;
|
||||||
|
|
||||||
|
if (wav_path != NULL)
|
||||||
|
{
|
||||||
|
int rc = load_wav(signal, &num_samples, &sample_rate, wav_path);
|
||||||
|
if (rc < 0)
|
||||||
|
{
|
||||||
|
LOG(LOG_ERROR, "ERROR: cannot load wave file %s\n", wav_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
LOG(LOG_INFO, "Sample rate %d Hz, %d samples, %.3f seconds\n", sample_rate, num_samples, (double)num_samples / sample_rate);
|
||||||
|
}
|
||||||
|
else if (dev_name != NULL)
|
||||||
|
{
|
||||||
|
audio_init();
|
||||||
|
audio_open(dev_name);
|
||||||
|
num_samples = (slot_period - 0.4f) * sample_rate;
|
||||||
|
is_live = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = protocol
|
||||||
|
};
|
||||||
|
|
||||||
|
hashtable_init();
|
||||||
|
|
||||||
|
monitor_init(&mon, &mon_cfg);
|
||||||
|
LOG(LOG_DEBUG, "Waterfall allocated %d symbols\n", mon.wf.max_blocks);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
struct tm tm_slot_start = { 0 };
|
||||||
|
if (is_live)
|
||||||
|
{
|
||||||
|
// Wait for the start of time slot
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
struct timespec spec;
|
||||||
|
clock_gettime(CLOCK_REALTIME, &spec);
|
||||||
|
double time = (double)spec.tv_sec + (spec.tv_nsec / 1e9);
|
||||||
|
double time_within_slot = fmod(time - time_shift, slot_period);
|
||||||
|
if (time_within_slot > slot_period / 4)
|
||||||
|
{
|
||||||
|
audio_read(signal, mon.block_size);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
time_t time_slot_start = (time_t)(time - time_within_slot);
|
||||||
|
gmtime_r(&time_slot_start, &tm_slot_start);
|
||||||
|
LOG(LOG_INFO, "Time within slot %02d%02d%02d: %.3f s\n", tm_slot_start.tm_hour,
|
||||||
|
tm_slot_start.tm_min, tm_slot_start.tm_sec, time_within_slot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and accumulate audio data in a monitor/waterfall instance
|
||||||
|
for (int frame_pos = 0; frame_pos + mon.block_size <= num_samples; frame_pos += mon.block_size)
|
||||||
|
{
|
||||||
|
if (dev_name != NULL)
|
||||||
|
{
|
||||||
|
audio_read(signal + frame_pos, mon.block_size);
|
||||||
|
}
|
||||||
|
// LOG(LOG_DEBUG, "Frame pos: %.3fs\n", (float)(frame_pos + mon.block_size) / sample_rate);
|
||||||
|
fprintf(stderr, "#");
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
LOG(LOG_DEBUG, "Waterfall accumulated %d symbols\n", mon.wf.num_blocks);
|
||||||
|
LOG(LOG_INFO, "Max magnitude: %.1f dB\n", mon.max_mag);
|
||||||
|
|
||||||
|
// Decode accumulated data (containing slightly less than a full time slot)
|
||||||
|
decode(&mon, &tm_slot_start);
|
||||||
|
|
||||||
|
// Reset internal variables for the next time slot
|
||||||
|
monitor_reset(&mon);
|
||||||
|
} while (is_live);
|
||||||
|
|
||||||
|
monitor_free(&mon);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -6,12 +6,12 @@
|
||||||
|
|
||||||
#include "common/common.h"
|
#include "common/common.h"
|
||||||
#include "common/wave.h"
|
#include "common/wave.h"
|
||||||
#include "common/debug.h"
|
#include "ft8/message.h"
|
||||||
#include "ft8/pack.h"
|
|
||||||
#include "ft8/encode.h"
|
#include "ft8/encode.h"
|
||||||
#include "ft8/constants.h"
|
#include "ft8/constants.h"
|
||||||
|
|
||||||
#define LOG_LEVEL LOG_INFO
|
#define LOG_LEVEL LOG_INFO
|
||||||
|
#include "ft8/debug.h"
|
||||||
|
|
||||||
#define FT8_SYMBOL_BT 2.0f ///< symbol smoothing filter bandwidth factor (BT)
|
#define FT8_SYMBOL_BT 2.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||||
#define FT4_SYMBOL_BT 1.0f ///< symbol smoothing filter bandwidth factor (BT)
|
#define FT4_SYMBOL_BT 1.0f ///< symbol smoothing filter bandwidth factor (BT)
|
||||||
|
@ -130,19 +130,19 @@ int main(int argc, char** argv)
|
||||||
bool is_ft4 = (argc > 4) && (0 == strcmp(argv[4], "-ft4"));
|
bool is_ft4 = (argc > 4) && (0 == strcmp(argv[4], "-ft4"));
|
||||||
|
|
||||||
// First, pack the text data into binary message
|
// First, pack the text data into binary message
|
||||||
uint8_t packed[FTX_LDPC_K_BYTES];
|
ftx_message_t msg;
|
||||||
int rc = pack77(message, packed);
|
ftx_message_rc_t rc = ftx_message_encode(&msg, NULL, message);
|
||||||
if (rc < 0)
|
if (rc != FTX_MESSAGE_RC_OK)
|
||||||
{
|
{
|
||||||
printf("Cannot parse message!\n");
|
printf("Cannot parse message!\n");
|
||||||
printf("RC = %d\n", rc);
|
printf("RC = %d\n", (int)rc);
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Packed data: ");
|
printf("Packed data: ");
|
||||||
for (int j = 0; j < 10; ++j)
|
for (int j = 0; j < 10; ++j)
|
||||||
{
|
{
|
||||||
printf("%02x ", packed[j]);
|
printf("%02x ", msg.payload[j]);
|
||||||
}
|
}
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
|
||||||
|
@ -155,11 +155,11 @@ int main(int argc, char** argv)
|
||||||
uint8_t tones[num_tones]; // Array of 79 tones (symbols)
|
uint8_t tones[num_tones]; // Array of 79 tones (symbols)
|
||||||
if (is_ft4)
|
if (is_ft4)
|
||||||
{
|
{
|
||||||
ft4_encode(packed, tones);
|
ft4_encode(msg.payload, tones);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ft8_encode(packed, tones);
|
ft8_encode(msg.payload, tones);
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("FSK tones: ");
|
printf("FSK tones: ");
|
|
@ -49,39 +49,39 @@ extern "C"
|
||||||
#define FT8_CRC_POLYNOMIAL ((uint16_t)0x2757u) ///< CRC-14 polynomial without the leading (MSB) 1
|
#define FT8_CRC_POLYNOMIAL ((uint16_t)0x2757u) ///< CRC-14 polynomial without the leading (MSB) 1
|
||||||
#define FT8_CRC_WIDTH (14)
|
#define FT8_CRC_WIDTH (14)
|
||||||
|
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
PROTO_FT4,
|
FTX_PROTOCOL_FT4,
|
||||||
PROTO_FT8
|
FTX_PROTOCOL_FT8
|
||||||
} ftx_protocol_t;
|
} ftx_protocol_t;
|
||||||
|
|
||||||
/// Costas 7x7 tone pattern for synchronization
|
/// Costas 7x7 tone pattern for synchronization
|
||||||
extern const uint8_t kFT8_Costas_pattern[7];
|
extern const uint8_t kFT8_Costas_pattern[7];
|
||||||
extern const uint8_t kFT4_Costas_pattern[4][4];
|
extern const uint8_t kFT4_Costas_pattern[4][4];
|
||||||
|
|
||||||
/// Gray code map to encode 8 symbols (tones)
|
/// Gray code map to encode 8 symbols (tones)
|
||||||
extern const uint8_t kFT8_Gray_map[8];
|
extern const uint8_t kFT8_Gray_map[8];
|
||||||
extern const uint8_t kFT4_Gray_map[4];
|
extern const uint8_t kFT4_Gray_map[4];
|
||||||
|
|
||||||
extern const uint8_t kFT4_XOR_sequence[10];
|
extern const uint8_t kFT4_XOR_sequence[10];
|
||||||
|
|
||||||
/// Parity generator matrix for (174,91) LDPC code, stored in bitpacked format (MSB first)
|
/// Parity generator matrix for (174,91) LDPC code, stored in bitpacked format (MSB first)
|
||||||
extern const uint8_t kFTX_LDPC_generator[FTX_LDPC_M][FTX_LDPC_K_BYTES];
|
extern const uint8_t kFTX_LDPC_generator[FTX_LDPC_M][FTX_LDPC_K_BYTES];
|
||||||
|
|
||||||
/// LDPC(174,91) parity check matrix, containing 83 rows,
|
/// LDPC(174,91) parity check matrix, containing 83 rows,
|
||||||
/// each row describes one parity check,
|
/// each row describes one parity check,
|
||||||
/// each number is an index into the codeword (1-origin).
|
/// each number is an index into the codeword (1-origin).
|
||||||
/// The codeword bits mentioned in each row must xor to zero.
|
/// The codeword bits mentioned in each row must xor to zero.
|
||||||
/// From WSJT-X's ldpc_174_91_c_reordered_parity.f90.
|
/// From WSJT-X's ldpc_174_91_c_reordered_parity.f90.
|
||||||
extern const uint8_t kFTX_LDPC_Nm[FTX_LDPC_M][7];
|
extern const uint8_t kFTX_LDPC_Nm[FTX_LDPC_M][7];
|
||||||
|
|
||||||
/// Mn from WSJT-X's bpdecode174.f90. Each row corresponds to a codeword bit.
|
/// Mn from WSJT-X's bpdecode174.f90. Each row corresponds to a codeword bit.
|
||||||
/// The numbers indicate which three parity checks (rows in Nm) refer to the codeword bit.
|
/// The numbers indicate which three parity checks (rows in Nm) refer to the codeword bit.
|
||||||
/// The numbers use 1 as the origin (first entry).
|
/// The numbers use 1 as the origin (first entry).
|
||||||
extern const uint8_t kFTX_LDPC_Mn[FTX_LDPC_N][3];
|
extern const uint8_t kFTX_LDPC_Mn[FTX_LDPC_N][3];
|
||||||
|
|
||||||
/// Number of rows (columns in C/C++) in the array Nm.
|
/// Number of rows (columns in C/C++) in the array Nm.
|
||||||
extern const uint8_t kFTX_LDPC_Num_rows[FTX_LDPC_M];
|
extern const uint8_t kFTX_LDPC_Num_rows[FTX_LDPC_M];
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
24
ft8/crc.h
24
ft8/crc.h
|
@ -9,20 +9,20 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Compute 14-bit CRC for a sequence of given number of bits using FT8/FT4 CRC polynomial
|
// Compute 14-bit CRC for a sequence of given number of bits using FT8/FT4 CRC polynomial
|
||||||
// [IN] message - byte sequence (MSB first)
|
// [IN] message - byte sequence (MSB first)
|
||||||
// [IN] num_bits - number of bits in the sequence
|
// [IN] num_bits - number of bits in the sequence
|
||||||
uint16_t ftx_compute_crc(const uint8_t message[], int num_bits);
|
uint16_t ftx_compute_crc(const uint8_t message[], int num_bits);
|
||||||
|
|
||||||
/// Extract the FT8/FT4 CRC of a packed message (during decoding)
|
/// Extract the FT8/FT4 CRC of a packed message (during decoding)
|
||||||
/// @param[in] a91 77 bits of payload data + CRC
|
/// @param[in] a91 77 bits of payload data + CRC
|
||||||
/// @return Extracted CRC
|
/// @return Extracted CRC
|
||||||
uint16_t ftx_extract_crc(const uint8_t a91[]);
|
uint16_t ftx_extract_crc(const uint8_t a91[]);
|
||||||
|
|
||||||
/// Add FT8/FT4 CRC to a packed message (during encoding)
|
/// Add FT8/FT4 CRC to a packed message (during encoding)
|
||||||
/// @param[in] payload 77 bits of payload data
|
/// @param[in] payload 77 bits of payload data
|
||||||
/// @param[out] a91 91 bits of payload data + CRC
|
/// @param[out] a91 91 bits of payload data + CRC
|
||||||
void ftx_add_crc(const uint8_t payload[], uint8_t a91[]);
|
void ftx_add_crc(const uint8_t payload[], uint8_t a91[]);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_
|
218
ft8/decode.c
218
ft8/decode.c
|
@ -2,18 +2,35 @@
|
||||||
#include "constants.h"
|
#include "constants.h"
|
||||||
#include "crc.h"
|
#include "crc.h"
|
||||||
#include "ldpc.h"
|
#include "ldpc.h"
|
||||||
#include "unpack.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
// #define LOG_LEVEL LOG_DEBUG
|
||||||
|
// #include "debug.h"
|
||||||
|
|
||||||
|
// Lookup table for y = 10*log10(1 + 10^(x/10)), where
|
||||||
|
// y - increase in signal level dB when adding a weaker independent signal
|
||||||
|
// x - specific relative strength of the weaker signal in dB
|
||||||
|
// Table index corresponds to x in dB (index 0: 0 dB, index 1: -1 dB etc)
|
||||||
|
static const float db_power_sum[40] = {
|
||||||
|
3.01029995663981f, 2.53901891043867f, 2.1244260279434f, 1.76434862436485f, 1.45540463109294f,
|
||||||
|
1.19331048066095f, 0.973227937086954f, 0.790097496525665f, 0.638920341433796f, 0.514969420252302f,
|
||||||
|
0.413926851582251f, 0.331956199884278f, 0.265723755961025f, 0.212384019142551f, 0.16954289279533f,
|
||||||
|
0.135209221080382f, 0.10774225511957f, 0.085799992300358f, 0.06829128312453f, 0.054333142200458f,
|
||||||
|
0.043213737826426f, 0.034360947517284f, 0.027316043349389f, 0.021711921641451f, 0.017255250287928f,
|
||||||
|
0.013711928326833f, 0.010895305999614f, 0.008656680827934f, 0.006877654943187f, 0.005464004928574f,
|
||||||
|
0.004340774793186f, 0.003448354310253f, 0.002739348814965f, 0.002176083232619f, 0.001728613409904f,
|
||||||
|
0.001373142636584f, 0.001090761428665f, 0.000866444976964f, 0.000688255828734f, 0.000546709946839f
|
||||||
|
};
|
||||||
|
|
||||||
/// Compute log likelihood log(p(1) / p(0)) of 174 message bits for later use in soft-decision LDPC decoding
|
/// 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] wf Waterfall data collected during message slot
|
||||||
/// @param[in] cand Candidate to extract the message from
|
/// @param[in] cand Candidate to extract the message from
|
||||||
/// @param[in] code_map Symbol encoding map
|
/// @param[in] code_map Symbol encoding map
|
||||||
/// @param[out] log174 Output of decoded log likelihoods for each of the 174 message bits
|
/// @param[out] log174 Output of decoded log likelihoods for each of the 174 message bits
|
||||||
static void ft4_extract_likelihood(const waterfall_t* wf, const candidate_t* cand, float* log174);
|
static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174);
|
||||||
static void ft8_extract_likelihood(const waterfall_t* wf, const candidate_t* cand, float* log174);
|
static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174);
|
||||||
|
|
||||||
/// Packs a string of bits each represented as a zero/non-zero byte in bit_array[],
|
/// Packs a string of bits each represented as a zero/non-zero byte in bit_array[],
|
||||||
/// as a string of packed bits starting from the MSB of the first byte of packed[]
|
/// as a string of packed bits starting from the MSB of the first byte of packed[]
|
||||||
|
@ -24,30 +41,30 @@ static void pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[])
|
||||||
|
|
||||||
static float max2(float a, float b);
|
static float max2(float a, float b);
|
||||||
static float max4(float a, float b, float c, float d);
|
static float max4(float a, float b, float c, float d);
|
||||||
static void heapify_down(candidate_t heap[], int heap_size);
|
static void heapify_down(ftx_candidate_t heap[], int heap_size);
|
||||||
static void heapify_up(candidate_t heap[], int heap_size);
|
static void heapify_up(ftx_candidate_t heap[], int heap_size);
|
||||||
|
|
||||||
static void ftx_normalize_logl(float* log174);
|
static void ftx_normalize_logl(float* log174);
|
||||||
static void ft4_extract_symbol(const uint8_t* wf, float* logl);
|
static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl);
|
||||||
static void ft8_extract_symbol(const uint8_t* wf, float* logl);
|
static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl);
|
||||||
static void ft8_decode_multi_symbols(const uint8_t* wf, int num_bins, int n_syms, int bit_idx, float* log174);
|
static void ft8_decode_multi_symbols(const WF_ELEM_T* wf, int num_bins, int n_syms, int bit_idx, float* log174);
|
||||||
|
|
||||||
static int get_index(const waterfall_t* wf, const candidate_t* candidate)
|
static const WF_ELEM_T* get_cand_mag(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
{
|
{
|
||||||
int offset = candidate->time_offset;
|
int offset = candidate->time_offset;
|
||||||
offset = (offset * wf->time_osr) + candidate->time_sub;
|
offset = (offset * wf->time_osr) + candidate->time_sub;
|
||||||
offset = (offset * wf->freq_osr) + candidate->freq_sub;
|
offset = (offset * wf->freq_osr) + candidate->freq_sub;
|
||||||
offset = (offset * wf->num_bins) + candidate->freq_offset;
|
offset = (offset * wf->num_bins) + candidate->freq_offset;
|
||||||
return offset;
|
return wf->mag + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int ft8_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
static int ft8_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
{
|
{
|
||||||
int score = 0;
|
int score = 0;
|
||||||
int num_average = 0;
|
int num_average = 0;
|
||||||
|
|
||||||
// Get the pointer to symbol 0 of the candidate
|
// Get the pointer to symbol 0 of the candidate
|
||||||
const uint8_t* mag_cand = wf->mag + get_index(wf, candidate);
|
const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate);
|
||||||
|
|
||||||
// Compute average score over sync symbols (m+k = 0-7, 36-43, 72-79)
|
// Compute average score over sync symbols (m+k = 0-7, 36-43, 72-79)
|
||||||
for (int m = 0; m < FT8_NUM_SYNC; ++m)
|
for (int m = 0; m < FT8_NUM_SYNC; ++m)
|
||||||
|
@ -63,7 +80,7 @@ static int ft8_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Get the pointer to symbol 'block' of the candidate
|
// Get the pointer to symbol 'block' of the candidate
|
||||||
const uint8_t* p8 = mag_cand + (block * wf->block_stride);
|
const WF_ELEM_T* p8 = mag_cand + (block * wf->block_stride);
|
||||||
|
|
||||||
// Weighted difference between the expected and all other symbols
|
// Weighted difference between the expected and all other symbols
|
||||||
// Does not work as well as the alternative score below
|
// Does not work as well as the alternative score below
|
||||||
|
@ -77,25 +94,25 @@ static int ft8_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
if (sm > 0)
|
if (sm > 0)
|
||||||
{
|
{
|
||||||
// look at one frequency bin lower
|
// look at one frequency bin lower
|
||||||
score += p8[sm] - p8[sm - 1];
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm - 1]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if (sm < 7)
|
if (sm < 7)
|
||||||
{
|
{
|
||||||
// look at one frequency bin higher
|
// look at one frequency bin higher
|
||||||
score += p8[sm] - p8[sm + 1];
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm + 1]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if ((k > 0) && (block_abs > 0))
|
if ((k > 0) && (block_abs > 0))
|
||||||
{
|
{
|
||||||
// look one symbol back in time
|
// look one symbol back in time
|
||||||
score += p8[sm] - p8[sm - wf->block_stride];
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm - wf->block_stride]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if (((k + 1) < FT8_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
if (((k + 1) < FT8_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
||||||
{
|
{
|
||||||
// look one symbol forward in time
|
// look one symbol forward in time
|
||||||
score += p8[sm] - p8[sm + wf->block_stride];
|
score += WF_ELEM_MAG_INT(p8[sm]) - WF_ELEM_MAG_INT(p8[sm + wf->block_stride]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,13 +124,13 @@ static int ft8_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int ft4_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
static int ft4_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* candidate)
|
||||||
{
|
{
|
||||||
int score = 0;
|
int score = 0;
|
||||||
int num_average = 0;
|
int num_average = 0;
|
||||||
|
|
||||||
// Get the pointer to symbol 0 of the candidate
|
// Get the pointer to symbol 0 of the candidate
|
||||||
const uint8_t* mag_cand = wf->mag + get_index(wf, candidate);
|
const WF_ELEM_T* mag_cand = get_cand_mag(wf, candidate);
|
||||||
|
|
||||||
// Compute average score over sync symbols (block = 1-4, 34-37, 67-70, 100-103)
|
// Compute average score over sync symbols (block = 1-4, 34-37, 67-70, 100-103)
|
||||||
for (int m = 0; m < FT4_NUM_SYNC; ++m)
|
for (int m = 0; m < FT4_NUM_SYNC; ++m)
|
||||||
|
@ -129,7 +146,7 @@ static int ft4_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Get the pointer to symbol 'block' of the candidate
|
// Get the pointer to symbol 'block' of the candidate
|
||||||
const uint8_t* p4 = mag_cand + (block * wf->block_stride);
|
const WF_ELEM_T* p4 = mag_cand + (block * wf->block_stride);
|
||||||
|
|
||||||
int sm = kFT4_Costas_pattern[m][k]; // Index of the expected bin
|
int sm = kFT4_Costas_pattern[m][k]; // Index of the expected bin
|
||||||
|
|
||||||
|
@ -140,25 +157,25 @@ static int ft4_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
if (sm > 0)
|
if (sm > 0)
|
||||||
{
|
{
|
||||||
// look at one frequency bin lower
|
// look at one frequency bin lower
|
||||||
score += p4[sm] - p4[sm - 1];
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm - 1]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if (sm < 3)
|
if (sm < 3)
|
||||||
{
|
{
|
||||||
// look at one frequency bin higher
|
// look at one frequency bin higher
|
||||||
score += p4[sm] - p4[sm + 1];
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm + 1]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if ((k > 0) && (block_abs > 0))
|
if ((k > 0) && (block_abs > 0))
|
||||||
{
|
{
|
||||||
// look one symbol back in time
|
// look one symbol back in time
|
||||||
score += p4[sm] - p4[sm - wf->block_stride];
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm - wf->block_stride]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
if (((k + 1) < FT4_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
if (((k + 1) < FT4_LENGTH_SYNC) && ((block_abs + 1) < wf->num_blocks))
|
||||||
{
|
{
|
||||||
// look one symbol forward in time
|
// look one symbol forward in time
|
||||||
score += p4[sm] - p4[sm + wf->block_stride];
|
score += WF_ELEM_MAG_INT(p4[sm]) - WF_ELEM_MAG_INT(p4[sm + wf->block_stride]);
|
||||||
++num_average;
|
++num_average;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,10 +187,13 @@ static int ft4_sync_score(const waterfall_t* wf, const candidate_t* candidate)
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[], int min_score)
|
int ftx_find_candidates(const ftx_waterfall_t* wf, int num_candidates, ftx_candidate_t heap[], int min_score)
|
||||||
{
|
{
|
||||||
|
int (*sync_fun)(const ftx_waterfall_t*, const ftx_candidate_t*) = (wf->protocol == FTX_PROTOCOL_FT4) ? ft4_sync_score : ft8_sync_score;
|
||||||
|
int num_tones = (wf->protocol == FTX_PROTOCOL_FT4) ? 4 : 8;
|
||||||
|
|
||||||
int heap_size = 0;
|
int heap_size = 0;
|
||||||
candidate_t candidate;
|
ftx_candidate_t candidate;
|
||||||
|
|
||||||
// Here we allow time offsets that exceed signal boundaries, as long as we still have all data bits.
|
// Here we allow time offsets that exceed signal boundaries, as long as we still have all data bits.
|
||||||
// I.e. we can afford to skip the first 7 or the last 7 Costas symbols, as long as we track how many
|
// I.e. we can afford to skip the first 7 or the last 7 Costas symbols, as long as we track how many
|
||||||
|
@ -182,28 +202,21 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[],
|
||||||
{
|
{
|
||||||
for (candidate.freq_sub = 0; candidate.freq_sub < wf->freq_osr; ++candidate.freq_sub)
|
for (candidate.freq_sub = 0; candidate.freq_sub < wf->freq_osr; ++candidate.freq_sub)
|
||||||
{
|
{
|
||||||
for (candidate.time_offset = -12; candidate.time_offset < 24; ++candidate.time_offset)
|
for (candidate.time_offset = -10; candidate.time_offset < 20; ++candidate.time_offset)
|
||||||
{
|
{
|
||||||
for (candidate.freq_offset = 0; (candidate.freq_offset + 7) < wf->num_bins; ++candidate.freq_offset)
|
for (candidate.freq_offset = 0; (candidate.freq_offset + num_tones - 1) < wf->num_bins; ++candidate.freq_offset)
|
||||||
{
|
{
|
||||||
if (wf->protocol == PROTO_FT4)
|
candidate.score = sync_fun(wf, &candidate);
|
||||||
{
|
|
||||||
candidate.score = ft4_sync_score(wf, &candidate);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
candidate.score = ft8_sync_score(wf, &candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.score < min_score)
|
if (candidate.score < min_score)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// If the heap is full AND the current candidate is better than
|
// If the heap is full AND the current candidate is better than
|
||||||
// the worst in the heap, we remove the worst and make space
|
// the worst in the heap, we remove the worst and make space
|
||||||
if (heap_size == num_candidates && candidate.score > heap[0].score)
|
if ((heap_size == num_candidates) && (candidate.score > heap[0].score))
|
||||||
{
|
{
|
||||||
heap[0] = heap[heap_size - 1];
|
|
||||||
--heap_size;
|
--heap_size;
|
||||||
|
heap[0] = heap[heap_size];
|
||||||
heapify_down(heap, heap_size);
|
heapify_down(heap, heap_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +236,11 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[],
|
||||||
int len_unsorted = heap_size;
|
int len_unsorted = heap_size;
|
||||||
while (len_unsorted > 1)
|
while (len_unsorted > 1)
|
||||||
{
|
{
|
||||||
candidate_t tmp = heap[len_unsorted - 1];
|
// Take the top (index 0) element which is guaranteed to have the smallest score,
|
||||||
|
// exchange it with the last element in the heap, and decrease the heap size.
|
||||||
|
// Then restore the heap property in the new, smaller heap.
|
||||||
|
// At the end the elements will be sorted in descending order.
|
||||||
|
ftx_candidate_t tmp = heap[len_unsorted - 1];
|
||||||
heap[len_unsorted - 1] = heap[0];
|
heap[len_unsorted - 1] = heap[0];
|
||||||
heap[0] = tmp;
|
heap[0] = tmp;
|
||||||
len_unsorted--;
|
len_unsorted--;
|
||||||
|
@ -233,9 +250,9 @@ int ft8_find_sync(const waterfall_t* wf, int num_candidates, candidate_t heap[],
|
||||||
return heap_size;
|
return heap_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ft4_extract_likelihood(const waterfall_t* wf, const candidate_t* cand, float* log174)
|
static void ft4_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174)
|
||||||
{
|
{
|
||||||
const uint8_t* mag_cand = wf->mag + get_index(wf, cand);
|
const WF_ELEM_T* mag = get_cand_mag(wf, cand); // Pointer to 4 magnitude bins of the first symbol
|
||||||
|
|
||||||
// Go over FSK tones and skip Costas sync symbols
|
// Go over FSK tones and skip Costas sync symbols
|
||||||
for (int k = 0; k < FT4_ND; ++k)
|
for (int k = 0; k < FT4_ND; ++k)
|
||||||
|
@ -254,17 +271,14 @@ static void ft4_extract_likelihood(const waterfall_t* wf, const candidate_t* can
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Pointer to 4 bins of the current symbol
|
ft4_extract_symbol(mag + (sym_idx * wf->block_stride), log174 + bit_idx);
|
||||||
const uint8_t* ps = mag_cand + (sym_idx * wf->block_stride);
|
|
||||||
|
|
||||||
ft4_extract_symbol(ps, log174 + bit_idx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ft8_extract_likelihood(const waterfall_t* wf, const candidate_t* cand, float* log174)
|
static void ft8_extract_likelihood(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, float* log174)
|
||||||
{
|
{
|
||||||
const uint8_t* mag_cand = wf->mag + get_index(wf, cand);
|
const WF_ELEM_T* mag = get_cand_mag(wf, cand); // Pointer to 8 magnitude bins of the first symbol
|
||||||
|
|
||||||
// Go over FSK tones and skip Costas sync symbols
|
// Go over FSK tones and skip Costas sync symbols
|
||||||
for (int k = 0; k < FT8_ND; ++k)
|
for (int k = 0; k < FT8_ND; ++k)
|
||||||
|
@ -284,10 +298,7 @@ static void ft8_extract_likelihood(const waterfall_t* wf, const candidate_t* can
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Pointer to 8 bins of the current symbol
|
ft8_extract_symbol(mag + (sym_idx * wf->block_stride), log174 + bit_idx);
|
||||||
const uint8_t* ps = mag_cand + (sym_idx * wf->block_stride);
|
|
||||||
|
|
||||||
ft8_extract_symbol(ps, log174 + bit_idx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,10 +324,10 @@ static void ftx_normalize_logl(float* log174)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, message_t* message, int max_iterations, decode_status_t* status)
|
bool ftx_decode_candidate(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status)
|
||||||
{
|
{
|
||||||
float log174[FTX_LDPC_N]; // message bits encoded as likelihood
|
float log174[FTX_LDPC_N]; // message bits encoded as likelihood
|
||||||
if (wf->protocol == PROTO_FT4)
|
if (wf->protocol == FTX_PROTOCOL_FT4)
|
||||||
{
|
{
|
||||||
ft4_extract_likelihood(wf, cand, log174);
|
ft4_extract_likelihood(wf, cand, log174);
|
||||||
}
|
}
|
||||||
|
@ -352,26 +363,27 @@ bool ft8_decode(const waterfall_t* wf, const candidate_t* cand, message_t* messa
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wf->protocol == PROTO_FT4)
|
// Reuse CRC value as a hash for the message (TODO: 14 bits only, should perhaps use full 16 or 32 bits?)
|
||||||
|
message->hash = status->crc_calculated;
|
||||||
|
|
||||||
|
if (wf->protocol == FTX_PROTOCOL_FT4)
|
||||||
{
|
{
|
||||||
// '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages,
|
// '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages,
|
||||||
// the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits'
|
// the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits'
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
{
|
{
|
||||||
a91[i] ^= kFT4_XOR_sequence[i];
|
message->payload[i] = a91[i] ^ kFT4_XOR_sequence[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
status->unpack_status = unpack77(a91, message->text);
|
|
||||||
|
|
||||||
if (status->unpack_status < 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
for (int i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
message->payload[i] = a91[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse binary message CRC as hash value for the message
|
// LOG(LOG_DEBUG, "Decoded message (CRC %04x), trying to unpack...\n", status->crc_extracted);
|
||||||
message->hash = status->crc_extracted;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,49 +397,53 @@ static float max4(float a, float b, float c, float d)
|
||||||
return max2(max2(a, b), max2(c, d));
|
return max2(max2(a, b), max2(c, d));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void heapify_down(candidate_t heap[], int heap_size)
|
static void heapify_down(ftx_candidate_t heap[], int heap_size)
|
||||||
{
|
{
|
||||||
// heapify from the root down
|
// heapify from the root down
|
||||||
int current = 0;
|
int current = 0; // root node
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
int largest = current;
|
|
||||||
int left = 2 * current + 1;
|
int left = 2 * current + 1;
|
||||||
int right = left + 1;
|
int right = left + 1;
|
||||||
|
|
||||||
if (left < heap_size && heap[left].score < heap[largest].score)
|
// Find the smallest value of (parent, left child, right child)
|
||||||
|
int smallest = current;
|
||||||
|
if ((left < heap_size) && (heap[left].score < heap[smallest].score))
|
||||||
{
|
{
|
||||||
largest = left;
|
smallest = left;
|
||||||
}
|
}
|
||||||
if (right < heap_size && heap[right].score < heap[largest].score)
|
if ((right < heap_size) && (heap[right].score < heap[smallest].score))
|
||||||
{
|
{
|
||||||
largest = right;
|
smallest = right;
|
||||||
}
|
}
|
||||||
if (largest == current)
|
|
||||||
|
if (smallest == current)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate_t tmp = heap[largest];
|
// Exchange the current node with the smallest child and move down to it
|
||||||
heap[largest] = heap[current];
|
ftx_candidate_t tmp = heap[smallest];
|
||||||
|
heap[smallest] = heap[current];
|
||||||
heap[current] = tmp;
|
heap[current] = tmp;
|
||||||
current = largest;
|
current = smallest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void heapify_up(candidate_t heap[], int heap_size)
|
static void heapify_up(ftx_candidate_t heap[], int heap_size)
|
||||||
{
|
{
|
||||||
// heapify from the last node up
|
// heapify from the last node up
|
||||||
int current = heap_size - 1;
|
int current = heap_size - 1;
|
||||||
while (current > 0)
|
while (current > 0)
|
||||||
{
|
{
|
||||||
int parent = (current - 1) / 2;
|
int parent = (current - 1) / 2;
|
||||||
if (heap[current].score >= heap[parent].score)
|
if (!(heap[current].score < heap[parent].score))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate_t tmp = heap[parent];
|
// Exchange the current node with its parent and move up
|
||||||
|
ftx_candidate_t tmp = heap[parent];
|
||||||
heap[parent] = heap[current];
|
heap[parent] = heap[current];
|
||||||
heap[current] = tmp;
|
heap[current] = tmp;
|
||||||
current = parent;
|
current = parent;
|
||||||
|
@ -435,14 +451,14 @@ static void heapify_up(candidate_t heap[], int heap_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute unnormalized log likelihood log(p(1) / p(0)) of 2 message bits (1 FSK symbol)
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of 2 message bits (1 FSK symbol)
|
||||||
static void ft4_extract_symbol(const uint8_t* wf, float* logl)
|
static void ft4_extract_symbol(const WF_ELEM_T* wf, float* logl)
|
||||||
{
|
{
|
||||||
// Cleaned up code for the simple case of n_syms==1
|
// Cleaned up code for the simple case of n_syms==1
|
||||||
float s2[4];
|
float s2[4];
|
||||||
|
|
||||||
for (int j = 0; j < 4; ++j)
|
for (int j = 0; j < 4; ++j)
|
||||||
{
|
{
|
||||||
s2[j] = (float)wf[kFT4_Gray_map[j]];
|
s2[j] = WF_ELEM_MAG(wf[kFT4_Gray_map[j]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
logl[0] = max2(s2[2], s2[3]) - max2(s2[0], s2[1]);
|
logl[0] = max2(s2[2], s2[3]) - max2(s2[0], s2[1]);
|
||||||
|
@ -450,23 +466,49 @@ static void ft4_extract_symbol(const uint8_t* wf, float* logl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute unnormalized log likelihood log(p(1) / p(0)) of 3 message bits (1 FSK symbol)
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of 3 message bits (1 FSK symbol)
|
||||||
static void ft8_extract_symbol(const uint8_t* wf, float* logl)
|
static void ft8_extract_symbol(const WF_ELEM_T* wf, float* logl)
|
||||||
{
|
{
|
||||||
// Cleaned up code for the simple case of n_syms==1
|
// Cleaned up code for the simple case of n_syms==1
|
||||||
|
#if 1
|
||||||
float s2[8];
|
float s2[8];
|
||||||
|
|
||||||
for (int j = 0; j < 8; ++j)
|
for (int j = 0; j < 8; ++j)
|
||||||
{
|
{
|
||||||
s2[j] = (float)wf[kFT8_Gray_map[j]];
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
logl[0] = max4(s2[4], s2[5], s2[6], s2[7]) - max4(s2[0], s2[1], s2[2], s2[3]);
|
logl[0] = max4(s2[4], s2[5], s2[6], s2[7]) - max4(s2[0], s2[1], s2[2], s2[3]);
|
||||||
logl[1] = max4(s2[2], s2[3], s2[6], s2[7]) - max4(s2[0], s2[1], s2[4], s2[5]);
|
logl[1] = max4(s2[2], s2[3], s2[6], s2[7]) - max4(s2[0], s2[1], s2[4], s2[5]);
|
||||||
logl[2] = max4(s2[1], s2[3], s2[5], s2[7]) - max4(s2[0], s2[2], s2[4], s2[6]);
|
logl[2] = max4(s2[1], s2[3], s2[5], s2[7]) - max4(s2[0], s2[2], s2[4], s2[6]);
|
||||||
|
#else
|
||||||
|
float a[7] = {
|
||||||
|
// (float)wf[7] - (float)wf[0], // 0: p(111) / p(000)
|
||||||
|
(float)wf[5] - (float)wf[2], // 0: p(100) / p(011)
|
||||||
|
(float)wf[3] - (float)wf[0], // 1: p(010) / p(000)
|
||||||
|
(float)wf[6] - (float)wf[3], // 2: p(101) / p(010)
|
||||||
|
(float)wf[6] - (float)wf[2], // 3: p(101) / p(011)
|
||||||
|
(float)wf[7] - (float)wf[4], // 4: p(111) / p(110)
|
||||||
|
(float)wf[4] - (float)wf[1], // 5: p(110) / p(001)
|
||||||
|
(float)wf[5] - (float)wf[1] // 6: p(100) / p(001)
|
||||||
|
};
|
||||||
|
float k = 1.0f;
|
||||||
|
|
||||||
|
// logl[0] = k * (a[0] + a[2] + a[3] + a[5] + a[6]) / 5;
|
||||||
|
// logl[1] = k * (a[0] / 4 + (a[1] - a[3]) * 5 / 24 + (a[5] - a[2]) / 6 + (a[4] - a[6]) / 24);
|
||||||
|
// logl[2] = k * (a[0] / 4 + (a[1] - a[3]) / 24 + (a[2] - a[5]) / 6 + (a[4] - a[6]) * 5 / 24);
|
||||||
|
logl[0] = k * (a[1] / 6 + a[2] / 3 + a[3] / 6 + a[4] / 6 + a[5] / 3 + a[6] / 6);
|
||||||
|
logl[1] = k * (-a[0] / 4 + a[1] * 7 / 24 + (a[4] - a[3]) / 8 + a[5] / 3 + a[6] / 24);
|
||||||
|
logl[2] = k * (-a[0] / 4 + (a[1] - a[6]) / 8 + a[2] / 3 + a[3] / 24 + a[4] * 7 / 24 - a[5] * 5 / 18);
|
||||||
|
#endif
|
||||||
|
// for (int i = 0; i < 8; ++i)
|
||||||
|
// printf("%d ", WF_ELEM_MAG_INT(wf[i]));
|
||||||
|
// for (int i = 0; i < 3; ++i)
|
||||||
|
// printf("%.1f ", logl[i]);
|
||||||
|
// printf("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute unnormalized log likelihood log(p(1) / p(0)) of bits corresponding to several FSK symbols at once
|
// Compute unnormalized log likelihood log(p(1) / p(0)) of bits corresponding to several FSK symbols at once
|
||||||
static void ft8_decode_multi_symbols(const uint8_t* wf, int num_bins, int n_syms, int bit_idx, float* log174)
|
static void ft8_decode_multi_symbols(const WF_ELEM_T* wf, int num_bins, int n_syms, int bit_idx, float* log174)
|
||||||
{
|
{
|
||||||
const int n_bits = 3 * n_syms;
|
const int n_bits = 3 * n_syms;
|
||||||
const int n_tones = (1 << n_bits);
|
const int n_tones = (1 << n_bits);
|
||||||
|
@ -478,20 +520,20 @@ static void ft8_decode_multi_symbols(const uint8_t* wf, int num_bins, int n_syms
|
||||||
int j1 = j & 0x07;
|
int j1 = j & 0x07;
|
||||||
if (n_syms == 1)
|
if (n_syms == 1)
|
||||||
{
|
{
|
||||||
s2[j] = (float)wf[kFT8_Gray_map[j1]];
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j1]]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int j2 = (j >> 3) & 0x07;
|
int j2 = (j >> 3) & 0x07;
|
||||||
if (n_syms == 2)
|
if (n_syms == 2)
|
||||||
{
|
{
|
||||||
s2[j] = (float)wf[kFT8_Gray_map[j2]];
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j2]]);
|
||||||
s2[j] += (float)wf[kFT8_Gray_map[j1] + 4 * num_bins];
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j1] + 4 * num_bins]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int j3 = (j >> 6) & 0x07;
|
int j3 = (j >> 6) & 0x07;
|
||||||
s2[j] = (float)wf[kFT8_Gray_map[j3]];
|
s2[j] = WF_ELEM_MAG(wf[kFT8_Gray_map[j3]]);
|
||||||
s2[j] += (float)wf[kFT8_Gray_map[j2] + 4 * num_bins];
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j2] + 4 * num_bins]);
|
||||||
s2[j] += (float)wf[kFT8_Gray_map[j1] + 8 * num_bins];
|
s2[j] += WF_ELEM_MAG(wf[kFT8_Gray_map[j1] + 8 * num_bins]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract bit significance (and convert them to float)
|
// Extract bit significance (and convert them to float)
|
||||||
|
|
103
ft8/decode.h
103
ft8/decode.h
|
@ -5,76 +5,89 @@
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#include "constants.h"
|
#include "constants.h"
|
||||||
|
#include "message.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Input structure to ft8_find_sync() function. This structure describes stored waterfall data over the whole message slot.
|
typedef struct
|
||||||
/// 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.
|
float mag;
|
||||||
/// Values time_osr > 1 mean each symbol is further subdivided in time.
|
float phase;
|
||||||
/// If freq_osr=1, each bin in the FFT magnitude data corresponds to 6.25 Hz, which is the tone spacing.
|
} waterfall_cpx_t;
|
||||||
/// Values freq_osr > 1 mean the tone spacing is further subdivided by FFT analysis.
|
|
||||||
typedef struct
|
// #define WATERFALL_USE_PHASE
|
||||||
{
|
|
||||||
|
#ifdef WATERFALL_USE_PHASE
|
||||||
|
#define WF_ELEM_T waterfall_cpx_t
|
||||||
|
#define WF_ELEM_MAG(x) ((x).mag)
|
||||||
|
#define WF_ELEM_MAG_INT(x) (int)(2 * ((x).mag + 120.0f))
|
||||||
|
#else
|
||||||
|
#define WF_ELEM_T uint8_t
|
||||||
|
#define WF_ELEM_MAG(x) ((float)(x)*0.5f - 120.0f)
|
||||||
|
#define WF_ELEM_MAG_INT(x) (int)(x)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Input structure to ftx_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.
|
||||||
|
/// Values time_osr > 1 mean each symbol is further subdivided in time.
|
||||||
|
/// If freq_osr=1, each bin in the FFT magnitude data corresponds to 6.25 Hz, which is the tone spacing.
|
||||||
|
/// Values freq_osr > 1 mean the tone spacing is further subdivided by FFT analysis.
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
int max_blocks; ///< number of blocks (symbols) allocated in the mag array
|
int max_blocks; ///< number of blocks (symbols) allocated in the mag array
|
||||||
int num_blocks; ///< number of blocks (symbols) stored in the mag array
|
int num_blocks; ///< number of blocks (symbols) stored in the mag array
|
||||||
int num_bins; ///< number of FFT bins in terms of 6.25 Hz
|
int num_bins; ///< number of FFT bins in terms of 6.25 Hz
|
||||||
int time_osr; ///< number of time subdivisions
|
int time_osr; ///< number of time subdivisions
|
||||||
int freq_osr; ///< number of frequency subdivisions
|
int freq_osr; ///< number of frequency subdivisions
|
||||||
uint8_t* mag; ///< FFT magnitudes stored as uint8_t[blocks][time_osr][freq_osr][num_bins]
|
WF_ELEM_T* mag; ///< FFT magnitudes stored as uint8_t[blocks][time_osr][freq_osr][num_bins]
|
||||||
int block_stride; ///< Helper value = time_osr * freq_osr * num_bins
|
int block_stride; ///< Helper value = time_osr * freq_osr * num_bins
|
||||||
ftx_protocol_t protocol; ///< Indicate if using FT4 or FT8
|
ftx_protocol_t protocol; ///< Indicate if using FT4 or FT8
|
||||||
} waterfall_t;
|
} ftx_waterfall_t;
|
||||||
|
|
||||||
/// Output structure of ft8_find_sync() and input structure of ft8_decode().
|
/// Output structure of ftx_find_sync() and input structure of ftx_decode().
|
||||||
/// Holds the position of potential start of a message in time and frequency.
|
/// Holds the position of potential start of a message in time and frequency.
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
int16_t score; ///< Candidate score (non-negative number; higher score means higher likelihood)
|
int16_t score; ///< Candidate score (non-negative number; higher score means higher likelihood)
|
||||||
int16_t time_offset; ///< Index of the time block
|
int16_t time_offset; ///< Index of the time block
|
||||||
int16_t freq_offset; ///< Index of the frequency bin
|
int16_t freq_offset; ///< Index of the frequency bin
|
||||||
uint8_t time_sub; ///< Index of the time subdivision used
|
uint8_t time_sub; ///< Index of the time subdivision used
|
||||||
uint8_t freq_sub; ///< Index of the frequency subdivision used
|
uint8_t freq_sub; ///< Index of the frequency subdivision used
|
||||||
} candidate_t;
|
} ftx_candidate_t;
|
||||||
|
|
||||||
/// Structure that holds the decoded message
|
/// Structure that contains the status of various steps during decoding of a message
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
// TODO: check again that this size is enough
|
float freq;
|
||||||
char text[25]; ///< Plain text
|
float time;
|
||||||
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
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
int ldpc_errors; ///< Number of LDPC errors during decoding
|
int ldpc_errors; ///< Number of LDPC errors during decoding
|
||||||
uint16_t crc_extracted; ///< CRC value recovered from the message
|
uint16_t crc_extracted; ///< CRC value recovered from the message
|
||||||
uint16_t crc_calculated; ///< CRC value calculated over the payload
|
uint16_t crc_calculated; ///< CRC value calculated over the payload
|
||||||
int unpack_status; ///< Return value of the unpack routine
|
// int unpack_status; ///< Return value of the unpack routine
|
||||||
} decode_status_t;
|
} ftx_decode_status_t;
|
||||||
|
|
||||||
/// Localize top N candidates in frequency and time according to their sync strength (looking at Costas symbols)
|
/// Localize top N candidates in frequency and time according to their sync strength (looking at Costas symbols)
|
||||||
/// We treat and organize the candidate list as a min-heap (empty initially).
|
/// We treat and organize the candidate list as a min-heap (empty initially).
|
||||||
/// @param[in] power Waterfall data collected during message slot
|
/// @param[in] power Waterfall data collected during message slot
|
||||||
/// @param[in] sync_pattern Synchronization pattern
|
/// @param[in] sync_pattern Synchronization pattern
|
||||||
/// @param[in] num_candidates Number of maximum candidates (size of heap array)
|
/// @param[in] num_candidates Number of maximum candidates (size of heap array)
|
||||||
/// @param[in,out] heap Array of candidate_t type entries (with num_candidates allocated entries)
|
/// @param[in,out] heap Array of ftx_candidate_t type entries (with num_candidates allocated entries)
|
||||||
/// @param[in] min_score Minimal score allowed for pruning unlikely candidates (can be zero for no effect)
|
/// @param[in] min_score Minimal score allowed for pruning unlikely candidates (can be zero for no effect)
|
||||||
/// @return Number of candidates filled in the heap
|
/// @return Number of candidates filled in the heap
|
||||||
int ft8_find_sync(const waterfall_t* power, int num_candidates, candidate_t heap[], int min_score);
|
int ftx_find_candidates(const ftx_waterfall_t* power, int num_candidates, ftx_candidate_t heap[], int min_score);
|
||||||
|
|
||||||
/// Attempt to decode a message candidate. Extracts the bit probabilities, runs LDPC decoder, checks CRC and unpacks the message in plain text.
|
/// Attempt to decode a message candidate. Extracts the bit probabilities, runs LDPC decoder, checks CRC and unpacks the message in plain text.
|
||||||
/// @param[in] power Waterfall data collected during message slot
|
/// @param[in] power Waterfall data collected during message slot
|
||||||
/// @param[in] cand Candidate to decode
|
/// @param[in] cand Candidate to decode
|
||||||
/// @param[out] message message_t structure that will receive the decoded message
|
/// @param[in] max_iterations Maximum allowed LDPC iterations (lower number means faster decode, but less precise)
|
||||||
/// @param[in] max_iterations Maximum allowed LDPC iterations (lower number means faster decode, but less precise)
|
/// @param[out] message ftx_message_t structure that will receive the decoded message
|
||||||
/// @param[out] status decode_status_t structure that will be filled with the status of various decoding steps
|
/// @param[out] status ftx_decode_status_t structure that will be filled with the status of various decoding steps
|
||||||
/// @return True if the decoding was successful, false otherwise (check status for details)
|
/// @return True if the decoding was successful, false otherwise (check status for details)
|
||||||
bool ft8_decode(const waterfall_t* power, const candidate_t* cand, message_t* message, int max_iterations, decode_status_t* status);
|
bool ftx_decode_candidate(const ftx_waterfall_t* power, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
44
ft8/encode.h
44
ft8/encode.h
|
@ -8,31 +8,31 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// typedef struct
|
// typedef struct
|
||||||
// {
|
// {
|
||||||
// uint8_t tones[FT8_NN];
|
// uint8_t tones[FT8_NN];
|
||||||
// // for waveform readout:
|
// // for waveform readout:
|
||||||
// int n_spsym; // Number of waveform samples per symbol
|
// int n_spsym; // Number of waveform samples per symbol
|
||||||
// float *pulse; // [3 * n_spsym]
|
// float *pulse; // [3 * n_spsym]
|
||||||
// int idx_symbol; // Index of the current symbol
|
// int idx_symbol; // Index of the current symbol
|
||||||
// float f0; // Base frequency, Hertz
|
// float f0; // Base frequency, Hertz
|
||||||
// float signal_rate; // Waveform sample rate, Hertz
|
// float signal_rate; // Waveform sample rate, Hertz
|
||||||
// } encoder_t;
|
// } encoder_t;
|
||||||
|
|
||||||
// void encoder_init(float signal_rate, float *pulse_buffer);
|
// void encoder_init(float signal_rate, float *pulse_buffer);
|
||||||
// void encoder_set_f0(float f0);
|
// void encoder_set_f0(float f0);
|
||||||
// void encoder_process(const message_t *message); // in: message
|
// void encoder_process(const message_t *message); // in: message
|
||||||
// void encoder_generate(float *block); // out: block of waveforms
|
// void encoder_generate(float *block); // out: block of waveforms
|
||||||
|
|
||||||
/// Generate FT8 tone sequence from payload data
|
/// Generate FT8 tone sequence from payload data
|
||||||
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
||||||
/// @param[out] tones - array of FT8_NN (79) bytes to store the generated tones (encoded as 0..7)
|
/// @param[out] tones - array of FT8_NN (79) bytes to store the generated tones (encoded as 0..7)
|
||||||
void ft8_encode(const uint8_t* payload, uint8_t* tones);
|
void ft8_encode(const uint8_t* payload, uint8_t* tones);
|
||||||
|
|
||||||
/// Generate FT4 tone sequence from payload data
|
/// Generate FT4 tone sequence from payload data
|
||||||
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
/// @param[in] payload - 10 byte array consisting of 77 bit payload
|
||||||
/// @param[out] tones - array of FT4_NN (105) bytes to store the generated tones (encoded as 0..3)
|
/// @param[out] tones - array of FT4_NN (105) bytes to store the generated tones (encoded as 0..3)
|
||||||
void ft4_encode(const uint8_t* payload, uint8_t* tones);
|
void ft4_encode(const uint8_t* payload, uint8_t* tones);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
12
ft8/ldpc.h
12
ft8/ldpc.h
|
@ -8,13 +8,13 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// codeword is 174 log-likelihoods.
|
// codeword is 174 log-likelihoods.
|
||||||
// plain is a return value, 174 ints, to be 0 or 1.
|
// plain is a return value, 174 ints, to be 0 or 1.
|
||||||
// iters is how hard to try.
|
// iters is how hard to try.
|
||||||
// ok == 87 means success.
|
// ok == 87 means success.
|
||||||
void ldpc_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
void ldpc_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
||||||
|
|
||||||
void bp_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
void bp_decode(float codeword[], int max_iters, uint8_t plain[], int* ok);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,994 @@
|
||||||
|
#include "message.h"
|
||||||
|
#include "text.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_WARN
|
||||||
|
#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_t 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ftx_message_get_i3(const ftx_message_t* msg)
|
||||||
|
{
|
||||||
|
// Extract i3 (bits 74..76)
|
||||||
|
uint8_t i3 = (msg->payload[9] >> 3) & 0x07u;
|
||||||
|
return i3;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t ftx_message_get_n3(const ftx_message_t* msg)
|
||||||
|
{
|
||||||
|
// Extract n3 (bits 71..73)
|
||||||
|
uint8_t n3 = ((msg->payload[8] << 2) & 0x04u) | ((msg->payload[9] >> 6) & 0x03u);
|
||||||
|
return n3;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[35]; // 13 + 13 + 6 (std/nonstd) / 14 (free text) / 19 (telemetry)
|
||||||
|
char* field1 = buf;
|
||||||
|
char* field2 = buf + 14;
|
||||||
|
char* field3 = buf + 14 + 14;
|
||||||
|
|
||||||
|
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);
|
||||||
|
field2 = NULL;
|
||||||
|
field3 = NULL;
|
||||||
|
rc = FTX_MESSAGE_RC_OK;
|
||||||
|
break;
|
||||||
|
case FTX_MESSAGE_TYPE_TELEMETRY:
|
||||||
|
ftx_message_decode_telemetry_hex(msg, field1);
|
||||||
|
field2 = NULL;
|
||||||
|
field3 = NULL;
|
||||||
|
rc = FTX_MESSAGE_RC_OK;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// not handled yet
|
||||||
|
field1 = NULL;
|
||||||
|
rc = FTX_MESSAGE_RC_ERROR_TYPE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field1 != NULL)
|
||||||
|
{
|
||||||
|
// TODO join fields via whitespace
|
||||||
|
message = append_string(message, field1);
|
||||||
|
if (field2 != NULL)
|
||||||
|
{
|
||||||
|
message = append_string(message, " ");
|
||||||
|
message = append_string(message, field2);
|
||||||
|
if (field3 != NULL)
|
||||||
|
{
|
||||||
|
message = append_string(message, " ");
|
||||||
|
message = append_string(message, field3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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[14];
|
||||||
|
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] & 0x0Fu);
|
||||||
|
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] & 0x01u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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)) & (0x3FFFFFul);
|
||||||
|
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_t 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;
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
#ifndef _INCLUDE_MESSAGE_H_
|
||||||
|
#define _INCLUDE_MESSAGE_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define FTX_PAYLOAD_LENGTH_BYTES 10 ///< number of bytes to hold 77 bits of FTx payload data
|
||||||
|
#define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator
|
||||||
|
|
||||||
|
/// Structure that holds the decoded message
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t payload[FTX_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_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
/// Called when a callsign is looked up by its 22/12/10 bit hash code
|
||||||
|
bool (*lookup_hash)(ftx_callsign_hash_type_t 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;
|
||||||
|
|
||||||
|
// Callsign types and sizes:
|
||||||
|
// * Std. call (basecall) - 1-2 letter/digit prefix (at least one letter), 1 digit area code, 1-3 letter suffix,
|
||||||
|
// total 3-6 chars (exception: 7 character calls with prefixes 3DA0- and 3XA..3XZ-)
|
||||||
|
// * Ext. std. call - basecall followed by /R or /P
|
||||||
|
// * Nonstd. call - all the rest, limited to 3-11 characters either alphanumeric or stroke (/)
|
||||||
|
// In case a call is looked up from its hash value, the call is enclosed in angular brackets (<CA0LL>).
|
||||||
|
|
||||||
|
void ftx_message_init(ftx_message_t* msg);
|
||||||
|
|
||||||
|
uint8_t ftx_message_get_i3(const ftx_message_t* msg);
|
||||||
|
uint8_t ftx_message_get_n3(const ftx_message_t* msg);
|
||||||
|
ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg);
|
||||||
|
|
||||||
|
// bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign);
|
||||||
|
|
||||||
|
/// 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_
|
366
ft8/pack.c
366
ft8/pack.c
|
@ -1,366 +0,0 @@
|
||||||
#include "pack.h"
|
|
||||||
#include "text.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
#define NTOKENS ((uint32_t)2063592L)
|
|
||||||
#define MAX22 ((uint32_t)4194304L)
|
|
||||||
#define MAXGRID4 ((uint16_t)32400)
|
|
||||||
|
|
||||||
// TODO: This is wasteful, should figure out something more elegant
|
|
||||||
const char A0[] = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?";
|
|
||||||
const char A1[] = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
||||||
const char A2[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
||||||
const char A3[] = "0123456789";
|
|
||||||
const char A4[] = " ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
||||||
|
|
||||||
// Pack a special token, a 22-bit hash code, or a valid base call
|
|
||||||
// into a 28-bit integer.
|
|
||||||
int32_t pack28(const char* callsign)
|
|
||||||
{
|
|
||||||
// Check for special tokens first
|
|
||||||
if (starts_with(callsign, "DE "))
|
|
||||||
return 0;
|
|
||||||
if (starts_with(callsign, "QRZ "))
|
|
||||||
return 1;
|
|
||||||
if (starts_with(callsign, "CQ "))
|
|
||||||
return 2;
|
|
||||||
|
|
||||||
if (starts_with(callsign, "CQ_"))
|
|
||||||
{
|
|
||||||
int nnum = 0, nlet = 0;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check for <...> callsign
|
|
||||||
|
|
||||||
char c6[6] = { ' ', ' ', ' ', ' ', ' ', ' ' };
|
|
||||||
|
|
||||||
int length = 0; // strlen(callsign); // We will need it later
|
|
||||||
while (callsign[length] != ' ' && callsign[length] != 0)
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy callsign to 6 character buffer
|
|
||||||
if (starts_with(callsign, "3DA0") && 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
|
|
||||||
{
|
|
||||||
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, i1, i2, i3, i4, i5;
|
|
||||||
if ((i0 = char_index(A1, c6[0])) >= 0 && (i1 = char_index(A2, c6[1])) >= 0 && (i2 = char_index(A3, c6[2])) >= 0 && (i3 = char_index(A4, c6[3])) >= 0 && (i4 = char_index(A4, c6[4])) >= 0 && (i5 = char_index(A4, c6[5])) >= 0)
|
|
||||||
{
|
|
||||||
// This is a standard callsign
|
|
||||||
int32_t n28 = i0;
|
|
||||||
n28 = n28 * 36 + i1;
|
|
||||||
n28 = n28 * 10 + i2;
|
|
||||||
n28 = n28 * 27 + i3;
|
|
||||||
n28 = n28 * 27 + i4;
|
|
||||||
n28 = n28 * 27 + i5;
|
|
||||||
return NTOKENS + MAX22 + n28;
|
|
||||||
}
|
|
||||||
|
|
||||||
//char text[13];
|
|
||||||
//if (length > 13) return -1;
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// Treat this as a nonstandard callsign: compute its 22-bit hash
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a string could be a valid standard callsign or a valid
|
|
||||||
// compound callsign.
|
|
||||||
// Return base call "bc" and a logical "cok" indicator.
|
|
||||||
bool chkcall(const char* call, char* bc)
|
|
||||||
{
|
|
||||||
int length = strlen(call); // n1=len_trim(w)
|
|
||||||
if (length > 11)
|
|
||||||
return false;
|
|
||||||
if (0 != strchr(call, '.'))
|
|
||||||
return false;
|
|
||||||
if (0 != strchr(call, '+'))
|
|
||||||
return false;
|
|
||||||
if (0 != strchr(call, '-'))
|
|
||||||
return false;
|
|
||||||
if (0 != strchr(call, '?'))
|
|
||||||
return false;
|
|
||||||
if (length > 6 && 0 != strchr(call, '/'))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO: implement suffix parsing (or rework?)
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t packgrid(const char* grid4)
|
|
||||||
{
|
|
||||||
if (grid4 == 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;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pack Type 1 (Standard 77-bit message) and Type 2 (ditto, with a "/P" call)
|
|
||||||
int pack77_1(const char* msg, uint8_t* b77)
|
|
||||||
{
|
|
||||||
// Locate the first delimiter
|
|
||||||
const char* s1 = strchr(msg, ' ');
|
|
||||||
if (s1 == 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
const char* call1 = msg; // 1st call
|
|
||||||
const char* call2 = s1 + 1; // 2nd call
|
|
||||||
|
|
||||||
int32_t n28a = pack28(call1);
|
|
||||||
int32_t n28b = pack28(call2);
|
|
||||||
|
|
||||||
if (n28a < 0 || n28b < 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
uint16_t igrid4;
|
|
||||||
|
|
||||||
// Locate the second delimiter
|
|
||||||
const char* s2 = strchr(s1 + 1, ' ');
|
|
||||||
if (s2 != 0)
|
|
||||||
{
|
|
||||||
igrid4 = packgrid(s2 + 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Two callsigns, no grid/report
|
|
||||||
igrid4 = packgrid(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t i3 = 1; // No suffix or /R
|
|
||||||
|
|
||||||
// TODO: check for suffixes
|
|
||||||
|
|
||||||
// Shift in ipa and ipb bits into n28a and n28b
|
|
||||||
n28a <<= 1; // ipa = 0
|
|
||||||
n28b <<= 1; // ipb = 0
|
|
||||||
|
|
||||||
// Pack into (28 + 1) + (28 + 1) + (1 + 15) + 3 bits
|
|
||||||
b77[0] = (n28a >> 21);
|
|
||||||
b77[1] = (n28a >> 13);
|
|
||||||
b77[2] = (n28a >> 5);
|
|
||||||
b77[3] = (uint8_t)(n28a << 3) | (uint8_t)(n28b >> 26);
|
|
||||||
b77[4] = (n28b >> 18);
|
|
||||||
b77[5] = (n28b >> 10);
|
|
||||||
b77[6] = (n28b >> 2);
|
|
||||||
b77[7] = (uint8_t)(n28b << 6) | (uint8_t)(igrid4 >> 10);
|
|
||||||
b77[8] = (igrid4 >> 2);
|
|
||||||
b77[9] = (uint8_t)(igrid4 << 6) | (uint8_t)(i3 << 3);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void packtext77(const char* text, uint8_t* b77)
|
|
||||||
{
|
|
||||||
int length = strlen(text);
|
|
||||||
|
|
||||||
// Skip leading and trailing spaces
|
|
||||||
while (*text == ' ' && *text != 0)
|
|
||||||
{
|
|
||||||
++text;
|
|
||||||
--length;
|
|
||||||
}
|
|
||||||
while (length > 0 && text[length - 1] == ' ')
|
|
||||||
{
|
|
||||||
--length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the first 72 bits representing a long number
|
|
||||||
for (int i = 0; i < 9; ++i)
|
|
||||||
{
|
|
||||||
b77[i] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now express the text as base-42 number stored
|
|
||||||
// in the first 72 bits of b77
|
|
||||||
for (int j = 0; j < 13; ++j)
|
|
||||||
{
|
|
||||||
// Multiply the long integer in b77 by 42
|
|
||||||
uint16_t x = 0;
|
|
||||||
for (int i = 8; i >= 0; --i)
|
|
||||||
{
|
|
||||||
x += b77[i] * (uint16_t)42;
|
|
||||||
b77[i] = (x & 0xFF);
|
|
||||||
x >>= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the index of the current char
|
|
||||||
if (j < length)
|
|
||||||
{
|
|
||||||
int q = char_index(A0, text[j]);
|
|
||||||
x = (q > 0) ? q : 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = 0;
|
|
||||||
}
|
|
||||||
// Here we double each added number in order to have the result multiplied
|
|
||||||
// by two as well, so that it's a 71 bit number left-aligned in 72 bits (9 bytes)
|
|
||||||
x <<= 1;
|
|
||||||
|
|
||||||
// Now add the number to our long number
|
|
||||||
for (int i = 8; i >= 0; --i)
|
|
||||||
{
|
|
||||||
if (x == 0)
|
|
||||||
break;
|
|
||||||
x += b77[i];
|
|
||||||
b77[i] = (x & 0xFF);
|
|
||||||
x >>= 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set n3=0 (bits 71..73) and i3=0 (bits 74..76)
|
|
||||||
b77[8] &= 0xFE;
|
|
||||||
b77[9] &= 0x00;
|
|
||||||
}
|
|
||||||
|
|
||||||
int pack77(const char* msg, uint8_t* c77)
|
|
||||||
{
|
|
||||||
// Check Type 1 (Standard 77-bit message) or Type 2, with optional "/P"
|
|
||||||
if (0 == pack77_1(msg, c77))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// Check 0.5 (telemetry)
|
|
||||||
|
|
||||||
// Check Type 4 (One nonstandard call and one hashed call)
|
|
||||||
|
|
||||||
// Default to free text
|
|
||||||
// i3=0 n3=0
|
|
||||||
packtext77(msg, c77);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef UNIT_TEST
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
bool test1()
|
|
||||||
{
|
|
||||||
const char* inputs[] = {
|
|
||||||
"",
|
|
||||||
" ",
|
|
||||||
"ABC",
|
|
||||||
"A9",
|
|
||||||
"L9A",
|
|
||||||
"L7BC",
|
|
||||||
"L0ABC",
|
|
||||||
"LL3JG",
|
|
||||||
"LL3AJG",
|
|
||||||
"CQ ",
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int i = 0; inputs[i]; ++i)
|
|
||||||
{
|
|
||||||
int32_t result = ft8_v2::pack28(inputs[i]);
|
|
||||||
printf("pack28(\"%s\") = %d\n", inputs[i], result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool test2()
|
|
||||||
{
|
|
||||||
const char* inputs[] = {
|
|
||||||
"CQ LL3JG",
|
|
||||||
"CQ LL3JG KO26",
|
|
||||||
"L0UAA LL3JG KO26",
|
|
||||||
"L0UAA LL3JG +02",
|
|
||||||
"L0UAA LL3JG RRR",
|
|
||||||
"L0UAA LL3JG 73",
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int i = 0; inputs[i]; ++i)
|
|
||||||
{
|
|
||||||
uint8_t result[10];
|
|
||||||
int rc = ft8_v2::pack77_1(inputs[i], result);
|
|
||||||
printf("pack77_1(\"%s\") = %d\t[", inputs[i], rc);
|
|
||||||
for (int j = 0; j < 10; ++j)
|
|
||||||
{
|
|
||||||
printf("%02x ", result[j]);
|
|
||||||
}
|
|
||||||
printf("]\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
test1();
|
|
||||||
test2();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
20
ft8/pack.h
20
ft8/pack.h
|
@ -1,20 +0,0 @@
|
||||||
#ifndef _INCLUDE_PACK_H_
|
|
||||||
#define _INCLUDE_PACK_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C"
|
|
||||||
{
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Pack FT8 text message into 72 bits
|
|
||||||
// [IN] msg - FT8 message (e.g. "CQ TE5T KN01")
|
|
||||||
// [OUT] c77 - 10 byte array to store the 77 bit payload (MSB first)
|
|
||||||
int pack77(const char* msg, uint8_t* c77);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // _INCLUDE_PACK_H_
|
|
107
ft8/text.c
107
ft8/text.c
|
@ -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)
|
char* trim(char* str)
|
||||||
{
|
{
|
||||||
str = (char*)trim_front(str);
|
str = (char*)trim_front(str);
|
||||||
|
@ -32,6 +30,18 @@ char* trim(char* str)
|
||||||
return 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)
|
char to_upper(char c)
|
||||||
{
|
{
|
||||||
return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
|
return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c;
|
||||||
|
@ -62,23 +72,21 @@ bool starts_with(const char* string, const char* prefix)
|
||||||
return 0 == memcmp(string, prefix, strlen(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)
|
bool equals(const char* string1, const char* string2)
|
||||||
{
|
{
|
||||||
return 0 == strcmp(string1, string2);
|
return 0 == strcmp(string1, string2);
|
||||||
}
|
}
|
||||||
|
|
||||||
int char_index(const char* string, char c)
|
|
||||||
{
|
|
||||||
for (int i = 0; *string; ++i, ++string)
|
|
||||||
{
|
|
||||||
if (c == *string)
|
|
||||||
{
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1; // Not found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text message formatting:
|
// Text message formatting:
|
||||||
// - replaces lowercase letters with uppercase
|
// - replaces lowercase letters with uppercase
|
||||||
// - merges consecutive spaces into single space
|
// - merges consecutive spaces into single space
|
||||||
|
@ -99,6 +107,46 @@ void fmtmsg(char* msg_out, const char* msg_in)
|
||||||
*msg_out = 0; // Add zero termination
|
*msg_out = 0; // Add zero termination
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char* append_string(char* string, const char* token)
|
||||||
|
{
|
||||||
|
while (*token != '\0')
|
||||||
|
{
|
||||||
|
*string = *token;
|
||||||
|
string++;
|
||||||
|
token++;
|
||||||
|
}
|
||||||
|
*string = '\0';
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Parse a 2 digit integer from string
|
||||||
int dd_to_int(const char* str, int length)
|
int dd_to_int(const char* str, int length)
|
||||||
{
|
{
|
||||||
|
@ -164,40 +212,33 @@ void int_to_dd(char* str, int value, int width, bool full_sign)
|
||||||
*str = 0; // Add zero terminator
|
*str = 0; // Add zero terminator
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert integer index to ASCII character according to one of 6 tables:
|
char charn(int c, ft8_char_table_e table)
|
||||||
// table 0: " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"
|
|
||||||
// table 1: " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
// table 2: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
// table 3: "0123456789"
|
|
||||||
// table 4: " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
// table 5: " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
|
|
||||||
char charn(int c, int table_idx)
|
|
||||||
{
|
{
|
||||||
if (table_idx != 2 && table_idx != 3)
|
if ((table != FT8_CHAR_TABLE_ALPHANUM) && (table != FT8_CHAR_TABLE_NUMERIC))
|
||||||
{
|
{
|
||||||
if (c == 0)
|
if (c == 0)
|
||||||
return ' ';
|
return ' ';
|
||||||
c -= 1;
|
c -= 1;
|
||||||
}
|
}
|
||||||
if (table_idx != 4)
|
if (table != FT8_CHAR_TABLE_LETTERS_SPACE)
|
||||||
{
|
{
|
||||||
if (c < 10)
|
if (c < 10)
|
||||||
return '0' + c;
|
return '0' + c;
|
||||||
c -= 10;
|
c -= 10;
|
||||||
}
|
}
|
||||||
if (table_idx != 3)
|
if (table != FT8_CHAR_TABLE_NUMERIC)
|
||||||
{
|
{
|
||||||
if (c < 26)
|
if (c < 26)
|
||||||
return 'A' + c;
|
return 'A' + c;
|
||||||
c -= 26;
|
c -= 26;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (table_idx == 0)
|
if (table == FT8_CHAR_TABLE_FULL)
|
||||||
{
|
{
|
||||||
if (c < 5)
|
if (c < 5)
|
||||||
return "+-./?"[c];
|
return "+-./?"[c];
|
||||||
}
|
}
|
||||||
else if (table_idx == 5)
|
else if (table == FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH)
|
||||||
{
|
{
|
||||||
if (c == 0)
|
if (c == 0)
|
||||||
return '/';
|
return '/';
|
||||||
|
@ -207,29 +248,29 @@ char charn(int c, int table_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert character to its index (charn in reverse) according to a table
|
// Convert character to its index (charn in reverse) according to a table
|
||||||
int nchar(char c, int table_idx)
|
int nchar(char c, ft8_char_table_e table)
|
||||||
{
|
{
|
||||||
int n = 0;
|
int n = 0;
|
||||||
if (table_idx != 2 && table_idx != 3)
|
if ((table != FT8_CHAR_TABLE_ALPHANUM) && (table != FT8_CHAR_TABLE_NUMERIC))
|
||||||
{
|
{
|
||||||
if (c == ' ')
|
if (c == ' ')
|
||||||
return n + 0;
|
return n + 0;
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
if (table_idx != 4)
|
if (table != FT8_CHAR_TABLE_LETTERS_SPACE)
|
||||||
{
|
{
|
||||||
if (c >= '0' && c <= '9')
|
if (c >= '0' && c <= '9')
|
||||||
return n + (c - '0');
|
return n + (c - '0');
|
||||||
n += 10;
|
n += 10;
|
||||||
}
|
}
|
||||||
if (table_idx != 3)
|
if (table != FT8_CHAR_TABLE_NUMERIC)
|
||||||
{
|
{
|
||||||
if (c >= 'A' && c <= 'Z')
|
if (c >= 'A' && c <= 'Z')
|
||||||
return n + (c - 'A');
|
return n + (c - 'A');
|
||||||
n += 26;
|
n += 26;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (table_idx == 0)
|
if (table == FT8_CHAR_TABLE_FULL)
|
||||||
{
|
{
|
||||||
if (c == '+')
|
if (c == '+')
|
||||||
return n + 0;
|
return n + 0;
|
||||||
|
@ -242,7 +283,7 @@ int nchar(char c, int table_idx)
|
||||||
if (c == '?')
|
if (c == '?')
|
||||||
return n + 4;
|
return n + 4;
|
||||||
}
|
}
|
||||||
else if (table_idx == 5)
|
else if (table == FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH)
|
||||||
{
|
{
|
||||||
if (c == '/')
|
if (c == '/')
|
||||||
return n + 0;
|
return n + 0;
|
||||||
|
|
74
ft8/text.h
74
ft8/text.h
|
@ -9,35 +9,65 @@ extern "C"
|
||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Utility functions for characters and strings
|
// Utility functions for characters and strings
|
||||||
|
|
||||||
const char* trim_front(const char* str);
|
const char* trim_front(const char* str);
|
||||||
void trim_back(char* str);
|
void trim_back(char* str);
|
||||||
char* trim(char* str);
|
|
||||||
|
|
||||||
char to_upper(char c);
|
/// In-place whitespace trim from front and back:
|
||||||
bool is_digit(char c);
|
/// 1) trims the back by changing whitespaces to '\0'
|
||||||
bool is_letter(char c);
|
/// 2) trims the front by skipping whitespaces
|
||||||
bool is_space(char c);
|
/// @return trimmed string (pointer to first non-whitespace character)
|
||||||
bool in_range(char c, char min, char max);
|
char* trim(char* str);
|
||||||
bool starts_with(const char* string, const char* prefix);
|
|
||||||
bool equals(const char* string1, const char* string2);
|
|
||||||
|
|
||||||
int char_index(const char* string, char c);
|
/// Trim whitespace from start and end of string
|
||||||
|
void trim_copy(char* trimmed, const char* str);
|
||||||
|
|
||||||
// Text message formatting:
|
char to_upper(char c);
|
||||||
// - replaces lowercase letters with uppercase
|
bool is_digit(char c);
|
||||||
// - merges consecutive spaces into single space
|
bool is_letter(char c);
|
||||||
void fmtmsg(char* msg_out, const char* msg_in);
|
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);
|
||||||
|
|
||||||
// Parse a 2 digit integer from string
|
// Text message formatting:
|
||||||
int dd_to_int(const char* str, int length);
|
// - replaces lowercase letters with uppercase
|
||||||
|
// - merges consecutive spaces into single space
|
||||||
|
void fmtmsg(char* msg_out, const char* msg_in);
|
||||||
|
|
||||||
// Convert a 2 digit integer to string
|
/// Extract and copy a space-delimited token from a string.
|
||||||
void int_to_dd(char* str, int value, int width, bool full_sign);
|
/// 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);
|
||||||
|
|
||||||
char charn(int c, int table_idx);
|
char* append_string(char* string, const char* token);
|
||||||
int nchar(char c, int table_idx);
|
|
||||||
|
// Parse a 2 digit integer from string
|
||||||
|
int dd_to_int(const char* str, int length);
|
||||||
|
|
||||||
|
// Convert a 2 digit integer to string
|
||||||
|
void int_to_dd(char* str, int value, int width, bool full_sign);
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FT8_CHAR_TABLE_FULL, // table[42] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM_SPACE_SLASH, // table[38] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM_SPACE, // table[37] " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_LETTERS_SPACE, // table[27] " ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_ALPHANUM, // table[36] "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
FT8_CHAR_TABLE_NUMERIC, // table[10] "0123456789"
|
||||||
|
} ft8_char_table_e;
|
||||||
|
|
||||||
|
/// Convert integer index to ASCII character according to one of character tables
|
||||||
|
char charn(int c, ft8_char_table_e table);
|
||||||
|
|
||||||
|
/// Look up the index of an ASCII character in one of character tables
|
||||||
|
int nchar(char c, ft8_char_table_e table);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
427
ft8/unpack.c
427
ft8/unpack.c
|
@ -1,427 +0,0 @@
|
||||||
#ifdef __linux__
|
|
||||||
#ifndef _GNU_SOURCE
|
|
||||||
#define _GNU_SOURCE
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "unpack.h"
|
|
||||||
#include "text.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define MAX22 ((uint32_t)4194304L)
|
|
||||||
#define NTOKENS ((uint32_t)2063592L)
|
|
||||||
#define MAXGRID4 ((uint16_t)32400L)
|
|
||||||
|
|
||||||
// n28 is a 28-bit integer, e.g. n28a or n28b, containing all the
|
|
||||||
// call sign bits from a packed message.
|
|
||||||
int unpack_callsign(uint32_t n28, uint8_t ip, uint8_t i3, char* result)
|
|
||||||
{
|
|
||||||
// Check for special tokens DE, QRZ, CQ, CQ_nnn, CQ_aaaa
|
|
||||||
if (n28 < NTOKENS)
|
|
||||||
{
|
|
||||||
if (n28 <= 2)
|
|
||||||
{
|
|
||||||
if (n28 == 0)
|
|
||||||
strcpy(result, "DE");
|
|
||||||
if (n28 == 1)
|
|
||||||
strcpy(result, "QRZ");
|
|
||||||
if (n28 == 2)
|
|
||||||
strcpy(result, "CQ");
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
if (n28 <= 1002)
|
|
||||||
{
|
|
||||||
// CQ_nnn with 3 digits
|
|
||||||
strcpy(result, "CQ ");
|
|
||||||
int_to_dd(result + 3, n28 - 3, 3, false);
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
if (n28 <= 532443L)
|
|
||||||
{
|
|
||||||
// CQ_aaaa with 4 alphanumeric symbols
|
|
||||||
uint32_t n = n28 - 1003;
|
|
||||||
char aaaa[5];
|
|
||||||
|
|
||||||
aaaa[4] = '\0';
|
|
||||||
for (int i = 3; /* */; --i)
|
|
||||||
{
|
|
||||||
aaaa[i] = charn(n % 27, 4);
|
|
||||||
if (i == 0)
|
|
||||||
break;
|
|
||||||
n /= 27;
|
|
||||||
}
|
|
||||||
|
|
||||||
strcpy(result, "CQ ");
|
|
||||||
strcat(result, trim_front(aaaa));
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
// ? TODO: unspecified in the WSJT-X code
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
n28 = n28 - NTOKENS;
|
|
||||||
if (n28 < MAX22)
|
|
||||||
{
|
|
||||||
// This is a 22-bit hash of a result
|
|
||||||
// TODO: implement
|
|
||||||
strcpy(result, "<...>");
|
|
||||||
// result[0] = '<';
|
|
||||||
// int_to_dd(result + 1, n28, 7, false);
|
|
||||||
// result[8] = '>';
|
|
||||||
// result[9] = '\0';
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard callsign
|
|
||||||
uint32_t n = n28 - MAX22;
|
|
||||||
|
|
||||||
char callsign[7];
|
|
||||||
callsign[6] = '\0';
|
|
||||||
callsign[5] = charn(n % 27, 4);
|
|
||||||
n /= 27;
|
|
||||||
callsign[4] = charn(n % 27, 4);
|
|
||||||
n /= 27;
|
|
||||||
callsign[3] = charn(n % 27, 4);
|
|
||||||
n /= 27;
|
|
||||||
callsign[2] = charn(n % 10, 3);
|
|
||||||
n /= 10;
|
|
||||||
callsign[1] = charn(n % 36, 2);
|
|
||||||
n /= 36;
|
|
||||||
callsign[0] = charn(n % 37, 1);
|
|
||||||
|
|
||||||
// Skip trailing and leading whitespace in case of a short callsign
|
|
||||||
strcpy(result, trim(callsign));
|
|
||||||
if (strlen(result) == 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
// Check if we should append /R or /P suffix
|
|
||||||
if (ip)
|
|
||||||
{
|
|
||||||
if (i3 == 1)
|
|
||||||
{
|
|
||||||
strcat(result, "/R");
|
|
||||||
}
|
|
||||||
else if (i3 == 2)
|
|
||||||
{
|
|
||||||
strcat(result, "/P");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
|
|
||||||
int unpack_type1(const uint8_t* a77, uint8_t i3, char* call_to, char* call_de, char* extra)
|
|
||||||
{
|
|
||||||
uint32_t n28a, n28b;
|
|
||||||
uint16_t igrid4;
|
|
||||||
uint8_t ir;
|
|
||||||
|
|
||||||
// Extract packed fields
|
|
||||||
n28a = (a77[0] << 21);
|
|
||||||
n28a |= (a77[1] << 13);
|
|
||||||
n28a |= (a77[2] << 5);
|
|
||||||
n28a |= (a77[3] >> 3);
|
|
||||||
n28b = ((a77[3] & 0x07) << 26);
|
|
||||||
n28b |= (a77[4] << 18);
|
|
||||||
n28b |= (a77[5] << 10);
|
|
||||||
n28b |= (a77[6] << 2);
|
|
||||||
n28b |= (a77[7] >> 6);
|
|
||||||
ir = ((a77[7] & 0x20) >> 5);
|
|
||||||
igrid4 = ((a77[7] & 0x1F) << 10);
|
|
||||||
igrid4 |= (a77[8] << 2);
|
|
||||||
igrid4 |= (a77[9] >> 6);
|
|
||||||
|
|
||||||
// Unpack both callsigns
|
|
||||||
if (unpack_callsign(n28a >> 1, n28a & 0x01, i3, call_to) < 0)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (unpack_callsign(n28b >> 1, n28b & 0x01, i3, call_de) < 0)
|
|
||||||
{
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
// Fix "CQ_" to "CQ " -> already done in unpack_callsign()
|
|
||||||
|
|
||||||
// TODO: add to recent calls
|
|
||||||
// if (call_to[0] != '<' && strlen(call_to) >= 4) {
|
|
||||||
// save_hash_call(call_to)
|
|
||||||
// }
|
|
||||||
// if (call_de[0] != '<' && strlen(call_de) >= 4) {
|
|
||||||
// save_hash_call(call_de)
|
|
||||||
// }
|
|
||||||
|
|
||||||
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);
|
|
||||||
n /= 10;
|
|
||||||
dst[2] = '0' + (n % 10);
|
|
||||||
n /= 10;
|
|
||||||
dst[1] = 'A' + (n % 18);
|
|
||||||
n /= 18;
|
|
||||||
dst[0] = 'A' + (n % 18);
|
|
||||||
// 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)
|
|
||||||
switch (irpt)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
extra[0] = '\0';
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
strcpy(dst, "RRR");
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
strcpy(dst, "RR73");
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
strcpy(dst, "73");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// 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);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// if (irpt >= 2 && strncmp(call_to, "CQ", 2) == 0) return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
uint8_t carry = 0;
|
|
||||||
for (int i = 0; i < 9; ++i)
|
|
||||||
{
|
|
||||||
b71[i] = carry | (a71[i] >> 1);
|
|
||||||
carry = (a71[i] & 1) ? 0x80 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
strcpy(text, trim(c14));
|
|
||||||
return 0; // Success
|
|
||||||
}
|
|
||||||
|
|
||||||
int unpack_telemetry(const uint8_t* a71, char* telemetry)
|
|
||||||
{
|
|
||||||
uint8_t b71[9];
|
|
||||||
|
|
||||||
// Shift bits in a71 right by 1 bit
|
|
||||||
uint8_t carry = 0;
|
|
||||||
for (int i = 0; i < 9; ++i)
|
|
||||||
{
|
|
||||||
b71[i] = (carry << 7) | (a71[i] >> 1);
|
|
||||||
carry = (a71[i] & 0x01);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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[i * 2] = c1;
|
|
||||||
telemetry[i * 2 + 1] = c2;
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetry[18] = '\0';
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//none standard for wsjt-x 2.0
|
|
||||||
//by KD8CEC
|
|
||||||
int unpack_nonstandard(const uint8_t* a77, char* call_to, char* call_de, char* extra)
|
|
||||||
{
|
|
||||||
uint32_t n12, iflip, nrpt, icq;
|
|
||||||
uint64_t n58;
|
|
||||||
n12 = (a77[0] << 4); //11 ~4 : 8
|
|
||||||
n12 |= (a77[1] >> 4); //3~0 : 12
|
|
||||||
|
|
||||||
n58 = ((uint64_t)(a77[1] & 0x0F) << 54); //57 ~ 54 : 4
|
|
||||||
n58 |= ((uint64_t)a77[2] << 46); //53 ~ 46 : 12
|
|
||||||
n58 |= ((uint64_t)a77[3] << 38); //45 ~ 38 : 12
|
|
||||||
n58 |= ((uint64_t)a77[4] << 30); //37 ~ 30 : 12
|
|
||||||
n58 |= ((uint64_t)a77[5] << 22); //29 ~ 22 : 12
|
|
||||||
n58 |= ((uint64_t)a77[6] << 14); //21 ~ 14 : 12
|
|
||||||
n58 |= ((uint64_t)a77[7] << 6); //13 ~ 6 : 12
|
|
||||||
n58 |= ((uint64_t)a77[8] >> 2); //5 ~ 0 : 765432 10
|
|
||||||
|
|
||||||
iflip = (a77[8] >> 1) & 0x01; //76543210
|
|
||||||
nrpt = ((a77[8] & 0x01) << 1);
|
|
||||||
nrpt |= (a77[9] >> 7); //76543210
|
|
||||||
icq = ((a77[9] >> 6) & 0x01);
|
|
||||||
|
|
||||||
char c11[12];
|
|
||||||
c11[11] = '\0';
|
|
||||||
|
|
||||||
for (int i = 10; /* no condition */; --i)
|
|
||||||
{
|
|
||||||
c11[i] = charn(n58 % 38, 5);
|
|
||||||
if (i == 0)
|
|
||||||
break;
|
|
||||||
n58 /= 38;
|
|
||||||
}
|
|
||||||
|
|
||||||
char call_3[15];
|
|
||||||
// should replace with hash12(n12, call_3);
|
|
||||||
strcpy(call_3, "<...>");
|
|
||||||
// call_3[0] = '<';
|
|
||||||
// int_to_dd(call_3 + 1, n12, 4, false);
|
|
||||||
// call_3[5] = '>';
|
|
||||||
// call_3[6] = '\0';
|
|
||||||
|
|
||||||
char* call_1 = (iflip) ? c11 : call_3;
|
|
||||||
char* call_2 = (iflip) ? call_3 : c11;
|
|
||||||
//save_hash_call(c11_trimmed);
|
|
||||||
|
|
||||||
if (icq == 0)
|
|
||||||
{
|
|
||||||
strcpy(call_to, trim(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, trim(call_2));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int unpack77_fields(const uint8_t* a77, char* call_to, char* call_de, char* extra)
|
|
||||||
{
|
|
||||||
call_to[0] = call_de[0] = extra[0] = '\0';
|
|
||||||
|
|
||||||
// Extract i3 (bits 74..76)
|
|
||||||
uint8_t i3 = (a77[9] >> 3) & 0x07;
|
|
||||||
|
|
||||||
if (i3 == 0)
|
|
||||||
{
|
|
||||||
// Extract n3 (bits 71..73)
|
|
||||||
uint8_t n3 = ((a77[8] << 2) & 0x04) | ((a77[9] >> 6) & 0x03);
|
|
||||||
|
|
||||||
if (n3 == 0)
|
|
||||||
{
|
|
||||||
// 0.0 Free text
|
|
||||||
return unpack_text(a77, extra);
|
|
||||||
}
|
|
||||||
// else if (i3 == 0 && n3 == 1) {
|
|
||||||
// // 0.1 K1ABC RR73; W9XYZ <KH1/KH7Z> -11 28 28 10 5 71 DXpedition Mode
|
|
||||||
// }
|
|
||||||
// else if (i3 == 0 && n3 == 2) {
|
|
||||||
// // 0.2 PA3XYZ/P R 590003 IO91NP 28 1 1 3 12 25 70 EU VHF contest
|
|
||||||
// }
|
|
||||||
// else if (i3 == 0 && (n3 == 3 || n3 == 4)) {
|
|
||||||
// // 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
|
|
||||||
// }
|
|
||||||
else if (n3 == 5)
|
|
||||||
{
|
|
||||||
// 0.5 0123456789abcdef01 71 71 Telemetry (18 hex)
|
|
||||||
return unpack_telemetry(a77, extra);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (i3 == 1 || i3 == 2)
|
|
||||||
{
|
|
||||||
// Type 1 (standard message) or Type 2 ("/P" form for EU VHF contest)
|
|
||||||
return unpack_type1(a77, i3, call_to, call_de, extra);
|
|
||||||
}
|
|
||||||
// else if (i3 == 3) {
|
|
||||||
// // Type 3: ARRL RTTY Contest
|
|
||||||
// }
|
|
||||||
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.
|
|
||||||
return unpack_nonstandard(a77, call_to, call_de, extra);
|
|
||||||
}
|
|
||||||
// else if (i3 == 5) {
|
|
||||||
// // Type 5: TU; W9XYZ K1ABC R-09 FN 1 28 28 1 7 9 74 WWROF contest
|
|
||||||
// }
|
|
||||||
|
|
||||||
// unknown type, should never get here
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int unpack77(const uint8_t* a77, char* message)
|
|
||||||
{
|
|
||||||
char call_to[14];
|
|
||||||
char call_de[14];
|
|
||||||
char extra[19];
|
|
||||||
|
|
||||||
int rc = unpack77_fields(a77, call_to, call_de, extra);
|
|
||||||
if (rc < 0)
|
|
||||||
return rc;
|
|
||||||
|
|
||||||
// int msg_sz = strlen(call_to) + strlen(call_de) + strlen(extra) + 2;
|
|
||||||
char* dst = message;
|
|
||||||
|
|
||||||
dst[0] = '\0';
|
|
||||||
|
|
||||||
if (call_to[0] != '\0')
|
|
||||||
{
|
|
||||||
dst = stpcpy(dst, call_to);
|
|
||||||
*dst++ = ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call_de[0] != '\0')
|
|
||||||
{
|
|
||||||
dst = stpcpy(dst, call_de);
|
|
||||||
*dst++ = ' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
dst = stpcpy(dst, extra);
|
|
||||||
*dst = '\0';
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
23
ft8/unpack.h
23
ft8/unpack.h
|
@ -1,23 +0,0 @@
|
||||||
#ifndef _INCLUDE_UNPACK_H_
|
|
||||||
#define _INCLUDE_UNPACK_H_
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C"
|
|
||||||
{
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// field1 - at least 14 bytes
|
|
||||||
// field2 - at least 14 bytes
|
|
||||||
// field3 - at least 7 bytes
|
|
||||||
int unpack77_fields(const uint8_t* a77, char* field1, char* field2, char* field3);
|
|
||||||
|
|
||||||
// message should have at least 35 bytes allocated (34 characters + zero terminator)
|
|
||||||
int unpack77(const uint8_t* a77, char* message);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // _INCLUDE_UNPACK_H_
|
|
159
test.c
159
test.c
|
@ -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;
|
|
||||||
}
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "ft8/text.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
|
||||||
|
#include "ft8/debug.h"
|
||||||
|
|
||||||
|
// 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_t 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
Ładowanie…
Reference in New Issue