mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-13 11:23:41 +00:00
396f8aef41
Use per-character error tracking to substitute '?' when a character's bits came from an uncorrectable codeword (BCH error_count >= 3). Previously these displayed as garbage.
478 lines
18 KiB
C++
478 lines
18 KiB
C++
/*
|
|
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
|
* Copyright (C) 2016 Furrtek
|
|
*
|
|
* This file is part of PortaPack.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "pocsag_app.hpp"
|
|
|
|
#include "audio.hpp"
|
|
#include "baseband_api.hpp"
|
|
#include "portapack_persistent_memory.hpp"
|
|
#include "string_format.hpp"
|
|
#include "utility.hpp"
|
|
#include "file_path.hpp"
|
|
#include "usb_serial_asyncmsg.hpp"
|
|
|
|
using namespace portapack;
|
|
using namespace pocsag;
|
|
namespace pmem = portapack::persistent_memory;
|
|
|
|
void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency) {
|
|
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz " +
|
|
to_string_dec_uint(packet.bitrate()) + " Codewords:";
|
|
|
|
// Raw hex dump of all the codewords
|
|
for (size_t c = 0; c < 16; c++)
|
|
entry += to_string_hex(packet[c], 8) + " ";
|
|
|
|
log_file.write_entry(packet.timestamp(), entry);
|
|
}
|
|
|
|
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
|
log_file.write_entry(timestamp, text);
|
|
}
|
|
|
|
namespace ui {
|
|
|
|
POCSAGSettingsView::POCSAGSettingsView(
|
|
NavigationView& nav,
|
|
POCSAGSettings& settings)
|
|
: settings_{settings} {
|
|
add_children(
|
|
{&labels,
|
|
&opt_baud_rate,
|
|
&check_log,
|
|
&check_log_raw,
|
|
&check_small_font,
|
|
&check_hide_bad,
|
|
&check_hide_addr_only,
|
|
&check_numeric_detect,
|
|
&opt_filter_mode,
|
|
&field_filter_address,
|
|
&button_save});
|
|
|
|
opt_baud_rate.set_by_value(settings_.baud_rate);
|
|
check_log.set_value(settings_.enable_logging);
|
|
check_log_raw.set_value(settings_.enable_raw_log);
|
|
check_small_font.set_value(settings_.enable_small_font);
|
|
check_hide_bad.set_value(settings_.hide_bad_data);
|
|
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);
|
|
field_filter_address.set_value(settings_.filter_address);
|
|
|
|
button_save.on_select = [this, &nav](Button&) {
|
|
settings_.enable_logging = check_log.value();
|
|
settings_.enable_raw_log = check_log_raw.value();
|
|
settings_.enable_small_font = check_small_font.value();
|
|
settings_.hide_bad_data = check_hide_bad.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_address = field_filter_address.to_integer();
|
|
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
|
nav.pop();
|
|
};
|
|
}
|
|
|
|
POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
|
: nav_{nav} {
|
|
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
|
|
|
add_children(
|
|
{&rssi,
|
|
&audio,
|
|
&field_rf_amp,
|
|
&field_lna,
|
|
&field_vga,
|
|
&field_frequency,
|
|
&field_squelch,
|
|
&field_volume,
|
|
&image_status,
|
|
&text_packet_count,
|
|
&widget_baud,
|
|
&widget_bits,
|
|
&widget_frames,
|
|
&button_filter_last,
|
|
&button_config,
|
|
&console});
|
|
|
|
// No app settings, use fallbacks from pmem.
|
|
if (!app_settings_.loaded()) {
|
|
settings_.filter_address = pmem::pocsag_ignore_address();
|
|
settings_.filter_mode = (settings_.filter_address == 0)
|
|
? FILTER_NONE
|
|
: FILTER_DROP;
|
|
}
|
|
|
|
logger.append(logs_dir / u"POCSAG.TXT");
|
|
|
|
field_squelch.set_value(receiver_model.squelch_level());
|
|
field_squelch.on_change = [this](int32_t v) {
|
|
receiver_model.set_squelch_level(v);
|
|
};
|
|
|
|
button_filter_last.on_select = [this](Button&) {
|
|
if (settings_.filter_mode == FILTER_NONE)
|
|
settings_.filter_mode = FILTER_DROP;
|
|
settings_.filter_address = last_address;
|
|
refresh_ui();
|
|
};
|
|
|
|
button_config.on_select = [this](Button&) {
|
|
nav_.push<POCSAGSettingsView>(settings_);
|
|
nav_.set_on_pop([this]() { refresh_ui(); });
|
|
};
|
|
|
|
refresh_ui();
|
|
|
|
if (pmem::beep_on_packets())
|
|
audio::set_rate(audio::Rate::Hz_24000);
|
|
|
|
audio::output::start();
|
|
receiver_model.enable();
|
|
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
|
}
|
|
|
|
void POCSAGAppView::focus() {
|
|
field_frequency.focus();
|
|
}
|
|
|
|
POCSAGAppView::~POCSAGAppView() {
|
|
audio::output::stop();
|
|
receiver_model.disable();
|
|
baseband::shutdown();
|
|
|
|
// Save pmem settings.
|
|
pmem::set_pocsag_ignore_address(settings_.filter_address);
|
|
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
|
|
}
|
|
|
|
void POCSAGAppView::refresh_ui() {
|
|
// Set console font style.
|
|
console.set_style(
|
|
settings_.enable_small_font
|
|
? Theme::getInstance()->bg_darkest_small
|
|
: Theme::getInstance()->bg_darkest);
|
|
|
|
// Update filter button text.
|
|
std::string btn_text = "Filter Last";
|
|
switch (settings_.filter_mode) {
|
|
case FILTER_DROP:
|
|
btn_text = "Ignore Last";
|
|
break;
|
|
|
|
case FILTER_KEEP:
|
|
btn_text = "Keep Last";
|
|
break;
|
|
|
|
case FILTER_NONE:
|
|
default:
|
|
btn_text = "Filter Last";
|
|
break;
|
|
}
|
|
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
|
button_filter_last.set_text(btn_text);
|
|
}
|
|
|
|
bool POCSAGAppView::ignore_address(uint32_t address) const {
|
|
switch (settings_.filter_mode) {
|
|
case FILTER_DROP:
|
|
return address == settings_.filter_address;
|
|
|
|
case FILTER_KEEP:
|
|
return address != settings_.filter_address;
|
|
|
|
case FILTER_NONE:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
|
|
bool bad_data = pocsag_state.errors >= 3;
|
|
|
|
// Too many errors for reliable decode.
|
|
if (bad_data && hide_bad_data()) {
|
|
console.write("\n" STR_COLOR_MAGENTA + prefix + " Too many decode errors.");
|
|
last_address = 0;
|
|
return;
|
|
}
|
|
|
|
// Ignore address? TODO: could filter earlier.
|
|
if (ignore_address(pocsag_state.address)) {
|
|
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
|
|
last_address = pocsag_state.address;
|
|
return;
|
|
}
|
|
|
|
// Type indicator with its own color.
|
|
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 = "";
|
|
|
|
// Header: timestamp+baud in light grey, capcode+function in white.
|
|
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);
|
|
if (!type_str.empty())
|
|
console_info += " " + type_str;
|
|
|
|
if (pocsag_state.out_type == ADDRESS) {
|
|
last_address = pocsag_state.address;
|
|
|
|
if (!hide_addr_only()) {
|
|
console.write(console_info);
|
|
|
|
if (logging()) {
|
|
logger.log_decoded(
|
|
timestamp,
|
|
to_string_dec_uint(pocsag_state.address) +
|
|
" F" + to_string_dec_uint(pocsag_state.function));
|
|
}
|
|
}
|
|
|
|
/* 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) {
|
|
if (pocsag_state.new_message) {
|
|
last_address = pocsag_state.address;
|
|
serial_numeric_sent = 0;
|
|
console.writeln(console_info);
|
|
|
|
// 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 chars show as '.' (0x2E),
|
|
* uncorrectable chars show as '?' (0x3F) — 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 {
|
|
// Message continues from previous batch.
|
|
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()) {
|
|
std::string log_entry = to_string_dec_uint(pocsag_state.address) +
|
|
" F" + to_string_dec_uint(pocsag_state.function);
|
|
if (numeric_detect &&
|
|
pocsag_state.detected == pocsag::DET_NUMERIC &&
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Color get_status_color(const POCSAGState& state) {
|
|
if (state.out_type == IDLE)
|
|
return Theme::getInstance()->bg_darkest->foreground;
|
|
|
|
switch (state.mode) {
|
|
case STATE_CLEAR:
|
|
return Theme::getInstance()->fg_cyan->foreground;
|
|
case STATE_HAVE_ADDRESS:
|
|
return Theme::getInstance()->fg_yellow->foreground;
|
|
case STATE_GETTING_MSG:
|
|
return Theme::getInstance()->fg_green->foreground;
|
|
}
|
|
|
|
// Shouldn't get here...
|
|
return Theme::getInstance()->fg_red->foreground;
|
|
}
|
|
|
|
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
|
const uint32_t roundVal = 50;
|
|
const uint32_t bitrate_rounded = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
|
|
auto bitrate = to_string_dec_uint(bitrate_rounded);
|
|
auto timestamp = to_string_datetime(message->packet.timestamp(), HM);
|
|
auto prefix = timestamp + " " + bitrate;
|
|
|
|
// Display packet count to be able to tell whether baseband sent a packet for a tone.
|
|
++packet_count;
|
|
text_packet_count.set(to_string_dec_uint(packet_count));
|
|
|
|
if (logging_raw())
|
|
logger.log_raw_data(message->packet, receiver_model.target_frequency());
|
|
|
|
if (message->packet.flag() != NORMAL) {
|
|
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
|
last_address = 0;
|
|
} else {
|
|
// Set color before to be able to see if decode gets stuck.
|
|
image_status.set_foreground(Theme::getInstance()->fg_magenta->foreground);
|
|
pocsag_state.codeword_index = 0;
|
|
pocsag_state.errors = 0;
|
|
current_bitrate = message->packet.bitrate();
|
|
current_inverted = message->packet.inverted();
|
|
|
|
// Handle multiple messages (if any).
|
|
while (pocsag_decode_batch(message->packet, pocsag_state))
|
|
handle_decoded(message->packet.timestamp(), prefix);
|
|
|
|
// Handle the remainder. Skip if decoder is still in
|
|
// 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.
|
|
image_status.set_foreground(get_status_color(pocsag_state));
|
|
|
|
if (pmem::beep_on_packets()) {
|
|
baseband::request_audio_beep(1000, 24000, 60);
|
|
}
|
|
}
|
|
|
|
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
|
|
widget_baud.set_rate(stats->baud_rate);
|
|
widget_bits.set_bits(stats->current_bits);
|
|
widget_frames.set_frames(stats->current_frames);
|
|
widget_frames.set_sync(stats->has_sync);
|
|
}
|
|
|
|
void POCSAGAppView::on_freqchg(int64_t freq) {
|
|
field_frequency.set_value(freq);
|
|
}
|
|
|
|
void BaudIndicator::paint(Painter& painter) {
|
|
auto p = screen_pos();
|
|
char top = '-';
|
|
char bot = '-';
|
|
|
|
if (rate_ > 0) {
|
|
auto r = rate_ / 100;
|
|
top = (r / 10) + '0';
|
|
bot = (r % 10) + '0';
|
|
}
|
|
|
|
painter.draw_char(p, *Theme::getInstance()->bg_darkest_small, top);
|
|
painter.draw_char({p.x(), p.y() + 8}, *Theme::getInstance()->bg_darkest_small, bot);
|
|
}
|
|
|
|
void BitsIndicator::paint(Painter&) {
|
|
auto p = screen_pos();
|
|
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
|
|
auto is_set = ((bits_ >> i) & 0x1) == 1;
|
|
|
|
int x = p.x() + (i / height);
|
|
int y = p.y() + (i % height);
|
|
display.draw_pixel({x, y}, is_set ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background);
|
|
}
|
|
}
|
|
|
|
void FrameIndicator::paint(Painter& painter) {
|
|
auto p = screen_pos();
|
|
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Theme::getInstance()->fg_green->foreground : Theme::getInstance()->bg_medium->background);
|
|
|
|
for (size_t i = 0; i < height; ++i) {
|
|
auto p2 = p + Point{2, 15 - (int)i};
|
|
painter.draw_hline(p2, 2, i < frame_count_ ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background);
|
|
}
|
|
}
|
|
|
|
} /* namespace ui */
|