mirror of
https://github.com/RFnexus/modem73.git
synced 2026-04-27 14:30:33 +00:00
Project restructuring, enable auto-fragmentation detection, MFSK-8, 16, 32/R modes, control port spec doc
This commit is contained in:
commit
9bfcf0b564
12 changed files with 1168 additions and 93 deletions
119
CONTROL_PORT.md
Normal file
119
CONTROL_PORT.md
Normal file
|
|
@ -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": "<base64-encoded payload>", "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 |
|
||||
14
Makefile
14
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/
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
#include <poll.h>
|
||||
|
||||
extern "C" {
|
||||
#include "cJSON.h"
|
||||
#include "deps/cJSON.h"
|
||||
}
|
||||
|
||||
// Base64 decode (RFC 4648)
|
||||
|
|
|
|||
0
cJSON.c → deps/cJSON.c
vendored
0
cJSON.c → deps/cJSON.c
vendored
0
cJSON.h → deps/cJSON.h
vendored
0
cJSON.h → deps/cJSON.h
vendored
0
miniaudio.c → deps/miniaudio.c
vendored
0
miniaudio.c → deps/miniaudio.c
vendored
0
miniaudio.h → deps/miniaudio.h
vendored
0
miniaudio.h → deps/miniaudio.h
vendored
193
kiss_tnc.cc
193
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<Encoder48k>();
|
||||
std::cerr << " Creating decoder" << std::endl;
|
||||
decoder_ = std::make_unique<Decoder48k>();
|
||||
std::cerr << " Encoder/decoder created" << std::endl;
|
||||
|
||||
|
||||
// Allocate MFSK encoder/decoder
|
||||
std::cerr << " Creating MFSK encoder/decoder" << std::endl;
|
||||
mfsk_encoder_ = std::make_unique<MFSKEncoder>();
|
||||
mfsk_decoder_ = std::make_unique<MFSKDecoder>(
|
||||
(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<float>* 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<float> 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<Encoder48k> encoder_;
|
||||
std::unique_ptr<Decoder48k> decoder_;
|
||||
|
||||
std::unique_ptr<MFSKEncoder> mfsk_encoder_;
|
||||
std::unique_ptr<MFSKDecoder> mfsk_decoder_;
|
||||
|
||||
std::unique_ptr<MiniAudio> audio_;
|
||||
std::unique_ptr<RigctlPTT> rigctl_;
|
||||
std::unique_ptr<SerialPTT> 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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <vector>
|
||||
#include <string>
|
||||
|
|
|
|||
731
phy/mfsk_modem.hh
Normal file
731
phy/mfsk_modem.hh
Normal file
|
|
@ -0,0 +1,731 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
#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<int> tmp(data, data + n);
|
||||
for (int i = 0; i < n; i++)
|
||||
data[bit_reverse(i, bits)] = tmp[i];
|
||||
}
|
||||
|
||||
inline void interleave_vectors(std::vector<std::vector<float>>& data) {
|
||||
int n = data.size();
|
||||
int bits = 0;
|
||||
while ((1 << bits) < n) bits++;
|
||||
std::vector<std::vector<float>> tmp(data);
|
||||
for (int i = 0; i < n; i++)
|
||||
data[bit_reverse(i, bits)] = std::move(tmp[i]);
|
||||
}
|
||||
|
||||
inline std::vector<int> 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<int> 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<uint8_t> finish(int data_bits) {
|
||||
int best = 0;
|
||||
for (int s = 1; s < STATES; s++)
|
||||
if (metric_[s] > metric_[best]) best = s;
|
||||
std::vector<int> 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<uint8_t> 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<int> puncture_34(const std::vector<int>& coded) {
|
||||
std::vector<int> 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<float> depuncture_34(const float* soft, int n_soft) {
|
||||
std::vector<float> 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<int> bits_to_gray_symbols(const std::vector<int>& bits, int bps) {
|
||||
std::vector<int> 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<float> 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<uint8_t> 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<int> 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<float> generate_audio(const std::vector<int>& tones, int base_bin) {
|
||||
const int N = MFSKParams::SYMBOL_LEN;
|
||||
const int G = MFSKParams::GUARD_SAMPLES;
|
||||
const float amp = 0.8f;
|
||||
std::vector<float> 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<void(const uint8_t*, size_t)>;
|
||||
|
||||
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<float> 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<float> 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<std::vector<float>> 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<float> 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<float> 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<float> 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;
|
||||
}
|
||||
};
|
||||
196
tnc_ui.hh
196
tnc_ui.hh
|
|
@ -25,9 +25,13 @@
|
|||
#include <fcntl.h>
|
||||
|
||||
#include "kiss_tnc.hh"
|
||||
#include "phy/mfsk_modem.hh"
|
||||
|
||||
constexpr size_t MAX_LOG_ENTRIES = 500;
|
||||
|
||||
const std::vector<std::string> MODEM_TYPE_OPTIONS = {"OFDM", "MFSK"};
|
||||
const std::vector<std::string> MFSK_MODE_OPTIONS = {"MFSK-8", "MFSK-16", "MFSK-32", "MFSK-32R"};
|
||||
|
||||
const std::vector<std::string> MODULATION_OPTIONS = {
|
||||
"BPSK", "QPSK", "8PSK", "QAM16", "QAM64", "QAM256", "QAM1024", "QAM4096"
|
||||
};
|
||||
|
|
@ -49,9 +53,11 @@ const std::vector<std::string> 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue