2016-08-23 06:45:33 +00:00
|
|
|
/*
|
|
|
|
* 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"
|
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
#include "audio.hpp"
|
2016-08-23 06:45:33 +00:00
|
|
|
#include "baseband_api.hpp"
|
2016-08-23 09:27:10 +00:00
|
|
|
#include "portapack_persistent_memory.hpp"
|
2023-08-25 21:45:02 +00:00
|
|
|
#include "string_format.hpp"
|
|
|
|
#include "utility.hpp"
|
2017-04-18 20:29:55 +00:00
|
|
|
|
2016-08-23 06:45:33 +00:00
|
|
|
using namespace portapack;
|
2017-02-05 20:57:20 +00:00
|
|
|
using namespace pocsag;
|
2023-08-25 21:45:02 +00:00
|
|
|
namespace pmem = portapack::persistent_memory;
|
2016-08-23 06:45:33 +00:00
|
|
|
|
2017-04-18 20:29:55 +00:00
|
|
|
void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency) {
|
2023-05-18 20:16:05 +00:00
|
|
|
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);
|
2016-08-23 06:45:33 +00:00
|
|
|
}
|
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
|
|
|
log_file.write_entry(timestamp, text);
|
2016-08-23 12:35:14 +00:00
|
|
|
}
|
|
|
|
|
2016-08-23 06:45:33 +00:00
|
|
|
namespace ui {
|
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
POCSAGSettingsView::POCSAGSettingsView(
|
|
|
|
NavigationView& nav,
|
|
|
|
POCSAGSettings& settings)
|
|
|
|
: settings_{settings} {
|
|
|
|
add_children(
|
2023-09-19 09:46:30 +00:00
|
|
|
{&labels,
|
2023-08-31 06:05:49 +00:00
|
|
|
&check_log,
|
2023-08-25 21:45:02 +00:00
|
|
|
&check_log_raw,
|
|
|
|
&check_small_font,
|
2023-08-27 02:32:02 +00:00
|
|
|
&check_hide_bad,
|
|
|
|
&check_hide_addr_only,
|
2023-09-19 09:46:30 +00:00
|
|
|
&opt_filter_mode,
|
|
|
|
&field_filter_address,
|
2023-08-25 21:45:02 +00:00
|
|
|
&button_save});
|
|
|
|
|
|
|
|
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);
|
2023-08-27 02:32:02 +00:00
|
|
|
check_hide_bad.set_value(settings_.hide_bad_data);
|
|
|
|
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
2023-09-19 09:46:30 +00:00
|
|
|
opt_filter_mode.set_by_value(settings_.filter_mode);
|
|
|
|
field_filter_address.set_value(settings_.filter_address);
|
2023-08-25 21:45:02 +00:00
|
|
|
|
|
|
|
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();
|
2023-08-27 02:32:02 +00:00
|
|
|
settings_.hide_bad_data = check_hide_bad.value();
|
|
|
|
settings_.hide_addr_only = check_hide_addr_only.value();
|
2023-09-19 09:46:30 +00:00
|
|
|
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
|
|
|
settings_.filter_address = field_filter_address.to_integer();
|
2023-08-25 21:45:02 +00:00
|
|
|
|
|
|
|
nav.pop();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-18 19:11:04 +00:00
|
|
|
POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
|
|
|
: nav_{nav} {
|
2023-09-19 09:46:30 +00:00
|
|
|
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
add_children(
|
|
|
|
{&rssi,
|
|
|
|
&audio,
|
|
|
|
&field_rf_amp,
|
|
|
|
&field_lna,
|
|
|
|
&field_vga,
|
|
|
|
&field_frequency,
|
2023-08-27 22:56:40 +00:00
|
|
|
&field_squelch,
|
2023-08-25 21:45:02 +00:00
|
|
|
&field_volume,
|
|
|
|
&image_status,
|
2023-08-26 18:43:34 +00:00
|
|
|
&text_packet_count,
|
2023-09-08 17:41:09 +00:00
|
|
|
&widget_baud,
|
2023-09-04 04:49:44 +00:00
|
|
|
&widget_bits,
|
|
|
|
&widget_frames,
|
2023-09-19 09:46:30 +00:00
|
|
|
&button_filter_last,
|
2023-08-25 21:45:02 +00:00
|
|
|
&button_config,
|
|
|
|
&console});
|
|
|
|
|
2023-09-11 00:04:20 +00:00
|
|
|
// No app settings, use fallbacks from pmem.
|
2023-08-25 21:45:02 +00:00
|
|
|
if (!app_settings_.loaded()) {
|
2023-09-19 09:46:30 +00:00
|
|
|
settings_.filter_address = pmem::pocsag_ignore_address();
|
|
|
|
settings_.filter_mode = (settings_.filter_address == 0)
|
|
|
|
? FILTER_NONE
|
|
|
|
: FILTER_DROP;
|
2023-08-25 21:45:02 +00:00
|
|
|
}
|
2023-06-14 14:54:19 +00:00
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
logger.append(LOG_ROOT_DIR "/POCSAG.TXT");
|
2023-06-04 19:56:46 +00:00
|
|
|
|
2023-08-27 22:56:40 +00:00
|
|
|
field_squelch.set_value(receiver_model.squelch_level());
|
|
|
|
field_squelch.on_change = [this](int32_t v) {
|
|
|
|
receiver_model.set_squelch_level(v);
|
|
|
|
};
|
|
|
|
|
2023-09-19 09:46:30 +00:00
|
|
|
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();
|
2023-08-25 21:45:02 +00:00
|
|
|
};
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
button_config.on_select = [this](Button&) {
|
|
|
|
nav_.push<POCSAGSettingsView>(settings_);
|
|
|
|
nav_.set_on_pop([this]() { refresh_ui(); });
|
|
|
|
};
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
refresh_ui();
|
2023-05-18 20:16:05 +00:00
|
|
|
audio::output::start();
|
2023-08-27 22:56:40 +00:00
|
|
|
receiver_model.enable();
|
2023-05-18 20:16:05 +00:00
|
|
|
baseband::set_pocsag();
|
2016-08-23 06:45:33 +00:00
|
|
|
}
|
|
|
|
|
2023-06-11 18:47:13 +00:00
|
|
|
void POCSAGAppView::focus() {
|
2023-06-18 06:14:29 +00:00
|
|
|
field_frequency.focus();
|
2023-06-11 18:47:13 +00:00
|
|
|
}
|
2022-04-15 12:43:44 +00:00
|
|
|
|
2023-06-11 18:47:13 +00:00
|
|
|
POCSAGAppView::~POCSAGAppView() {
|
2023-05-18 20:16:05 +00:00
|
|
|
audio::output::stop();
|
|
|
|
receiver_model.disable();
|
|
|
|
baseband::shutdown();
|
2023-08-25 21:45:02 +00:00
|
|
|
|
2023-09-11 00:04:20 +00:00
|
|
|
// Save pmem settings.
|
2023-09-19 09:46:30 +00:00
|
|
|
pmem::set_pocsag_ignore_address(settings_.filter_address);
|
2023-09-12 19:38:19 +00:00
|
|
|
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
|
2016-08-23 06:45:33 +00:00
|
|
|
}
|
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
void POCSAGAppView::refresh_ui() {
|
2023-09-19 09:46:30 +00:00
|
|
|
// Set console font style.
|
2023-08-25 21:45:02 +00:00
|
|
|
console.set_style(
|
|
|
|
settings_.enable_small_font
|
|
|
|
? &Styles::white_small
|
|
|
|
: &Styles::white);
|
2023-09-19 09:46:30 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
2023-08-25 21:45:02 +00:00
|
|
|
}
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-09-19 09:46:30 +00:00
|
|
|
// Ignore address? TODO: could filter earlier.
|
|
|
|
if (ignore_address(pocsag_state.address)) {
|
2023-08-26 18:43:34 +00:00
|
|
|
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
|
|
|
|
last_address = pocsag_state.address;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Color indicates the message has a lot of decoding errors.
|
|
|
|
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
|
2023-08-25 21:45:02 +00:00
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
std::string console_info = "\n" + color + prefix;
|
|
|
|
console_info += " #" + to_string_dec_uint(pocsag_state.address);
|
|
|
|
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
|
|
|
|
|
|
|
if (pocsag_state.out_type == ADDRESS) {
|
|
|
|
last_address = pocsag_state.address;
|
|
|
|
|
2023-08-27 02:32:02 +00:00
|
|
|
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));
|
|
|
|
}
|
2023-08-26 18:43:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} else if (pocsag_state.out_type == MESSAGE) {
|
|
|
|
if (pocsag_state.address != last_address) {
|
|
|
|
// New message
|
|
|
|
last_address = pocsag_state.address;
|
|
|
|
console.writeln(console_info);
|
|
|
|
console.write(color + pocsag_state.output);
|
|
|
|
} else {
|
|
|
|
// Message continues...
|
|
|
|
console.write(color + pocsag_state.output);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (logging()) {
|
|
|
|
logger.log_decoded(
|
|
|
|
timestamp,
|
|
|
|
to_string_dec_uint(pocsag_state.address) +
|
|
|
|
" F" + to_string_dec_uint(pocsag_state.function) +
|
2023-08-27 02:32:02 +00:00
|
|
|
" " + pocsag_state.output);
|
2023-08-26 18:43:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static Color get_status_color(const POCSAGState& state) {
|
|
|
|
if (state.out_type == IDLE)
|
|
|
|
return Color::white();
|
|
|
|
|
|
|
|
switch (state.mode) {
|
|
|
|
case STATE_CLEAR:
|
|
|
|
return Color::cyan();
|
|
|
|
case STATE_HAVE_ADDRESS:
|
|
|
|
return Color::yellow();
|
|
|
|
case STATE_GETTING_MSG:
|
|
|
|
return Color::green();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shouldn't get here...
|
|
|
|
return Color::red();
|
|
|
|
}
|
|
|
|
|
|
|
|
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
2023-08-25 21:45:02 +00:00
|
|
|
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;
|
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
// 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));
|
|
|
|
|
2023-08-25 21:45:02 +00:00
|
|
|
if (logging_raw())
|
|
|
|
logger.log_raw_data(message->packet, receiver_model.target_frequency());
|
|
|
|
|
|
|
|
if (message->packet.flag() != NORMAL) {
|
2023-08-26 18:43:34 +00:00
|
|
|
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
2023-08-25 21:45:02 +00:00
|
|
|
last_address = 0;
|
|
|
|
} else {
|
2023-08-26 18:43:34 +00:00
|
|
|
// Set color before to be able to see if decode gets stuck.
|
|
|
|
image_status.set_foreground(Color::magenta());
|
|
|
|
pocsag_state.codeword_index = 0;
|
|
|
|
pocsag_state.errors = 0;
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
// Handle multiple messages (if any).
|
|
|
|
while (pocsag_decode_batch(message->packet, pocsag_state))
|
|
|
|
handle_decoded(message->packet.timestamp(), prefix);
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-08-26 18:43:34 +00:00
|
|
|
// Handle the remainder.
|
|
|
|
handle_decoded(message->packet.timestamp(), prefix);
|
2023-05-18 20:16:05 +00:00
|
|
|
}
|
2023-08-26 18:43:34 +00:00
|
|
|
|
|
|
|
// Set status icon color to indicate state machine state.
|
|
|
|
image_status.set_foreground(get_status_color(pocsag_state));
|
2023-08-25 21:45:02 +00:00
|
|
|
}
|
2023-05-18 20:16:05 +00:00
|
|
|
|
2023-09-04 04:49:44 +00:00
|
|
|
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
|
2023-09-08 17:41:09 +00:00
|
|
|
widget_baud.set_rate(stats->baud_rate);
|
2023-09-04 04:49:44 +00:00
|
|
|
widget_bits.set_bits(stats->current_bits);
|
|
|
|
widget_frames.set_frames(stats->current_frames);
|
|
|
|
widget_frames.set_sync(stats->has_sync);
|
|
|
|
}
|
|
|
|
|
2023-09-08 17:41:09 +00:00
|
|
|
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, Styles::white_small, top);
|
|
|
|
painter.draw_char({p.x(), p.y() + 8}, Styles::white_small, bot);
|
|
|
|
}
|
|
|
|
|
2023-09-04 04:49:44 +00:00
|
|
|
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 ? Color::white() : Color::black());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void FrameIndicator::paint(Painter& painter) {
|
|
|
|
auto p = screen_pos();
|
|
|
|
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey());
|
|
|
|
|
|
|
|
for (size_t i = 0; i < height; ++i) {
|
2023-09-08 17:41:09 +00:00
|
|
|
auto p2 = p + Point{2, 15 - (int)i};
|
2023-09-04 04:49:44 +00:00
|
|
|
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
|
|
|
|
}
|
2016-08-23 06:45:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} /* namespace ui */
|