mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-16 12:53:25 +00:00
FLEX RX: comprehensive decoder rewrite (#3131)
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.
This commit is contained in:
+293
-21
@@ -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
|
||||
+18
-3
@@ -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<std::string> 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_{
|
||||
|
||||
+666
-49
@@ -7,7 +7,42 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdio> // 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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user