mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-12 17:49:31 +00:00
POCSAG: decoder fixes, serial output, TX polarity cleanup (#3130)
RX: fix address/batch-boundary handling, add numeric continuation, add heuristic type detection toggle, pass inverted polarity flag, stream decoded messages over USB serial. TX: rename phase to polarity (Standard/Inverted), fix function validation (&&→||), fix serial shell payload read (offset tracking, 31-byte cap, preserve msglen). Decoder: remove color escapes from output (plain text for all consumers), guard count_alpha_fill against empty string, keep numeric_len across continuation batches. Serial format: POCSAG <baud> <addr> <func> <pol> <type> "<msg>" hex:<hex> Quote escaping for embedded " and \\ in alpha messages.
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
#include "string_format.hpp"
|
#include "string_format.hpp"
|
||||||
#include "utility.hpp"
|
#include "utility.hpp"
|
||||||
#include "file_path.hpp"
|
#include "file_path.hpp"
|
||||||
|
#include "usb_serial_asyncmsg.hpp"
|
||||||
|
|
||||||
using namespace portapack;
|
using namespace portapack;
|
||||||
using namespace pocsag;
|
using namespace pocsag;
|
||||||
@@ -62,6 +63,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
|||||||
&check_small_font,
|
&check_small_font,
|
||||||
&check_hide_bad,
|
&check_hide_bad,
|
||||||
&check_hide_addr_only,
|
&check_hide_addr_only,
|
||||||
|
&check_numeric_detect,
|
||||||
&opt_filter_mode,
|
&opt_filter_mode,
|
||||||
&field_filter_address,
|
&field_filter_address,
|
||||||
&button_save});
|
&button_save});
|
||||||
@@ -72,6 +74,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
|||||||
check_small_font.set_value(settings_.enable_small_font);
|
check_small_font.set_value(settings_.enable_small_font);
|
||||||
check_hide_bad.set_value(settings_.hide_bad_data);
|
check_hide_bad.set_value(settings_.hide_bad_data);
|
||||||
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
||||||
|
check_numeric_detect.set_value(settings_.enable_numeric_detect);
|
||||||
opt_filter_mode.set_by_value(settings_.filter_mode);
|
opt_filter_mode.set_by_value(settings_.filter_mode);
|
||||||
field_filter_address.set_value(settings_.filter_address);
|
field_filter_address.set_value(settings_.filter_address);
|
||||||
|
|
||||||
@@ -81,6 +84,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
|||||||
settings_.enable_small_font = check_small_font.value();
|
settings_.enable_small_font = check_small_font.value();
|
||||||
settings_.hide_bad_data = check_hide_bad.value();
|
settings_.hide_bad_data = check_hide_bad.value();
|
||||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||||
|
settings_.enable_numeric_detect = check_numeric_detect.value();
|
||||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||||
settings_.filter_address = field_filter_address.to_integer();
|
settings_.filter_address = field_filter_address.to_integer();
|
||||||
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
||||||
@@ -219,12 +223,24 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color indicates the message has a lot of decoding errors.
|
// Type indicator with its own color.
|
||||||
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
|
std::string type_str;
|
||||||
|
bool numeric_detect = settings_.enable_numeric_detect;
|
||||||
|
if (numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC)
|
||||||
|
type_str = STR_COLOR_GREEN "n";
|
||||||
|
else if (numeric_detect && pocsag_state.detected == pocsag::DET_ALPHA)
|
||||||
|
type_str = STR_COLOR_LIGHT_GREY "a";
|
||||||
|
else if (pocsag_state.out_type == ADDRESS)
|
||||||
|
type_str = STR_COLOR_DARK_YELLOW "t";
|
||||||
|
else
|
||||||
|
type_str = "";
|
||||||
|
|
||||||
std::string console_info = "\n" + color + prefix;
|
// Header: timestamp+baud in light grey, capcode+function in white.
|
||||||
console_info += " #" + to_string_dec_uint(pocsag_state.address);
|
std::string console_info = "\n" STR_COLOR_LIGHT_GREY + prefix;
|
||||||
|
console_info += STR_COLOR_WHITE " #" + to_string_dec_uint(pocsag_state.address);
|
||||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||||
|
if (!type_str.empty())
|
||||||
|
console_info += " " + type_str;
|
||||||
|
|
||||||
if (pocsag_state.out_type == ADDRESS) {
|
if (pocsag_state.out_type == ADDRESS) {
|
||||||
last_address = pocsag_state.address;
|
last_address = pocsag_state.address;
|
||||||
@@ -240,23 +256,110 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Serial: tone-only page */
|
||||||
|
if (portapack::usb_serial.serial_connected()) {
|
||||||
|
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S") + " tone";
|
||||||
|
UsbSerialAsyncmsg::asyncmsg(s);
|
||||||
|
}
|
||||||
|
|
||||||
} else if (pocsag_state.out_type == MESSAGE) {
|
} else if (pocsag_state.out_type == MESSAGE) {
|
||||||
if (pocsag_state.address != last_address) {
|
if (pocsag_state.new_message) {
|
||||||
// New message
|
|
||||||
last_address = pocsag_state.address;
|
last_address = pocsag_state.address;
|
||||||
|
serial_numeric_sent = 0;
|
||||||
console.writeln(console_info);
|
console.writeln(console_info);
|
||||||
console.write(color + pocsag_state.output);
|
|
||||||
|
// If heuristic chose numeric, show numeric line first (green).
|
||||||
|
if (numeric_detect &&
|
||||||
|
pocsag_state.detected == pocsag::DET_NUMERIC &&
|
||||||
|
pocsag_state.numeric_len > 0) {
|
||||||
|
std::string num_str(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||||
|
console.write(STR_COLOR_GREEN + num_str);
|
||||||
|
console.writeln("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpha decode (already has per-char color escapes from decoder).
|
||||||
|
console.write(pocsag_state.output);
|
||||||
|
|
||||||
|
/* Serial: header + first chunk.
|
||||||
|
* hex field contains rendered alpha as hex bytes (color escapes
|
||||||
|
* stripped). Non-printable and uncorrectable chars show as '.'
|
||||||
|
* (0x2E) — original 7-bit values are not preserved.
|
||||||
|
* Numeric decode goes in the quoted message field only. */
|
||||||
|
if (portapack::usb_serial.serial_connected()) {
|
||||||
|
/* Build hex representation of decoded alpha characters. */
|
||||||
|
std::string raw;
|
||||||
|
for (size_t i = 0; i < pocsag_state.output.size(); ++i)
|
||||||
|
raw += to_string_hex((uint8_t)pocsag_state.output[i], 2);
|
||||||
|
|
||||||
|
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S");
|
||||||
|
|
||||||
|
if (numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC) {
|
||||||
|
s += " numeric \"";
|
||||||
|
if (pocsag_state.numeric_len > 0)
|
||||||
|
s += std::string(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||||
|
s += "\"";
|
||||||
|
serial_numeric_sent = pocsag_state.numeric_len;
|
||||||
|
} else {
|
||||||
|
/* For alpha, the quoted text is the same chars as raw but as ASCII.
|
||||||
|
* Escape " and \ to avoid breaking the quoted field. */
|
||||||
|
s += " alpha \"";
|
||||||
|
for (size_t i = 0; i < pocsag_state.output.size(); ++i) {
|
||||||
|
if (pocsag_state.output[i] == '"' || pocsag_state.output[i] == '\\') {
|
||||||
|
s += '\\';
|
||||||
|
}
|
||||||
|
s += pocsag_state.output[i];
|
||||||
|
}
|
||||||
|
s += "\"";
|
||||||
|
}
|
||||||
|
s += " hex:" + raw;
|
||||||
|
UsbSerialAsyncmsg::asyncmsg(s);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Message continues...
|
// Message continues from previous batch.
|
||||||
console.write(color + pocsag_state.output);
|
bool is_numeric = numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC;
|
||||||
|
|
||||||
|
// GUI: show numeric continuation if applicable.
|
||||||
|
if (is_numeric && pocsag_state.numeric_len > serial_numeric_sent) {
|
||||||
|
std::string num_str(pocsag_state.numeric_buf + serial_numeric_sent,
|
||||||
|
pocsag_state.numeric_len - serial_numeric_sent);
|
||||||
|
console.write(STR_COLOR_GREEN + num_str);
|
||||||
|
}
|
||||||
|
console.write(pocsag_state.output);
|
||||||
|
|
||||||
|
/* Serial: continuation chunk with full header.
|
||||||
|
* Type label carries over from first batch (numeric+ or alpha+). */
|
||||||
|
if (portapack::usb_serial.serial_connected()) {
|
||||||
|
std::string raw;
|
||||||
|
std::string decoded;
|
||||||
|
for (size_t i = 0; i < pocsag_state.output.size(); ++i) {
|
||||||
|
if (pocsag_state.output[i] == '"' || pocsag_state.output[i] == '\\')
|
||||||
|
decoded += '\\';
|
||||||
|
decoded += pocsag_state.output[i];
|
||||||
|
raw += to_string_hex((uint8_t)pocsag_state.output[i], 2);
|
||||||
|
}
|
||||||
|
if (!decoded.empty() || pocsag_state.numeric_len > serial_numeric_sent) {
|
||||||
|
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S") + (is_numeric ? " numeric+" : " alpha+") + " \"";
|
||||||
|
if (is_numeric && pocsag_state.numeric_len > serial_numeric_sent)
|
||||||
|
s += std::string(pocsag_state.numeric_buf + serial_numeric_sent,
|
||||||
|
pocsag_state.numeric_len - serial_numeric_sent);
|
||||||
|
else
|
||||||
|
s += decoded;
|
||||||
|
s += "\" hex:" + raw;
|
||||||
|
UsbSerialAsyncmsg::asyncmsg(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serial_numeric_sent = pocsag_state.numeric_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logging()) {
|
if (logging()) {
|
||||||
logger.log_decoded(
|
std::string log_entry = to_string_dec_uint(pocsag_state.address) +
|
||||||
timestamp,
|
" F" + to_string_dec_uint(pocsag_state.function);
|
||||||
to_string_dec_uint(pocsag_state.address) +
|
if (numeric_detect &&
|
||||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
pocsag_state.detected == pocsag::DET_NUMERIC &&
|
||||||
" " + pocsag_state.output);
|
pocsag_state.numeric_len > 0)
|
||||||
|
log_entry += " N:" + std::string(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||||
|
log_entry += " " + pocsag_state.output;
|
||||||
|
logger.log_decoded(timestamp, log_entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,13 +403,19 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
|||||||
image_status.set_foreground(Theme::getInstance()->fg_magenta->foreground);
|
image_status.set_foreground(Theme::getInstance()->fg_magenta->foreground);
|
||||||
pocsag_state.codeword_index = 0;
|
pocsag_state.codeword_index = 0;
|
||||||
pocsag_state.errors = 0;
|
pocsag_state.errors = 0;
|
||||||
|
current_bitrate = message->packet.bitrate();
|
||||||
|
current_inverted = message->packet.inverted();
|
||||||
|
|
||||||
// Handle multiple messages (if any).
|
// Handle multiple messages (if any).
|
||||||
while (pocsag_decode_batch(message->packet, pocsag_state))
|
while (pocsag_decode_batch(message->packet, pocsag_state))
|
||||||
handle_decoded(message->packet.timestamp(), prefix);
|
handle_decoded(message->packet.timestamp(), prefix);
|
||||||
|
|
||||||
// Handle the remainder.
|
// Handle the remainder. Skip if decoder is still in
|
||||||
handle_decoded(message->packet.timestamp(), prefix);
|
// STATE_HAVE_ADDRESS — the address was at the end of this
|
||||||
|
// batch and we can't yet tell if it's tone-only or has
|
||||||
|
// message data in the next batch.
|
||||||
|
if (pocsag_state.mode != STATE_HAVE_ADDRESS)
|
||||||
|
handle_decoded(message->packet.timestamp(), prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set status icon color to indicate state machine state.
|
// Set status icon color to indicate state machine state.
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ struct POCSAGSettings {
|
|||||||
bool enable_raw_log = false;
|
bool enable_raw_log = false;
|
||||||
bool hide_bad_data = false;
|
bool hide_bad_data = false;
|
||||||
bool hide_addr_only = false;
|
bool hide_addr_only = false;
|
||||||
|
bool enable_numeric_detect = true; /* heuristic alpha/numeric type detection */
|
||||||
uint8_t filter_mode = false;
|
uint8_t filter_mode = false;
|
||||||
int32_t baud_rate = -1;
|
int32_t baud_rate = -1;
|
||||||
uint32_t filter_address = 0;
|
uint32_t filter_address = 0;
|
||||||
@@ -150,8 +151,8 @@ class POCSAGSettingsView : public View {
|
|||||||
|
|
||||||
Labels labels{
|
Labels labels{
|
||||||
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
{{2 * 8, 14 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
{{2 * 8, 15 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||||
};
|
};
|
||||||
|
|
||||||
Checkbox check_log{
|
Checkbox check_log{
|
||||||
@@ -179,21 +180,26 @@ class POCSAGSettingsView : public View {
|
|||||||
22,
|
22,
|
||||||
"Hide Addr Only"};
|
"Hide Addr Only"};
|
||||||
|
|
||||||
|
Checkbox check_numeric_detect{
|
||||||
|
{2 * 8, 12 * 16},
|
||||||
|
22,
|
||||||
|
"Detect Numeric"};
|
||||||
|
|
||||||
OptionsField opt_filter_mode{
|
OptionsField opt_filter_mode{
|
||||||
{15 * 8, 12 * 16},
|
{15 * 8, 14 * 16},
|
||||||
4,
|
4,
|
||||||
{{"None", FILTER_NONE},
|
{{"None", FILTER_NONE},
|
||||||
{"Drop", FILTER_DROP},
|
{"Drop", FILTER_DROP},
|
||||||
{"Keep", FILTER_KEEP}}};
|
{"Keep", FILTER_KEEP}}};
|
||||||
|
|
||||||
SymField field_filter_address{
|
SymField field_filter_address{
|
||||||
{15 * 8, 13 * 16},
|
{15 * 8, 15 * 16},
|
||||||
7,
|
7,
|
||||||
SymField::Type::Dec,
|
SymField::Type::Dec,
|
||||||
true /*explicit_edit*/};
|
true /*explicit_edit*/};
|
||||||
|
|
||||||
Button button_save{
|
Button button_save{
|
||||||
{UI_POS_X_CENTER(10), UI_POS_Y(16), 10 * 8, 2 * 16},
|
{UI_POS_X_CENTER(10), UI_POS_Y(17), 10 * 8, 2 * 16},
|
||||||
"Save"};
|
"Save"};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,6 +237,7 @@ class POCSAGAppView : public View {
|
|||||||
{"filter_address"sv, &settings_.filter_address},
|
{"filter_address"sv, &settings_.filter_address},
|
||||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||||
|
{"numeric_detect"sv, &settings_.enable_numeric_detect},
|
||||||
{"baud_rate"sv, &settings_.baud_rate},
|
{"baud_rate"sv, &settings_.baud_rate},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@@ -241,6 +248,10 @@ class POCSAGAppView : public View {
|
|||||||
void on_stats(const POCSAGStatsMessage* stats);
|
void on_stats(const POCSAGStatsMessage* stats);
|
||||||
|
|
||||||
uint32_t last_address = 0;
|
uint32_t last_address = 0;
|
||||||
|
uint16_t current_bitrate = 0; /* bitrate of current packet being decoded */
|
||||||
|
bool current_inverted = false; /* polarity of current packet being decoded */
|
||||||
|
uint8_t serial_numeric_sent = 0; /* numeric chars already sent to serial/GUI */
|
||||||
|
|
||||||
pocsag::EccContainer ecc{};
|
pocsag::EccContainer ecc{};
|
||||||
pocsag::POCSAGState pocsag_state{&ecc};
|
pocsag::POCSAGState pocsag_state{&ecc};
|
||||||
POCSAGLogger logger{};
|
POCSAGLogger logger{};
|
||||||
|
|||||||
+20
-10
@@ -56,7 +56,8 @@ void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
|
|||||||
if (data.function == 'C') tmp = 2;
|
if (data.function == 'C') tmp = 2;
|
||||||
if (data.function == 'D') tmp = 3;
|
if (data.function == 'D') tmp = 3;
|
||||||
options_function.set_selected_index(tmp);
|
options_function.set_selected_index(tmp);
|
||||||
options_phase.set_selected_index(data.phase == 'P' ? 0 : 1);
|
/* 'S' = Standard (CCIR Rec. 584 polarity), 'I' = Inverted */
|
||||||
|
options_polarity.set_selected_index(data.polarity == 'S' ? 0 : 1);
|
||||||
field_address.set_value(data.addr);
|
field_address.set_value(data.addr);
|
||||||
message = (char*)data.msg;
|
message = (char*)data.msg;
|
||||||
buffer = message;
|
buffer = message;
|
||||||
@@ -64,7 +65,7 @@ void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
|
|||||||
options_bitrate.dirty();
|
options_bitrate.dirty();
|
||||||
options_type.dirty();
|
options_type.dirty();
|
||||||
options_function.dirty();
|
options_function.dirty();
|
||||||
options_phase.dirty();
|
options_polarity.dirty();
|
||||||
field_address.dirty();
|
field_address.dirty();
|
||||||
text_message.dirty();
|
text_message.dirty();
|
||||||
tx_view.focus();
|
tx_view.focus();
|
||||||
@@ -100,7 +101,17 @@ bool POCSAGTXView::start_tx() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageType phase = (MessageType)options_phase.selected_index_value();
|
/*
|
||||||
|
* TX polarity inversion (CCIR Rec. 584):
|
||||||
|
*
|
||||||
|
* The FSK modulator (proc_fsk.cpp) maps bit 1 to positive deviation.
|
||||||
|
* Standard POCSAG requires bit 1 to negative deviation.
|
||||||
|
*
|
||||||
|
* "Standard" (value 0): invert codewords so that original bit 1 becomes
|
||||||
|
* bit 0 in the modulator, producing negative deviation. This is the standard.
|
||||||
|
* "Inverted" (value 1): send codewords as-is. Bit 1 produces positive deviation.
|
||||||
|
*/
|
||||||
|
bool invert_polarity = (options_polarity.selected_index_value() == 0);
|
||||||
|
|
||||||
pocsag_encode(type, BCH_code, options_function.selected_index_value(), message, address, codewords);
|
pocsag_encode(type, BCH_code, options_function.selected_index_value(), message, address, codewords);
|
||||||
|
|
||||||
@@ -118,10 +129,7 @@ bool POCSAGTXView::start_tx() {
|
|||||||
|
|
||||||
bi = 0;
|
bi = 0;
|
||||||
for (i = 0; i < codewords.size(); i++) {
|
for (i = 0; i < codewords.size(); i++) {
|
||||||
if (phase == 0)
|
codeword = invert_polarity ? ~(codewords[i]) : codewords[i];
|
||||||
codeword = ~(codewords[i]);
|
|
||||||
else
|
|
||||||
codeword = codewords[i];
|
|
||||||
|
|
||||||
data_ptr[bi++] = (codeword >> 24) & 0xFF;
|
data_ptr[bi++] = (codeword >> 24) & 0xFF;
|
||||||
data_ptr[bi++] = (codeword >> 16) & 0xFF;
|
data_ptr[bi++] = (codeword >> 16) & 0xFF;
|
||||||
@@ -166,15 +174,17 @@ POCSAGTXView::POCSAGTXView(
|
|||||||
&field_address,
|
&field_address,
|
||||||
&options_type,
|
&options_type,
|
||||||
&options_function,
|
&options_function,
|
||||||
&options_phase,
|
&options_polarity,
|
||||||
&text_message,
|
&text_message,
|
||||||
&text_message_l2,
|
&text_message_l2,
|
||||||
&button_message,
|
&button_message,
|
||||||
&progressbar,
|
&progressbar,
|
||||||
&tx_view});
|
&tx_view});
|
||||||
|
|
||||||
options_bitrate.set_selected_index(1); // 1200bps
|
options_bitrate.set_selected_index(1); // 1200 bps
|
||||||
options_type.set_selected_index(0); // Address only
|
options_type.set_selected_index(2); // Alphanumeric
|
||||||
|
options_function.set_selected_index(0); // Function A
|
||||||
|
options_polarity.set_selected_index(0); // Standard (CCIR Rec. 584)
|
||||||
|
|
||||||
field_address.set_value(persistent_memory::pocsag_last_address());
|
field_address.set_value(persistent_memory::pocsag_last_address());
|
||||||
|
|
||||||
|
|||||||
+13
-5
@@ -84,7 +84,7 @@ class POCSAGTXView : public View {
|
|||||||
{{3 * 8, 6 * 8}, "Address:", Theme::getInstance()->fg_light->foreground},
|
{{3 * 8, 6 * 8}, "Address:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{6 * 8, 8 * 8}, "Type:", Theme::getInstance()->fg_light->foreground},
|
{{6 * 8, 8 * 8}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{2 * 8, 10 * 8}, "Function:", Theme::getInstance()->fg_light->foreground},
|
{{2 * 8, 10 * 8}, "Function:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{5 * 8, 12 * 8}, "Phase:", Theme::getInstance()->fg_light->foreground},
|
{{2 * 8, 12 * 8}, "Polarity:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{UI_POS_X(0), 14 * 8}, "Message:", Theme::getInstance()->fg_light->foreground}};
|
{{UI_POS_X(0), 14 * 8}, "Message:", Theme::getInstance()->fg_light->foreground}};
|
||||||
|
|
||||||
OptionsField options_bitrate{
|
OptionsField options_bitrate{
|
||||||
@@ -113,12 +113,20 @@ class POCSAGTXView : public View {
|
|||||||
{"C", 2},
|
{"C", 2},
|
||||||
{"D", 3}}};
|
{"D", 3}}};
|
||||||
|
|
||||||
OptionsField options_phase{
|
/*
|
||||||
|
* TX polarity (CCIR Rec. 584):
|
||||||
|
* "Standard" (value 0): bit 1 = negative deviation (CCIR Rec. 584 standard).
|
||||||
|
* Codewords are bitwise-inverted before the FSK modulator
|
||||||
|
* because the modulator maps bit 1 to positive deviation.
|
||||||
|
* "Inverted" (value 1): bit 1 = positive deviation (non-standard).
|
||||||
|
* Codewords are sent as-is to the modulator.
|
||||||
|
*/
|
||||||
|
OptionsField options_polarity{
|
||||||
{11 * 8, 12 * 8},
|
{11 * 8, 12 * 8},
|
||||||
1,
|
8,
|
||||||
{
|
{
|
||||||
{"P", 0},
|
{"Standard", 0},
|
||||||
{"N", 1},
|
{"Inverted", 1},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Text text_message{
|
Text text_message{
|
||||||
|
|||||||
@@ -1262,7 +1262,7 @@ static void cmd_settingsreset(BaseSequentialStream* chp, int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||||
const char* usage = "usage: sendpocsag <addr> <msglen> [baud] [type] [function] [phase] \r\n";
|
const char* usage = "usage: sendpocsag <addr> <msglen> [baud] [type] [function] [polarity:S|I] \r\n";
|
||||||
(void)argv;
|
(void)argv;
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
chprintf(chp, usage);
|
chprintf(chp, usage);
|
||||||
@@ -1293,34 +1293,48 @@ static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
char function = 'D';
|
char function = 'A';
|
||||||
if (argc >= 5) {
|
if (argc >= 5) {
|
||||||
function = *argv[4];
|
function = *argv[4];
|
||||||
if (function < 'A' && function > 'D') {
|
if (function < 'A' || function > 'D') {
|
||||||
chprintf(chp, "error, function can be A, B, C or D\r\n");
|
chprintf(chp, "error, function can be A, B, C or D\r\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
char phase = 'P';
|
char polarity = 'S'; /* Standard = CCIR Rec. 584 (bit 1 = negative deviation) */
|
||||||
if (argc >= 6) {
|
if (argc >= 6) {
|
||||||
phase = *argv[5];
|
polarity = *argv[5];
|
||||||
if (phase != 'P' && phase != 'N') {
|
/*
|
||||||
chprintf(chp, "error, phase can be P or N\r\n");
|
* Legacy compatibility: old firmware used 'P'/'N' with opposite semantics.
|
||||||
|
* Old 'P' (phase=positive) = inverted codewords = standard POCSAG = new 'S' (Standard)
|
||||||
|
* Old 'N' (phase=negative) = no inversion = inverted POCSAG = new 'I' (Inverted)
|
||||||
|
* New firmware uses 'S'/'I':
|
||||||
|
* 'S' = Standard (CCIR Rec. 584, bit 1 = negative deviation)
|
||||||
|
* 'I' = Inverted (bit 1 = positive deviation)
|
||||||
|
*/
|
||||||
|
if (polarity == 'P') polarity = 'S'; /* legacy 'P' maps to Standard */
|
||||||
|
if (polarity == 'N') polarity = 'I'; /* legacy 'N' maps to Inverted */
|
||||||
|
if (polarity != 'S' && polarity != 'I') {
|
||||||
|
chprintf(chp, "error, polarity can be S (Standard) or I (Inverted)\r\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t msg[81] = {0};
|
uint8_t msg[31] = {0};
|
||||||
if (msglen > 0) {
|
uint8_t original_msglen = (msglen > 31) ? 31 : msglen;
|
||||||
chprintf(chp, "send %d bytes\r\n", msglen);
|
if (original_msglen > 0) {
|
||||||
do {
|
chprintf(chp, "send %d bytes\r\n", original_msglen);
|
||||||
size_t bytes_to_read = msglen > USB_BULK_BUFFER_SIZE ? USB_BULK_BUFFER_SIZE : msglen;
|
size_t offset = 0;
|
||||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], bytes_to_read);
|
size_t remaining = original_msglen;
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t bytes_to_read = remaining > USB_BULK_BUFFER_SIZE ? USB_BULK_BUFFER_SIZE : remaining;
|
||||||
|
size_t bytes_read = chSequentialStreamRead(chp, &msg[offset], bytes_to_read);
|
||||||
if (bytes_read != bytes_to_read)
|
if (bytes_read != bytes_to_read)
|
||||||
return;
|
return;
|
||||||
msglen -= bytes_read;
|
offset += bytes_read;
|
||||||
} while (msglen > 0);
|
remaining -= bytes_read;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto evtd = getEventDispatcherInstance();
|
auto evtd = getEventDispatcherInstance();
|
||||||
@@ -1334,7 +1348,7 @@ static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chThdSleepMilliseconds(1000); // wait for app to start
|
chThdSleepMilliseconds(1000); // wait for app to start
|
||||||
PocsagTosendMessage message{(uint16_t)baud, (uint8_t)type, function, phase, (uint8_t)msglen, msg, addr};
|
PocsagTosendMessage message{(uint16_t)baud, (uint8_t)type, function, polarity, original_msglen, msg, addr};
|
||||||
EventDispatcher::send_message(message);
|
EventDispatcher::send_message(message);
|
||||||
chprintf(chp, "ok\r\n");
|
chprintf(chp, "ok\r\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,7 @@ using namespace std;
|
|||||||
namespace {
|
namespace {
|
||||||
/* Count of bits that differ between the two values. */
|
/* Count of bits that differ between the two values. */
|
||||||
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
|
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
|
||||||
uint32_t diff = left ^ right;
|
return __builtin_popcount(left ^ right);
|
||||||
uint8_t count = 0;
|
|
||||||
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
|
|
||||||
if (((diff >> i) & 0x1) == 1)
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -426,6 +419,7 @@ void POCSAGProcessor::send_packet() {
|
|||||||
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
||||||
packet.set_timestamp(Timestamp::now());
|
packet.set_timestamp(Timestamp::now());
|
||||||
packet.set_bitrate(bit_extractor.baud_rate());
|
packet.set_bitrate(bit_extractor.baud_rate());
|
||||||
|
packet.set_inverted(word_extractor.inverted());
|
||||||
packet.set(word_extractor.batch());
|
packet.set(word_extractor.batch());
|
||||||
|
|
||||||
POCSAGPacketMessage message(packet);
|
POCSAGPacketMessage message(packet);
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ class CodewordExtractor {
|
|||||||
/* Returns true if the batch has as sync frame. */
|
/* Returns true if the batch has as sync frame. */
|
||||||
bool has_sync() const { return has_sync_; }
|
bool has_sync() const { return has_sync_; }
|
||||||
|
|
||||||
|
/* Returns true if the signal was received with inverted polarity. */
|
||||||
|
bool inverted() const { return inverted_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/* Sync frame codeword. */
|
/* Sync frame codeword. */
|
||||||
static constexpr uint32_t sync_codeword = 0x7cd215d8;
|
static constexpr uint32_t sync_codeword = 0x7cd215d8;
|
||||||
|
|||||||
@@ -1569,8 +1569,8 @@ class PocsagTosendMessage : public Message {
|
|||||||
constexpr PocsagTosendMessage(
|
constexpr PocsagTosendMessage(
|
||||||
uint16_t baud = 1200,
|
uint16_t baud = 1200,
|
||||||
uint8_t type = 2,
|
uint8_t type = 2,
|
||||||
char function = 'D',
|
char function = 'A',
|
||||||
char phase = 'N',
|
char polarity = 'S', /* 'S' = Standard (CCIR Rec. 584), 'I' = Inverted */
|
||||||
uint8_t msglen = 0,
|
uint8_t msglen = 0,
|
||||||
uint8_t msg[31] = {0},
|
uint8_t msg[31] = {0},
|
||||||
uint64_t addr = 0)
|
uint64_t addr = 0)
|
||||||
@@ -1578,15 +1578,15 @@ class PocsagTosendMessage : public Message {
|
|||||||
baud{baud},
|
baud{baud},
|
||||||
type{type},
|
type{type},
|
||||||
function{function},
|
function{function},
|
||||||
phase{phase},
|
polarity{polarity},
|
||||||
msglen{msglen},
|
msglen{msglen},
|
||||||
addr{addr} {
|
addr{addr} {
|
||||||
memcpy(this->msg, msg, 31);
|
memcpy(this->msg, msg, 31);
|
||||||
}
|
}
|
||||||
uint16_t baud = 1200;
|
uint16_t baud = 1200;
|
||||||
uint8_t type = 2;
|
uint8_t type = 2;
|
||||||
char function = 'D';
|
char function = 'A';
|
||||||
char phase = 'N';
|
char polarity = 'S'; /* 'S' = Standard, 'I' = Inverted */
|
||||||
uint8_t msglen = 0;
|
uint8_t msglen = 0;
|
||||||
uint8_t msg[31] = {0};
|
uint8_t msg[31] = {0};
|
||||||
uint64_t addr = 0;
|
uint64_t addr = 0;
|
||||||
|
|||||||
+294
-21
@@ -39,8 +39,6 @@ std::string bitrate_str(BitRate bitrate) {
|
|||||||
return "1200bps";
|
return "1200bps";
|
||||||
case BitRate::FSK2400:
|
case BitRate::FSK2400:
|
||||||
return "2400bps";
|
return "2400bps";
|
||||||
case BitRate::FSK3200:
|
|
||||||
return "3200bps";
|
|
||||||
default:
|
default:
|
||||||
return "????";
|
return "????";
|
||||||
}
|
}
|
||||||
@@ -352,30 +350,225 @@ int EccContainer::error_correct(uint32_t& val) {
|
|||||||
return errl;
|
return errl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Numeric character table: 4-bit BCD -> ASCII
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
static const char numeric_chars[16] = {
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||||
|
'8', '9', 'R', 'U', ' ', '-', ']', '['};
|
||||||
|
|
||||||
|
// Extract and bit-reverse a 4-bit nibble from a message codeword.
|
||||||
|
// POCSAG numeric digits are transmitted LSB first, so bit 30 (first transmitted)
|
||||||
|
// is the LSB of the digit value, not the MSB.
|
||||||
|
static uint8_t decode_nibble(uint32_t codeword, int nibble_idx) {
|
||||||
|
int bit_pos = 30 - nibble_idx * 4;
|
||||||
|
// bit_pos is the first transmitted bit (LSB of digit)
|
||||||
|
// bit_pos-3 is the last transmitted bit (MSB of digit)
|
||||||
|
uint8_t n = 0;
|
||||||
|
n |= ((codeword >> (bit_pos - 3)) & 1) << 3; // MSB of digit
|
||||||
|
n |= ((codeword >> (bit_pos - 2)) & 1) << 2;
|
||||||
|
n |= ((codeword >> (bit_pos - 1)) & 1) << 1;
|
||||||
|
n |= ((codeword >> (bit_pos - 0)) & 1) << 0; // LSB of digit
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Heuristic message type detection (first batch only)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Count trailing fill characters in alpha buffer (NULL or space).
|
||||||
|
static int count_alpha_fill(const std::string& data) {
|
||||||
|
if (data.empty()) return 0;
|
||||||
|
int fill = 0;
|
||||||
|
for (int i = data.size() - 1; i >= 0; --i) {
|
||||||
|
char c = data[i];
|
||||||
|
if (c == '\0' || c == ' ')
|
||||||
|
fill++;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count trailing fill nibbles (0xC = space) in numeric buffer.
|
||||||
|
static int count_numeric_fill(const uint8_t* nibbles, int count) {
|
||||||
|
int fill = 0;
|
||||||
|
for (int i = count - 1; i >= 0; --i) {
|
||||||
|
if (nibbles[i] == 0x0C)
|
||||||
|
fill++;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score alpha interpretation: +3 alphanumeric/space, -2 other printable, -5 control.
|
||||||
|
static int score_alpha(const std::string& data, int fill) {
|
||||||
|
int score = 0;
|
||||||
|
int content = 0;
|
||||||
|
int len = data.size();
|
||||||
|
|
||||||
|
for (int i = 0; i < len; ++i) {
|
||||||
|
unsigned char c = data[i];
|
||||||
|
|
||||||
|
// Skip trailing fill
|
||||||
|
if (i >= len - fill && (c == 0 || c == ' '))
|
||||||
|
continue;
|
||||||
|
if (c == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
content++;
|
||||||
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == ' ')
|
||||||
|
score += 3;
|
||||||
|
else if (c >= 0x20 && c <= 0x7E)
|
||||||
|
score -= 2;
|
||||||
|
else if (c == '\n' || c == '\r' || c == '\t' || c == 0x04)
|
||||||
|
score += 0;
|
||||||
|
else
|
||||||
|
score -= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content > 0)
|
||||||
|
score += fill * fill * 3 + fill * 5;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score numeric interpretation on raw nibbles.
|
||||||
|
static int score_numeric(const uint8_t* nibbles, int count, int fill) {
|
||||||
|
int raw_score = 0;
|
||||||
|
int scored = 0;
|
||||||
|
int digits = 0;
|
||||||
|
int u_count = 0;
|
||||||
|
|
||||||
|
// Pre-scan for U nibbles
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
if (nibbles[i] == 0x0C && i >= count - fill)
|
||||||
|
continue;
|
||||||
|
if (nibbles[i] == 0x0B)
|
||||||
|
u_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool urgent_prefix = (u_count == 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
uint8_t n = nibbles[i];
|
||||||
|
if (n == 0x0C && i >= count - fill)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
scored++;
|
||||||
|
if (n <= 0x09) {
|
||||||
|
raw_score += 3;
|
||||||
|
digits++;
|
||||||
|
} else if (n == 0x0B) {
|
||||||
|
raw_score += urgent_prefix ? -1 : -15;
|
||||||
|
} else if (n == 0x0A) {
|
||||||
|
raw_score -= 5;
|
||||||
|
} else {
|
||||||
|
raw_score -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int score = scored > 0 ? raw_score * 4 / 7 : 0;
|
||||||
|
|
||||||
|
// Phone numbers have at most ~15 digits
|
||||||
|
if (digits > 15)
|
||||||
|
score -= (digits - 15) * 5;
|
||||||
|
|
||||||
|
// Fill bonus (weaker than alpha: 1/16 vs 1/128 coincidence rate)
|
||||||
|
score += fill * fill;
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectedType detect_message_type(const std::string& alpha,
|
||||||
|
const uint8_t* nibbles,
|
||||||
|
uint8_t nibble_count,
|
||||||
|
uint8_t msg_codewords) {
|
||||||
|
if (alpha.empty() && nibble_count == 0)
|
||||||
|
return DET_TONE;
|
||||||
|
|
||||||
|
// Long messages can't be numeric (phone numbers are short)
|
||||||
|
if (msg_codewords >= 8)
|
||||||
|
return DET_ALPHA;
|
||||||
|
|
||||||
|
int alpha_fill = count_alpha_fill(alpha);
|
||||||
|
int numeric_fill = count_numeric_fill(nibbles, nibble_count);
|
||||||
|
|
||||||
|
int sa = score_alpha(alpha, alpha_fill) + 2; // Alpha prior bias
|
||||||
|
int sn = score_numeric(nibbles, nibble_count, numeric_fill);
|
||||||
|
|
||||||
|
// Short message boost for alpha (1-2 data codewords)
|
||||||
|
if (msg_codewords <= 3)
|
||||||
|
sa += 3;
|
||||||
|
|
||||||
|
return (sn > sa) ? DET_NUMERIC : DET_ALPHA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Batch decoder
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
||||||
constexpr uint8_t codeword_max = 16;
|
constexpr uint8_t codeword_max = 16;
|
||||||
state.output.clear();
|
state.output.clear();
|
||||||
|
|
||||||
|
/* Only reset numeric accumulator when starting a new message,
|
||||||
|
* not on continuation batches — numeric_buf must be cumulative
|
||||||
|
* across multi-batch messages. */
|
||||||
|
const bool continuing_numeric =
|
||||||
|
(state.mode != STATE_HAVE_ADDRESS) &&
|
||||||
|
(state.out_type == MESSAGE) &&
|
||||||
|
state.type_decided &&
|
||||||
|
(state.detected == DET_NUMERIC);
|
||||||
|
if (!continuing_numeric)
|
||||||
|
state.numeric_len = 0;
|
||||||
|
|
||||||
|
/* Preserve new_message across batch boundary when STATE_HAVE_ADDRESS
|
||||||
|
* persists — the address was at the end of the previous batch and
|
||||||
|
* we haven't displayed it yet. Otherwise reset for this batch. */
|
||||||
|
if (state.mode != STATE_HAVE_ADDRESS)
|
||||||
|
state.new_message = false;
|
||||||
|
|
||||||
|
// Temporary nibble buffer for first-batch numeric decode.
|
||||||
|
uint8_t nibbles[max_batch_nibbles];
|
||||||
|
uint8_t nibble_count = 0;
|
||||||
|
uint8_t msg_codewords = 0;
|
||||||
|
// Also build raw alpha for heuristic (before non-printable replacement).
|
||||||
|
std::string raw_alpha{};
|
||||||
|
|
||||||
while (state.codeword_index < codeword_max) {
|
while (state.codeword_index < codeword_max) {
|
||||||
auto codeword = batch[state.codeword_index];
|
auto codeword = batch[state.codeword_index];
|
||||||
bool is_address = (codeword & 0x80000000U) == 0;
|
bool is_address = (codeword & 0x80000000U) == 0;
|
||||||
|
|
||||||
// Error correct twice. First time to fix any errors it can,
|
// Single ECC call: fix errors and get error count.
|
||||||
// second time to count number of errors that couldn't be fixed.
|
|
||||||
state.ecc->error_correct(codeword);
|
|
||||||
auto error_count = state.ecc->error_correct(codeword);
|
auto error_count = state.ecc->error_correct(codeword);
|
||||||
|
|
||||||
switch (state.mode) {
|
switch (state.mode) {
|
||||||
case STATE_CLEAR:
|
case STATE_CLEAR:
|
||||||
if (is_address && codeword != POCSAG_IDLEWORD) {
|
if (is_address && codeword != POCSAG_IDLEWORD) {
|
||||||
state.function = (codeword >> 11) & 3;
|
state.function = (codeword >> 11) & 3;
|
||||||
state.address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
|
state.address = (codeword >> 10) & 0x1FFFF8U;
|
||||||
|
/* Frame number = lower 3 bits of RIC, derived from the
|
||||||
|
* address codeword's position in the batch (not the
|
||||||
|
* message codeword's position). codeword_index 0-15
|
||||||
|
* maps to frames 0-7 via index >> 1. */
|
||||||
|
state.address |= (state.codeword_index >> 1);
|
||||||
state.mode = STATE_HAVE_ADDRESS;
|
state.mode = STATE_HAVE_ADDRESS;
|
||||||
state.out_type = ADDRESS;
|
state.out_type = ADDRESS;
|
||||||
state.errors = error_count;
|
state.errors = error_count;
|
||||||
|
state.new_message = true;
|
||||||
|
state.type_decided = false;
|
||||||
|
state.detected = DET_UNKNOWN;
|
||||||
|
|
||||||
state.ascii_idx = 0;
|
state.ascii_idx = 0;
|
||||||
state.ascii_data = 0;
|
state.ascii_data = 0;
|
||||||
|
state.prev_cw_err = 0;
|
||||||
|
state.cur_cw_err = 0;
|
||||||
|
nibble_count = 0;
|
||||||
|
msg_codewords = 0;
|
||||||
|
raw_alpha.clear();
|
||||||
} else if (codeword == POCSAG_IDLEWORD) {
|
} else if (codeword == POCSAG_IDLEWORD) {
|
||||||
state.out_type = IDLE;
|
state.out_type = IDLE;
|
||||||
}
|
}
|
||||||
@@ -383,54 +576,134 @@ bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
|||||||
|
|
||||||
case STATE_HAVE_ADDRESS:
|
case STATE_HAVE_ADDRESS:
|
||||||
if (is_address) {
|
if (is_address) {
|
||||||
// Got another address, return the current state.
|
// Got another address. Run heuristic before returning if we have pending data.
|
||||||
|
if (!state.type_decided && msg_codewords > 0) {
|
||||||
|
state.detected = detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||||
|
state.type_decided = true;
|
||||||
|
state.msg_codewords = msg_codewords;
|
||||||
|
if (state.detected == DET_NUMERIC) {
|
||||||
|
state.numeric_len = 0;
|
||||||
|
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||||
|
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
state.mode = STATE_CLEAR;
|
state.mode = STATE_CLEAR;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First message codeword, complete the address.
|
/* Frame number already applied in STATE_CLEAR.
|
||||||
state.address |= (state.codeword_index >> 1); // Add in the 3 LSBs (frame #).
|
* Transition to message decoding. */
|
||||||
state.mode = STATE_GETTING_MSG;
|
state.mode = STATE_GETTING_MSG;
|
||||||
[[fallthrough]];
|
[[fallthrough]];
|
||||||
|
|
||||||
case STATE_GETTING_MSG:
|
case STATE_GETTING_MSG:
|
||||||
if (is_address) {
|
if (is_address) {
|
||||||
// Codeword isn't a message, return the current state.
|
// Message ended. Run heuristic before returning.
|
||||||
|
if (!state.type_decided && msg_codewords > 0) {
|
||||||
|
state.detected = detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||||
|
state.type_decided = true;
|
||||||
|
state.msg_codewords = msg_codewords;
|
||||||
|
if (state.detected == DET_NUMERIC) {
|
||||||
|
state.numeric_len = 0;
|
||||||
|
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||||
|
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
state.mode = STATE_CLEAR;
|
state.mode = STATE_CLEAR;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.out_type = MESSAGE;
|
state.out_type = MESSAGE;
|
||||||
state.errors += error_count;
|
state.errors += error_count;
|
||||||
state.ascii_data |= (codeword >> 11) & 0xFFFFF; // Get 20 message bits.
|
msg_codewords++;
|
||||||
|
|
||||||
|
// Track per-codeword error level for character coloring.
|
||||||
|
// 0=clean, 1-2=corrected, 3=uncorrectable.
|
||||||
|
state.prev_cw_err = state.cur_cw_err;
|
||||||
|
state.cur_cw_err = (error_count >= 3) ? 3 : error_count;
|
||||||
|
|
||||||
|
// --- Alpha decode (always) ---
|
||||||
|
// Bits remaining from previous codeword inherit prev_cw_err.
|
||||||
|
// New 20 bits from this codeword use cur_cw_err.
|
||||||
|
// Characters spanning boundary get the worst of both.
|
||||||
|
uint32_t bits_from_prev = state.ascii_idx; // leftover bits before adding new ones
|
||||||
|
|
||||||
|
state.ascii_data |= ((uint64_t)((codeword >> 11) & 0xFFFFF)) << (44 - state.ascii_idx);
|
||||||
state.ascii_idx += 20;
|
state.ascii_idx += 20;
|
||||||
|
|
||||||
// Raw 20 bits to 7 bit reversed ASCII.
|
|
||||||
// NB: This is processed MSB first, any remaining bits are shifted
|
|
||||||
// up so a whole 7 bits are processed with the next codeword.
|
|
||||||
while (state.ascii_idx >= 7) {
|
while (state.ascii_idx >= 7) {
|
||||||
|
// Determine error level for this character.
|
||||||
|
// If some bits came from previous codeword and some from current,
|
||||||
|
// use the worst of both error levels.
|
||||||
|
uint8_t char_err;
|
||||||
|
if (bits_from_prev >= 7) {
|
||||||
|
// Entire character from previous codeword's leftover bits.
|
||||||
|
char_err = state.prev_cw_err;
|
||||||
|
bits_from_prev -= 7;
|
||||||
|
} else if (bits_from_prev > 0) {
|
||||||
|
// Character spans boundary.
|
||||||
|
char_err = std::max(state.prev_cw_err, state.cur_cw_err);
|
||||||
|
bits_from_prev = 0;
|
||||||
|
} else {
|
||||||
|
// Entirely from current codeword.
|
||||||
|
char_err = state.cur_cw_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract top 7 bits from accumulator
|
||||||
|
char ascii_char = (state.ascii_data >> 57) & 0x7F;
|
||||||
|
state.ascii_data <<= 7;
|
||||||
state.ascii_idx -= 7;
|
state.ascii_idx -= 7;
|
||||||
char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F;
|
|
||||||
|
|
||||||
// Reverse the bits. (TODO: __RBIT?)
|
// Reverse bits (LSB-first encoding)
|
||||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
|
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4;
|
||||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
|
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2;
|
||||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210
|
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55);
|
||||||
|
|
||||||
// Translate non-printable chars. TODO: Leave CRLF?
|
// Store raw char for heuristic
|
||||||
|
if (!state.type_decided)
|
||||||
|
raw_alpha += ascii_char;
|
||||||
|
|
||||||
|
// Translate non-printable chars.
|
||||||
if (ascii_char < 32 || ascii_char > 126)
|
if (ascii_char < 32 || ascii_char > 126)
|
||||||
state.output += ".";
|
state.output += ".";
|
||||||
else
|
else
|
||||||
state.output += ascii_char;
|
state.output += ascii_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ascii_data <<= 20; // Remaining bits are for next iteration...
|
// --- Numeric decode ---
|
||||||
|
// First batch: accumulate nibbles locally for heuristic scoring.
|
||||||
|
// Continuation batches: if already decided numeric, decode directly
|
||||||
|
// into numeric_buf for the app layer.
|
||||||
|
if (!state.type_decided && nibble_count + 5 <= max_batch_nibbles) {
|
||||||
|
for (int n = 0; n < 5; ++n) {
|
||||||
|
nibbles[nibble_count++] = decode_nibble(codeword, n);
|
||||||
|
}
|
||||||
|
} else if (state.type_decided && state.detected == DET_NUMERIC &&
|
||||||
|
state.numeric_len + 5 <= (uint8_t)sizeof(state.numeric_buf)) {
|
||||||
|
for (int n = 0; n < 5; ++n) {
|
||||||
|
uint8_t nib = decode_nibble(codeword, n);
|
||||||
|
state.numeric_buf[state.numeric_len++] = numeric_chars[nib & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.codeword_index++;
|
state.codeword_index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// End of batch. If we have message data and type not yet decided, run heuristic.
|
||||||
|
if (state.out_type == MESSAGE && !state.type_decided && msg_codewords > 0) {
|
||||||
|
state.detected = detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||||
|
state.type_decided = true;
|
||||||
|
state.msg_codewords = msg_codewords;
|
||||||
|
if (state.detected == DET_NUMERIC) {
|
||||||
|
state.numeric_len = 0;
|
||||||
|
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||||
|
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ class EccContainer {
|
|||||||
uint32_t bch[1025];
|
uint32_t bch[1025];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Detected message type from heuristic scoring. */
|
||||||
|
enum DetectedType : uint8_t {
|
||||||
|
DET_UNKNOWN,
|
||||||
|
DET_TONE,
|
||||||
|
DET_ALPHA,
|
||||||
|
DET_NUMERIC
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Max numeric nibbles per batch: 16 codewords * 5 nibbles. */
|
||||||
|
constexpr uint8_t max_batch_nibbles = 80;
|
||||||
|
|
||||||
struct POCSAGState {
|
struct POCSAGState {
|
||||||
EccContainer* ecc = nullptr;
|
EccContainer* ecc = nullptr;
|
||||||
uint8_t codeword_index = 0;
|
uint8_t codeword_index = 0;
|
||||||
@@ -84,17 +95,28 @@ struct POCSAGState {
|
|||||||
uint32_t address = 0;
|
uint32_t address = 0;
|
||||||
Mode mode = STATE_CLEAR;
|
Mode mode = STATE_CLEAR;
|
||||||
OutputType out_type = EMPTY;
|
OutputType out_type = EMPTY;
|
||||||
uint32_t ascii_data = 0;
|
uint64_t ascii_data = 0; // Was uint32_t — fixed overflow for long messages.
|
||||||
uint32_t ascii_idx = 0;
|
uint32_t ascii_idx = 0;
|
||||||
uint32_t errors = 0;
|
uint32_t errors = 0;
|
||||||
std::string output{};
|
uint8_t prev_cw_err = 0; // Error level of previous codeword (for chars spanning boundary).
|
||||||
|
uint8_t cur_cw_err = 0; // Error level of current codeword being decoded.
|
||||||
|
bool new_message = false; // True when decoder starts a new address.
|
||||||
|
bool type_decided = false; // True after first-batch heuristic runs.
|
||||||
|
DetectedType detected = DET_UNKNOWN;
|
||||||
|
uint8_t msg_codewords = 0; // Message codewords in current batch (for heuristic).
|
||||||
|
std::string output{}; // Alpha decode (always populated, with color escapes).
|
||||||
|
// Numeric output is built into a separate local in pocsag_decode_batch
|
||||||
|
// and stored here only when heuristic detects numeric.
|
||||||
|
// Using a fixed buffer to avoid std::string overhead in external apps
|
||||||
|
// that embed POCSAGState (e.g. battleship).
|
||||||
|
char numeric_buf[80]{};
|
||||||
|
uint8_t numeric_len = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pocsag::BitRate pocsag_bitrates[4] = {
|
const pocsag::BitRate pocsag_bitrates[3] = {
|
||||||
pocsag::BitRate::FSK512,
|
pocsag::BitRate::FSK512,
|
||||||
pocsag::BitRate::FSK1200,
|
pocsag::BitRate::FSK1200,
|
||||||
pocsag::BitRate::FSK2400,
|
pocsag::BitRate::FSK2400,
|
||||||
pocsag::BitRate::FSK3200,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string bitrate_str(BitRate bitrate);
|
std::string bitrate_str(BitRate bitrate);
|
||||||
@@ -107,6 +129,10 @@ void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t fun
|
|||||||
// Returns true if the batch has more to process.
|
// Returns true if the batch has more to process.
|
||||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state);
|
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state);
|
||||||
|
|
||||||
|
// Heuristic: detect whether message content is numeric or alpha.
|
||||||
|
// Called once after first batch of a new message.
|
||||||
|
DetectedType detect_message_type(const std::string& alpha, const uint8_t* nibbles, uint8_t nibble_count, uint8_t msg_codewords);
|
||||||
|
|
||||||
} /* namespace pocsag */
|
} /* namespace pocsag */
|
||||||
|
|
||||||
#endif /*__POCSAG_H__*/
|
#endif /*__POCSAG_H__*/
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ enum BitRate : uint32_t {
|
|||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
FSK512 = 512,
|
FSK512 = 512,
|
||||||
FSK1200 = 1200,
|
FSK1200 = 1200,
|
||||||
FSK2400 = 2400,
|
FSK2400 = 2400
|
||||||
FSK3200 = 3200
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum PacketFlag : uint32_t {
|
enum PacketFlag : uint32_t {
|
||||||
@@ -88,15 +87,25 @@ class POCSAGPacket {
|
|||||||
return flag_;
|
return flag_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_inverted(bool inverted) {
|
||||||
|
inverted_ = inverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inverted() const {
|
||||||
|
return inverted_;
|
||||||
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
codewords.fill(0);
|
codewords.fill(0);
|
||||||
bitrate_ = 0u;
|
bitrate_ = 0u;
|
||||||
flag_ = NORMAL;
|
flag_ = NORMAL;
|
||||||
|
inverted_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint16_t bitrate_{0};
|
uint16_t bitrate_{0};
|
||||||
PacketFlag flag_{NORMAL};
|
PacketFlag flag_{NORMAL};
|
||||||
|
bool inverted_{false};
|
||||||
batch_t codewords{};
|
batch_t codewords{};
|
||||||
Timestamp timestamp_{};
|
Timestamp timestamp_{};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user