From fc5beb0c093e631389aab390446476951b80911f Mon Sep 17 00:00:00 2001 From: VasylSamoilov <20946668+VasylSamoilov@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:15:05 +0300 Subject: [PATCH] FLEX RX: fix long-address numeric decode, improve short message format (#3136) - Fix long-address numeric message decoding: read body[0] from Vy (j+1) for multi-word messages, from w1 for single-word. Fix MF word count for long addresses (n_field words, not n_field+1). - Fix long-address short message: read 5 additional BCD digits from Vy for 8-digit decode. - Fix BCD table: index 10 is '.' (dot) not ' ' (space) in both RX and TX. - Clean up address decode: identify address type by value range per Table 3.8.1-1. Remove is_group/is_temp_group from packet structs. - Rename SMSG to SHORT. Format message payload directly in baseband: TONE, NUM abc, SRC N, SRC N N=M R=R, RESERVED hex. - Add BIW1 heartbeat packet (biw_field=0xFF) on every decoded frame. - Move timezone table to file scope, simplify console log_message, add No signal initial status, simplify serial BIW output format. --- .../external/flex_rx/ui_flex_rx.cpp | 140 +++-------- .../external/flex_rx/ui_flex_rx.hpp | 5 - .../external/flex_tx/ui_flex_tx.cpp | 8 +- firmware/baseband/proc_flex.cpp | 235 +++++++++++------- firmware/baseband/proc_flex.hpp | 4 +- firmware/common/flex_defs.hpp | 26 +- 6 files changed, 197 insertions(+), 221 deletions(-) diff --git a/firmware/application/external/flex_rx/ui_flex_rx.cpp b/firmware/application/external/flex_rx/ui_flex_rx.cpp index 3f2abefd6..7740f5a17 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.cpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -10,6 +10,9 @@ using namespace portapack; namespace ui::external_app::flex_rx { +static const int flex_tz_table[] = {0, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 660, 720, + 210, 270, 330, 0, 345, 390, 570, -210, -660, -600, -540, -480, -420, -360, -300, -240, -180, -120, -60}; + FlexAppView::FlexAppView(NavigationView& nav) : nav_{nav} { // Load baseband image for FLEX decoding @@ -42,6 +45,7 @@ FlexAppView::FlexAppView(NavigationView& nav) // Initialize FLEX baseband baseband::set_flex_config(); + text_status1.set("No signal"); console.writeln("Ready"); } @@ -54,42 +58,9 @@ void FlexAppView::focus() { field_frequency.focus(); } -// Redraw all messages to console -void FlexAppView::redraw_console() { - console.clear(true); - for (const auto& msg : messages) { - console.writeln(msg); - } -} - -// Add message to log with automatic line wrapping +// Add message to console void FlexAppView::log_message(const std::string& message) { - const size_t chars_per_line = screen_width / 8; - // Console height matches widget: starts at 3*16, height = screen_height - 4*16 - const size_t console_lines = (screen_height - 4 * 16) / 16; - - messages.push_back(message); - - size_t total_lines = 0; - for (size_t i = 0; i < messages.size(); i++) { - size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line; - if (msg_lines == 0) msg_lines = 1; - total_lines += msg_lines; - } - - // If console would overflow, remove oldest messages and redraw - if (total_lines > console_lines) { - while (total_lines > console_lines && !messages.empty()) { - const auto& oldest = messages.front(); - size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line; - if (oldest_lines == 0) oldest_lines = 1; - total_lines -= oldest_lines; - messages.erase(messages.begin()); - } - redraw_console(); - } else { - console.writeln(message); - } + console.writeln(message); } // Update frequency and save for persistence @@ -118,7 +89,7 @@ static const char* flex_type_tag(uint32_t type) { case 7: return "NNUM"; case 8: - return "SMSG"; + return "SHORT"; case 9: return "BIW"; default: @@ -168,12 +139,14 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { } case 5: { // SysInfo (timezone) if (pkt.biw_v1 == 4 || pkt.biw_v1 == 5) { - static const int tz[] = {0, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 660, 720, - 210, 270, 330, 0, 345, 390, 570, -210, -660, -600, -540, -480, -420, -360, -300, -240, -180, -120, -60}; uint16_t zone = pkt.biw_v2 & 0x1F; - int ofs = (zone < 32) ? tz[zone] : 0; + int ofs = (zone < 32) ? flex_tz_table[zone] : 0; + int hrs = ofs / 60; + int mins = (ofs < 0 ? -ofs : ofs) % 60; auto tzs = std::string("UTC") + (ofs >= 0 ? "+" : "") + - to_string_dec_int(ofs / 60); + to_string_dec_int(hrs); + if (mins != 0) + tzs += ":" + to_string_dec_int(mins, 2, '0'); memcpy(status_tz_, tzs.c_str(), tzs.size() + 1); } break; @@ -197,8 +170,10 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { text_status2.set(s2); } - // Console: skip BIW events (shown in status bar), show messages only - if (pkt.type != 9) { + // Console: skip BIW, tone-only (V=010), SHORT reserved (t=3) + bool skip_gui = (pkt.type == 9 || pkt.type == 2); + if (pkt.type == 8 && pkt.function == 3) skip_gui = true; + if (!skip_gui) { auto cf = to_string_dec_uint(pkt.cycle) + "/" + to_string_dec_uint(pkt.frame); std::string line = cf + " " + to_string_dec_uint(pkt.bitrate) + " " + pol + " " + std::string(1, pkt.phase) + " "; @@ -206,7 +181,7 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { if (pkt.type == 1 && pkt.message[0] == 'i' && pkt.message[2] == 't') { // INS temp group: "1234567 +GRP5@F42" line += to_string_dec_uint(pkt.capcode); - line += " +GRP"; + line += " +TG"; line += to_string_dec_uint(pkt.biw_v1); line += "@F"; line += to_string_dec_uint(pkt.biw_v2); @@ -216,9 +191,9 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { line += " INS "; line += pkt.message; } else if (pkt.addr_type == 2) { - // Temp address delivery: "GRP5 ALN message" + // Temp group delivery: "GRP5 ALN message" uint32_t slot = (uint32_t)(pkt.capcode + 0x8000 - 0x1F7800) & 0x0F; - line += "GRP"; + line += "TG"; line += to_string_dec_uint(slot); line += " "; line += type; @@ -228,7 +203,6 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { } } else { line += to_string_dec_uint(pkt.capcode); - if (pkt.is_group) line += pkt.is_temp_group ? " TG" : " G"; if (pkt.is_priority) line += " P"; line += " "; line += type; @@ -243,7 +217,6 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { // Serial: pipe-delimited if (portapack::usb_serial.serial_connected()) { std::string s; - s.reserve(320); s = "FLEX|"; s += to_string_dec_uint(pkt.cycle); s += '/'; @@ -256,76 +229,21 @@ void FlexAppView::on_packet(const FlexPacketMessage* message) { s += pkt.phase; if (pkt.type == 9) { - // BIW: format from raw values for serial - s += "|BIW"; + s += "|BIW|w="; s += to_string_dec_uint(pkt.function); - switch (pkt.biw_field) { - case 0: - s += "|SSID|lid="; - s += to_string_dec_uint(pkt.biw_v1); - s += "|cz="; - s += to_string_dec_uint(pkt.biw_v2); - break; - case 1: - s += "|DATE|"; - s += to_string_dec_uint(pkt.biw_v1); - s += '-'; - s += to_string_dec_uint(pkt.biw_v2, 2, '0'); - s += '-'; - s += to_string_dec_uint(pkt.biw_v3, 2, '0'); - break; - case 2: { - uint32_t si = (pkt.biw_v3 * 75) / 10; - s += "|TIME|"; - s += to_string_dec_uint(pkt.biw_v1, 2, '0'); - s += ':'; - s += to_string_dec_uint(pkt.biw_v2, 2, '0'); - s += ':'; - s += to_string_dec_uint(si, 2, '0'); - break; - } - case 5: { - uint16_t a = pkt.biw_v1, info = pkt.biw_v2; - if (a == 4 || a == 5) { - static const int tz[] = {0, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 660, 720, - 210, 270, 330, 0, 345, 390, 570, -210, -660, -600, -540, -480, -420, -360, -300, -240, -180, -120, -60}; - uint16_t zone = info & 0x1F; - int ofs = (zone < 32) ? tz[zone] : 0; - int dst = (info >> 5) & 1; - s += "|TZ|UTC"; - s += (ofs >= 0 ? "+" : ""); - s += to_string_dec_int(ofs / 60); - s += "h"; - int m = (ofs < 0 ? -ofs : ofs) % 60; - if (m) { - s += to_string_dec_uint(m, 2, '0'); - s += "m"; - } - s += "|dst="; - s += dst ? "no" : "yes"; - } else if (a <= 3) { - static const char* t[] = {"all", "home", "roaming", "ssid"}; - s += "|SYSMSG|target="; - s += t[a]; - } else if (a == 6) { - s += "|CHAN|ofs="; - s += to_string_dec_uint(info & 0x3F); - } - break; - } - case 7: - s += "|SSID2|cc="; - s += to_string_dec_uint(pkt.biw_v1); - s += "|tmf="; - s += to_string_dec_uint(pkt.biw_v2); - break; - } + s += "|t="; + s += to_string_dec_uint(pkt.biw_field); + s += '|'; + s += to_string_dec_uint(pkt.biw_v1); + s += '|'; + s += to_string_dec_uint(pkt.biw_v2); + s += '|'; + s += to_string_dec_uint(pkt.biw_v3); } else { s += '|'; s += type; s += "|cap="; s += to_string_dec_uint(pkt.capcode); - if (pkt.is_group) s += pkt.is_temp_group ? "|grp=temp" : "|grp=1"; if (pkt.is_priority) s += "|pri=1"; if (pkt.addr_type == 2) { // Temporary address: show slot number diff --git a/firmware/application/external/flex_rx/ui_flex_rx.hpp b/firmware/application/external/flex_rx/ui_flex_rx.hpp index bce30a130..ed35e041a 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.hpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -37,13 +37,8 @@ class FlexAppView : public View { uint16_t status_cz_{0}; uint16_t status_cc_{0}; - // Message storage for console redraw - static constexpr size_t MAX_MESSAGES = 20; - std::vector messages{}; - // Helper methods void log_message(const std::string& message); - void redraw_console(); void update_freq(rf::Frequency f); // UI Elements - Row 0, dynamically positioned diff --git a/firmware/application/external/flex_tx/ui_flex_tx.cpp b/firmware/application/external/flex_tx/ui_flex_tx.cpp index 5ad893c6c..0330ce20a 100644 --- a/firmware/application/external/flex_tx/ui_flex_tx.cpp +++ b/firmware/application/external/flex_tx/ui_flex_tx.cpp @@ -211,7 +211,7 @@ static uint32_t flex_numeric_vector(uint32_t type, uint32_t mw_start, uint32_t m // Short addr: 3 digits in d0-d11. Long addr: 3 + 5 in 2nd word. // Tone-only: call with empty string (all digits become space 0xC). static uint32_t flex_short_vector(int is_long, const std::string& msg, uint32_t* vy_out) { - static const char bcd_chars[20] = "0123456789 U -]["; + static const char bcd_chars[20] = "0123456789.U -]["; auto to_bcd = [&](char c) -> uint8_t { for (int k = 0; k < 16; k++) if (bcd_chars[k] == c) return k; @@ -321,7 +321,7 @@ static int flex_encode_alpha(const std::string& msg, uint32_t* words, int max_wo // ===== Numeric BCD encoding ===== static int flex_encode_numeric(const std::string& msg, uint32_t* words, int max_words, uint32_t* k_out) { - static const char bcd[20] = "0123456789 U -]["; + static const char bcd[20] = "0123456789.U -]["; uint32_t mw[8] = {0}; int bit = 2; int word_idx = 0; @@ -721,8 +721,8 @@ bool FlexTXView::start_tx() { int msg_type = options_type.selected_index(); if (msg_type == 1) { - if (message.find_first_not_of("0123456789 U-][") != std::string::npos) { - nav_.display_modal("Bad message", "Numeric: 0-9 U - ] [ space"); + if (message.find_first_not_of("0123456789.U -][") != std::string::npos) { + nav_.display_modal("Bad message", "Numeric: 0-9 . U - ] [ space"); return false; } } diff --git a/firmware/baseband/proc_flex.cpp b/firmware/baseband/proc_flex.cpp index 37e8bfeb4..58318d8d1 100644 --- a/firmware/baseband/proc_flex.cpp +++ b/firmware/baseband/proc_flex.cpp @@ -8,6 +8,9 @@ #include #include +// BCD character table for FLEX numeric messages (index 0-15) +static const char flex_bcd[] = "0123456789.U -]["; + // Lightweight string helpers (no snprintf/heap on bare-metal M4) namespace { @@ -566,7 +569,7 @@ void FlexProcessor::flex_sym(unsigned char sym) { * register. If found, we validate timing. If not found, * we fall back to the nominal 25ms skip (current behavior). * - * Only the MSB (bit_a) matters for C detection — it's a + * Only the MSB (bit_a) matters for C detection - it's a * 2-level pattern even in 4FSK modes. */ unsigned char s2_sym = sync.polarity ? (3 - sym) : sym; int bit_a = (s2_sym > 1) ? 1 : 0; @@ -687,7 +690,7 @@ void FlexProcessor::decode_phase(char PhaseNo) { /* Check if phase is all idle BEFORE BCH correction. * Idle fill uses alternating 0xFFFFFFFF and 0x00000000 words. * If every word is one of these two patterns, the phase has no - * real data — skip it to avoid BCH "correcting" idle into garbage. */ + * real data - skip it to avoid BCH "correcting" idle into garbage. */ { int all_idle = 1; for (int i = 0; i < 88; i++) { @@ -722,6 +725,23 @@ void FlexProcessor::decode_phase(char PhaseNo) { if (voffset < aoffset || voffset >= 88) return; + /* Always send BIW1 packet so the app knows we decoded a frame. + * This updates the status bar even for idle frames. */ + { + flex::FlexPacket bpkt{}; + bpkt.type = 9; + bpkt.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); + bpkt.cycle = fiw.cycleno; + bpkt.frame = fiw.frameno; + bpkt.phase = PhaseNo; + bpkt.is_inverted = sync.polarity; + bpkt.fiw_roaming = fiw.roaming; + bpkt.function = 0; + bpkt.biw_field = 0xFF; + bpkt.message[0] = '\0'; + send_packet(bpkt); + } + /* Parse BIW words (indices 1 through aoffset-1). * Each BIW word has a 3-bit type field (bits 4-6) that determines content. * Send each as a BIW event packet. */ @@ -778,7 +798,7 @@ void FlexProcessor::decode_phase(char PhaseNo) { * Tone-only addresses sit at the end of the address field with no * corresponding vector. We find the last vector that passes checksum. * Note: for long addresses, the 2nd vector word (Vy) is a message word - * that won't pass checksum — so we count all passing words, not just + * that won't pass checksum - so we count all passing words, not just * consecutive ones from the start. */ int n_valid_vecs = 0; for (int vi = 0; vi < (voffset - aoffset); vi++) { @@ -801,16 +821,13 @@ void FlexProcessor::decode_phase(char PhaseNo) { if (j >= 88) break; if (phaseptr[i] == 0x00000000 || phaseptr[i] == 0x001FFFFF) continue; - /* Extract group/temp flags from raw address word (bits 20, 19) - * before parse_capcode classifies the word by range. */ - uint32_t raw_aw = phaseptr[i]; - int is_group = (raw_aw >> 20) & 1; - int is_temp_group = is_group ? ((raw_aw >> 19) & 1) : 0; + /* Address word - all 21 information bits are address data + * per 3.8.2. Address type is determined by value range + * (Table 3.8.1-1). Temporary addresses are range + * 0x1F7800-0x1F780F (3.8.2.3), identified via addr_type. */ int is_priority = (addr_count < prio_count) ? 1 : 0; parse_capcode(phaseptr[i]); - decode.is_group = is_group; - decode.is_temp_group = is_temp_group; decode.is_priority = is_priority; addr_count++; @@ -841,7 +858,7 @@ void FlexProcessor::decode_phase(char PhaseNo) { /* Set 2-3 */ cap = (int64_t)(aw1 - 2064383) + (int64_t)(aw2 - 1867776) * 32768LL + 2068479LL; } else { - /* Unknown set — skip */ + /* Unknown set - skip */ i++; addr_count++; // second address word counts vec_count += 2; @@ -852,7 +869,7 @@ void FlexProcessor::decode_phase(char PhaseNo) { i++; // consumed 2 address words addr_count++; // second address word also counts - /* Long addresses always have vectors — they cannot be tone-only. + /* Long addresses always have vectors - they cannot be tone-only. * (Tone-only is only for short addresses at the end of AF.) * The second vector word (Vy) contains the first message word, * not a checksummed vector, so skip the pre-scan check here. */ @@ -927,9 +944,9 @@ void FlexProcessor::decode_phase(char PhaseNo) { parse_alphanumeric(phaseptr, word_bad, PhaseNo, mw1, mw2, 0); } } else if (decode.type == flex::PageType::STANDARD_NUMERIC || decode.type == flex::PageType::SPECIAL_NUMERIC || decode.type == flex::PageType::NUMBERED_NUMERIC) { - parse_numeric(phaseptr, PhaseNo, j); + parse_numeric(phaseptr, word_bad, PhaseNo, j); } else if (decode.type == flex::PageType::TONE) { - /* Vector type 2: Short Message / Tone. + /* Vector type 2: Short Message (3.9.2). * Sub-type t1t0 in bits 7-8, data d0-d11 in bits 9-20. */ uint32_t t = (viw >> 7) & 0x03; uint32_t d = (viw >> 9) & 0x0FFF; @@ -937,65 +954,69 @@ void FlexProcessor::decode_phase(char PhaseNo) { flex::FlexPacket packet{}; packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); packet.capcode = decode.capcode; - packet.function = 0; + packet.function = t; packet.cycle = fiw.cycleno; packet.frame = fiw.frameno; packet.phase = PhaseNo; packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; + packet.type = 8; // SHORT - if (t == 0 && d == 0) { - /* No data — pure tone via vector */ - packet.type = 8; // SMSG - strcpy(packet.message, "sub=tone"); - } else if (t == 0) { - /* Numeric: 3 BCD digits in d0-d11 */ - const char bcd[] = "0123456789 U -]["; - char digits[4]; - digits[0] = bcd[(d >> 0) & 0xF]; - digits[1] = bcd[(d >> 4) & 0xF]; - digits[2] = bcd[(d >> 8) & 0xF]; - digits[3] = '\0'; - packet.type = 8; // SMSG - { - char *p = packet.message, *e = p + sizeof(packet.message); - p = str_append(p, e, "sub=numeric|digits="); - str_append(p, e, digits); + if (t == 0 && d == 0xCCC) { + /* Tone-only: all digits are space (0xC) per STD-43A + * Table 3.9.2-1 note. For long addresses, also check Vy. */ + bool tone = true; + if (decode.long_address && j + 1 < 88) { + uint32_t vy = phaseptr[j + 1] & 0xFFFFF; + if (vy != 0xCCCCC) tone = false; } + if (tone) + strcpy(packet.message, "TONE"); + else + goto short_numeric; + } else if (t == 0) { + short_numeric: + /* Numeric: 3 BCD digits from Vx (d0-d11). + * Long addresses: 5 more digits from Vy (d12-d31), + * 8 digits total. d32 is spare (set to 0). */ + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "NUM "); + *p++ = flex_bcd[(d >> 0) & 0xF]; + *p++ = flex_bcd[(d >> 4) & 0xF]; + *p++ = flex_bcd[(d >> 8) & 0xF]; + if (decode.long_address && j + 1 < 88) { + uint32_t vy = phaseptr[j + 1]; + *p++ = flex_bcd[(vy >> 0) & 0xF]; + *p++ = flex_bcd[(vy >> 4) & 0xF]; + *p++ = flex_bcd[(vy >> 8) & 0xF]; + *p++ = flex_bcd[(vy >> 12) & 0xF]; + *p++ = flex_bcd[(vy >> 16) & 0xF]; + } + *p = '\0'; } else if (t == 1) { /* Source: S2S1S0 in d0-d2 */ - packet.type = 8; - { - char *p = packet.message, *e = p + sizeof(packet.message); - p = str_append(p, e, "sub=source|src="); - str_uint(p, e, d & 0x07); - } + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "SRC "); + str_uint(p, e, d & 0x07); } else if (t == 2) { /* Numbered: S(3) + N(6) + R(1) */ uint32_t src = d & 0x07; uint32_t n = (d >> 3) & 0x3F; uint32_t r = (d >> 9) & 0x01; - packet.type = 8; - { - char *p = packet.message, *e = p + sizeof(packet.message); - p = str_append(p, e, "sub=numbered|src="); - p = str_uint(p, e, src); - p = str_append(p, e, "|seq="); - p = str_uint(p, e, n); - p = str_append(p, e, "|new="); - str_uint(p, e, r); - } + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "SRC "); + p = str_uint(p, e, src); + p = str_append(p, e, " N="); + p = str_uint(p, e, n); + p = str_append(p, e, " R="); + str_uint(p, e, r); } else { - packet.type = 8; - { - char *p = packet.message, *e = p + sizeof(packet.message); - p = str_append(p, e, "sub=reserved|raw="); - str_hex(p, e, d, 3); - } + /* Reserved */ + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "RESERVED "); + str_hex(p, e, d, 3); } send_packet(packet); } else if (decode.type == flex::PageType::BINARY) { @@ -1058,8 +1079,6 @@ void FlexProcessor::decode_phase(char PhaseNo) { packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; if (hex_hdr_valid) { packet.frag = hex_f; @@ -1093,8 +1112,6 @@ void FlexProcessor::decode_phase(char PhaseNo) { packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; if (itype == 0) { @@ -1253,8 +1270,6 @@ void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_b packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; if (hdr_valid) { packet.frag = hdr_f; @@ -1276,9 +1291,8 @@ void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_b send_packet(packet); } -void FlexProcessor::parse_numeric(uint32_t* phaseptr, char PhaseNo, int j) { +void FlexProcessor::parse_numeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int j) { char message[256] = {0}; - const char flex_bcd[] = "0123456789 U -]["; /* Extract NNUM header fields from first message word if applicable. * Layout: K5K4(2) + N0-N5(6) + R0(1) + S0(1) + BCD digits... */ @@ -1288,29 +1302,34 @@ void FlexProcessor::parse_numeric(uint32_t* phaseptr, char PhaseNo, int j) { int w1 = phaseptr[j] >> 7; int w2 = w1 >> 7; w1 = w1 & 0x7f; - w2 = (w2 & 0x07) + w1; + int n_field = w2 & 0x07; // word_count - 1 + w2 = n_field + w1; // Bounds check: phase buffer is 88 words (indices 0-87) - // w1 and w2 are incremented below, so clamp to 86 max - if (w1 > 86) return; - if (w2 > 86) w2 = 86; + if (w1 > 87) return; + if (w2 > 87) w2 = 87; - int dw; - dw = phaseptr[w1]; + /* For long addresses (3.9.1): + * 1-word: b field points to Vy. body[0] at w1. + * Multi-word: body[0] at Vy (j+1). b field points to MF body[1]. */ + int body0_idx; + if (decode.long_address && n_field > 0) + body0_idx = j + 1; // Vy = 2nd vector word + else + body0_idx = w1; + if (body0_idx < 0 || body0_idx >= 88) return; + + int dw = phaseptr[body0_idx]; if (is_nnum) { /* Extract N, R, S from the first message word's BCD stream. * After K5K4 (2 bits), next 6 bits = N, then R, then S. * These are consumed by the skip count (count starts at 4+10=14). */ - uint32_t first_word = phaseptr[w1]; - nnum_n = (first_word >> 2) & 0x3F; // bits 2-7 - nnum_r = (first_word >> 8) & 0x01; // bit 8 - nnum_s = (first_word >> 9) & 0x01; // bit 9 + nnum_n = (dw >> 2) & 0x3F; // bits 2-7 + nnum_r = (dw >> 8) & 0x01; // bit 8 + nnum_s = (dw >> 9) & 0x01; // bit 9 } - w1++; - w2++; - unsigned char digit = 0; int count = 4; if (is_nnum) @@ -1319,20 +1338,72 @@ void FlexProcessor::parse_numeric(uint32_t* phaseptr, char PhaseNo, int j) { count += 2; // skip K5K4(2) int idx = 0; - for (int i = w1; i <= w2; i++) { + + /* Phase 1: decode body[0] bits. + * For short addresses, body[0] is at w1 and we advance to w1+1. + * For long addresses, body[0] is at Vy (j+1), then we continue from w1. */ + if (word_bad[body0_idx]) { + /* Uncorrectable word — emit '?' for each digit slot */ + int data_bits = 21 - (count - 4); /* bits available after skip */ + int lost_digits = data_bits / 4; + while (lost_digits-- > 0 && idx < 255) + message[idx++] = '?'; + count = 4; /* reset for next word */ + digit = 0; + } else { for (int k = 0; k < 21; k++) { digit = (digit >> 1) & 0x0F; if (dw & 0x01) digit ^= 0x08; dw >>= 1; if (--count == 0) { - if (digit != 0x0C && idx < 255) { + if (idx < 255) { message[idx++] = flex_bcd[digit]; } count = 4; } } - dw = phaseptr[i]; } + + /* Phase 2: decode remaining body words from MF. + * Short: body[1..n] at w1+1 .. w2. + * Long: MF has n_field words at w1 .. w1+n_field-1. + * (n_field = total_words - 1; body[0] is at Vy, not in MF) */ + int start, end; + if (decode.long_address) { + start = w1; + end = w1 + n_field - 1; // empty when n_field=0 + } else { + start = w1 + 1; + end = w2; + } + for (int i = start; i <= end && i < 88; i++) { + if (word_bad[i]) { + /* Uncorrectable word — emit '?' for each digit slot (5 per word) */ + int lost_digits = 21 / 4; /* 5 digits per 21-bit word */ + while (lost_digits-- > 0 && idx < 255) + message[idx++] = '?'; + count = 4; + digit = 0; + continue; + } + dw = phaseptr[i]; + for (int k = 0; k < 21; k++) { + digit = (digit >> 1) & 0x0F; + if (dw & 0x01) digit ^= 0x08; + dw >>= 1; + if (--count == 0) { + if (idx < 255) { + message[idx++] = flex_bcd[digit]; + } + count = 4; + } + } + } + + /* Trim trailing BCD space padding (0x0C = ' '). + * The encoder pads unused nibble slots with 0x0C */ + while (idx > 0 && message[idx - 1] == ' ') + idx--; message[idx] = '\0'; flex::FlexPacket packet{}; @@ -1353,8 +1424,6 @@ void FlexProcessor::parse_numeric(uint32_t* phaseptr, char PhaseNo, int j) { packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; if (is_nnum) { packet.seq = nnum_n; @@ -1381,8 +1450,6 @@ void FlexProcessor::parse_tone_only(uint32_t*, char PhaseNo, int) { packet.is_inverted = sync.polarity; packet.fiw_roaming = fiw.roaming; packet.addr_type = static_cast(decode.addr_type); - packet.is_group = decode.is_group; - packet.is_temp_group = decode.is_temp_group; packet.is_priority = decode.is_priority; strcpy(packet.message, ""); diff --git a/firmware/baseband/proc_flex.hpp b/firmware/baseband/proc_flex.hpp index 276449a4c..0aaaaf4eb 100644 --- a/firmware/baseband/proc_flex.hpp +++ b/firmware/baseband/proc_flex.hpp @@ -121,8 +121,6 @@ struct FlexDecode { int long_address = 0; int64_t capcode = 0; AddrType addr_type = AddrType::SHORT; - int is_group = 0; - int is_temp_group = 0; int is_priority = 0; }; @@ -184,7 +182,7 @@ class FlexProcessor : public BasebandProcessor { // Parsing void parse_capcode(uint32_t aw1); void parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int mw1, int mw2, int flex_groupmessage); - void parse_numeric(uint32_t* phaseptr, char PhaseNo, int j); + void parse_numeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int j); void parse_tone_only(uint32_t* phaseptr, char PhaseNo, int j); void parse_unknown(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2); diff --git a/firmware/common/flex_defs.hpp b/firmware/common/flex_defs.hpp index 6147cb20f..06115f9f5 100644 --- a/firmware/common/flex_defs.hpp +++ b/firmware/common/flex_defs.hpp @@ -24,7 +24,7 @@ struct FlexPacket { uint32_t bitrate; // 1600, 3200, 6400 uint64_t capcode; // supports long addresses (up to 4,297,068,542) uint32_t function; // 0-3 (or BIW word index for type=9) - uint32_t type; // 0=SEC 1=INS 2=TON 3=NUM 4=SNUM 5=ALN 6=HEX 7=NNUM 8=SMSG 9=BIW + uint32_t type; // 0=SEC 1=INS 2=TON 3=NUM 4=SNUM 5=ALN 6=HEX 7=NNUM 8=SHORT 9=BIW char message[256]; // Decoded message text (not used for BIW) uint32_t status; // 0=OK, other=Errors uint8_t cycle; // FIW cycle (0-14) @@ -34,19 +34,17 @@ struct FlexPacket { uint8_t addr_type; // 0=short 1=long 2=temp 3=oper 4=net 5=info 6=rsvd 7=unk // Fragment flags (ALN/SEC/HEX) - uint8_t frag; // F field: 3=first, 0/1/2=continuation - uint8_t more_frag; // C bit - uint8_t seq; // N field (0-63) - uint8_t is_new; // R bit - uint8_t maildrop; // M bit - uint8_t sig; // 7-bit signature - uint8_t has_flags; // 1=fragment flags valid - uint8_t sec_enc; // secure encoding (0-3) - uint8_t nnum_s; // NNUM S flag - uint8_t fiw_roaming; // FIW n bit: 1=roaming supported - uint8_t is_group; // 1=group address - uint8_t is_temp_group; // 1=temporary group - uint8_t is_priority; // 1=priority address (in BIW1 P section) + uint8_t frag; // F field: 3=first, 0/1/2=continuation + uint8_t more_frag; // C bit + uint8_t seq; // N field (0-63) + uint8_t is_new; // R bit + uint8_t maildrop; // M bit + uint8_t sig; // 7-bit signature + uint8_t has_flags; // 1=fragment flags valid + uint8_t sec_enc; // secure encoding (0-3) + uint8_t nnum_s; // NNUM S flag + uint8_t fiw_roaming; // FIW n bit: 1=roaming supported + uint8_t is_priority; // 1=priority address (in BIW1 P section) // BIW raw values (type=9 only). biw_field identifies the content. // 0=SSID1 1=DATE 2=TIME 5=SYSINFO 7=SSID2