From cf44076a34e16dedd2dcac3ca0a84729ecadfe0d Mon Sep 17 00:00:00 2001 From: VasylSamoilov <20946668+VasylSamoilov@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:37:38 +0300 Subject: [PATCH] FLEX RX: comprehensive decoder rewrite (#3131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseband (proc_flex.cpp): - Fix numeric vector field width (3-bit n+1 for types 3,4,7) - Fix capcode range classification for long address components - Add long address decode (Set 1-2, 1-3/1-4, 2-3) - Add vector checksum validation (4-bit nibble sum) - Add tone-only boundary detection via vector pre-scan - Add idle phase detection (skip all-zeros/all-ones before BCH) - Skip spurious TON for capcode 1 (BCH-corrected idle artifact) - Add BIW parsing: SSID1/SSID2, DATE, TIME, TZ, SYSMSG, CHAN - Add alpha fragment flags: frag, more_frag, seq, new, maildrop, sig - Add secure message enc= field (alpha/binary/separate) - Add numbered numeric flags: seq, new, fmt - Add HEX/Binary header decode: block_bits, new, maildrop, rtl - Add short instruction decode: temp group slot/target, sys events - Add short message (SMSG) decode: tone/numeric/source/numbered - Add group/temp-group/priority flags per address - Add S2 C-pattern detection with ±1 symbol boundary correction - Extract FIW roaming, repeat, traffic fields - Replace snprintf with lightweight str_* helpers (no heap/_sbrk) - Mark uncorrectable BCH words as ? instead of aborting phase - ETX/NUL handling: trim trailing padding, show mid-message as ? App (ui_flex_rx.cpp): - Add status bar: cycle/frame, bitrate, polarity, time, timezone - Add network info bar: LID, CZ, CC, roaming flag - Pipe-delimited serial output for all message types - BIW events to serial and status bar (not console) - Human-readable console: +GRP for temp group, G/TG/P flags - USB serial via UsbSerialAsyncmsg Packet (flex_defs.hpp): - uint64_t capcode (long address support) - 256-byte message buffer - Fragment flags, BIW raw values, address type, group/priority Known limitation: phase label unreliable on multi-phase modes (3200/2FSK, 6400/4FSK). Data always decodes correctly. --- .../external/flex_rx/ui_flex_rx.cpp | 314 +++++++- .../external/flex_rx/ui_flex_rx.hpp | 21 +- firmware/baseband/proc_flex.cpp | 715 ++++++++++++++++-- firmware/baseband/proc_flex.hpp | 26 +- firmware/common/flex_defs.hpp | 40 +- 5 files changed, 1035 insertions(+), 81 deletions(-) diff --git a/firmware/application/external/flex_rx/ui_flex_rx.cpp b/firmware/application/external/flex_rx/ui_flex_rx.cpp index ccc7410d5..3f2abefd6 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.cpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -4,6 +4,7 @@ #include "portapack_persistent_memory.hpp" #include "string_format.hpp" #include "memory_map.hpp" +#include "usb_serial_asyncmsg.hpp" using namespace portapack; @@ -19,6 +20,8 @@ FlexAppView::FlexAppView(NavigationView& nav) &field_lna, &field_vga, &rssi, + &text_status1, + &text_status2, &console}); // Restore saved frequency @@ -54,12 +57,7 @@ void FlexAppView::focus() { // Redraw all messages to console void FlexAppView::redraw_console() { console.clear(true); - bool first = true; for (const auto& msg : messages) { - if (!first) { - console.writeln(""); // Blank line between messages - } - first = false; console.writeln(msg); } } @@ -67,15 +65,13 @@ void FlexAppView::redraw_console() { // Add message to log with automatic line wrapping void FlexAppView::log_message(const std::string& message) { const size_t chars_per_line = screen_width / 8; - // Console height accounts for status bar and controls row - const size_t console_lines = (screen_height - 2 * 16) / 16; + // 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); - // Calculate total lines used (messages + blank lines between them) size_t total_lines = 0; for (size_t i = 0; i < messages.size(); i++) { - if (i > 0) total_lines++; // Count blank line separator 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; @@ -88,15 +84,10 @@ void FlexAppView::log_message(const std::string& message) { size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line; if (oldest_lines == 0) oldest_lines = 1; total_lines -= oldest_lines; - if (messages.size() > 1) total_lines--; // Remove separator line too messages.erase(messages.begin()); } redraw_console(); } else { - // Just append new message - if (messages.size() > 1) { - console.writeln(""); // Blank line before new message - } console.writeln(message); } } @@ -107,9 +98,287 @@ void FlexAppView::update_freq(rf::Frequency f) { receiver_model.set_target_frequency(f); } +// Type tag from numeric type code +static const char* flex_type_tag(uint32_t type) { + switch (type) { + case 0: + return "SEC"; + case 1: + return "INS"; + case 2: + return "TON"; + case 3: + return "NUM"; + case 4: + return "SNUM"; + case 5: + return "ALN"; + case 6: + return "HEX"; + case 7: + return "NNUM"; + case 8: + return "SMSG"; + case 9: + return "BIW"; + default: + return "UNK"; + } +} + // Handle decoded FLEX packet from baseband void FlexAppView::on_packet(const FlexPacketMessage* message) { - log_message(message->packet.message); + const auto& pkt = message->packet; + const char* type = flex_type_tag(pkt.type); + const char* pol = pkt.is_inverted ? "I" : "N"; + + // Update status row 1: C/F speed polarity time timezone + { + std::string s1 = "C" + to_string_dec_uint(pkt.cycle) + + "/F" + to_string_dec_uint(pkt.frame) + + " " + to_string_dec_uint(pkt.bitrate) + + " " + pol; + if (status_time_[0]) { + s1 += " "; + s1 += status_time_; + } + if (status_tz_[0]) { + s1 += " "; + s1 += status_tz_; + } + text_status1.set(s1); + } + + // Update status row 2 from BIW data + if (pkt.type == 9) { + switch (pkt.biw_field) { + case 0: // SSID1 + status_lid_ = pkt.biw_v1; + status_cz_ = pkt.biw_v2; + break; + case 2: { // Time + uint32_t si = (pkt.biw_v3 * 75) / 10; + auto h = to_string_dec_uint(pkt.biw_v1, 2, '0'); + auto m = to_string_dec_uint(pkt.biw_v2, 2, '0'); + auto sec = to_string_dec_uint(si, 2, '0'); + // Store for row 1 + auto ts = h + ":" + m + ":" + sec; + memcpy(status_time_, ts.c_str(), ts.size() + 1); + break; + } + 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; + auto tzs = std::string("UTC") + (ofs >= 0 ? "+" : "") + + to_string_dec_int(ofs / 60); + memcpy(status_tz_, tzs.c_str(), tzs.size() + 1); + } + break; + } + case 7: // SSID2 + status_cc_ = pkt.biw_v1; + break; + } + // Rebuild row 2 + std::string s2; + if (status_lid_) s2 += "LID:" + to_string_dec_uint(status_lid_); + if (status_cz_) { + s2 += " CZ:"; + s2 += to_string_dec_uint(status_cz_); + } + if (status_cc_) { + s2 += " CC:"; + s2 += to_string_dec_uint(status_cc_); + } + if (pkt.fiw_roaming) s2 += " R"; + text_status2.set(s2); + } + + // Console: skip BIW events (shown in status bar), show messages only + if (pkt.type != 9) { + 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) + " "; + + 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 += to_string_dec_uint(pkt.biw_v1); + line += "@F"; + line += to_string_dec_uint(pkt.biw_v2); + } else if (pkt.type == 1) { + // Other INS types + line += to_string_dec_uint(pkt.capcode); + line += " INS "; + line += pkt.message; + } else if (pkt.addr_type == 2) { + // Temp address delivery: "GRP5 ALN message" + uint32_t slot = (uint32_t)(pkt.capcode + 0x8000 - 0x1F7800) & 0x0F; + line += "GRP"; + line += to_string_dec_uint(slot); + line += " "; + line += type; + if (pkt.message[0]) { + line += " "; + line += pkt.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; + if (pkt.message[0]) { + line += " "; + line += pkt.message; + } + } + log_message(line); + } + + // 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 += '/'; + s += to_string_dec_uint(pkt.frame); + s += '|'; + s += to_string_dec_uint(pkt.bitrate); + s += '|'; + s += pol; + s += '|'; + s += pkt.phase; + + if (pkt.type == 9) { + // BIW: format from raw values for serial + s += "|BIW"; + 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; + } + } 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 + // capcode = aw - 0x8000, aw = capcode + 0x8000 + // slot = (aw - 0x1F7800) & 0x0F = (capcode + 0x8000 - 0x1F7800) & 0x0F + uint32_t slot = (uint32_t)(pkt.capcode + 0x8000 - 0x1F7800) & 0x0F; + s += "|slot="; + s += to_string_dec_uint(slot); + } + if (pkt.has_flags) { + if (pkt.type == 7) { + s += "|seq="; + s += to_string_dec_uint(pkt.seq); + s += "|new="; + s += to_string_dec_uint(pkt.is_new); + s += "|fmt="; + s += pkt.nnum_s ? "idrom" : "std"; + } else { + s += "|frag="; + s += (pkt.frag == 3) ? "first" : to_string_dec_uint(pkt.frag).c_str(); + s += "|mf="; + s += to_string_dec_uint(pkt.more_frag); + s += "|seq="; + s += to_string_dec_uint(pkt.seq); + if (pkt.frag == 3) { + s += "|new="; + s += to_string_dec_uint(pkt.is_new); + s += "|md="; + s += to_string_dec_uint(pkt.maildrop); + s += "|sig="; + s += to_string_hex(pkt.sig, 2); + } + if (pkt.type == 0) { + static const char* enc[] = {"alpha", "sep", "bin", "rsvd"}; + s += "|enc="; + s += enc[pkt.sec_enc & 3]; + } + if (pkt.type == 6 && pkt.frag == 3) { + uint8_t b = pkt.function & 0x0F; + s += "|bb="; + s += to_string_dec_uint(b == 0 ? 16 : b); + if (pkt.function & 0x10) s += "|rtl=1"; + } + } + } + if (pkt.message[0]) { + s += "|\""; + s += pkt.message; + s += '"'; + } + } + UsbSerialAsyncmsg::asyncmsg(s); + } } // Handle stats message (currently unused) @@ -118,12 +387,15 @@ void FlexAppView::on_stats(const FlexStatsMessage*) { // Debug handler - uncomment to see baseband debug messages void FlexAppView::on_debug(const FlexDebugMessage* message) { - (void)message; // Suppress unused parameter warning - // std::string text = "DBG: "; - // text += message->text; - // text += " " + to_string_hex(message->val1, 8); - // text += " " + to_string_hex(message->val2, 8); - // log_message(text); + if (portapack::usb_serial.serial_connected()) { + std::string s = "DBG|"; + s += message->text; + s += "|"; + s += to_string_dec_int(message->val1); + s += "|"; + s += to_string_dec_int(message->val2); + UsbSerialAsyncmsg::asyncmsg(s); + } } } // namespace ui::external_app::flex_rx \ No newline at end of file diff --git a/firmware/application/external/flex_rx/ui_flex_rx.hpp b/firmware/application/external/flex_rx/ui_flex_rx.hpp index 7f182878a..bce30a130 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.hpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -26,10 +26,17 @@ class FlexAppView : public View { NavigationView& nav_; // Saved settings - rf::Frequency frequency_value{931740000}; // Default FLEX frequency + rf::Frequency frequency_value{931740000}; RxRadioState radio_state_{}; + // Status bar state (updated from BIW packets) + char status_time_[12]{}; // "HH:MM:SS" + char status_tz_[12]{}; // "UTC+N" + uint16_t status_lid_{0}; + 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{}; @@ -54,9 +61,17 @@ class FlexAppView : public View { RSSI rssi{ {UI_POS_X(21), 0, UI_POS_WIDTH(9), 4}}; - // Message display area (below controls, account for status bar) + // Status rows (rows 1-2) + Text text_status1{ + {0, 1 * 16, screen_width, 16}, + ""}; + Text text_status2{ + {0, 2 * 16, screen_width, 16}, + ""}; + + // Message display area (below status rows) Console console{ - {0, 1 * 16, screen_width, screen_height - 2 * 16}}; + {0, 3 * 16, screen_width, screen_height - 4 * 16}}; // Persistent settings manager app_settings::SettingsManager settings_{ diff --git a/firmware/baseband/proc_flex.cpp b/firmware/baseband/proc_flex.cpp index d44588e5d..37e8bfeb4 100644 --- a/firmware/baseband/proc_flex.cpp +++ b/firmware/baseband/proc_flex.cpp @@ -7,7 +7,42 @@ #include #include -#include // for snprintf + +// Lightweight string helpers (no snprintf/heap on bare-metal M4) +namespace { + +char* str_append(char* dst, const char* end, const char* src) { + while (*src && dst < end - 1) *dst++ = *src++; + *dst = '\0'; + return dst; +} + +char* str_uint(char* dst, const char* end, uint32_t val, int min_digits = 1) { + char tmp[11]; + int i = 0; + if (val == 0) { + tmp[i++] = '0'; + } else { + while (val > 0) { + tmp[i++] = '0' + (val % 10); + val /= 10; + } + } + while (i < min_digits) tmp[i++] = '0'; + for (int j = i - 1; j >= 0 && dst < end - 1; j--) *dst++ = tmp[j]; + *dst = '\0'; + return dst; +} + +char* str_hex(char* dst, const char* end, uint32_t val, int digits) { + static const char hex[] = "0123456789ABCDEF"; + for (int i = digits - 1; i >= 0 && dst < end - 1; i--) + *dst++ = hex[(val >> (i * 4)) & 0xF]; + *dst = '\0'; + return dst; +} + +} // namespace // Constants from demod_flex.c #define FREQ_SAMP 24000 // Our sample rate @@ -155,7 +190,6 @@ uint32_t bit_reverse_32(uint32_t x) { void FlexProcessor::send_debug(const char* text, uint32_t v1, uint32_t v2) { if (shared_memory.application_queue.is_empty()) return; - FlexDebugMessage message(v1, v2, text); shared_memory.application_queue.push(message); } @@ -403,7 +437,9 @@ int FlexProcessor::decode_fiw() { fiw.checksum = fiw_val & 0xF; fiw.cycleno = (fiw_val >> 4) & 0xF; fiw.frameno = (fiw_val >> 8) & 0x7F; - fiw.fix3 = (fiw_val >> 15) & 0x3F; + fiw.roaming = (fiw_val >> 15) & 0x01; + fiw.repeat = (fiw_val >> 16) & 0x01; + fiw.traffic = (fiw_val >> 17) & 0x0F; unsigned int checksum = (fiw_val & 0xF); checksum += ((fiw_val >> 4) & 0xF); @@ -509,6 +545,9 @@ void FlexProcessor::flex_sym(unsigned char sym) { if (state.fiwcount == 48) { if (decode_fiw() == 0) { state.sync2_count = 0; + state.sync2_shiftreg = 0; + state.sync2_c_pos = -1; + state.sync2_cinv_pos = -1; demodulator.baud = sync.baud; state.Current = flex::State::SYNC2; send_debug("FIW OK", fiw.frameno, fiw.cycleno); @@ -520,8 +559,48 @@ void FlexProcessor::flex_sym(unsigned char sym) { break; } case flex::State::SYNC2: { - if (++state.sync2_count == sync.baud * 25 / 1000) { - state.data_count = 0; + /* S2 structure: BS2 + C(16 bits) + inv.BS2 + inv.C(16 bits) + * Total duration: 25ms at the data symbol rate. + * + * We scan for the 16-bit C pattern (0xED84) using a shift + * 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 + * 2-level pattern even in 4FSK modes. */ + unsigned char s2_sym = sync.polarity ? (3 - sym) : sym; + int bit_a = (s2_sym > 1) ? 1 : 0; + state.sync2_shiftreg = (state.sync2_shiftreg << 1) | bit_a; + state.sync2_count++; + + /* Check for C pattern match (Hamming distance <= 2) */ + if (state.sync2_count >= 16) { + uint16_t diff_c = state.sync2_shiftreg ^ 0xED84; + uint16_t diff_cinv = state.sync2_shiftreg ^ 0x127B; + int errs_c = __builtin_popcount(diff_c); + int errs_cinv = __builtin_popcount(diff_cinv); + + if (errs_c <= 2 && state.sync2_c_pos < 0) + state.sync2_c_pos = (int)state.sync2_count; + if (errs_cinv <= 2 && state.sync2_cinv_pos < 0) + state.sync2_cinv_pos = (int)state.sync2_count; + } + + /* Nominal S2 duration in symbols */ + unsigned int s2_nominal = sync.baud * 25 / 1000; + + /* Data starts after inv.C ends. If we detected inv.C, + * use its position as the true data boundary. Otherwise + * fall back to the nominal count. */ + unsigned int s2_end = s2_nominal; + if (state.sync2_cinv_pos > 0) { + unsigned int cinv_end = (unsigned int)state.sync2_cinv_pos; + int diff = (int)cinv_end - (int)s2_nominal; + if (diff >= -1 && diff <= 1) + s2_end = cinv_end; + } + + if (state.sync2_count == s2_end) { // Clear phase data for (int i = 0; i < 88; i++) { data.PhaseA.buf[i] = 0; @@ -535,9 +614,21 @@ void FlexProcessor::flex_sym(unsigned char sym) { data.PhaseD.idle_count = 0; data.phase_toggle = 0; data.data_bit_counter = 0; + state.data_count = 0; + + state.sync2_shiftreg = 0; + state.sync2_c_pos = -1; + state.sync2_cinv_pos = -1; state.Current = flex::State::DATA; } + /* Safety: don't get stuck past nominal */ + if (state.sync2_count > s2_nominal + 1) { + state.sync2_shiftreg = 0; + state.sync2_c_pos = -1; + state.sync2_cinv_pos = -1; + state.Current = flex::State::SYNC1; + } break; } case flex::State::DATA: { @@ -593,26 +684,190 @@ void FlexProcessor::decode_phase(char PhaseNo) { return; } + /* 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. */ + { + int all_idle = 1; + for (int i = 0; i < 88; i++) { + if (phaseptr[i] != 0xFFFFFFFF && phaseptr[i] != 0x00000000) { + all_idle = 0; + break; + } + } + if (all_idle) return; + } + + /* BCH decode each word. Mark uncorrectable words but continue. */ + uint8_t word_bad[88] = {0}; for (int i = 0; i < 88; i++) { int decode_error = bch_fix_errors(&phaseptr[i]); - if (decode_error > 2) return; - phaseptr[i] &= 0x001FFFFF; // Extract message bits + if (decode_error > 2) { + word_bad[i] = 1; + phaseptr[i] = 0; + } + phaseptr[i] &= 0x001FFFFF; } + /* BIW must be good to proceed */ + if (word_bad[0]) return; + uint32_t biw = phaseptr[0]; if (biw == 0 || biw == 0x001FFFFF) return; int voffset = (biw >> 10) & 0x3f; int aoffset = ((biw >> 8) & 0x03) + 1; + int prio_count = (biw >> 4) & 0x0F; // number of priority address words + if (voffset < aoffset || voffset >= 88) return; + + /* 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. */ + for (int bw = 1; bw < aoffset && bw < 88; bw++) { + if (word_bad[bw]) continue; + uint32_t bword = phaseptr[bw]; + uint32_t btype = (bword >> 4) & 0x07; + + /* Skip reserved types (3, 4, 6) */ + if (btype == 3 || btype == 4 || btype == 6) continue; + + flex::FlexPacket bpkt{}; + bpkt.type = 9; // BIW event + 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 = bw; // BIW word index + bpkt.biw_field = btype; // BIW type (0,1,2,5,7) + bpkt.message[0] = '\0'; + + switch (btype) { + case 0: // SSID1: v1=lid, v2=cz + bpkt.biw_v1 = (bword >> 12) & 0x01FF; + bpkt.biw_v2 = (bword >> 7) & 0x1F; + break; + case 1: // Date: v1=year(+1994), v2=month, v3=day + bpkt.biw_v1 = ((bword >> 7) & 0x1F) + 1994; + bpkt.biw_v2 = (bword >> 17) & 0x0F; + bpkt.biw_v3 = (bword >> 12) & 0x1F; + break; + case 2: // Time: v1=hour, v2=minute, v3=sec_raw(0-7) + bpkt.biw_v1 = (bword >> 7) & 0x1F; + bpkt.biw_v2 = (bword >> 12) & 0x3F; + bpkt.biw_v3 = (bword >> 18) & 0x07; + break; + case 5: // SysInfo: v1=a_type, v2=info(10 bits) + bpkt.biw_v1 = (bword >> 7) & 0x0F; + bpkt.biw_v2 = (bword >> 11) & 0x03FF; + break; + case 7: // SSID2: v1=country, v2=tmf + bpkt.biw_v1 = (bword >> 11) & 0x03FF; + bpkt.biw_v2 = (bword >> 7) & 0x0F; + break; + default: + continue; + } + send_packet(bpkt); + } + + /* Pre-scan: count valid vector words using 4-bit nibble checksum. + * 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 + * consecutive ones from the start. */ + int n_valid_vecs = 0; + for (int vi = 0; vi < (voffset - aoffset); vi++) { + int wi = voffset + vi; + if (wi >= 88) break; + uint32_t vw = phaseptr[wi]; + uint32_t csum = (vw & 0xF) + ((vw >> 4) & 0xF) + ((vw >> 8) & 0xF) + + ((vw >> 12) & 0xF) + ((vw >> 16) & 0xF) + ((vw >> 20) & 0x1); + if ((csum & 0xF) == 0xF) + n_valid_vecs = vi + 1; // track highest passing index + 1 + } + + /* No addresses if voffset == aoffset */ + if (voffset <= aoffset) return; + + int vec_count = 0; + int addr_count = 0; // tracks address word position for priority detection for (int i = aoffset; i < voffset; i++) { - int j = voffset + i - aoffset; + int j = voffset + vec_count; + if (j >= 88) break; if (phaseptr[i] == 0x00000000 || phaseptr[i] == 0x001FFFFF) continue; - parse_capcode(phaseptr[i]); - if (decode.long_address) continue; // Skip long addresses for now + /* 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; + int is_priority = (addr_count < prio_count) ? 1 : 0; - if (decode.capcode > 4297068542ll || decode.capcode < 0) continue; + parse_capcode(phaseptr[i]); + decode.is_group = is_group; + decode.is_temp_group = is_temp_group; + decode.is_priority = is_priority; + addr_count++; + + if (decode.long_address) { + /* Long address: 2 address words, 2 vector words. + * Read second address word and compute capcode from set. */ + if (i + 1 >= voffset) break; // truncated + uint32_t aw1 = phaseptr[i]; + uint32_t aw2 = phaseptr[i + 1]; + if (aw2 == 0x00000000 || aw2 == 0x001FFFFF) { + i++; + addr_count++; // second address word counts + vec_count += 2; + continue; + } + + int64_t cap = 0; + if (aw1 >= 0x000001 && aw1 <= 0x008000 && + aw2 >= 0x1F7FFF && aw2 <= 0x1FFFFE) { + /* Set 1-2 */ + cap = (int64_t)aw1 + (int64_t)(0x1FFFFF - aw2) * 32768LL + 2068480LL; + } else if (aw1 >= 0x000001 && aw1 <= 0x008000 && + aw2 >= 0x1E0001 && aw2 <= 0x1F0000) { + /* Set 1-3 / 1-4 */ + cap = (int64_t)aw1 + (int64_t)(aw2 - 1933312) * 32768LL + 2068480LL; + } else if (aw1 >= 0x1F7FFF && aw1 <= 0x1FFFFE && + aw2 >= 0x1E0001 && aw2 <= 0x1F0000) { + /* Set 2-3 */ + cap = (int64_t)(aw1 - 2064383) + (int64_t)(aw2 - 1867776) * 32768LL + 2068479LL; + } else { + /* Unknown set — skip */ + i++; + addr_count++; // second address word counts + vec_count += 2; + continue; + } + + decode.capcode = cap; + i++; // consumed 2 address words + addr_count++; // second address word also counts + + /* 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. */ + vec_count += 2; // consumed 2 vector words + j = voffset + vec_count - 2; // point to first vector word of pair + } else { + if (decode.capcode > 4297068542ll || decode.capcode <= 0) continue; + + /* Tone-only: address beyond valid vector range */ + if (vec_count >= n_valid_vecs) { + parse_tone_only(phaseptr, PhaseNo, 0); + continue; + } + vec_count++; + } uint32_t viw = phaseptr[j]; int type_val = (viw >> 4) & 0x07; @@ -645,7 +900,14 @@ void FlexProcessor::decode_phase(char PhaseNo) { } int mw1 = (viw >> 7) & 0x7F; - int len = (viw >> 14) & 0x7F; + int len; + /* Numeric types (3, 4, 7) have a 3-bit n field (bits 14-16) + * encoding word_count - 1. Bits 17-20 are the K checksum. + * Alpha/hex/secure types use the full 7-bit field (bits 14-20). */ + if (type_val == 3 || type_val == 4 || type_val == 7) + len = ((viw >> 14) & 0x07) + 1; + else + len = (viw >> 14) & 0x7F; int mw2 = mw1 + (len - 1); if (mw1 == 0 && mw2 == 0) continue; @@ -653,84 +915,408 @@ void FlexProcessor::decode_phase(char PhaseNo) { if (decode.type == flex::PageType::ALPHANUMERIC || decode.type == flex::PageType::SECURE) { if (mw1 > 87 || mw2 > 87) continue; - parse_alphanumeric(phaseptr, PhaseNo, mw1, mw2, 0); + if (decode.long_address) { + /* For long addresses, body[0] (header with K,C,F,N,R,M) is at + * Vy (j+1), not at mw1. The vector's mw1 points to body[1] + * in the message field, and len includes body[0]. + * parse_alphanumeric expects mw1 = header word index (it does + * mw1++ internally to skip header). So pass mw1-1 so the + * skip lands on mw1 (first real data word). */ + parse_alphanumeric(phaseptr, word_bad, PhaseNo, mw1 - 1, mw2 - 1, 0); + } else { + 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); } else if (decode.type == flex::PageType::TONE) { - parse_tone_only(phaseptr, PhaseNo, j); - } else { - // Unknown or unsupported + /* Vector type 2: Short Message / Tone. + * 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; + + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); + packet.capcode = decode.capcode; + packet.function = 0; + 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; + + 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); + } + } 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); + } + } 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); + } + } 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); + } + } + send_packet(packet); + } else if (decode.type == flex::PageType::BINARY) { + /* HEX/Binary message. + * Word 1 (mw1): K(12) C(1) F(2) N(6) = header + * Word 2 (mw1+1, first frag only): R(1) M(1) D(1) H(1) B(4) I(1) rsvd(4) S(8) + * Words 3+: data */ + if (mw1 > 87 || mw2 > 87) continue; + + /* Extract header from word 1 */ + uint8_t hex_c = 0, hex_f = 0, hex_n = 0; + int hex_hdr_valid = 0; + if (!word_bad[mw1]) { + uint32_t hw1 = phaseptr[mw1]; + hex_c = (hw1 >> 12) & 0x01; + hex_f = (hw1 >> 13) & 0x03; + hex_n = (hw1 >> 15) & 0x3F; + hex_hdr_valid = 1; + } + + /* Extract word 2 flags (first fragment: F=3) */ + uint8_t hex_r = 0, hex_m = 0, hex_d = 0, hex_b = 0; + int data_start = mw1 + 1; // default: data starts after header + if (hex_f == 3 && (mw1 + 1) <= mw2 && !word_bad[mw1 + 1]) { + uint32_t hw2 = phaseptr[mw1 + 1]; + hex_r = (hw2 >> 0) & 0x01; + hex_m = (hw2 >> 1) & 0x01; + hex_d = (hw2 >> 2) & 0x01; + hex_b = (hw2 >> 4) & 0x0F; + data_start = mw1 + 2; // skip both header words + } + + /* Dump data words as hex */ + char message[256] = {0}; + char *mp = message, *me = message + 250; + for (int w = data_start; w <= mw2 && mp < me; w++) { + if (word_bad[w]) { + mp = str_append(mp, me, "?????? "); + } else { + mp = str_hex(mp, me, phaseptr[w] & 0x1FFFFF, 5); + if (mp < me) *mp++ = ' '; + *mp = '\0'; + } + } + if (mp > message && *(mp - 1) == ' ') { + mp--; + *mp = '\0'; + } + int pos = (int)(mp - message); + + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); + packet.capcode = decode.capcode; + packet.function = 0; + packet.type = 6; // HEX + packet.status = 0; + 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; + if (hex_hdr_valid) { + packet.frag = hex_f; + packet.more_frag = hex_c; + packet.seq = hex_n; + packet.has_flags = 1; + if (hex_f == 3) { + packet.is_new = hex_r; + packet.maildrop = hex_m; + /* Store b and d in function field: low nibble=b, bit4=d */ + packet.function = (hex_d << 4) | hex_b; + } + } + memcpy(packet.message, message, pos + 1); + send_packet(packet); + } else if (decode.type == flex::PageType::SHORT_INSTRUCTION) { + /* Short instruction: 14-bit data in vector bits 7-20. + * i2i1i0 (bits 0-2 of data) = instruction type. + * Remaining bits = instruction-specific data. */ + uint32_t instr_data = (viw >> 7) & 0x3FFF; + uint32_t itype = instr_data & 0x07; + + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); + packet.capcode = decode.capcode; + packet.function = 0; + packet.type = 1; // INS + 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; + + if (itype == 0) { + uint32_t tgt_frame = (instr_data >> 3) & 0x7F; + uint32_t slot = (instr_data >> 10) & 0x0F; + packet.biw_v1 = slot; + packet.biw_v2 = tgt_frame; + { + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "i=temp|slot="); + p = str_uint(p, e, slot); + p = str_append(p, e, "|target="); + str_uint(p, e, tgt_frame); + } + } else if (itype == 1) { + uint32_t flags = (instr_data >> 3) & 0x7FF; + { + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "i=event|flags="); + str_hex(p, e, flags, 3); + } + } else { + { + char *p = packet.message, *e = p + sizeof(packet.message); + p = str_append(p, e, "i=rsvd|type="); + p = str_uint(p, e, itype); + p = str_append(p, e, "|raw="); + str_hex(p, e, instr_data, 4); + } + } + send_packet(packet); } } } void FlexProcessor::parse_capcode(uint32_t aw1) { - decode.long_address = (aw1 < 0x008001L) || (aw1 > 0x1E0000L) || (aw1 > 0x1E7FFEL); + /* Classify address word by range. */ + decode.long_address = 0; + decode.addr_type = flex::AddrType::SHORT; + + if ((aw1 >= 0x000001 && aw1 <= 0x008000) || /* LA1 */ + (aw1 >= 0x1E0001 && aw1 <= 0x1E8000) || /* LA3 */ + (aw1 >= 0x1E8001 && aw1 <= 0x1F0000) || /* LA4 */ + (aw1 >= 0x1F7FFF && aw1 <= 0x1FFFFE)) { /* LA2 */ + decode.long_address = 1; + decode.addr_type = flex::AddrType::LONG; + } else if (aw1 >= 0x1F7800 && aw1 <= 0x1F780F) { + decode.addr_type = flex::AddrType::TEMPORARY; + } else if (aw1 >= 0x1F7810 && aw1 <= 0x1F781F) { + decode.addr_type = flex::AddrType::OPERATOR; + } else if (aw1 >= 0x1F6800 && aw1 <= 0x1F77FF) { + decode.addr_type = flex::AddrType::NETWORK; + } else if (aw1 >= 0x1F2800 && aw1 <= 0x1F67FF) { + decode.addr_type = flex::AddrType::INFO_SVC; + } else if ((aw1 >= 0x1F0001 && aw1 <= 0x1F27FF) || + (aw1 >= 0x1F7820 && aw1 <= 0x1F7FFE)) { + decode.addr_type = flex::AddrType::RESERVED; + } else if (aw1 >= 0x008001 && aw1 <= 0x1E0000) { + decode.addr_type = flex::AddrType::SHORT; + } else { + decode.addr_type = flex::AddrType::UNKNOWN; + } + decode.capcode = aw1 - 0x8000; } -void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, char, int mw1, int mw2, int) { - char message[128] = {0}; // Fixed buffer for message +void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int mw1, int mw2, int) { + char message[256] = {0}; int currentChar = 0; - // int frag = (phaseptr[mw1] >> 11) & 0x03; - // int cont = (phaseptr[mw1] >> 0x0A) & 0x01; - // Helper logic for fragmentation (ignored for basic display) - + /* First message word is the header (K, C, F, N, R, M fields). + * Extract flags before skipping to content. */ + uint8_t hdr_c = 0, hdr_f = 0, hdr_n = 0, hdr_r = 0, hdr_m = 0; + uint8_t hdr_sig = 0; + int hdr_valid = 0; + if (mw1 >= 0 && mw1 < 88 && !word_bad[mw1]) { + uint32_t hdr = phaseptr[mw1]; + hdr_c = (hdr >> 10) & 0x01; // bit 10 + hdr_f = (hdr >> 11) & 0x03; // bits 11-12 + hdr_n = (hdr >> 13) & 0x3F; // bits 13-18 + hdr_r = (hdr >> 19) & 0x01; // bit 19 + hdr_m = (hdr >> 20) & 0x01; // bit 20 + hdr_valid = 1; + } mw1++; + /* Extract signature from first data word (bits 0-6) */ + if (mw1 >= 0 && mw1 < 88 && !word_bad[mw1]) { + hdr_sig = phaseptr[mw1] & 0x7F; + } + for (int i = mw1; i <= mw2; i++) { unsigned int dw = phaseptr[i]; unsigned char ch; + int bad = (i >= 0 && i < 88) ? word_bad[i] : 1; - // Extract chars (7-bit ASCII) - // If i > mw1 (not first word) or fragment check (simplified here) if (i > mw1) { ch = dw & 0x7F; - if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch; + if (bad) { + if (currentChar < 255) message[currentChar++] = '?'; + } else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) { + if (currentChar < 255) message[currentChar++] = ch; + } else if (ch == 0x03 || ch == 0x00) { + if (currentChar < 255) message[currentChar++] = '\x03'; + } } ch = (dw >> 7) & 0x7F; - if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch; + if (bad) { + if (currentChar < 255) message[currentChar++] = '?'; + } else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) { + if (currentChar < 255) message[currentChar++] = ch; + } else if (ch == 0x03 || ch == 0x00) { + if (currentChar < 255) message[currentChar++] = '\x03'; + } ch = (dw >> 14) & 0x7F; - if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch; + if (bad) { + if (currentChar < 255) message[currentChar++] = '?'; + } else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) { + if (currentChar < 255) message[currentChar++] = ch; + } else if (ch == 0x03 || ch == 0x00) { + if (currentChar < 255) message[currentChar++] = '\x03'; + } + } + + /* Post-process: trim trailing ETX/NUL padding, but if printable chars + * appear after an ETX/NUL, show each ETX/NUL as '?' (invalid char). */ + { + /* First find the last printable character */ + int last_printable = -1; + for (int k = 0; k < currentChar; k++) { + if (message[k] != '\x03') last_printable = k; + } + /* Now output up to last_printable, replacing ETX with '?' */ + int out = 0; + for (int k = 0; k <= last_printable && out < 255; k++) { + if (message[k] == '\x03') + message[out++] = '?'; + else + message[out++] = message[k]; + } + currentChar = out; } message[currentChar] = '\0'; - flex::FlexPacket packet; - packet.bitrate = sync.baud; + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); packet.capcode = decode.capcode; - packet.function = 0; // TODO extract function if available - packet.type = 5; // ALPHANUMERIC - packet.status = 0; // OK + packet.function = 0; + packet.type = (decode.type == flex::PageType::SECURE) ? 0 : 5; + packet.status = 0; + 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; + if (hdr_valid) { + packet.frag = hdr_f; + packet.more_frag = hdr_c; + packet.seq = hdr_n; + packet.is_new = hdr_r; + packet.maildrop = hdr_m; + packet.sig = hdr_sig; + packet.has_flags = 1; + if (decode.type == flex::PageType::SECURE) { + /* Secure: bits 19-20 are t1t0 (encoding type), not R/M */ + packet.sec_enc = (hdr_r) | (hdr_m << 1); // t0=bit19, t1=bit20 + packet.is_new = 0; + packet.maildrop = 0; + } + } memcpy(packet.message, message, currentChar + 1); send_packet(packet); } -void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) { - // Simplified numeric parsing - char message[128] = {0}; +void FlexProcessor::parse_numeric(uint32_t* phaseptr, 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... */ + uint8_t nnum_n = 0, nnum_r = 0, nnum_s = 0; + int is_nnum = (decode.type == flex::PageType::NUMBERED_NUMERIC); + int w1 = phaseptr[j] >> 7; int w2 = w1 >> 7; w1 = w1 & 0x7f; w2 = (w2 & 0x07) + 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; + int dw; - // Handle short vs long logic if needed (simplified) dw = phaseptr[w1]; + + 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 + } + w1++; w2++; unsigned char digit = 0; - int count = 4; // Standard numeric skip - if (decode.type == flex::PageType::NUMBERED_NUMERIC) - count += 10; + int count = 4; + if (is_nnum) + count += 10; // skip K5K4(2) + N(6) + R(1) + S(1) else - count += 2; + count += 2; // skip K5K4(2) int idx = 0; for (int i = w1; i <= w2; i++) { @@ -739,7 +1325,7 @@ void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) { if (dw & 0x01) digit ^= 0x08; dw >>= 1; if (--count == 0) { - if (digit != 0x0C && idx < 127) { + if (digit != 0x0C && idx < 255) { message[idx++] = flex_bcd[digit]; } count = 4; @@ -749,25 +1335,56 @@ void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) { } message[idx] = '\0'; - flex::FlexPacket packet; - packet.bitrate = sync.baud; + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); packet.capcode = decode.capcode; packet.function = 0; - packet.type = 3; // NUMERIC + /* Set correct type: 3=NUM, 4=SNUM, 7=NNUM */ + if (decode.type == flex::PageType::SPECIAL_NUMERIC) + packet.type = 4; + else if (is_nnum) + packet.type = 7; + else + packet.type = 3; packet.status = 0; + 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; + if (is_nnum) { + packet.seq = nnum_n; + packet.is_new = nnum_r; + packet.nnum_s = nnum_s; + packet.has_flags = 1; + } memcpy(packet.message, message, idx + 1); send_packet(packet); } -void FlexProcessor::parse_tone_only(uint32_t*, char, int) { - flex::FlexPacket packet; - packet.bitrate = sync.baud; +void FlexProcessor::parse_tone_only(uint32_t*, char PhaseNo, int) { + if (decode.capcode == 1) return; // idle artifact + flex::FlexPacket packet{}; + packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1); packet.capcode = decode.capcode; packet.function = 0; packet.type = 2; // TONE packet.status = 0; - snprintf(packet.message, sizeof(packet.message), "Tone Only"); + 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; + strcpy(packet.message, ""); send_packet(packet); } diff --git a/firmware/baseband/proc_flex.hpp b/firmware/baseband/proc_flex.hpp index 3de318810..276449a4c 100644 --- a/firmware/baseband/proc_flex.hpp +++ b/firmware/baseband/proc_flex.hpp @@ -66,6 +66,11 @@ struct FlexStateInfo { unsigned int fiwcount = 0; State Current = State::SYNC1; State Previous = State::SYNC1; + + // S2 C-pattern detection + uint16_t sync2_shiftreg = 0; // 16-bit shift register for C match + int sync2_c_pos = -1; // symbol position where C was found (-1=not found) + int sync2_cinv_pos = -1; // symbol position where inv.C was found }; struct FlexSync { @@ -81,7 +86,9 @@ struct FlexFIW { unsigned int checksum = 0; unsigned int cycleno = 0; unsigned int frameno = 0; - unsigned int fix3 = 0; + unsigned int roaming = 0; // bit 15: n (1=roaming provided) + unsigned int repeat = 0; // bit 16: r (1=multiple transmission) + unsigned int traffic = 0; // bits 17-20: t3-t0 }; struct FlexPhase { @@ -98,10 +105,25 @@ struct FlexData { FlexPhase PhaseD; }; +enum class AddrType : uint8_t { + SHORT, // normal individual + LONG, // 9-10 digit + TEMPORARY, // 0x1F7800-0F (16 group slots) + OPERATOR, // 0x1F7810-1F (system messages) + NETWORK, // 0x1F6800-77FF (NID) + INFO_SVC, // 0x1F2800-67FF (under study) + RESERVED, // reserved ranges + UNKNOWN +}; + struct FlexDecode { PageType type = PageType::ALPHANUMERIC; 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; }; } // namespace flex @@ -161,7 +183,7 @@ class FlexProcessor : public BasebandProcessor { // Parsing void parse_capcode(uint32_t aw1); - void parse_alphanumeric(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2, int flex_groupmessage); + 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_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 b1ff63ebd..6147cb20f 100644 --- a/firmware/common/flex_defs.hpp +++ b/firmware/common/flex_defs.hpp @@ -21,12 +21,40 @@ struct FlexStats { }; struct FlexPacket { - uint32_t bitrate; // 1600, 3200, 6400 - uint32_t capcode; - uint32_t function; // 0-3 - uint32_t type; // Message type (e.g. ALN, NUM, etc - could use enum) - char message[128]; // Decoded message text - uint32_t status; // 0=OK, other=Errors + 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 + char message[256]; // Decoded message text (not used for BIW) + uint32_t status; // 0=OK, other=Errors + uint8_t cycle; // FIW cycle (0-14) + uint8_t frame; // FIW frame (0-127) + char phase; // 'A','B','C','D' + uint8_t is_inverted; // 1=inverted polarity + 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) + + // BIW raw values (type=9 only). biw_field identifies the content. + // 0=SSID1 1=DATE 2=TIME 5=SYSINFO 7=SSID2 + uint8_t biw_field; // BIW type field (0-7) + uint16_t biw_v1; // field-dependent value 1 + uint16_t biw_v2; // field-dependent value 2 + uint16_t biw_v3; // field-dependent value 3 + uint16_t biw_v4; // field-dependent value 4 }; } /* namespace flex */