From 0f0dd2b499fc073d517e4780f7f5602ea680f504 Mon Sep 17 00:00:00 2001 From: zenith <157907903+RFnexus@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:49:06 -0400 Subject: [PATCH] Add successive MFSK-8, MFSK-16, and MFSK-32/R modes, misc fixes --- CONTROL_PORT.md | 119 ++++++ Makefile | 14 +- control_port.hh | 2 +- cJSON.c => deps/cJSON.c | 0 cJSON.h => deps/cJSON.h | 0 deps/cJSON.o | Bin 0 -> 36576 bytes miniaudio.c => deps/miniaudio.c | 0 miniaudio.h => deps/miniaudio.h | 0 kiss_tnc.cc | 193 +++++++-- kiss_tnc.hh | 4 +- miniaudio_audio.hh | 2 +- phy/mfsk_modem.hh | 731 ++++++++++++++++++++++++++++++++ tnc_ui.hh | 196 +++++++-- 13 files changed, 1168 insertions(+), 93 deletions(-) create mode 100644 CONTROL_PORT.md rename cJSON.c => deps/cJSON.c (100%) rename cJSON.h => deps/cJSON.h (100%) create mode 100644 deps/cJSON.o rename miniaudio.c => deps/miniaudio.c (100%) rename miniaudio.h => deps/miniaudio.h (100%) create mode 100644 phy/mfsk_modem.hh diff --git a/CONTROL_PORT.md b/CONTROL_PORT.md new file mode 100644 index 0000000..4cb83d8 --- /dev/null +++ b/CONTROL_PORT.md @@ -0,0 +1,119 @@ +# Control Port API + +TCP JSON protocol on port 8073 + +Wire format: 4-byte big-endian length prefix + JSON payload. + +## Commands + +| Command | Description | +|---|---| +| `get_status` | Current modem/channel state | +| `get_config` | Current configuration | +| `set_config` | Update configuration (partial updates OK) | +| `rigctl` | Passthrough command to rigctld | +| `tx` | Transmit data via KISS | + +--- + +## `get_status` + +**Request:** `{"cmd": "get_status"}` + +**Response:** + +| Field | Type | Description | +|---|---|---| +| `channel_state` | string | `"idle"`, `"tx"`, or `"rx"` | +| `ptt_on` | bool | PTT currently keyed | +| `rx_frame_count` | int | Successfully decoded frames | +| `tx_frame_count` | int | Transmitted frames | +| `rx_error_count` | int | Preamble + CRC errors | +| `sync_count` | int | Preamble sync detections | +| `preamble_errors` | int | Sync found but preamble decode failed | +| `symbol_errors` | int | Symbol-level errors (OFDM only) | +| `crc_errors` | int | CRC check failures | +| `last_snr` | float | Last decoded frame SNR (dB) | +| `last_ber` | float | Last decoded frame BER (0.0-1.0, -1 if unavailable) | +| `ber_ema` | float | Exponential moving average BER | +| `client_count` | int | Connected KISS clients | +| `rigctl_connected` | bool | rigctld connection status | +| `audio_connected` | bool | Audio device health | + +Stats switch between OFDM and MFSK decoder based on active `modem_type`. + +--- + +## `get_config` + +**Request:** `{"cmd": "get_config"}` + +**Response:** + +| Field | Type | Description | +|---|---|---| +| `callsign` | string | Station callsign | +| `modem_type` | int | `0` = OFDM, `1` = MFSK | +| `mfsk_mode` | int | `0` = MFSK-8, `1` = MFSK-16, `2` = MFSK-32, `3` = MFSK-32R | +| `modulation` | string | OFDM: `"BPSK"`..`"QAM4096"`. MFSK: `"MFSK-8"`..`"MFSK-32R"` | +| `code_rate` | string | `"1/2"`, `"2/3"`, `"3/4"`, `"5/6"`, `"1/4"` (OFDM only) | +| `short_frame` | bool | Short frame mode (OFDM only) | +| `center_freq` | int | Center frequency in Hz | +| `payload_size` | int | Current PHY payload capacity in bytes | +| `csma_enabled` | bool | CSMA carrier sense enabled | +| `carrier_threshold_db` | float | CSMA threshold (dB) | +| `p_persistence` | int | P-persistence value (0-255) | +| `slot_time_ms` | int | CSMA slot time (ms) | +| `tx_blanking_enabled` | bool | Suppress decoder during TX | + +--- + +## `set_config` + +**Request:** `{"cmd": "set_config", ...fields...}` + +Send only the fields you want to change. All fields from `get_config` are accepted. + + +Example: +```json +{"cmd": "set_config", "modulation": "8PSK", "code_rate": "1/2"} +``` + +**Response:** `{"ok": true}` or `{"ok": false}` + +--- + +## `rigctl` + +**Request:** `{"cmd": "rigctl", "command": "F"}` + +Passes the command string to rigctld and returns the response. + +**Response:** `{"ok": true, "response": "145000000\n"}` + +--- + +## `tx` + +**Request:** +```json +{"cmd": "tx", "data": "", "oper_mode": -1} +``` + +| Field | Type | Description | +|---|---|---| +| `data` | string | Base64-encoded raw payload bytes | +| `oper_mode` | int | OFDM mode override (-1 = use current config) | + +**Response:** `{"ok": true, "size": 123}` + +--- + +## Events + +The control port broadcasts events to all connected clients: + +| Event | When | +|---|---| +| `config_changed` | Any configuration change | diff --git a/Makefile b/Makefile index 5cf1a71..6e205e5 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ INCLUDES = -I$(AICODIX_DSP) -I$(AICODIX_CODE) -I$(MODEM_SRC) TARGET = modem73 SRCS = kiss_tnc.cc -HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh tnc_ui.hh control_port.hh -OBJS = miniaudio.o cJSON.o +HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh phy/mfsk_modem.hh tnc_ui.hh control_port.hh +OBJS = deps/miniaudio.o deps/cJSON.o # defualt to build with UI, headless operations through --headless UI_FLAGS = -DWITH_UI @@ -37,11 +37,11 @@ endif all: $(TARGET) -miniaudio.o: miniaudio.c miniaudio.h - $(CC) -c -O2 -o $@ miniaudio.c +deps/miniaudio.o: deps/miniaudio.c deps/miniaudio.h + $(CC) -c -O2 -o $@ deps/miniaudio.c -cJSON.o: cJSON.c cJSON.h - $(CC) -c -O2 -o $@ cJSON.c +deps/cJSON.o: deps/cJSON.c deps/cJSON.h + $(CC) -c -O2 -o $@ deps/cJSON.c $(TARGET): $(SRCS) $(HDRS) $(OBJS) $(CXX) $(CXXFLAGS) $(UI_FLAGS) $(CM108_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS) @@ -53,7 +53,7 @@ ifneq ($(HIDAPI_LIBS),) endif clean: - rm -f $(TARGET) $(OBJS) cJSON.o + rm -f $(TARGET) $(OBJS) install: $(TARGET) install -m 755 $(TARGET) /usr/local/bin/ diff --git a/control_port.hh b/control_port.hh index 93d1528..9550aea 100644 --- a/control_port.hh +++ b/control_port.hh @@ -19,7 +19,7 @@ #include extern "C" { -#include "cJSON.h" +#include "deps/cJSON.h" } // Base64 decode (RFC 4648) diff --git a/cJSON.c b/deps/cJSON.c similarity index 100% rename from cJSON.c rename to deps/cJSON.c diff --git a/cJSON.h b/deps/cJSON.h similarity index 100% rename from cJSON.h rename to deps/cJSON.h diff --git a/deps/cJSON.o b/deps/cJSON.o new file mode 100644 index 0000000000000000000000000000000000000000..f43caddfa0ce12f39b977814dd896aa88e42468d GIT binary patch literal 36576 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 literal 0 HcmV?d00001 diff --git a/miniaudio.c b/deps/miniaudio.c similarity index 100% rename from miniaudio.c rename to deps/miniaudio.c diff --git a/miniaudio.h b/deps/miniaudio.h similarity index 100% rename from miniaudio.h rename to deps/miniaudio.h diff --git a/kiss_tnc.cc b/kiss_tnc.cc index eb2b97e..5f34922 100644 --- a/kiss_tnc.cc +++ b/kiss_tnc.cc @@ -34,6 +34,7 @@ #include "cm108_ptt.hh" #endif #include "modem.hh" +#include "phy/mfsk_modem.hh" #include "control_port.hh" #ifdef WITH_UI @@ -132,13 +133,19 @@ public: class KISSTNC { public: KISSTNC(const TNCConfig& config) : config_(config) { - // Allocate encoder/decoder on heap - std::cerr << " Creating encoder" << std::endl; + // Allocate OFDM encoder/decoder + std::cerr << " Creating OFDM encoder/decoder" << std::endl; encoder_ = std::make_unique(); - std::cerr << " Creating decoder" << std::endl; decoder_ = std::make_unique(); - std::cerr << " Encoder/decoder created" << std::endl; - + + // Allocate MFSK encoder/decoder + std::cerr << " Creating MFSK encoder/decoder" << std::endl; + mfsk_encoder_ = std::make_unique(); + mfsk_decoder_ = std::make_unique( + (MFSKMode)config.mfsk_mode, config.center_freq); + + std::cerr << " All encoders/decoders created" << std::endl; + // Set up constellation callback for UI display #ifdef WITH_UI decoder_->constellation_callback = [this](const DSP::Complex* symbols, int count, int mod_bits) { @@ -153,7 +160,7 @@ public: } }; #endif - + // Init modem configuration modem_config_.sample_rate = config.sample_rate; modem_config_.center_freq = config.center_freq; @@ -163,15 +170,19 @@ public: config.code_rate.c_str(), config.short_frame ); - + if (modem_config_.call_sign < 0) { throw std::runtime_error("Invalid callsign"); } if (modem_config_.oper_mode < 0) { throw std::runtime_error("Invalid modulation or code rate"); } - - payload_size_ = encoder_->get_payload_size(modem_config_.oper_mode); + + if (config.modem_type == 1) { + payload_size_ = mfsk_encoder_->get_payload_size((MFSKMode)config.mfsk_mode); + } else { + payload_size_ = encoder_->get_payload_size(modem_config_.oper_mode); + } std::cerr << "Payload size: " << payload_size_ << " bytes" << std::endl; } @@ -558,12 +569,21 @@ private: auto framed_data = frame_with_length(data); // Encode to audio - auto samples = encoder_->encode( - framed_data.data(), framed_data.size(), - modem_config_.center_freq, - modem_config_.call_sign, - tx_mode - ); + std::vector samples; + if (config_.modem_type == 1) { + samples = mfsk_encoder_->encode( + framed_data.data(), framed_data.size(), + modem_config_.center_freq, + (MFSKMode)config_.mfsk_mode + ); + } else { + samples = encoder_->encode( + framed_data.data(), framed_data.size(), + modem_config_.center_freq, + modem_config_.call_sign, + tx_mode + ); + } if (samples.empty()) { ui_log("TX: Encoding failed"); @@ -726,6 +746,7 @@ private: } }; + // OFDM frame callback auto frame_callback = [this, &deliver_to_clients](const uint8_t* data, size_t len) { set_tx_lockout(RX_LOCKOUT_SECONDS); @@ -754,7 +775,7 @@ private: return; } - if (config_.fragmentation_enabled && reassembler_.is_fragment(payload)) { + if (reassembler_.is_fragment(payload)) { if (g_verbose) { std::cerr << packet_visualize(payload.data(), payload.size(), false, true) << std::endl; } @@ -768,28 +789,67 @@ private: deliver_to_clients(payload, snr, ber_pct, false); } }; - + + // MFSK frame callback + auto mfsk_frame_callback = [this, &deliver_to_clients](const uint8_t* data, size_t len) { + set_tx_lockout(RX_LOCKOUT_SECONDS); + + float snr = mfsk_decoder_->get_last_snr(); + float ber_pct = -1.0f; + +#ifdef WITH_UI + if (g_ui_state) { + g_ui_state->rx_frame_count++; + g_ui_state->receiving = false; + g_ui_state->last_rx_snr = snr; + } +#endif + + auto payload = unframe_length(data, len); + + if (payload.empty()) { + ui_log("MFSK RX: Empty payload after unframing"); +#ifdef WITH_UI + if (g_ui_state) g_ui_state->rx_error_count++; +#endif + return; + } + + if (reassembler_.is_fragment(payload)) { + auto reassembled = reassembler_.process(payload); + if (!reassembled.empty()) { + ui_log("MFSK RX: Reassembled " + std::to_string(reassembled.size()) + " bytes"); + deliver_to_clients(reassembled, snr, ber_pct, true); + } + } else { + deliver_to_clients(payload, snr, ber_pct, false); + } + }; + bool was_blanking = false; - + while (rx_running_ && g_running) { int n = audio_->read(buffer.data(), buffer.size()); if (n > 0) { bool blanking = tx_blanking_active_.load(); - + if (blanking) { was_blanking = true; } else { if (was_blanking) { decoder_->reset(); + mfsk_decoder_->reset(); was_blanking = false; } + // Feed same audio to both decod,ers decoder_->process(buffer.data(), n, frame_callback); + mfsk_decoder_->process(buffer.data(), n, mfsk_frame_callback); } - + #ifdef WITH_UI if (g_ui_state && ++level_update_counter >= LEVEL_UPDATE_INTERVAL) { level_update_counter = 0; - + // Calculate RMS level in dB float sum_sq = 0.0f; for (int i = 0; i < n; i++) { @@ -797,9 +857,9 @@ private: } float rms = std::sqrt(sum_sq / n); float db = 20.0f * std::log10(rms + 1e-10f); - + g_ui_state->update_level(db); - + // Copy decoder stats if (g_ui_state->stats_reset_requested.exchange(false)) { decoder_->stats_sync_count = 0; @@ -807,12 +867,20 @@ private: decoder_->stats_symbol_errors = 0; decoder_->stats_crc_errors = 0; decoder_->reset_ber(); + mfsk_decoder_->reset_stats(); g_ui_state->last_rx_ber = -1.0f; } - g_ui_state->sync_count = decoder_->stats_sync_count; - g_ui_state->preamble_errors = decoder_->stats_preamble_errors; - g_ui_state->symbol_errors = decoder_->stats_symbol_errors; - g_ui_state->crc_errors = decoder_->stats_crc_errors; + if (config_.modem_type == 1) { + g_ui_state->sync_count = mfsk_decoder_->stats_sync_count; + g_ui_state->preamble_errors = mfsk_decoder_->stats_preamble_errors; + g_ui_state->symbol_errors = 0; + g_ui_state->crc_errors = mfsk_decoder_->stats_crc_errors; + } else { + g_ui_state->sync_count = decoder_->stats_sync_count; + g_ui_state->preamble_errors = decoder_->stats_preamble_errors; + g_ui_state->symbol_errors = decoder_->stats_symbol_errors; + g_ui_state->crc_errors = decoder_->stats_crc_errors; + } } #endif } @@ -881,7 +949,9 @@ private: std::unique_ptr encoder_; std::unique_ptr decoder_; - + std::unique_ptr mfsk_encoder_; + std::unique_ptr mfsk_decoder_; + std::unique_ptr audio_; std::unique_ptr rigctl_; std::unique_ptr serial_ptt_; @@ -932,31 +1002,50 @@ public: if (config_.center_freq != new_config.center_freq) { config_.center_freq = new_config.center_freq; modem_config_.center_freq = config_.center_freq; + // Reconfigure MFSK decoder with new center freq + mfsk_decoder_->configure((MFSKMode)config_.mfsk_mode, config_.center_freq); ui_log("Center frequency changed to " + std::to_string(config_.center_freq) + " Hz"); } - - // Update modulation settings + + // Update modem type and sub-mode + if (config_.modem_type != new_config.modem_type || config_.mfsk_mode != new_config.mfsk_mode) { + config_.modem_type = new_config.modem_type; + config_.mfsk_mode = new_config.mfsk_mode; + if (config_.modem_type == 1) { + MFSKMode mmode = (MFSKMode)config_.mfsk_mode; + mfsk_decoder_->configure(mmode, config_.center_freq); + payload_size_ = mfsk_encoder_->get_payload_size(mmode); + ui_log("Mode changed to " + std::string(MFSK_MODE_NAMES[(int)mmode]) + + " (" + std::to_string(MFSKParams::max_payload(mmode)) + " bytes)"); + } else { + payload_size_ = encoder_->get_payload_size(modem_config_.oper_mode); + } + } + + // Update OFDM modulation settings bool mode_changed = (config_.modulation != new_config.modulation || config_.code_rate != new_config.code_rate || config_.short_frame != new_config.short_frame); - + if (mode_changed) { config_.modulation = new_config.modulation; config_.code_rate = new_config.code_rate; config_.short_frame = new_config.short_frame; - + int new_mode = ModemConfig::encode_mode( config_.modulation.c_str(), config_.code_rate.c_str(), config_.short_frame ); - + if (new_mode >= 0) { modem_config_.oper_mode = new_mode; - payload_size_ = encoder_->get_payload_size(modem_config_.oper_mode); - ui_log("Mode changed to " + config_.modulation + " " + config_.code_rate + + if (config_.modem_type == 0) { + payload_size_ = encoder_->get_payload_size(modem_config_.oper_mode); + } + ui_log("OFDM mode changed to " + config_.modulation + " " + config_.code_rate + " " + (config_.short_frame ? "short" : "normal") + - " (" + std::to_string(payload_size_) + " bytes)"); + " (" + std::to_string(encoder_->get_payload_size(modem_config_.oper_mode)) + " bytes)"); } } } @@ -971,6 +1060,17 @@ public: }; DecoderStats get_decoder_stats() const { + if (config_.modem_type == 1) { + return { + mfsk_decoder_->stats_sync_count, + mfsk_decoder_->stats_preamble_errors, + 0, // MFSK has no symbol errors stat + mfsk_decoder_->stats_crc_errors, + mfsk_decoder_->get_last_snr(), + mfsk_decoder_->get_last_ber(), + mfsk_decoder_->get_ber_ema() + }; + } return { decoder_->stats_sync_count, decoder_->stats_preamble_errors, @@ -1288,9 +1388,11 @@ int main(int argc, char** argv) { // Try to load saved settings if (ui_state.load_settings()) { - // Apply loaded settings to config + // Apply loaded settings to config if (!cli_callsign) config.callsign = ui_state.callsign; + config.modem_type = ui_state.modem_type_index; + config.mfsk_mode = ui_state.mfsk_mode_index; config.center_freq = ui_state.center_freq; config.modulation = MODULATION_OPTIONS[ui_state.modulation_index]; config.code_rate = CODE_RATE_OPTIONS[ui_state.code_rate_index]; @@ -1518,7 +1620,14 @@ int main(int argc, char** argv) { auto& cfg = tnc.get_config(); cJSON_AddStringToObject(j, "callsign", cfg.callsign.c_str()); - cJSON_AddStringToObject(j, "modulation", cfg.modulation.c_str()); + cJSON_AddNumberToObject(j, "modem_type", cfg.modem_type); + cJSON_AddNumberToObject(j, "mfsk_mode", cfg.mfsk_mode); + if (cfg.modem_type == 1) { + cJSON_AddStringToObject(j, "modulation", + MFSK_MODE_NAMES[cfg.mfsk_mode < 4 ? cfg.mfsk_mode : 0]); + } else { + cJSON_AddStringToObject(j, "modulation", cfg.modulation.c_str()); + } cJSON_AddStringToObject(j, "code_rate", cfg.code_rate.c_str()); cJSON_AddBoolToObject(j, "short_frame", cfg.short_frame); cJSON_AddNumberToObject(j, "center_freq", cfg.center_freq); @@ -1536,6 +1645,10 @@ int main(int argc, char** argv) { TNCConfig new_config = tnc.get_config(); cJSON* item; + if ((item = cJSON_GetObjectItemCaseSensitive(params, "modem_type")) && cJSON_IsNumber(item)) + new_config.modem_type = item->valueint; + if ((item = cJSON_GetObjectItemCaseSensitive(params, "mfsk_mode")) && cJSON_IsNumber(item)) + new_config.mfsk_mode = item->valueint; if ((item = cJSON_GetObjectItemCaseSensitive(params, "callsign")) && cJSON_IsString(item)) new_config.callsign = item->valuestring; if ((item = cJSON_GetObjectItemCaseSensitive(params, "modulation")) && cJSON_IsString(item)) @@ -1563,6 +1676,8 @@ int main(int argc, char** argv) { // Sync config back to TUI state so the UI reflects changes if (g_ui_state) { g_ui_state->callsign = new_config.callsign; + g_ui_state->modem_type_index = new_config.modem_type; + g_ui_state->mfsk_mode_index = new_config.mfsk_mode; g_ui_state->center_freq = new_config.center_freq; g_ui_state->short_frame = new_config.short_frame; g_ui_state->csma_enabled = new_config.csma_enabled; @@ -1609,6 +1724,8 @@ int main(int argc, char** argv) { if (g_use_ui) { ui_state.on_settings_changed = [&tnc, &ctrl](TNCUIState& state) { TNCConfig new_config = tnc.get_config(); + new_config.modem_type = state.modem_type_index; + new_config.mfsk_mode = state.mfsk_mode_index; new_config.callsign = state.callsign; new_config.center_freq = state.center_freq; new_config.modulation = MODULATION_OPTIONS[state.modulation_index]; diff --git a/kiss_tnc.hh b/kiss_tnc.hh index 283f4a5..162a38f 100644 --- a/kiss_tnc.hh +++ b/kiss_tnc.hh @@ -53,11 +53,13 @@ struct TNCConfig { int sample_rate = 48000; // Modem settings + int modem_type = 0; // 0=OFDM, 1=MFSK + int mfsk_mode = 1; // 0=MFSK-8, 1=MFSK-16, 2=MFSK-32, 3=MFSK-32R int center_freq = 1500; std::string callsign = "N0CALL"; std::string modulation = "QPSK"; std::string code_rate = "1/2"; - bool short_frame = false; + bool short_frame = false; // PTT settings PTTType ptt_type = PTTType::RIGCTL; diff --git a/miniaudio_audio.hh b/miniaudio_audio.hh index 6d539e9..9a82595 100644 --- a/miniaudio_audio.hh +++ b/miniaudio_audio.hh @@ -5,7 +5,7 @@ #define MA_NO_GENERATION #define MA_NO_ENGINE #define MA_NO_NODE_GRAPH -#include "miniaudio.h" +#include "deps/miniaudio.h" #include #include diff --git a/phy/mfsk_modem.hh b/phy/mfsk_modem.hh new file mode 100644 index 0000000..45989b5 --- /dev/null +++ b/phy/mfsk_modem.hh @@ -0,0 +1,731 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +enum class MFSKMode { + MFSK_8 = 0, // 8 tones, 250 Hz BW, 3 bits/sym, rate 1/2 + MFSK_16 = 1, // 16 tones, 500 Hz BW, 4 bits/sym, rate 1/2 + MFSK_32 = 2, // 32 tones, 1000 Hz BW, 5 bits/sym, rate 1/2 + MFSK_32R = 3 // 32 tones, 1000 Hz BW, 5 bits/sym, rate 3/4 +}; + + + +static const char* MFSK_MODE_NAMES[] = {"MFSK-8", "MFSK-16", "MFSK-32", "MFSK-32R"}; + + + + + +struct MFSKParams { + static constexpr int SAMPLE_RATE = 48000; + static constexpr int SYMBOL_LEN = 1536; + static constexpr float TONE_SPACING = 31.25f; + static constexpr int PREAMBLE_SYMBOLS = 8; + static constexpr int SYNC_SYMBOLS = 2; + static constexpr int OVERHEAD_SYMBOLS = PREAMBLE_SYMBOLS + SYNC_SYMBOLS; + static constexpr int DATA_SYMBOLS = 128; + static constexpr int FRAME_SYMBOLS = OVERHEAD_SYMBOLS + DATA_SYMBOLS; + static constexpr int GUARD_SAMPLES = 32; + static constexpr int SEARCH_STEP = SYMBOL_LEN / 4; + + static constexpr int CONV_K = 7; + static constexpr int CONV_STATES = 64; + static constexpr int CONV_TAIL = 6; + static constexpr uint8_t CONV_G0 = 0x79; + static constexpr uint8_t CONV_G1 = 0x5B; + + + + static int num_tones(MFSKMode mode) { + static const int t[] = {8, 16, 32, 32}; + return t[(int)mode]; + } + + static int bits_per_symbol(MFSKMode mode) { + static const int b[] = {3, 4, 5, 5}; + return b[(int)mode]; + } + + static bool is_rate34(MFSKMode mode) { + return mode == MFSKMode::MFSK_32R; + } + + static int coded_capacity(MFSKMode mode) { + return DATA_SYMBOLS * bits_per_symbol(mode); + } + + static int data_bytes(MFSKMode mode) { + int coded = coded_capacity(mode); + if (is_rate34(mode)) + return (coded * 3 / 4 - CONV_TAIL) / 8; + return (coded / 2 - CONV_TAIL) / 8; + } + + static int max_payload(MFSKMode mode) { + return data_bytes(mode) - 4; + } + + static int frame_capacity(MFSKMode mode) { + return data_bytes(mode) - 2; + } + + static int base_bin(MFSKMode mode, int center_freq) { + int center_bin = (center_freq * SYMBOL_LEN + SAMPLE_RATE / 2) / SAMPLE_RATE; + return center_bin - num_tones(mode) / 2; + } + + static float frame_duration() { + return FRAME_SYMBOLS * (float)SYMBOL_LEN / SAMPLE_RATE; + } + + static int bitrate(MFSKMode mode) { + return (int)(max_payload(mode) * 8.0f / frame_duration()); + } +}; + + +namespace mfsk_detail { + +inline uint16_t crc16_ccitt(const uint8_t* data, size_t len) { + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (int j = 0; j < 8; j++) + crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1; + } + return crc; +} + + + +inline int gray_encode(int n) { return n ^ (n >> 1); } + + + + + +inline int gray_decode(int n) { + int mask = n; + while (mask) { mask >>= 1; n ^= mask; } + return n; +} + + +inline int bit_reverse(int x, int bits) { + int result = 0; + for (int i = 0; i < bits; i++) { result = (result << 1) | (x & 1); x >>= 1; } + return result; +} + +inline float goertzel_mag2(const float* samples, int N, int bin) { + float w = 2.0f * (float)M_PI * bin / N; + float coeff = 2.0f * cosf(w); + float s1 = 0.0f, s2 = 0.0f; + for (int i = 0; i < N; i++) { + float s0 = samples[i] + coeff * s1 - s2; + s2 = s1; + s1 = s0; + } + return s1 * s1 + s2 * s2 - coeff * s1 * s2; +} + +inline int parity8(uint8_t x) { + x ^= x >> 4; x ^= x >> 2; x ^= x >> 1; + return x & 1; +} + +inline void interleave(int* data, int n) { + int bits = 0; + while ((1 << bits) < n) bits++; + std::vector tmp(data, data + n); + for (int i = 0; i < n; i++) + data[bit_reverse(i, bits)] = tmp[i]; +} + +inline void interleave_vectors(std::vector>& data) { + int n = data.size(); + int bits = 0; + while ((1 << bits) < n) bits++; + std::vector> tmp(data); + for (int i = 0; i < n; i++) + data[bit_reverse(i, bits)] = std::move(tmp[i]); +} + +inline std::vector conv_encode(const uint8_t* data, int data_bytes) { + int data_bits = data_bytes * 8; + int total_in = data_bits + MFSKParams::CONV_TAIL; + std::vector coded; + coded.reserve(total_in * 2); + uint8_t state = 0; + for (int i = 0; i < total_in; i++) { + int bit = (i < data_bits) ? (data[i / 8] >> (7 - (i % 8))) & 1 : 0; + uint8_t inp = ((uint8_t)bit << 6) | state; + coded.push_back(parity8(inp & MFSKParams::CONV_G0)); + coded.push_back(parity8(inp & MFSKParams::CONV_G1)); + state = (((uint8_t)bit << 5) | (state >> 1)) & 0x3F; + } + return coded; +} + +class ViterbiDecoder { +public: + static constexpr int STATES = MFSKParams::CONV_STATES; + static constexpr int MAX_STEPS = 512; + + void reset() { + for (int s = 0; s < STATES; s++) metric_[s] = -1e30f; + metric_[0] = 0.0f; + len_ = 0; + } + + void step(float s0, float s1) { + float new_metric[STATES]; + for (int s = 0; s < STATES; s++) new_metric[s] = -1e30f; + for (int prev = 0; prev < STATES; prev++) { + if (metric_[prev] < -1e29f) continue; + for (int bit = 0; bit < 2; bit++) { + uint8_t inp = ((uint8_t)bit << 6) | (uint8_t)prev; + int c0 = parity8(inp & MFSKParams::CONV_G0); + int c1 = parity8(inp & MFSKParams::CONV_G1); + float branch = s0 * (1 - 2 * c0) + s1 * (1 - 2 * c1); + int next = (((uint8_t)bit << 5) | ((uint8_t)prev >> 1)) & 0x3F; + float candidate = metric_[prev] + branch; + if (candidate > new_metric[next]) { + new_metric[next] = candidate; + survivor_[len_][next] = ((uint8_t)prev << 1) | (uint8_t)bit; + } + } + } + memcpy(metric_, new_metric, sizeof(metric_)); + len_++; + } + + std::vector finish(int data_bits) { + int best = 0; + for (int s = 1; s < STATES; s++) + if (metric_[s] > metric_[best]) best = s; + std::vector bits(len_); + int state = best; + for (int t = len_ - 1; t >= 0; t--) { + bits[t] = survivor_[t][state] & 1; + state = survivor_[t][state] >> 1; + } + int n_bytes = data_bits / 8; + std::vector result(n_bytes, 0); + for (int i = 0; i < data_bits && i < len_; i++) + result[i / 8] |= bits[i] << (7 - (i % 8)); + return result; + } + +private: + float metric_[STATES]; + uint8_t survivor_[MAX_STEPS][STATES]; + int len_ = 0; +}; + +inline void soft_demap(const float* energies, int n_tones, int bps, float* soft_bits) { + for (int j = 0; j < bps; j++) { + float e0 = 0, e1 = 0; + int bit_pos = bps - 1 - j; + for (int t = 0; t < n_tones; t++) { + int sym_val = gray_decode(t); + if ((sym_val >> bit_pos) & 1) + e1 += energies[t]; + else + e0 += energies[t]; + } + soft_bits[j] = (e0 - e1) / (e0 + e1 + 1e-20f); + } +} + +inline std::vector puncture_34(const std::vector& coded) { + std::vector out; + out.reserve(coded.size() * 2 / 3 + 4); + for (size_t i = 0; i + 5 < coded.size(); i += 6) { + out.push_back(coded[i]); + out.push_back(coded[i+1]); + out.push_back(coded[i+2]); + out.push_back(coded[i+5]); + } + size_t rem = (coded.size() / 6) * 6; + for (size_t i = rem; i < coded.size(); i++) out.push_back(coded[i]); + return out; +} + +inline std::vector depuncture_34(const float* soft, int n_soft) { + std::vector out; + out.reserve(n_soft * 3 / 2 + 8); + int si = 0; + while (si + 3 < n_soft) { + out.push_back(soft[si++]); + out.push_back(soft[si++]); + out.push_back(soft[si++]); + out.push_back(0.0f); + out.push_back(0.0f); + out.push_back(soft[si++]); + } + + + + + + while (si < n_soft) { out.push_back(soft[si++]); out.push_back(0.0f); } + return out; +} + +inline std::vector bits_to_gray_symbols(const std::vector& bits, int bps) { + std::vector symbols; + for (int i = 0; i + bps <= (int)bits.size(); i += bps) { + int sym = 0; + for (int b = 0; b < bps; b++) + sym = (sym << 1) | bits[i + b]; + symbols.push_back(gray_encode(sym)); + } + return symbols; +} + +} // namespace mfsk_detail + + +class MFSKEncoder { +public: + std::vector encode(const uint8_t* data, size_t len, + int center_freq, MFSKMode mode) { + using namespace mfsk_detail; + + int n_tones = MFSKParams::num_tones(mode); + int bps = MFSKParams::bits_per_symbol(mode); + int dbytes = MFSKParams::data_bytes(mode); + int base = MFSKParams::base_bin(mode, center_freq); + + int payload_len = dbytes - 2; + std::vector frame(payload_len, 0); + memcpy(frame.data(), data, std::min(len, (size_t)payload_len)); + + uint16_t crc = crc16_ccitt(frame.data(), frame.size()); + frame.push_back(crc >> 8); + frame.push_back(crc & 0xFF); + + auto coded_bits = conv_encode(frame.data(), dbytes); + if (MFSKParams::is_rate34(mode)) + coded_bits = puncture_34(coded_bits); + + int capacity = MFSKParams::DATA_SYMBOLS * bps; + while ((int)coded_bits.size() < capacity) + coded_bits.push_back(0); + + auto symbols = bits_to_gray_symbols(coded_bits, bps); + symbols.resize(MFSKParams::DATA_SYMBOLS, 0); + interleave(symbols.data(), symbols.size()); + + std::vector frame_tones; + frame_tones.reserve(MFSKParams::FRAME_SYMBOLS); + for (int i = 0; i < MFSKParams::PREAMBLE_SYMBOLS; i++) + frame_tones.push_back(i % 2 == 0 ? 0 : n_tones - 1); + frame_tones.push_back(n_tones / 4); + frame_tones.push_back(3 * n_tones / 4); + frame_tones.insert(frame_tones.end(), symbols.begin(), symbols.end()); + + return generate_audio(frame_tones, base); + } + + int get_payload_size(MFSKMode mode) { + return MFSKParams::frame_capacity(mode); + } + +private: + float phase_ = 0.0f; + + std::vector generate_audio(const std::vector& tones, int base_bin) { + const int N = MFSKParams::SYMBOL_LEN; + const int G = MFSKParams::GUARD_SAMPLES; + const float amp = 0.8f; + std::vector audio; + audio.reserve(tones.size() * N); + phase_ = 0.0f; + for (int tone : tones) { + float freq = (base_bin + tone) * MFSKParams::TONE_SPACING; + float phase_inc = 2.0f * (float)M_PI * freq / MFSKParams::SAMPLE_RATE; + for (int i = 0; i < N; i++) { + float env = 1.0f; + if (i < G) + env = 0.5f * (1.0f - cosf((float)M_PI * i / G)); + else if (i >= N - G) + env = 0.5f * (1.0f + cosf((float)M_PI * (i - N + G) / G)); + audio.push_back(amp * env * sinf(phase_)); + phase_ += phase_inc; + } + phase_ = fmodf(phase_, 2.0f * (float)M_PI); + } + return audio; + } +}; + + +class MFSKDecoder { +public: + using FrameCallback = std::function; + + MFSKDecoder() {} + MFSKDecoder(MFSKMode mode, int center_freq = 1500) { configure(mode, center_freq); } + + void configure(MFSKMode mode, int center_freq) { + mode_ = mode; + n_tones_ = MFSKParams::num_tones(mode); + bps_ = MFSKParams::bits_per_symbol(mode); + base_bin_ = MFSKParams::base_bin(mode, center_freq); + sync1_tone_ = n_tones_ / 4; + sync3_tone_ = 3 * n_tones_ / 4; + reset(); + } + + void process(const float* samples, size_t count, FrameCallback callback) { + buf_.insert(buf_.end(), samples, samples + count); + + while (true) { + if (buf_.size() - buf_pos_ < (size_t)MFSKParams::SYMBOL_LEN) + break; + + const float* window = buf_.data() + buf_pos_; + + switch (state_) { + case State::SEARCHING: { + float e0 = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, base_bin_); + float en = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, base_bin_ + n_tones_ - 1); + float eq1 = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, base_bin_ + sync1_tone_); + float eq3 = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, base_bin_ + sync3_tone_); + + int p = step_count_ % 4; + update_tracker(p, e0, en, eq1, eq3); + + if (trackers_[p].tstate == TState::READY) { + ++stats_sync_count; + + freq_offset_ = 0; + float best_afc_e = 0; + for (int foff = -3; foff <= 3; foff++) { + float e = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, + base_bin_ + foff + sync3_tone_); + if (e > best_afc_e) { + best_afc_e = e; + freq_offset_ = foff; + } + } + + int best_off = 0; + float best_e = 0; + int sync_bin = base_bin_ + freq_offset_ + sync3_tone_; + int half_step = MFSKParams::SEARCH_STEP / 2; + for (int off = -half_step; off <= half_step; off += 16) { + int64_t pos = (int64_t)buf_pos_ + off; + if (pos < 0 || pos + MFSKParams::SYMBOL_LEN > (int64_t)buf_.size()) + continue; + float e = mfsk_detail::goertzel_mag2( + buf_.data() + pos, MFSKParams::SYMBOL_LEN, sync_bin); + if (e > best_e) { + best_e = e; + best_off = off; + } + } + if (best_off >= 0) + buf_pos_ += best_off; + else + buf_pos_ -= (size_t)(-best_off); + + std::cerr << "MFSK: Sync (phase " << p + << " t=" << best_off + << " f=" << freq_offset_ << ")" << std::endl; + + buf_pos_ += MFSKParams::SYMBOL_LEN; + state_ = State::COLLECTING; + collect_count_ = 0; + collected_.clear(); + collected_.reserve(MFSKParams::DATA_SYMBOLS); + reset_trackers(); + continue; + } + + step_count_++; + buf_pos_ += MFSKParams::SEARCH_STEP; + break; + } + + case State::COLLECTING: { + if (collect_count_ == 0) + collect_start_pos_ = buf_pos_; + + if (collect_count_ > 0 && (collect_count_ % 16) == 0) { + float center_ratio = 0, best_ratio = 0; + int best_adj = 0; + for (int adj = -32; adj <= 32; adj += 8) { + int64_t pos = (int64_t)buf_pos_ + adj; + if (pos < 0 || pos + MFSKParams::SYMBOL_LEN > (int64_t)buf_.size()) + continue; + const float* w = buf_.data() + pos; + float max_e = 0, total_e = 0; + for (int t = 0; t < n_tones_; t++) { + float e = mfsk_detail::goertzel_mag2(w, MFSKParams::SYMBOL_LEN, base_bin_ + freq_offset_ + t); + total_e += e; + if (e > max_e) max_e = e; + } + float ratio = max_e / (total_e + 1e-20f); + if (adj == 0) center_ratio = ratio; + if (ratio > best_ratio) { best_ratio = ratio; best_adj = adj; } + } + if (best_adj != 0 && best_ratio > center_ratio * 1.02f) { + if (best_adj >= 0) buf_pos_ += best_adj; + else buf_pos_ -= (size_t)(-best_adj); + window = buf_.data() + buf_pos_; + } + } + + std::vector energies(n_tones_); + for (int t = 0; t < n_tones_; t++) + energies[t] = mfsk_detail::goertzel_mag2(window, MFSKParams::SYMBOL_LEN, + base_bin_ + freq_offset_ + t); + collected_.push_back(std::move(energies)); + collect_count_++; + buf_pos_ += MFSKParams::SYMBOL_LEN; + + if (collect_count_ >= MFSKParams::DATA_SYMBOLS) { + bool decoded = try_decode_auto(callback); + if (!decoded) { + for (int retry_off : {8, -8, 16, -16}) { + if (recompute_with_offset(retry_off) && try_decode_auto(callback)) { + decoded = true; + break; + } + } + } + if (!decoded) ++stats_crc_errors; + state_ = State::SEARCHING; + step_count_ = 0; + } + break; + } + } + } + + if (buf_pos_ > 8192 && state_ == State::SEARCHING) { + buf_.erase(buf_.begin(), buf_.begin() + buf_pos_); + buf_pos_ = 0; + } + } + + void reset() { + buf_.clear(); + buf_pos_ = 0; + state_ = State::SEARCHING; + step_count_ = 0; + collect_count_ = 0; + freq_offset_ = 0; + collected_.clear(); + reset_trackers(); + } + + float get_last_snr() const { return last_snr_; } + float get_last_ber() const { return last_ber_; } + float get_ber_ema() const { return ber_ema_; } + + int stats_sync_count = 0; + int stats_preamble_errors = 0; + int stats_crc_errors = 0; + + void reset_stats() { + stats_sync_count = 0; + stats_preamble_errors = 0; + stats_crc_errors = 0; + last_snr_ = 0; + last_ber_ = -1; + ber_ema_ = -1; + } + +private: + MFSKMode mode_ = MFSKMode::MFSK_16; + int n_tones_ = 16; + int bps_ = 4; + int base_bin_ = 40; + int freq_offset_ = 0; + int sync1_tone_ = 4; + int sync3_tone_ = 12; + + std::vector buf_; + size_t buf_pos_ = 0; + + enum class State { SEARCHING, COLLECTING }; + State state_ = State::SEARCHING; + int step_count_ = 0; + + int collect_count_ = 0; + size_t collect_start_pos_ = 0; + std::vector> collected_; + + float last_snr_ = 0; + float last_ber_ = -1; + float ber_ema_ = -1; + + enum class TState { PREAMBLE, SYNC1, SYNC2, READY }; + struct Tracker { TState tstate = TState::PREAMBLE; int count = 0; int last_tone = -1; }; + Tracker trackers_[4]; + + void reset_trackers() { + for (auto& t : trackers_) { t.tstate = TState::PREAMBLE; t.count = 0; t.last_tone = -1; } + } + + + + + + void update_tracker(int p, float e0, float en, float eq1, float eq3) { + auto& t = trackers_[p]; + float total = e0 + en + eq1 + eq3 + 1e-20f; + + switch (t.tstate) { + case TState::PREAMBLE: { + bool low_dom = (e0 / total > 0.4f); + bool high_dom = (en / total > 0.4f); + if (low_dom && !high_dom) { + t.count = (t.last_tone == 1) ? t.count + 1 : 1; + t.last_tone = 0; + } else if (high_dom && !low_dom) { + t.count = (t.last_tone == 0) ? t.count + 1 : 1; + t.last_tone = 1; + } else { + t.count = 0; t.last_tone = -1; + } + if (t.count >= MFSKParams::PREAMBLE_SYMBOLS) + t.tstate = TState::SYNC1; + break; + } + case TState::SYNC1: + if (eq1 / total > 0.25f) { + t.tstate = TState::SYNC2; + } else { + bool low_dom = (e0 / total > 0.4f); + bool high_dom = (en / total > 0.4f); + if ((low_dom && t.last_tone == 1) || (high_dom && t.last_tone == 0)) + t.last_tone = low_dom ? 0 : 1; + else { ++stats_preamble_errors; t.tstate = TState::PREAMBLE; t.count = 0; t.last_tone = -1; } + } + break; + case TState::SYNC2: + if (eq3 / total > 0.25f) + t.tstate = TState::READY; + else { ++stats_preamble_errors; t.tstate = TState::PREAMBLE; t.count = 0; t.last_tone = -1; } + break; + case TState::READY: + break; + } + } + + bool try_decode_auto(FrameCallback callback) { + if (try_decode(callback, mode_)) return true; + if (n_tones_ == 32) { + MFSKMode alt = MFSKParams::is_rate34(mode_) ? MFSKMode::MFSK_32 : MFSKMode::MFSK_32R; + if (try_decode(callback, alt)) return true; + } + return false; + } + + bool recompute_with_offset(int offset) { + collected_.clear(); + collected_.reserve(MFSKParams::DATA_SYMBOLS); + int64_t pos = (int64_t)collect_start_pos_ + offset; + if (pos < 0) return false; + + for (int i = 0; i < MFSKParams::DATA_SYMBOLS; i++) { + if ((size_t)pos + MFSKParams::SYMBOL_LEN > buf_.size()) return false; + const float* w = buf_.data() + pos; + std::vector energies(n_tones_); + for (int t = 0; t < n_tones_; t++) + energies[t] = mfsk_detail::goertzel_mag2(w, MFSKParams::SYMBOL_LEN, base_bin_ + freq_offset_ + t); + collected_.push_back(std::move(energies)); + pos += MFSKParams::SYMBOL_LEN; + } + return true; + } + + bool try_decode(FrameCallback callback, MFSKMode decode_mode) { + using namespace mfsk_detail; + + auto deinterleaved = collected_; + interleave_vectors(deinterleaved); + + int total_soft = MFSKParams::DATA_SYMBOLS * bps_; + std::vector soft_wire(total_soft); + float soft_buf[12]; + for (int i = 0; i < MFSKParams::DATA_SYMBOLS; i++) { + soft_demap(deinterleaved[i].data(), n_tones_, bps_, soft_buf); + for (int b = 0; b < bps_; b++) + soft_wire[i * bps_ + b] = soft_buf[b]; + } + + std::vector soft_full; + const float* soft_ptr; + int soft_len; + if (MFSKParams::is_rate34(decode_mode)) { + soft_full = depuncture_34(soft_wire.data(), total_soft); + soft_ptr = soft_full.data(); + soft_len = (int)soft_full.size(); + } else { + soft_ptr = soft_wire.data(); + soft_len = total_soft; + } + + int dbytes = MFSKParams::data_bytes(decode_mode); + int data_bits = dbytes * 8; + int n_steps = data_bits + MFSKParams::CONV_TAIL; + + ViterbiDecoder viterbi; + viterbi.reset(); + for (int s = 0; s < n_steps && s * 2 + 1 < soft_len; s++) + viterbi.step(soft_ptr[s * 2], soft_ptr[s * 2 + 1]); + + auto bytes = viterbi.finish(data_bits); + bytes.resize(dbytes, 0); + + uint16_t computed = crc16_ccitt(bytes.data(), dbytes - 2); + uint16_t received = ((uint16_t)bytes[dbytes - 2] << 8) | bytes[dbytes - 1]; + if (computed != received) return false; + + auto re_coded = conv_encode(bytes.data(), dbytes); + if (MFSKParams::is_rate34(decode_mode)) + re_coded = puncture_34(re_coded); + int re_capacity = MFSKParams::DATA_SYMBOLS * bps_; + while ((int)re_coded.size() < re_capacity) re_coded.push_back(0); + auto expected_tones = bits_to_gray_symbols(re_coded, bps_); + expected_tones.resize(MFSKParams::DATA_SYMBOLS, 0); + + float signal_e = 0, noise_e = 0; + for (int i = 0; i < MFSKParams::DATA_SYMBOLS; i++) { + int expected = expected_tones[i]; + float total_e = 0; + for (int t = 0; t < n_tones_; t++) total_e += deinterleaved[i][t]; + signal_e += deinterleaved[i][expected]; + noise_e += (total_e - deinterleaved[i][expected]); + } + + if (noise_e > 1e-10f) { + float sig = signal_e / MFSKParams::DATA_SYMBOLS; + float noi = noise_e / (MFSKParams::DATA_SYMBOLS * (n_tones_ - 1)); + last_snr_ = 10.0f * log10f(sig / noi); + } else { + last_snr_ = 50.0f; + } + + std::cerr << "MFSK: Decoded SNR=" << (int)last_snr_ << "dB" << std::endl; + callback(bytes.data(), dbytes - 2); + return true; + } +}; diff --git a/tnc_ui.hh b/tnc_ui.hh index 6f5e29e..4af2698 100644 --- a/tnc_ui.hh +++ b/tnc_ui.hh @@ -25,9 +25,13 @@ #include #include "kiss_tnc.hh" +#include "phy/mfsk_modem.hh" constexpr size_t MAX_LOG_ENTRIES = 500; +const std::vector MODEM_TYPE_OPTIONS = {"OFDM", "MFSK"}; +const std::vector MFSK_MODE_OPTIONS = {"MFSK-8", "MFSK-16", "MFSK-32", "MFSK-32R"}; + const std::vector MODULATION_OPTIONS = { "BPSK", "QPSK", "8PSK", "QAM16", "QAM64", "QAM256", "QAM1024", "QAM4096" }; @@ -49,9 +53,11 @@ const std::vector PTT_LINE_OPTIONS = { struct TNCUIState { std::string callsign = "N0CALL"; - int modulation_index = 1; // default QSPK N 1/2 - int code_rate_index = 0; - bool short_frame = false; + int modem_type_index = 0; // 0=OFDM, 1=MFSK + int mfsk_mode_index = 1; // 0=MFSK-8, 1=MFSK-16, 2=MFSK-32, 3=MFSK-32R + int modulation_index = 1; // default QPSK N 1/2 + int code_rate_index = 0; + bool short_frame = false; int center_freq = 1500; bool csma_enabled = true; @@ -114,7 +120,10 @@ struct TNCUIState { // Presets struct Preset { std::string name; - // Modem + // Modem type + int modem_type_index = 0; // 0=OFDM, 1=MFSK + int mfsk_mode_index = 1; // 0=MFSK-8, 1=MFSK-16, 2=MFSK-32, 3=MFSK-32R + // OFDM modem int modulation_index; int code_rate_index; bool short_frame; @@ -284,6 +293,17 @@ struct TNCUIState { // TEMP modem tables void update_modem_info() { + // MFSK mode + if (modem_type_index == 1) { + MFSKMode mmode = (MFSKMode)mfsk_mode_index; + mtu_bytes = MFSKParams::max_payload(mmode); + bitrate_bps = MFSKParams::bitrate(mmode); + airtime_seconds = MFSKParams::frame_duration(); + if (random_data_size == 0 || (!fragmentation_enabled && random_data_size > mtu_bytes)) + random_data_size = mtu_bytes; + return; + } + // Modulations: BPSK=0, QPSK=1, 8PSK=2, QAM16=3, QAM64=4, QAM256=5, QAM1024=6, QAM4096=7 // Code rates: 1/2=0, 2/3=1, 3/4=2, 5/6=3, 1/4=4 // Columns: [1/2, 2/3, 3/4, 5/6, 1/4] @@ -425,6 +445,8 @@ struct TNCUIState { fprintf(f, "# MODEM73 Settings\n"); fprintf(f, "callsign=%s\n", callsign.c_str()); + fprintf(f, "modem_type=%d\n", modem_type_index); + fprintf(f, "mfsk_mode=%d\n", mfsk_mode_index); fprintf(f, "modulation=%d\n", modulation_index); fprintf(f, "code_rate=%d\n", code_rate_index); fprintf(f, "short_frame=%d\n", short_frame ? 1 : 0); @@ -474,6 +496,8 @@ struct TNCUIState { char key[64], value[192]; if (sscanf(line, "%63[^=]=%191[^\n]", key, value) == 2) { if (strcmp(key, "callsign") == 0) callsign = value; + else if (strcmp(key, "modem_type") == 0) modem_type_index = atoi(value); + else if (strcmp(key, "mfsk_mode") == 0) mfsk_mode_index = atoi(value); else if (strcmp(key, "modulation") == 0) modulation_index = atoi(value); else if (strcmp(key, "code_rate") == 0) code_rate_index = atoi(value); else if (strcmp(key, "short_frame") == 0) short_frame = atoi(value) != 0; @@ -520,8 +544,8 @@ struct TNCUIState { fprintf(f, "# MODEM73 Presets \n"); for (const auto& p : presets) { - // name,mod,rate,sf,freq,csma,thresh,slot,persist,ptt,vox_freq,vox_lead,vox_tail - fprintf(f, "preset=%s,%d,%d,%d,%d,%d,%.1f,%d,%d,%d,%d,%d,%d\n", + // name,mod,rate,sf,freq,csma,thresh,slot,persist,ptt,vox_freq,vox_lead,vox_tail,modem_type,mfsk_mode + fprintf(f, "preset=%s,%d,%d,%d,%d,%d,%.1f,%d,%d,%d,%d,%d,%d,%d,%d\n", p.name.c_str(), p.modulation_index, p.code_rate_index, @@ -534,7 +558,9 @@ struct TNCUIState { p.ptt_type_index, p.vox_tone_freq, p.vox_lead_ms, - p.vox_tail_ms); + p.vox_tail_ms, + p.modem_type_index, + p.mfsk_mode_index); } fclose(f); @@ -557,14 +583,16 @@ struct TNCUIState { char name[64]; int mod, rate, sf, freq, csma, slot, persist; - int ptt_type = 1, vox_freq = 1200, vox_lead = 150, vox_tail = 100; + int ptt_type = 1, vox_freq = 1200, vox_lead = 150, vox_tail = 100; + int modem_type = 0, mfsk_mode = 1; float thresh; - - int n = sscanf(line + 7, "%63[^,],%d,%d,%d,%d,%d,%f,%d,%d,%d,%d,%d,%d", + + int n = sscanf(line + 7, "%63[^,],%d,%d,%d,%d,%d,%f,%d,%d,%d,%d,%d,%d,%d,%d", name, &mod, &rate, &sf, &freq, &csma, &thresh, &slot, &persist, - &ptt_type, &vox_freq, &vox_lead, &vox_tail); - - if (n >= 9) { + &ptt_type, &vox_freq, &vox_lead, &vox_tail, + &modem_type, &mfsk_mode); + + if (n >= 9) { Preset p; p.name = name; p.modulation_index = mod; @@ -580,6 +608,10 @@ struct TNCUIState { p.vox_tone_freq = (n >= 11) ? vox_freq : 1200; p.vox_lead_ms = (n >= 12) ? vox_lead : 150; p.vox_tail_ms = (n >= 13) ? vox_tail : 100; + + + p.modem_type_index = (n >= 14) ? modem_type : 0; + p.mfsk_mode_index = (n >= 15) ? mfsk_mode : 1; presets.push_back(p); } } @@ -600,6 +632,8 @@ struct TNCUIState { Preset p; p.name = name; + p.modem_type_index = modem_type_index; + p.mfsk_mode_index = mfsk_mode_index; p.modulation_index = modulation_index; p.code_rate_index = code_rate_index; p.short_frame = short_frame; @@ -623,6 +657,8 @@ struct TNCUIState { if (index < 0 || index >= (int)presets.size()) return false; const Preset& p = presets[index]; + modem_type_index = p.modem_type_index; + mfsk_mode_index = p.mfsk_mode_index; modulation_index = p.modulation_index; code_rate_index = p.code_rate_index; short_frame = p.short_frame; @@ -753,9 +789,11 @@ public: private: enum Field { FIELD_CALLSIGN = 0, + FIELD_MODEM_TYPE, FIELD_MODULATION, FIELD_CODERATE, FIELD_FRAMESIZE, + FIELD_MFSK_MODE, FIELD_FREQ, FIELD_CSMA, FIELD_THRESHOLD, @@ -851,7 +889,7 @@ private: state_.selected_preset = state_.presets.size() - 1; } } - } else if (current_field_ >= FIELD_MODULATION && current_field_ != FIELD_PRESET) { + } else if (current_field_ >= FIELD_MODEM_TYPE && current_field_ != FIELD_PRESET) { adjust_field(-1); } } else if (current_tab_ == 3 && (utils_selection_ == 0 || utils_selection_ == 1)) { @@ -872,7 +910,7 @@ private: state_.selected_preset = 0; } } - } else if (current_field_ >= FIELD_MODULATION && current_field_ != FIELD_PRESET) { + } else if (current_field_ >= FIELD_MODEM_TYPE && current_field_ != FIELD_PRESET) { adjust_field(1); } } else if (current_tab_ == 3 && (utils_selection_ == 0 || utils_selection_ == 1)) { @@ -1216,6 +1254,15 @@ private: } bool should_skip_field(int field) { + // Hide OFDM-only fields when in MFSK mode + if (state_.modem_type_index == 1) { + if (field == FIELD_MODULATION || field == FIELD_CODERATE || field == FIELD_FRAMESIZE) + return true; + } + // Hide MFSK-only fields when in OFDM mode + if (state_.modem_type_index == 0) { + if (field == FIELD_MFSK_MODE) return true; + } if (state_.ptt_type_index != 2) { // not VOX if (field == FIELD_VOX_FREQ || field == FIELD_VOX_LEAD || field == FIELD_VOX_TAIL) { return true; @@ -1238,6 +1285,14 @@ private: void adjust_field(int delta) { switch (current_field_) { + case FIELD_MODEM_TYPE: + state_.modem_type_index = (state_.modem_type_index + delta + 2) % 2; + state_.update_modem_info(); + break; + case FIELD_MFSK_MODE: + state_.mfsk_mode_index = (state_.mfsk_mode_index + delta + 4) % 4; + state_.update_modem_info(); + break; case FIELD_MODULATION: state_.modulation_index = (state_.modulation_index + delta + 8) % 8; break; @@ -1629,11 +1684,17 @@ private: attron(A_BOLD); addstr(state_.callsign.c_str()); attroff(A_BOLD); - printw(" %s %s %s %dHz", - MODULATION_OPTIONS[state_.modulation_index].c_str(), - CODE_RATE_OPTIONS[state_.code_rate_index].c_str(), - state_.short_frame ? "S" : "N", - state_.center_freq); + if (state_.modem_type_index == 1) { + printw(" %s %dHz", + MFSK_MODE_OPTIONS[state_.mfsk_mode_index].c_str(), + state_.center_freq); + } else { + printw(" %s %s %s %dHz", + MODULATION_OPTIONS[state_.modulation_index].c_str(), + CODE_RATE_OPTIONS[state_.code_rate_index].c_str(), + state_.short_frame ? "S" : "N", + state_.center_freq); + } // Stats int rx = cols - 20; @@ -2170,12 +2231,20 @@ private: row++; // header if (field == FIELD_CALLSIGN) return row; row++; - if (field == FIELD_MODULATION) return row; - row++; - if (field == FIELD_CODERATE) return row; - row++; - if (field == FIELD_FRAMESIZE) return row; + if (field == FIELD_MODEM_TYPE) return row; row++; + if (state_.modem_type_index == 0) { + if (field == FIELD_MODULATION) return row; + row++; + if (field == FIELD_CODERATE) return row; + row++; + if (field == FIELD_FRAMESIZE) return row; + row++; + } else { + // MFSK field + if (field == FIELD_MFSK_MODE) return row; + row++; + } if (field == FIELD_FREQ) return row; row += 2; // CSMA section @@ -2269,22 +2338,36 @@ private: dy = visible_y(row); if (dy >= 0) draw_field(dy, c1, c2, "Callsign", FIELD_CALLSIGN, state_.callsign, true); row++; - + dy = visible_y(row); - if (dy >= 0) draw_selector_field(dy, c1, c2, "Modulation", FIELD_MODULATION, - MODULATION_OPTIONS[state_.modulation_index]); + if (dy >= 0) draw_selector_field(dy, c1, c2, "Modem", FIELD_MODEM_TYPE, + MODEM_TYPE_OPTIONS[state_.modem_type_index]); row++; - - dy = visible_y(row); - if (dy >= 0) draw_selector_field(dy, c1, c2, "Code Rate", FIELD_CODERATE, - CODE_RATE_OPTIONS[state_.code_rate_index]); - row++; - - dy = visible_y(row); - if (dy >= 0) draw_selector_field(dy, c1, c2, "Frame Size", FIELD_FRAMESIZE, - state_.short_frame ? "SHORT" : "NORMAL"); - row++; - + + if (state_.modem_type_index == 0) { + // OFDM fields + dy = visible_y(row); + if (dy >= 0) draw_selector_field(dy, c1, c2, "Modulation", FIELD_MODULATION, + MODULATION_OPTIONS[state_.modulation_index]); + row++; + + dy = visible_y(row); + if (dy >= 0) draw_selector_field(dy, c1, c2, "Code Rate", FIELD_CODERATE, + CODE_RATE_OPTIONS[state_.code_rate_index]); + row++; + + dy = visible_y(row); + if (dy >= 0) draw_selector_field(dy, c1, c2, "Frame Size", FIELD_FRAMESIZE, + state_.short_frame ? "SHORT" : "NORMAL"); + row++; + } else { + // MFSK field + dy = visible_y(row); + if (dy >= 0) draw_selector_field(dy, c1, c2, "MFSK Mode", FIELD_MFSK_MODE, + MFSK_MODE_OPTIONS[state_.mfsk_mode_index]); + row++; + } + dy = visible_y(row); if (dy >= 0) { char freq_buf[32]; @@ -2353,12 +2436,6 @@ private: } row++; - dy = visible_y(row); - if (dy >= 0) { - attron(A_DIM); - mvaddstr(dy, c1, "Both sides must have frag enabled"); - attroff(A_DIM); - } row++; dy = visible_y(row); @@ -2569,6 +2646,35 @@ private: printw(" TX "); if (tx_time < 60) printw("%.0fs", tx_time); else printw("%.1fm", tx_time / 60.0f); + y++; + + + + + { + bool hf_ok = (state_.modem_type_index == 1) || + (state_.modulation_index <= 2); // BPSK, QPSK, 8PSK + mvaddstr(y, c3, "Band "); + if (hf_ok) { + attron(COLOR_PAIR(3) | A_BOLD); + addstr("HF/VHF"); + attroff(COLOR_PAIR(3) | A_BOLD); + } else { + attron(A_DIM); + addstr("HF/VHF"); + attroff(A_DIM); + } + addstr(" "); + if (!hf_ok) { + attron(COLOR_PAIR(3) | A_BOLD); + addstr("VHF/UHF"); + attroff(COLOR_PAIR(3) | A_BOLD); + } else { + attron(A_DIM); + addstr("VHF/UHF"); + attroff(A_DIM); + } + } y += 2; // Right side, for audio / ptt status