From 30002af3011b0d934ab046aaceabc5a5f3e10edc Mon Sep 17 00:00:00 2001 From: zenith <157907903+RFnexus@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:09:38 -0400 Subject: [PATCH] BER calculation and remove seed tones from SNR --- kiss_tnc.cc | 31 ++++++++------ modem.hh | 121 ++++++++++++++++++++++++++++++++++++++++++++-------- tnc_ui.hh | 47 ++++++++++++++++++-- 3 files changed, 166 insertions(+), 33 deletions(-) diff --git a/kiss_tnc.cc b/kiss_tnc.cc index 5a46a26..964e570 100644 --- a/kiss_tnc.cc +++ b/kiss_tnc.cc @@ -698,16 +698,16 @@ private: int level_update_counter = 0; const int LEVEL_UPDATE_INTERVAL = 5; - auto deliver_to_clients = [this](const std::vector& payload, float snr, bool was_reassembled) { - ui_log("RX: " + std::to_string(payload.size()) + " bytes, SNR=" + + auto deliver_to_clients = [this](const std::vector& payload, float snr, float ber_pct, bool was_reassembled) { + ui_log("RX: " + std::to_string(payload.size()) + " bytes, SNR=" + std::to_string((int)snr) + "dB" + (was_reassembled ? " (reassembled)" : "")); if (g_verbose) { std::cerr << packet_visualize(payload.data(), payload.size(), false, false) << std::endl; } - + #ifdef WITH_UI if (g_ui_state) { - g_ui_state->add_packet(false, payload.size(), snr); + g_ui_state->add_packet(false, payload.size(), snr, ber_pct); } #endif @@ -721,19 +721,24 @@ private: auto frame_callback = [this, &deliver_to_clients](const uint8_t* data, size_t len) { set_tx_lockout(RX_LOCKOUT_SECONDS); - + float snr = decoder_->get_last_snr(); - + float last_ber = decoder_->get_last_ber(); + float ber_pct = (last_ber >= 0) ? last_ber * 100.0f : -1.0f; + float ber_ema = decoder_->get_ber_ema(); + #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; + if (ber_ema >= 0) + g_ui_state->last_rx_ber = ber_ema; } #endif - + auto payload = unframe_length(data, len); - + if (payload.empty()) { ui_log("RX: Empty payload after unframing"); #ifdef WITH_UI @@ -741,19 +746,19 @@ private: #endif return; } - + if (config_.fragmentation_enabled && reassembler_.is_fragment(payload)) { if (g_verbose) { std::cerr << packet_visualize(payload.data(), payload.size(), false, true) << std::endl; } - + auto reassembled = reassembler_.process(payload); if (!reassembled.empty()) { ui_log("RX: Reassembled " + std::to_string(reassembled.size()) + " bytes from fragments"); - deliver_to_clients(reassembled, snr, true); + deliver_to_clients(reassembled, snr, ber_pct, true); } } else { - deliver_to_clients(payload, snr, false); + deliver_to_clients(payload, snr, ber_pct, false); } }; @@ -794,6 +799,8 @@ private: decoder_->stats_preamble_errors = 0; decoder_->stats_symbol_errors = 0; decoder_->stats_crc_errors = 0; + decoder_->reset_ber(); + 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; diff --git a/modem.hh b/modem.hh index e01ac26..bcc7138 100644 --- a/modem.hh +++ b/modem.hh @@ -448,12 +448,23 @@ public: // Get average SNR from last successful decode value get_last_snr() const { return last_avg_snr_; } - + // Get current modulation bits int get_mod_bits() const { return mod_bits; } - + + // Get last per-frame BER + value get_last_ber() const { return last_ber_; } + + // Get smoothed BER via EMA + value get_ber_ema() const { return ber_ema_; } + + void reset_ber() { + last_ber_ = -1; + ber_ema_ = -1; + } + // decode statistics - int stats_sync_count = 0; // corelator + int stats_sync_count = 0; // corelator int stats_preamble_errors = 0; // preamble decoding failed int stats_symbol_errors = 0; // seed damage int stats_crc_errors = 0; // polar CRC failed @@ -475,16 +486,23 @@ private: SchmidlCox* correlator_ptr = nullptr; CODE::HadamardDecoder<7> hadamard_decoder; CODE::PolarListDecoder polar_decoder; + CODE::PolarEncoder ber_encoder; + int8_t ber_mesg[bits_max], ber_code[bits_max]; DSP::Phasor osc; mesg_type mesg[bits_max]; code_type code[bits_max], perm[bits_max]; cmplx demod[tone_count], chan[tone_count], tone[tone_count]; + cmplx saved_demod[symbols_max * tone_count]; + int saved_seed_off[symbols_max]; + int fwd_perm_table[bits_max]; value index[tone_count], phase[tone_count]; value snr[symbols_max]; value cfo_rad; int symbol_pos; - value last_avg_snr_ = 0; + value last_avg_snr_ = 0; + value last_ber_ = -1; + value ber_ema_ = -1; State state_ = State::SEARCHING; size_t sample_count_ = 0; @@ -833,6 +851,30 @@ private: return true; } + void build_fwd_perm(int* table, int order) { + int len = 1 << order; + table[0] = 0; + if (order == 11) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } else if (order == 12) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } else if (order == 13) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } else if (order == 14) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } else if (order == 15) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } else if (order == 16) { + CODE::XorShiftMask seq; + for (int i = 1; i < len; ++i) table[i] = seq(); + } + } + bool process_symbol(int j) { seed_off = (block_skew * j + first_seed) % block_length; auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; }; @@ -887,15 +929,18 @@ private: demod[i] *= nrz(seq()); } + // Save demod for post-decode corrected SNR + std::memcpy(&saved_demod[j * tone_count], demod, tone_count * sizeof(cmplx)); + saved_seed_off[j] = seed_off; + // Notify constellation callback with fully-corrected demodulated symbols if (constellation_callback) { constellation_callback(demod, tone_count, mod_bits); } - - // SNR estimation and soft demapping + + // SNR estimation from data tones only excluding seed/pilot tones value sp = 0, np = 0; for (int i = 0, l = k_; i < tone_count; ++i) { - cmplx hard(1, 0); if (i % block_length != seed_off) { int bits = mod_bits; if (mod_bits == 3 && l % 32 == 30) bits = 2; @@ -903,12 +948,12 @@ private: if (mod_bits == 10 && l % 128 == 120) bits = 8; if (mod_bits == 12 && l % 128 == 120) bits = 8; demap_hard(perm + l, demod[i], bits); - hard = map_bits(perm + l, bits); + cmplx hard = map_bits(perm + l, bits); + cmplx error = demod[i] - hard; + sp += norm(hard); + np += norm(error); l += bits; } - cmplx error = demod[i] - hard; - sp += norm(hard); - np += norm(error); } value precision = sp / np; @@ -969,10 +1014,10 @@ private: return; } - // calculate average SNR from data symbols + // Fallback: average per-symbol SNR value total_snr = 0; int snr_count = 0; - for (int i = 1; i < symbol_index_; ++i) { // skip symbol 0 + for (int i = 1; i < symbol_index_; ++i) { if (snr[i] > 0) { total_snr += snr[i]; snr_count++; @@ -981,7 +1026,7 @@ private: if (snr_count > 0) { last_avg_snr_ = 10 * std::log10(total_snr / snr_count); } - + // Extract data for (int i = 0; i < data_bits; ++i) CODE::set_le_bit(data, i, mesg[i].v[best] < 0); @@ -990,9 +1035,51 @@ private: CODE::Xorshift32 scrambler; for (int i = 0; i < data_bytes; ++i) data[i] ^= scrambler(); - - std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB" << std::endl; - + + // BER: re-encode decoded message and compare against received hard decisions + int code_len = 1 << code_order; + for (int i = 0; i < data_bits + 32; ++i) + ber_mesg[i] = (mesg[i].v[best] < 0) ? -1 : 1; + ber_encoder(ber_code, ber_mesg, frozen_bits, code_order); + int bit_errors = 0; + for (int i = 0; i < code_len; ++i) { + if ((code[i] < 0) != (ber_code[i] < 0)) + bit_errors++; + } + last_ber_ = value(bit_errors) / value(code_len); + if (ber_ema_ < 0) + ber_ema_ = last_ber_; + else + ber_ema_ = value(0.3) * last_ber_ + value(0.7) * ber_ema_; + + // use known-correct codeword as referenc + build_fwd_perm(fwd_perm_table, code_order); + value corr_sp = 0, corr_np = 0; + int bk = 0; + for (int sj = 1; sj <= symbol_count; ++sj) { + int soff = saved_seed_off[sj]; + for (int i = 0; i < tone_count; ++i) { + if (i % block_length != soff) { + int bits = mod_bits; + if (mod_bits == 3 && bk % 32 == 30) bits = 2; + if (mod_bits == 6 && bk % 64 == 60) bits = 4; + if (mod_bits == 10 && bk % 128 == 120) bits = 8; + if (mod_bits == 12 && bk % 128 == 120) bits = 8; + code_type ideal_bits[mod_max]; + for (int b = 0; b < bits; ++b) + ideal_bits[b] = ber_code[fwd_perm_table[bk + b]]; + cmplx ideal = map_bits(ideal_bits, bits); + cmplx error = saved_demod[sj * tone_count + i] - ideal; + corr_sp += norm(ideal); + corr_np += norm(error); + bk += bits; + } + } + } + if (corr_np > 0) + last_avg_snr_ = 10 * std::log10(corr_sp / corr_np); + + std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB, BER=" << (last_ber_ * 100) << "%" << std::endl; callback(data, data_bytes); } diff --git a/tnc_ui.hh b/tnc_ui.hh index 0ee0f3f..6f5e29e 100644 --- a/tnc_ui.hh +++ b/tnc_ui.hh @@ -150,6 +150,7 @@ struct TNCUIState { std::atomic rx_frame_count{0}; std::atomic tx_frame_count{0}; std::atomic rx_error_count{0}; + std::atomic last_rx_ber{-1.0f}; // Decode statistics std::atomic sync_count{0}; @@ -237,6 +238,7 @@ struct TNCUIState { bool is_tx; int size; float snr; + float ber; // pre-FEC BER as percentage, -1 if unavailable std::chrono::steady_clock::time_point timestamp; }; static constexpr int MAX_RECENT_PACKETS = 8; @@ -382,10 +384,10 @@ struct TNCUIState { return result; } - void add_packet(bool is_tx, int size, float snr = 0.0f) { + void add_packet(bool is_tx, int size, float snr = 0.0f, float ber = -1.0f) { { std::lock_guard lock(packets_mutex); - recent_packets.push_back({is_tx, size, snr, std::chrono::steady_clock::now()}); + recent_packets.push_back({is_tx, size, snr, ber, std::chrono::steady_clock::now()}); if (recent_packets.size() > MAX_RECENT_PACKETS) { recent_packets.pop_front(); } @@ -1771,12 +1773,34 @@ private: attroff(COLOR_PAIR(3) | A_BOLD); y++; - // SNR history + // SNR history mvaddstr(y, c1, "SNR Hist"); move(y, c2); draw_snr_chart(20); y += 2; - + + mvaddstr(y, c1, "BER"); + { + float ber_pct = state_.last_rx_ber.load() * 100.0f; + if (ber_pct < 0) { + attron(A_DIM); + mvaddstr(y, c2, " ---"); + attroff(A_DIM); + } else if (ber_pct < 1.0f) { + attron(COLOR_PAIR(1) | A_BOLD); + mvprintw(y, c2, "%5.2f%%", ber_pct); + attroff(COLOR_PAIR(1) | A_BOLD); + } else if (ber_pct < 5.0f) { + attron(COLOR_PAIR(3) | A_BOLD); + mvprintw(y, c2, "%5.2f%%", ber_pct); + attroff(COLOR_PAIR(3) | A_BOLD); + } else { + attron(COLOR_PAIR(2) | A_BOLD); + mvprintw(y, c2, "%5.2f%%", ber_pct); + attroff(COLOR_PAIR(2) | A_BOLD); + } + } + y++; attron(A_DIM); mvaddstr(y, c1, "CSMA"); @@ -1949,6 +1973,21 @@ private: printw(" %.0fdB", pkt.snr); attroff(COLOR_PAIR(4) | A_BOLD); } + // BER + if (!pkt.is_tx && pkt.ber >= 0) { + float ber_pct = pkt.ber; + if (ber_pct < 1.0f) { + attron(COLOR_PAIR(1)); + } else if (ber_pct < 5.0f) { + attron(COLOR_PAIR(3)); + } else { + attron(COLOR_PAIR(2)); + } + printw(" %.1f%%", ber_pct); + attroff(COLOR_PAIR(1)); + attroff(COLOR_PAIR(2)); + attroff(COLOR_PAIR(3)); + } } }