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 0000000..f43cadd Binary files /dev/null and b/deps/cJSON.o differ 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