mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-16 03:22:59 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3312a704a | |||
| 31e8019642 | |||
| 9525738118 | |||
| 62310ad9a9 | |||
| 4819a2f4e2 | |||
| 2435ee780f | |||
| 5d602ece5c | |||
| 900086c1c9 | |||
| 07be74701f | |||
| f46e20c977 | |||
| 4bc752b7a8 | |||
| c28b608146 | |||
| bd4948602a | |||
| 4774b3097f | |||
| de81156223 | |||
| e7e1bedcad | |||
| d8930db8af | |||
| 014db9e233 | |||
| 933920edfd | |||
| cf25d85d51 |
@@ -316,6 +316,7 @@ set(CPPSRC
|
||||
# ui_replay_view.cpp
|
||||
# ui_script.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
config_mode.cpp
|
||||
${CPLD_20150901_DATA_CPP}
|
||||
${CPLD_20170522_DATA_CPP}
|
||||
${HACKRF_CPLD_DATA_CPP}
|
||||
|
||||
@@ -69,7 +69,7 @@ void LGEView::generate_frame_touche() {
|
||||
// 0D 96 02 12 0E 00 46 28 01 45 27 01 44 23 66 30
|
||||
std::vector<uint8_t> data{0x46, 0x28, 0x01, 0x45, 0x27, 0x01, 0x44, 0x23};
|
||||
|
||||
console.write("\n\x1B\x07Touche:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_LIGHT_GREY "Touche:");
|
||||
generate_lge_frame(0x96, (field_player.value() << 8) | field_room.value(), 0x0001, data);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ void LGEView::generate_frame_nickname() {
|
||||
|
||||
data.insert(data.end(), data_footer.begin(), data_footer.end());
|
||||
|
||||
console.write("\n\x1B\x0ESet nickname:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_YELLOW "Set nickname:");
|
||||
|
||||
generate_lge_frame(0x02, 0x001A, field_player.value(), data);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ void LGEView::generate_frame_team() {
|
||||
|
||||
data.push_back(field_team.value() - 1); // Color ?
|
||||
|
||||
console.write("\n\x1B\x0ASet team:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_GREEN "Set team:");
|
||||
|
||||
generate_lge_frame(0x03, data);
|
||||
}
|
||||
@@ -173,9 +173,7 @@ void LGEView::generate_frame_broadcast_nickname() {
|
||||
|
||||
data.push_back(field_team.value());
|
||||
|
||||
console.write(
|
||||
"\n\x1B\x09"
|
||||
"Broadcast nickname:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_BLUE "Broadcast nickname:");
|
||||
|
||||
generate_lge_frame(0x04, data);
|
||||
}
|
||||
@@ -187,14 +185,14 @@ void LGEView::generate_frame_start() {
|
||||
|
||||
// data[0] = field_room.value(); // ?
|
||||
|
||||
console.write("\n\x1B\x0DStart:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_MAGENTA "Start:");
|
||||
generate_lge_frame(0x05, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_gameover() {
|
||||
std::vector<uint8_t> data{(uint8_t)field_room.value()};
|
||||
|
||||
console.write("\n\x1B\x0CGameover:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_RED "Gameover:");
|
||||
generate_lge_frame(0x0D, data);
|
||||
}
|
||||
|
||||
@@ -229,9 +227,7 @@ void LGEView::generate_frame_collier() {
|
||||
|
||||
data.push_back(checksum - id);
|
||||
|
||||
console.write(
|
||||
"\n\x1B\x06"
|
||||
"Config:\x1B\x10");
|
||||
console.write("\n" STR_COLOR_DARK_YELLOW "Config:");
|
||||
generate_lge_frame(0x00, 0x3713, 0x3713, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,8 @@ void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32
|
||||
log_file.write_entry(packet.timestamp(), entry);
|
||||
}
|
||||
|
||||
void POCSAGLogger::log_decoded(
|
||||
const pocsag::POCSAGPacket& packet,
|
||||
const std::string text) {
|
||||
log_file.write_entry(packet.timestamp(), text);
|
||||
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
||||
log_file.write_entry(timestamp, text);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
@@ -56,23 +54,32 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
POCSAGSettings& settings)
|
||||
: settings_{settings} {
|
||||
add_children(
|
||||
{&check_log,
|
||||
{&check_beta,
|
||||
&check_log,
|
||||
&check_log_raw,
|
||||
&check_small_font,
|
||||
&check_hide_bad,
|
||||
&check_hide_addr_only,
|
||||
&check_ignore,
|
||||
&field_ignore,
|
||||
&button_save});
|
||||
|
||||
check_beta.set_value(settings_.use_new_proc);
|
||||
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_ignore.set_value(settings_.enable_ignore);
|
||||
field_ignore.set_value(settings_.address_to_ignore);
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
settings_.use_new_proc = check_beta.value();
|
||||
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_ignore = check_ignore.value();
|
||||
settings_.address_to_ignore = field_ignore.value();
|
||||
|
||||
@@ -82,7 +89,10 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
|
||||
POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
|
||||
if (settings_.use_new_proc)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||
else
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
|
||||
|
||||
add_children(
|
||||
{&rssi,
|
||||
@@ -91,8 +101,13 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&field_squelch,
|
||||
&field_volume,
|
||||
&image_status,
|
||||
&text_packet_count,
|
||||
&widget_baud,
|
||||
&widget_bits,
|
||||
&widget_frames,
|
||||
&button_ignore_last,
|
||||
&button_config,
|
||||
&console});
|
||||
@@ -106,6 +121,11 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
|
||||
logger.append(LOG_ROOT_DIR "/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_ignore_last.on_select = [this](Button&) {
|
||||
settings_.enable_ignore = true;
|
||||
settings_.address_to_ignore = last_address;
|
||||
@@ -117,8 +137,8 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
receiver_model.enable();
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
baseband::set_pocsag();
|
||||
}
|
||||
|
||||
@@ -143,86 +163,158 @@ void POCSAGAppView::refresh_ui() {
|
||||
: &Styles::white);
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
||||
packet_toggle = !packet_toggle;
|
||||
image_status.set_foreground(packet_toggle
|
||||
? Color::dark_grey()
|
||||
: Color::white());
|
||||
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;
|
||||
}
|
||||
|
||||
// Ignored address.
|
||||
if (ignore() && pocsag_state.address == settings_.address_to_ignore) {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
} 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) +
|
||||
" " + pocsag_state.output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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\x1B\x04" + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
last_address = 0;
|
||||
return;
|
||||
} else {
|
||||
pocsag_decode_batch(message->packet, &pocsag_state);
|
||||
// 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;
|
||||
|
||||
/*
|
||||
// Too many errors for reliable decode.
|
||||
if (pocsag_state.errors >= 3) {
|
||||
console.write("\n\x1B\x0D" + prefix + " Too many decode errors.");
|
||||
last_address = 0;
|
||||
return;
|
||||
}
|
||||
*/
|
||||
// Handle multiple messages (if any).
|
||||
while (pocsag_decode_batch(message->packet, pocsag_state))
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
|
||||
// Ignored address.
|
||||
if (ignore() && pocsag_state.address == settings_.address_to_ignore) {
|
||||
console.write("\n\x1B\x03" + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
|
||||
last_address = pocsag_state.address;
|
||||
return;
|
||||
}
|
||||
// Handle the remainder.
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
}
|
||||
|
||||
// Color indicates the message has lots of decoding errors.
|
||||
std::string color = pocsag_state.errors >= 3 ? "\x1B\x07" : "";
|
||||
// Set status icon color to indicate state machine state.
|
||||
image_status.set_foreground(get_status_color(pocsag_state));
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
last_address = pocsag_state.address;
|
||||
console.write(console_info);
|
||||
void BaudIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
char top = '-';
|
||||
char bot = '-';
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
message->packet,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" Address only");
|
||||
}
|
||||
if (rate_ > 0) {
|
||||
auto r = rate_ / 100;
|
||||
top = (r / 10) + '0';
|
||||
bot = (r % 10) + '0';
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
painter.draw_char(p, Styles::white_small, top);
|
||||
painter.draw_char({p.x(), p.y() + 8}, Styles::white_small, bot);
|
||||
}
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
message->packet,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" > " + pocsag_state.output);
|
||||
}
|
||||
}
|
||||
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 POCSAGAppView::on_stats(const POCSAGStatsMessage*) {
|
||||
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) {
|
||||
auto p2 = p + Point{2, 15 - (int)i};
|
||||
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -44,7 +44,7 @@ class POCSAGLogger {
|
||||
}
|
||||
|
||||
void log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency);
|
||||
void log_decoded(const pocsag::POCSAGPacket& packet, const std::string text);
|
||||
void log_decoded(Timestamp timestamp, const std::string& text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
@@ -52,12 +52,77 @@ class POCSAGLogger {
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BaudIndicator : public Widget {
|
||||
public:
|
||||
BaudIndicator(Point position)
|
||||
: Widget{{position, {5, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_rate(uint16_t rate) {
|
||||
if (rate != rate_) {
|
||||
rate_ = rate;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint16_t rate_ = 0;
|
||||
};
|
||||
|
||||
class BitsIndicator : public Widget {
|
||||
public:
|
||||
BitsIndicator(Point position)
|
||||
: Widget{{position, {2, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_bits(uint32_t bits) {
|
||||
if (bits != bits_) {
|
||||
bits_ = bits;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint32_t bits_ = 0;
|
||||
};
|
||||
|
||||
class FrameIndicator : public Widget {
|
||||
public:
|
||||
FrameIndicator(Point position)
|
||||
: Widget{{position, {4, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_frames(uint8_t frame_count) {
|
||||
if (frame_count != frame_count_) {
|
||||
frame_count_ = frame_count;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void set_sync(bool has_sync) {
|
||||
if (has_sync != has_sync_) {
|
||||
has_sync_ = has_sync;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint8_t frame_count_ = 0;
|
||||
bool has_sync_ = false;
|
||||
};
|
||||
|
||||
struct POCSAGSettings {
|
||||
bool enable_small_font = false;
|
||||
bool enable_logging = false;
|
||||
bool enable_raw_log = false;
|
||||
bool enable_ignore = false;
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
uint32_t address_to_ignore = 0;
|
||||
bool use_new_proc = false;
|
||||
};
|
||||
|
||||
class POCSAGSettingsView : public View {
|
||||
@@ -65,43 +130,56 @@ class POCSAGSettingsView : public View {
|
||||
POCSAGSettingsView(NavigationView& nav, POCSAGSettings& settings);
|
||||
|
||||
std::string title() const override { return "POCSAG Config"; };
|
||||
void focus() override { button_save.focus(); }
|
||||
|
||||
private:
|
||||
POCSAGSettings& settings_;
|
||||
|
||||
Checkbox check_beta{
|
||||
{0 * 8 + 2, 18 * 16 - 4},
|
||||
6,
|
||||
"Beta",
|
||||
true /*small*/};
|
||||
|
||||
Checkbox check_log{
|
||||
{2 * 8, 2 * 16},
|
||||
10,
|
||||
"Enable Log",
|
||||
false};
|
||||
"Enable Log"};
|
||||
|
||||
Checkbox check_log_raw{
|
||||
{2 * 8, 4 * 16},
|
||||
12,
|
||||
"Log Raw Data",
|
||||
false};
|
||||
"Log Raw Data"};
|
||||
|
||||
Checkbox check_small_font{
|
||||
{2 * 8, 6 * 16},
|
||||
4,
|
||||
"Use Small Font",
|
||||
false};
|
||||
"Use Small Font"};
|
||||
|
||||
Checkbox check_ignore{
|
||||
Checkbox check_hide_bad{
|
||||
{2 * 8, 8 * 16},
|
||||
22,
|
||||
"Enable Ignored Address",
|
||||
false};
|
||||
"Hide Bad Data"};
|
||||
|
||||
Checkbox check_hide_addr_only{
|
||||
{2 * 8, 10 * 16},
|
||||
22,
|
||||
"Hide Addr Only"};
|
||||
|
||||
Checkbox check_ignore{
|
||||
{2 * 8, 12 * 16},
|
||||
22,
|
||||
"Enable Ignored Address"};
|
||||
|
||||
NumberField field_ignore{
|
||||
{7 * 8, 9 * 16 + 8},
|
||||
{7 * 8, 13 * 16 + 8},
|
||||
7,
|
||||
{0, 9999999},
|
||||
1,
|
||||
'0'};
|
||||
|
||||
Button button_save{
|
||||
{12 * 8, 16 * 16, 10 * 8, 2 * 16},
|
||||
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
@@ -118,12 +196,11 @@ class POCSAGAppView : public View {
|
||||
bool logging() const { return settings_.enable_logging; };
|
||||
bool logging_raw() const { return settings_.enable_raw_log; };
|
||||
bool ignore() const { return settings_.enable_ignore; };
|
||||
bool hide_bad_data() const { return settings_.hide_bad_data; };
|
||||
bool hide_addr_only() const { return settings_.hide_addr_only; };
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
12'500, // POCSAG is FSK +/- 4.5MHz, 12k5 is a good filter.
|
||||
3'072'000, // Match baseband_fs in proc_pocsag.
|
||||
};
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
// Settings
|
||||
POCSAGSettings settings_{};
|
||||
@@ -133,41 +210,70 @@ class POCSAGAppView : public View {
|
||||
{
|
||||
{"small_font"sv, &settings_.enable_small_font},
|
||||
{"enable_logging"sv, &settings_.enable_logging},
|
||||
{"enable_raw_log"sv, &settings_.enable_raw_log},
|
||||
{"enable_ignore"sv, &settings_.enable_ignore},
|
||||
{"address_to_ignore"sv, &settings_.address_to_ignore},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
{"use_new_proc"sv, &settings_.use_new_proc},
|
||||
}};
|
||||
|
||||
void refresh_ui();
|
||||
void handle_decoded(Timestamp timestamp, const std::string& prefix);
|
||||
void on_packet(const POCSAGPacketMessage* message);
|
||||
void on_stats(const POCSAGStatsMessage* stats);
|
||||
|
||||
uint32_t last_address = 0xFFFFFFFF;
|
||||
pocsag::POCSAGState pocsag_state{};
|
||||
pocsag::EccContainer ecc{};
|
||||
pocsag::POCSAGState pocsag_state{&ecc};
|
||||
POCSAGLogger logger{};
|
||||
bool packet_toggle = false;
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 3, 6 * 8, 4}};
|
||||
Audio audio{
|
||||
{21 * 8, 8, 6 * 8, 4}};
|
||||
uint16_t packet_count = 0;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{11 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{13 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{19 * 8 - 4, 3, 6 * 8, 4}};
|
||||
Audio audio{
|
||||
{19 * 8 - 4, 8, 6 * 8, 4}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{25 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
true /*wrap*/};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
Image image_status{
|
||||
{7 * 8, 1 * 16 + 2, 16, 16},
|
||||
{0 * 8 + 4, 1 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_pocsag,
|
||||
Color::white(),
|
||||
Color::black()};
|
||||
|
||||
Text text_packet_count{
|
||||
{3 * 8, 1 * 16 + 2, 5 * 8, 16},
|
||||
"0"};
|
||||
|
||||
BitsIndicator widget_bits{
|
||||
{8 * 8 + 1, 1 * 16 + 2}};
|
||||
|
||||
FrameIndicator widget_frames{
|
||||
{8 * 8 + 4, 1 * 16 + 2}};
|
||||
|
||||
BaudIndicator widget_baud{
|
||||
{8 * 9 + 1, 1 * 16 + 2}};
|
||||
|
||||
Button button_ignore_last{
|
||||
{10 * 8, 1 * 16, 12 * 8, 20},
|
||||
"Ignore Last"};
|
||||
|
||||
@@ -8,7 +8,7 @@ AboutView::AboutView(NavigationView& nav) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
console.writeln("\x1B\x07List of contributors:\x1B\x10");
|
||||
console.writeln(STR_COLOR_LIGHT_GREY "List of contributors:");
|
||||
console.writeln("");
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ void AboutView::update() {
|
||||
case 1:
|
||||
// TODO: Generate this automatically from github
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2022-01-01&from=2020-04-12&type=c
|
||||
console.writeln("\x1B\x06Mayhem:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "Mayhem:");
|
||||
console.writeln("eried,euquiq,gregoryfenton");
|
||||
console.writeln("johnelder,jwetzell,nnemanjan00");
|
||||
console.writeln("N0vaPixel,klockee,GullCode");
|
||||
@@ -34,13 +34,15 @@ void AboutView::update() {
|
||||
console.writeln("notpike,jLynx,zigad");
|
||||
console.writeln("MichalLeonBorsuk,jimilinuxguy");
|
||||
console.writeln("kallanreed,bernd-herzog");
|
||||
break;
|
||||
case 2:
|
||||
console.writeln("NotherNgineer,zxkmm,u-foka");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 3:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2020-04-12&from=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06Havoc:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "Havoc:");
|
||||
console.writeln("furrtek,mrmookie,NotPike");
|
||||
console.writeln("mjwaxios,ImDroided,Giorgiofox");
|
||||
console.writeln("F4GEV,z4ziggy,xmycroftx");
|
||||
@@ -51,16 +53,16 @@ void AboutView::update() {
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?from=2014-07-05&to=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06PortaPack:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "PortaPack:");
|
||||
console.writeln("jboone,argilo");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
case 5:
|
||||
// https://github.com/mossmann/hackrf/graphs/contributors
|
||||
console.writeln("\x1B\x06HackRF:\x1B\x10");
|
||||
console.writeln(STR_COLOR_DARK_YELLOW "HackRF:");
|
||||
console.writeln("mossmann,dominicgs,bvernoux");
|
||||
console.writeln("bgamari,schneider42,miek");
|
||||
console.writeln("willcode,hessu,Sec42");
|
||||
|
||||
@@ -40,24 +40,22 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
char aged_color;
|
||||
Color target_color;
|
||||
auto entry_age = entry.age;
|
||||
std::string entry_string;
|
||||
|
||||
// Color decay for flights not being updated anymore
|
||||
if (entry_age < ADSB_CURRENT) {
|
||||
aged_color = 0x10;
|
||||
entry_string = "";
|
||||
target_color = Color::green();
|
||||
} else if (entry_age < ADSB_RECENT) {
|
||||
aged_color = 0x07;
|
||||
entry_string = STR_COLOR_LIGHT_GREY;
|
||||
target_color = Color::light_grey();
|
||||
} else {
|
||||
aged_color = 0x08;
|
||||
entry_string = STR_COLOR_DARK_GREY;
|
||||
target_color = Color::grey();
|
||||
}
|
||||
|
||||
std::string entry_string = "\x1B";
|
||||
entry_string += aged_color;
|
||||
entry_string +=
|
||||
(entry.callsign[0] != ' ' ? entry.callsign + " " : entry.icaoStr + " ") +
|
||||
to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4) +
|
||||
|
||||
@@ -42,15 +42,12 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
char aged_color;
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
target_color = Color::green();
|
||||
|
||||
aged_color = 0x10;
|
||||
std::string entry_string = "\x1B";
|
||||
entry_string += aged_color;
|
||||
std::string entry_string = "";
|
||||
|
||||
entry_string += entry.source_formatted;
|
||||
entry_string.append(10 - entry.source_formatted.size(), ' ');
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_debug.hpp"
|
||||
#include "debug.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
@@ -406,6 +407,7 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
{"Touch Test", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugScreenTest>(); }},
|
||||
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
|
||||
//{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }}, // temporarily disabled to conserve ROM space
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
|
||||
@@ -249,7 +249,10 @@ void FileManBaseView::refresh_list() {
|
||||
auto entry_name = truncate(entry.path, 20);
|
||||
|
||||
if (entry.is_directory) {
|
||||
auto size_str = (entry.path == parent_dir_path) ? "" : to_string_dec_uint(file_count(entry.path));
|
||||
auto size_str =
|
||||
(entry.path == parent_dir_path)
|
||||
? ""
|
||||
: to_string_dec_uint(file_count(current_path / entry.path));
|
||||
|
||||
menu_view.add_item(
|
||||
{entry_name + std::string(21 - entry_name.length(), ' ') + size_str,
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#include "ui_level.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "file.hpp"
|
||||
#include "oversample.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
@@ -174,48 +176,52 @@ size_t LevelView::change_mode(freqman_index_t new_mod) {
|
||||
switch (new_mod) {
|
||||
case AM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw DSB (0) default
|
||||
field_bw.set_by_value(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
receiver_model.set_am_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
|
||||
// bw DSB (0) default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
break;
|
||||
case NFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 16k (2) default
|
||||
field_bw.set_by_value(2);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
|
||||
// bw 16k (2) default
|
||||
field_bw.set_by_value(2);
|
||||
break;
|
||||
case WFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 200k (0) only/default
|
||||
field_bw.set_by_value(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
|
||||
// bw 200k (0) only/default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
break;
|
||||
case SPEC_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
field_bw.set_by_value(0);
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) {
|
||||
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
// Baseband needs to know the desired sampling and oversampling rates.
|
||||
baseband::set_sample_rate(sampling_rate, get_oversample_rate(sampling_rate));
|
||||
|
||||
// The radio needs to know the effective sampling rate.
|
||||
auto actual_sampling_rate = get_actual_sample_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(actual_sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
|
||||
};
|
||||
field_bw.set_by_value(0);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (new_mod != SPEC_MODULATION) {
|
||||
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
|
||||
// Reset receiver model to fix bug when going from SPEC to audio, the sound is distorted.
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
}
|
||||
|
||||
@@ -1124,48 +1124,50 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
|
||||
switch (new_mod) {
|
||||
case AM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw DSB (0) default
|
||||
field_bw.set_by_value(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
receiver_model.set_am_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_am_configuration(n); };
|
||||
// bw DSB (0) default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
recording_sampling_rate = 12000;
|
||||
break;
|
||||
case NFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 16k (2) default
|
||||
field_bw.set_by_value(2);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.set_nbfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_nbfm_configuration(n); };
|
||||
// bw 16k (2) default
|
||||
field_bw.set_by_value(2);
|
||||
recording_sampling_rate = 24000;
|
||||
break;
|
||||
case WFM_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 200k (0) default
|
||||
field_bw.set_by_value(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
receiver_model.set_wfm_configuration(field_bw.selected_index_value());
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t n) { receiver_model.set_wfm_configuration(n); };
|
||||
// bw 200k (0) default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
recording_sampling_rate = 48000;
|
||||
break;
|
||||
case SPEC_MODULATION:
|
||||
freqman_set_bandwidth_option(new_mod, field_bw);
|
||||
// bw 12k5 (0) default
|
||||
field_bw.set_by_value(0);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
field_bw.on_change = [this](size_t, OptionsField::value_t sampling_rate) {
|
||||
auto anti_alias_baseband_bandwidth_filter = filter_bandwidth_for_sampling_rate(sampling_rate);
|
||||
record_view->set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
auto actual_sampling_rate = record_view->set_sampling_rate(sampling_rate);
|
||||
|
||||
// The radio needs to know the effective sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
|
||||
};
|
||||
// bw 12k5 (0) default
|
||||
field_bw.set_by_value(0);
|
||||
text_ctcss.set(" ");
|
||||
break;
|
||||
default:
|
||||
@@ -1173,7 +1175,6 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
|
||||
}
|
||||
if (new_mod != SPEC_MODULATION) {
|
||||
button_audio_app.set_text("AUDIO");
|
||||
// TODO: Oversampling.
|
||||
record_view->set_sampling_rate(recording_sampling_rate);
|
||||
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorted
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
|
||||
@@ -169,6 +169,8 @@ SetRadioView::SetRadioView(
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
add_children({&disable_external_tcxo});
|
||||
|
||||
SetFrequencyCorrectionModel model{
|
||||
static_cast<int8_t>(pmem::correction_ppb() / 1000), 0};
|
||||
|
||||
@@ -221,10 +223,13 @@ SetRadioView::SetRadioView(
|
||||
send_system_refresh();
|
||||
};
|
||||
|
||||
disable_external_tcxo.set_value(pmem::config_disable_external_tcxo());
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
const auto model = this->form_collect();
|
||||
pmem::set_correction_ppb(model.ppm * 1000);
|
||||
pmem::set_clkout_freq(model.freq);
|
||||
pmem::set_config_disable_external_tcxo(disable_external_tcxo.value());
|
||||
clock_manager.enable_clock_output(pmem::clkout_enabled());
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
@@ -185,6 +185,11 @@ class SetRadioView : public View {
|
||||
5,
|
||||
"Turn on bias voltage"};
|
||||
|
||||
Checkbox disable_external_tcxo{
|
||||
{18, 14 * 16},
|
||||
5,
|
||||
"Disable external TCXO"};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Save"};
|
||||
@@ -365,7 +370,7 @@ class SetConverterSettingsView : public View {
|
||||
|
||||
Button button_return{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"return",
|
||||
"Return",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -325,12 +325,18 @@ uint32_t ClockManager::measure_gp_clkin_frequency() {
|
||||
}
|
||||
|
||||
bool ClockManager::loss_of_signal() {
|
||||
return hackrf_r9
|
||||
? clock_generator.plla_loss_of_signal()
|
||||
: clock_generator.clkin_loss_of_signal();
|
||||
if (hackrf_r9) {
|
||||
const auto frequency = measure_gp_clkin_frequency();
|
||||
return (frequency < 9850000) || (frequency > 10150000);
|
||||
} else {
|
||||
return clock_generator.clkin_loss_of_signal();
|
||||
}
|
||||
}
|
||||
|
||||
ClockManager::ReferenceSource ClockManager::detect_reference_source() {
|
||||
if (portapack::persistent_memory::config_disable_external_tcxo())
|
||||
return ReferenceSource::Xtal;
|
||||
|
||||
if (loss_of_signal()) {
|
||||
// No external reference. Turn on PortaPack reference (if present).
|
||||
portapack_tcxo_enable();
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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 "config_mode.hpp"
|
||||
#include "core_control.hpp"
|
||||
#include "hackrf_gpio.hpp"
|
||||
#include "portapack_hal.hpp"
|
||||
|
||||
void config_mode_blink_until_dfu();
|
||||
|
||||
void config_mode_set() {
|
||||
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_GUARD_VALUE);
|
||||
}
|
||||
|
||||
bool config_mode_should_enter() {
|
||||
return portapack::persistent_memory::config_mode_storage() == CONFIG_MODE_GUARD_VALUE;
|
||||
}
|
||||
|
||||
void config_mode_clear() {
|
||||
portapack::persistent_memory::set_config_mode_storage(CONFIG_MODE_NORMAL_VALUE);
|
||||
}
|
||||
|
||||
uint32_t blink_patterns[] = {
|
||||
0x00000000, // 0 Off
|
||||
0xFFFFFFFF, // 1 On
|
||||
0xF0F0F0F0, // 2 blink fast
|
||||
0x00FF00FF, // 3 blink slow
|
||||
0xFFF3FFF3 // 4 inverse blink slow
|
||||
};
|
||||
|
||||
void config_mode_run() {
|
||||
configure_pins_portapack();
|
||||
portapack::gpio_dfu.input();
|
||||
portapack::persistent_memory::cache::init();
|
||||
|
||||
if (hackrf_r9) {
|
||||
// When this runs on the hackrf r9 there likely was a crash during the last boot
|
||||
// caused by the external tcxo. So we disable it unless the user is intentially
|
||||
// running the config mode by pressing reset twice followed by pressing DFU.
|
||||
auto old_disable_external_tcxo = portapack::persistent_memory::config_disable_external_tcxo();
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo(true);
|
||||
portapack::persistent_memory::cache::persist();
|
||||
|
||||
config_mode_blink_until_dfu();
|
||||
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo(old_disable_external_tcxo);
|
||||
portapack::persistent_memory::cache::persist();
|
||||
} else {
|
||||
config_mode_blink_until_dfu();
|
||||
}
|
||||
|
||||
auto last_dfu_btn = portapack::gpio_dfu.read();
|
||||
|
||||
int32_t counter = 0;
|
||||
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
|
||||
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
|
||||
|
||||
while (true) {
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
|
||||
last_dfu_btn = dfu_btn;
|
||||
|
||||
if (dfu_clicked) {
|
||||
int8_t value = portapack::persistent_memory::config_cpld() +
|
||||
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
|
||||
|
||||
blink_pattern_value = value = (value + 1) % 10;
|
||||
|
||||
portapack::persistent_memory::set_config_cpld(value % 5);
|
||||
portapack::persistent_memory::set_config_disable_external_tcxo((value / 5) == 1);
|
||||
|
||||
portapack::persistent_memory::cache::persist();
|
||||
}
|
||||
|
||||
auto tx_blink_pattern = blink_patterns[blink_pattern_value % 5];
|
||||
auto rx_blink_pattern = blink_patterns[blink_pattern_value / 5];
|
||||
|
||||
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
|
||||
if (tx_value) {
|
||||
hackrf::one::led_tx.on();
|
||||
} else {
|
||||
hackrf::one::led_tx.off();
|
||||
}
|
||||
|
||||
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
|
||||
if (rx_value) {
|
||||
hackrf::one::led_rx.on();
|
||||
} else {
|
||||
hackrf::one::led_rx.off();
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
void config_mode_blink_until_dfu() {
|
||||
while (true) {
|
||||
hackrf::one::led_tx.on();
|
||||
hackrf::one::led_rx.on();
|
||||
hackrf::one::led_usb.on();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
hackrf::one::led_tx.off();
|
||||
hackrf::one::led_rx.off();
|
||||
hackrf::one::led_usb.off();
|
||||
chThdSleepMilliseconds(115);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
if (dfu_btn)
|
||||
break;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
if (!dfu_btn)
|
||||
break;
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(10);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef __CONFIG_MODE_H__
|
||||
#define __CONFIG_MODE_H__
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
void config_mode_set();
|
||||
bool config_mode_should_enter();
|
||||
void config_mode_clear();
|
||||
|
||||
void config_mode_run();
|
||||
|
||||
#endif /* __CONFIG_MODE_H__ */
|
||||
+184
-13
@@ -30,6 +30,7 @@
|
||||
#include "portapack.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
@@ -46,10 +47,12 @@ void __debug_log(const std::string& msg) {
|
||||
pg_debug_log->write_entry(msg);
|
||||
}
|
||||
|
||||
void runtime_error(LED);
|
||||
void runtime_error(uint8_t source);
|
||||
std::string number_to_hex_string(uint32_t);
|
||||
void draw_line(int32_t, const char*, regarm_t);
|
||||
void draw_stack_dump();
|
||||
static bool error_shown = false;
|
||||
uint32_t fault_address{0};
|
||||
|
||||
void draw_guru_meditation_header(uint8_t source, const char* hint) {
|
||||
Painter painter;
|
||||
@@ -67,8 +70,10 @@ void draw_guru_meditation_header(uint8_t source, const char* hint) {
|
||||
painter.draw_string({48, 24}, Styles::white, "M? Guru");
|
||||
painter.draw_string({48 + 8 * 8, 24}, Styles::white, "Meditation");
|
||||
|
||||
if (source == CORTEX_M0)
|
||||
if (source == CORTEX_M0) {
|
||||
painter.draw_string({48 + 8, 24}, Styles::white, "0");
|
||||
painter.draw_string({24, 320 - 32}, Styles::white, "Press DFU for Stack Dump");
|
||||
}
|
||||
|
||||
if (source == CORTEX_M4)
|
||||
painter.draw_string({48 + 8, 24}, Styles::white, "4");
|
||||
@@ -83,11 +88,7 @@ void draw_guru_meditation(uint8_t source, const char* hint) {
|
||||
draw_guru_meditation_header(source, hint);
|
||||
}
|
||||
|
||||
if (source == CORTEX_M0)
|
||||
runtime_error(hackrf::one::led_rx);
|
||||
|
||||
if (source == CORTEX_M4)
|
||||
runtime_error(hackrf::one::led_tx);
|
||||
runtime_error(source);
|
||||
}
|
||||
|
||||
void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp, uint32_t cfsr = 0) {
|
||||
@@ -110,6 +111,7 @@ void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp,
|
||||
// to see whats causing the fault.
|
||||
draw_line(80 + i++ * 20, "lr:", ctxp->lr_thd);
|
||||
draw_line(80 + i++ * 20, "pc:", ctxp->pc);
|
||||
fault_address = (uint32_t)ctxp->pc;
|
||||
|
||||
// see SCB_CFSR_* in libopencm3/cm3/scb.h for details
|
||||
if (cfsr != 0)
|
||||
@@ -117,11 +119,7 @@ void draw_guru_meditation(uint8_t source, const char* hint, struct extctx* ctxp,
|
||||
}
|
||||
}
|
||||
|
||||
if (source == CORTEX_M0)
|
||||
runtime_error(hackrf::one::led_rx);
|
||||
|
||||
if (source == CORTEX_M4)
|
||||
runtime_error(hackrf::one::led_tx);
|
||||
runtime_error(source);
|
||||
}
|
||||
|
||||
void draw_line(int32_t y_offset, const char* label, regarm_t value) {
|
||||
@@ -131,7 +129,9 @@ void draw_line(int32_t y_offset, const char* label, regarm_t value) {
|
||||
painter.draw_string({15 + 8 * 8, y_offset}, Styles::white, to_string_hex((uint32_t)value, 8));
|
||||
}
|
||||
|
||||
void runtime_error(LED led) {
|
||||
void runtime_error(uint8_t source) {
|
||||
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
|
||||
|
||||
led.off();
|
||||
|
||||
while (true) {
|
||||
@@ -139,9 +139,180 @@ void runtime_error(LED led) {
|
||||
while (n--)
|
||||
;
|
||||
led.toggle();
|
||||
|
||||
// Stack dump will cover entire screen, so wait for DFU button press to attempt it
|
||||
if (swizzled_switches() & (1 << (int)Switch::Dfu)) {
|
||||
draw_stack_dump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function should only be called with interrupts disabled due to reading swizzled_switches()
|
||||
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
|
||||
void draw_stack_dump() {
|
||||
Painter painter;
|
||||
uint32_t* p{&__process_stack_base__};
|
||||
constexpr int border{8};
|
||||
int x, y;
|
||||
int n{0};
|
||||
bool clear_rect{true};
|
||||
bool first_pass{true};
|
||||
|
||||
// NOTE: Stays in this loop forever unless LEFT button pressed
|
||||
while (1) {
|
||||
if (clear_rect) {
|
||||
painter.fill_rectangle(
|
||||
{border, border, portapack::display.width() - (border * 2), portapack::display.height() - (border * 2)},
|
||||
Color::black());
|
||||
x = y = border;
|
||||
clear_rect = false;
|
||||
}
|
||||
|
||||
// skip past unused stack words on 1st pass
|
||||
if (first_pass) {
|
||||
first_pass = false;
|
||||
while ((*p == CRT0_STACKS_FILL_PATTERN) && (p < &__process_stack_end__))
|
||||
p++;
|
||||
|
||||
auto stack_space_left = p - &__process_stack_base__;
|
||||
|
||||
// NOTE: in situations like a hard fault it seems not possible to write strings longer than 16 characters.
|
||||
painter.draw_string({x, y}, Styles::white_small, to_string_hex((uint32_t)&__process_stack_base__, 8));
|
||||
x += 8 * 5;
|
||||
painter.draw_string({x, y}, Styles::white_small, " M0 STACK free=");
|
||||
x += 15 * 5;
|
||||
painter.draw_string({x, y}, Styles::white_small, to_string_dec_uint(stack_space_left));
|
||||
x = border;
|
||||
y += 8;
|
||||
|
||||
// align subsequent lines to start on 16-byte boundaries
|
||||
p -= (stack_space_left & 3);
|
||||
}
|
||||
|
||||
// print one page of stack dump
|
||||
while ((p < &__process_stack_end__) && (y < portapack::display.height() - border - 8)) {
|
||||
// show address if start of line
|
||||
if (n++ == 0) {
|
||||
painter.draw_string({x, y}, Styles::white_small, to_string_hex((uint32_t)p, 8) + ":");
|
||||
x += 9 * 5 - 2; // note: saving 2 pixels here to prevent reaching right border
|
||||
}
|
||||
|
||||
// show stack word -- highlight if a possible code address (low bit will be set too for !thumb) or actual fault address
|
||||
bool code_addr = (*p > 0x1400) && (*p < 0x80000) && (((*p) & 0x1) == 0x1); // approximate address range of code .text region in ROM
|
||||
Style style = (fault_address && *p == fault_address) ? Styles::bg_yellow_small : (code_addr ? Styles::bg_white_small : Styles::white_small);
|
||||
painter.draw_string({x, y}, style, " " + to_string_hex(*p, 8));
|
||||
x += 9 * 5;
|
||||
|
||||
// new line?
|
||||
if (n == 4) {
|
||||
n = 0;
|
||||
x = border;
|
||||
y += 8;
|
||||
}
|
||||
|
||||
// point to next stack word
|
||||
p++;
|
||||
}
|
||||
|
||||
// Out of room on the screen or end of stack - allow Up/Down paging.
|
||||
// First wait for button release from previous press.
|
||||
// NOTE: can't call swizzle_switches() with interrupted enabled!
|
||||
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)))
|
||||
;
|
||||
|
||||
painter.draw_string({border, portapack::display.height() - border - 8}, Styles::white_small, "Use UP/DOWN key");
|
||||
|
||||
// Wait for button press.
|
||||
while (1) {
|
||||
auto switches = swizzled_switches();
|
||||
|
||||
// If LEFT button pressed, exit
|
||||
if (switches & (1 << (int)Switch::Left))
|
||||
return;
|
||||
|
||||
// If DOWN pressed; continue to show next page
|
||||
if (switches & (1 << (int)Switch::Down))
|
||||
break;
|
||||
|
||||
// If UP pressed, scroll back by one page
|
||||
if (switches & (1 << (int)Switch::Up)) {
|
||||
// Back up pointer by offset on this screen plus one full screen of 8 pixels per line & 4 words per line
|
||||
// (underflow can't happen here due to stack address range in RAM)
|
||||
p -= ((y - border) + (portapack::display.height() - (border * 2 + 1 * 8))) / (8 / 4);
|
||||
if (p < &__process_stack_base__)
|
||||
p = &__process_stack_base__;
|
||||
break;
|
||||
}
|
||||
|
||||
// If SEL button pressed, try writing to file (likely to hang in fault condition but heh)
|
||||
if (switches & (1 << (int)Switch::Sel))
|
||||
stack_dump();
|
||||
}
|
||||
|
||||
// clear screen to allow new range to be displayed, if we're not at the end
|
||||
if (p < &__process_stack_end__)
|
||||
clear_rect = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Disk I/O in this function doesn't work after a fault
|
||||
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
|
||||
bool stack_dump() {
|
||||
Painter painter;
|
||||
std::string debug_dir = "DEBUG";
|
||||
std::filesystem::path filename{};
|
||||
File stack_dump_file{};
|
||||
bool error;
|
||||
std::string str;
|
||||
uint32_t* p;
|
||||
int n{0};
|
||||
bool data_found{false};
|
||||
|
||||
make_new_directory(debug_dir);
|
||||
filename = next_filename_matching_pattern(debug_dir + "/STACK_DUMP_????.TXT");
|
||||
error = filename.empty();
|
||||
if (!error)
|
||||
error = stack_dump_file.create(filename) != 0;
|
||||
if (error) {
|
||||
painter.draw_string({16, 320 - 32}, ui::Styles::red, "ERROR DUMPING " + filename.filename().string() + "!");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (p = &__process_stack_base__; p < &__process_stack_end__; p++) {
|
||||
// skip past unused stack words
|
||||
if (!data_found) {
|
||||
if (*p == CRT0_STACKS_FILL_PATTERN)
|
||||
continue;
|
||||
else {
|
||||
data_found = true;
|
||||
auto stack_space_left = p - &__process_stack_base__;
|
||||
stack_dump_file.write_line(to_string_hex((uint32_t)&__process_stack_base__, 8) + ": Unused words " + to_string_dec_uint(stack_space_left));
|
||||
|
||||
// align subsequent lines to start on 16-byte boundaries
|
||||
p -= (stack_space_left & 3);
|
||||
}
|
||||
}
|
||||
|
||||
// write address
|
||||
if (n++ == 0) {
|
||||
str = to_string_hex((uint32_t)p, 8) + ":";
|
||||
stack_dump_file.write(str.data(), 9);
|
||||
}
|
||||
|
||||
// write stack dword
|
||||
str = " " + to_string_hex(*p, 8);
|
||||
stack_dump_file.write(str.data(), 9);
|
||||
|
||||
if (n == 4) {
|
||||
stack_dump_file.write("\r\n", 2);
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
|
||||
painter.draw_string({16, 320 - 32}, ui::Styles::green, filename.filename().string() + " dumped!");
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
#if CH_DBG_ENABLED
|
||||
void port_halt(void) {
|
||||
|
||||
@@ -47,4 +47,6 @@ inline uint32_t get_free_stack_space() {
|
||||
return stack_space_left;
|
||||
}
|
||||
|
||||
bool stack_dump();
|
||||
|
||||
#endif /*__DEBUG_H__*/
|
||||
|
||||
@@ -83,16 +83,22 @@ options_t freqman_bandwidths[4] = {
|
||||
{"100k", 100000},
|
||||
{"150k", 150000},
|
||||
{"250k", 250000},
|
||||
{"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
|
||||
{"600k", 600000}, /* We doubled x2 previous REC BW limit , now extended BW from 600k to 1M with fast enough SD card in C16 or C8 format .*/
|
||||
{"650k", 650000},
|
||||
{"500k", 500000},
|
||||
{"600k", 600000},
|
||||
{"750k", 750000},
|
||||
{"1000k", 1000000}, /* New limit bandwith option for recording in C16 (in fast SD card) or in C8 */
|
||||
{"1500k", 1500000}, /* From this BW onwards, the LCD is ok, but M4 CPU is having periodical sample rec dropps, (not real file size, accelerated replay) */
|
||||
{"1000k", 1000000}, // Max bandwith for recording in C16 (with fast SD card).
|
||||
{"1250k", 1250000},
|
||||
{"1500k", 1500000},
|
||||
{"1750k", 1750000},
|
||||
{"2000k", 2000000},
|
||||
{"2500k", 2500000},
|
||||
{"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC)
|
||||
{"2250k", 2250000}, // Max bandwith for recording in C8 (with fast SD card).
|
||||
{"2500k", 2500000}, // Here and up, LCD is ok, but M4 CPU drops samples.
|
||||
{"3000k", 3000000},
|
||||
{"3500k", 3500000},
|
||||
{"4000k", 4000000},
|
||||
{"4500k", 4500000},
|
||||
{"5000k", 5500000},
|
||||
{"5500k", 5500000}, // Max capture, needs /4 decimation, (22Mhz sampling ADC).
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -139,7 +139,9 @@ static uint8_t switches_raw = 0;
|
||||
// uint8_t rot_b() const { return (raw_ >> 6) & 1; }
|
||||
// uint8_t dfu() const { return (raw_ >> 7) & 1; }};
|
||||
|
||||
static uint8_t swizzle_raw(uint8_t raw) {
|
||||
uint8_t swizzled_switches() {
|
||||
uint8_t raw = io.io_update(touch_pins_configs[touch_phase]);
|
||||
|
||||
return (raw & 0x1F) | // Keep the bottom 5 bits the same.
|
||||
((raw >> 2) & 0x20) | // Shift the DFU bit down to bit 6.
|
||||
((raw << 1) & 0xC0); // Shift the encoder bits up to be 7 & 8.
|
||||
@@ -183,7 +185,7 @@ void timer0_callback(GPTDriver* const) {
|
||||
eventmask_t event_mask = 0;
|
||||
if (touch_update()) event_mask |= EVT_MASK_TOUCH;
|
||||
|
||||
switches_raw = swizzle_raw(io.io_update(touch_pins_configs[touch_phase]));
|
||||
switches_raw = swizzled_switches();
|
||||
if (switches_update(switches_raw))
|
||||
event_mask |= EVT_MASK_SWITCHES;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ using SwitchesState = std::bitset<6>;
|
||||
using EncoderPosition = uint32_t;
|
||||
|
||||
void controls_init();
|
||||
uint8_t swizzled_switches();
|
||||
SwitchesState get_switches_state();
|
||||
EncoderPosition get_encoder_position();
|
||||
touch::Frame get_touch_frame();
|
||||
|
||||
@@ -115,6 +115,7 @@ Continuous (Fox-oring)
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "config_mode.hpp"
|
||||
|
||||
#include "message_queue.hpp"
|
||||
|
||||
@@ -161,9 +162,17 @@ static void event_loop() {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (config_mode_should_enter()) {
|
||||
config_mode_clear();
|
||||
config_mode_run();
|
||||
}
|
||||
|
||||
config_mode_set();
|
||||
|
||||
first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */
|
||||
if (portapack::init()) {
|
||||
portapack::display.init();
|
||||
config_mode_clear();
|
||||
|
||||
// sdcStart(&SDCD1, nullptr); // Commented out as now happens in portapack.cpp
|
||||
|
||||
|
||||
@@ -408,6 +408,10 @@ bool init() {
|
||||
|
||||
portapack::io.init();
|
||||
|
||||
/* Cache some configuration data from persistent memory. */
|
||||
persistent_memory::cache::init();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
clock_manager.init_clock_generator();
|
||||
|
||||
i2c0.stop();
|
||||
@@ -467,9 +471,6 @@ bool init() {
|
||||
controls_init();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
/* Cache some configuration data from persistent memory. */
|
||||
persistent_memory::cache::init();
|
||||
|
||||
clock_manager.set_reference_ppb(persistent_memory::correction_ppb());
|
||||
clock_manager.enable_if_clocks();
|
||||
clock_manager.enable_codec_clocks();
|
||||
|
||||
@@ -85,7 +85,7 @@ const tone_key_t tone_keys = {
|
||||
{"Senn. 32.768k", F2Ix100(32768.0)}};
|
||||
|
||||
std::string fx100_string(uint32_t f) {
|
||||
return to_string_dec_uint(f / 100) + "." + to_string_dec_uint((f / 10) % 10);
|
||||
return to_string_dec_uint(f / 100) + "." + to_string_dec_uint(((f + 5) / 10) % 10);
|
||||
}
|
||||
|
||||
float tone_key_frequency(tone_index index) {
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace tonekey {
|
||||
|
||||
#define TONE_FREQ_TOLERANCE_CENTIHZ (4 * 100)
|
||||
#define TONE_DISPLAY_TOGGLE_COUNTER 3
|
||||
#define F2Ix100(x) (int32_t)(x * 100.0)
|
||||
#define F2Ix100(x) (int32_t)(x * 100.0 + 0.5) // add 0.5f to round vs truncate during FP->int conversion
|
||||
|
||||
using tone_index = int32_t;
|
||||
using tone_key_t = std::vector<std::pair<std::string, uint32_t>>;
|
||||
|
||||
@@ -380,36 +380,36 @@ void WaterfallView::on_audio_spectrum() {
|
||||
|
||||
} /* namespace spectrum */
|
||||
|
||||
// TODO: Comments below refer to a fixed oversample rate (8x), cleanup.
|
||||
uint32_t filter_bandwidth_for_sampling_rate(int32_t sampling_rate) {
|
||||
switch (sampling_rate) { // Use the var fs (sampling_rate) to set up BPF aprox < fs_max / 2 by Nyquist theorem.
|
||||
case 0 ... 3'500'000: // BW Captured range (0 <= 250kHz max) fs = 8x250 kHz =2000., 16x150 khz =2400, 32x100 khz =3200, (32x75k = 2400), (future 64x40 khz =2400)
|
||||
case 0 ... 3'500'000: // BW Captured range BW (<=250K) : fs = 8x250k = 2000k, 16x150k = 2400k, 16x100k=1600k,
|
||||
// 32x75k = 2400k, 32x50k=1600, 32x32k=1024, 64x25k = 1600k, 64x16k = 1024k, 64x12k5 = 800k.
|
||||
return 1'750'000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
|
||||
case 4'000'000 ... 6'000'000: // BW capture range (500k...750kHz max) fs_max = 8 x 750kHz = 6Mhz
|
||||
case 4'000'000 ... 7'000'000: // OVS x8, BW capture range (500k...750kHz max) fs_max = 8 x 750k = 6Mhz
|
||||
// BW 500k...750kHz, ex. 500kHz (fs = 8 x BW = 4Mhz), BW 600kHz (fs = 4,8Mhz), BW 750 kHz (fs = 6Mhz).
|
||||
return 2'500'000; // In some IC, MAX2837 appears as 2250000, but both work similarly.
|
||||
|
||||
case 8'000'000: // BW capture 1Mhz fs = 8 x 1Mhz = 8Mhz. (1Mhz showed slightly higher noise background).
|
||||
return 3'500'000; // some low SD cards, if not showing avg. writting speed >4MB/sec, they will produce sammples drop at REC with 1MB and C16 format.
|
||||
case 7'000'001 ... 10'000'000: // OVS x8 and x4, BW capture 1Mhz fs = 8 x 1Mhz = 8Mhz. (1Mhz showed slightly higher noise background).
|
||||
return 3'500'000; // some low SD cards, if not showing avg. writing speed >4MB/sec, they will produce sammples drop at REC with 1MB and C16 format.
|
||||
|
||||
case 12'000'000: // BW capture 1,5Mhz, fs = 8 x 1,5Mhz = 12Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
case 12'000'000 ... 14'000'000: // OVS x4, BW capture 3Mhz, fs = 4 x 3Mhz = 12Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 5'000'000;
|
||||
|
||||
case 14'000'000: // BW capture 1,75Mhz, fs = 8 x 1,75Mhz = 14Mhz
|
||||
case 16'000'000: // OVS x4, BW capture 4Mhz, fs = 4 x 4Mhz = 16Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 5'000'000;
|
||||
return 5'500'000;
|
||||
|
||||
case 16'000'000: // BW capture 2Mhz, fs = 8 x 2Mhz = 16Mhz
|
||||
case 18'000'000: // OVS x4, BW capture 4,5Mhz, fs = 4 x 4,5Mhz = 18Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 6'000'000;
|
||||
|
||||
case 20'000'000: // BW capture 2,5Mhz, fs = 8 x 2,5 Mhz = 20Mhz
|
||||
case 20'000'000: // OVS x4, BW capture 5Mhz, fs = 4 x 5Mhz = 20Mhz
|
||||
// Good BPF, good matching, we have some periodical M4 % samples drop.
|
||||
return 7'000'000;
|
||||
|
||||
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz = 22Mhz max ADC sampling and others.
|
||||
default: // BW capture 5,5Mhz, fs = 4 x 5,5Mhz = 22Mhz max ADC sampling and others.
|
||||
// We tested also 9Mhz FPB slightly too much noise floor, better at 8Mhz.
|
||||
return 8'000'000;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,18 @@ const Style Styles::white_small{
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
const Style Styles::bg_white_small{
|
||||
.font = ui::font::fixed_5x8,
|
||||
.background = ui::Color::white(),
|
||||
.foreground = ui::Color::black(),
|
||||
};
|
||||
|
||||
const Style Styles::bg_yellow_small{
|
||||
.font = ui::font::fixed_5x8,
|
||||
.background = ui::Color::yellow(),
|
||||
.foreground = ui::Color::black(),
|
||||
};
|
||||
|
||||
const Style Styles::yellow{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
@@ -102,4 +114,4 @@ const Style Styles::orange{
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::orange(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,6 +37,12 @@ class Styles {
|
||||
/* White foreground, small font. */
|
||||
static const Style white_small;
|
||||
|
||||
/* White background, small font. */
|
||||
static const Style bg_white_small;
|
||||
|
||||
/* Red background, small font. */
|
||||
static const Style bg_yellow_small;
|
||||
|
||||
/* Yellow foreground. */
|
||||
static const Style yellow;
|
||||
|
||||
@@ -70,4 +76,4 @@ class Styles {
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif /*__UI_STYLES_H__*/
|
||||
#endif /*__UI_STYLES_H__*/
|
||||
|
||||
@@ -107,13 +107,9 @@ uint32_t RecordView::set_sampling_rate(uint32_t new_sampling_rate) {
|
||||
auto oversample_rate = get_oversample_rate(new_sampling_rate);
|
||||
auto actual_sampling_rate = new_sampling_rate * toUType(oversample_rate);
|
||||
|
||||
/* We are changing "REC" icon background to yellow in BW rec Options >1Mhz
|
||||
* > 1Mhz BW options , we are NOT recording full IQ .C16 files (those files has some periodical missing dropped samples).
|
||||
* Those recorded files, has not the full IQ samples information, looks like decimated in file size.
|
||||
* They are ok as recorded spectrum indication, but they should not be used by Replay app. (the voice speed will be accelerated)
|
||||
|
||||
* We keep original black background in all the correct IQ .C16 files BW's Options. */
|
||||
if (actual_sampling_rate > 8'000'000) { // yellow REC button means not ok for REC, BW >1Mhz (BW from 12k5 till 1Mhz OK for REC and Replay)
|
||||
// Change the "REC" icon background to yellow when the selected rate exceeds hardware limits.
|
||||
// Above this threshold, samples will be dropped resulting incomplete capture files.
|
||||
if (new_sampling_rate > 1'250'000) {
|
||||
button_record.set_background(ui::Color::yellow());
|
||||
} else {
|
||||
button_record.set_background(ui::Color::black());
|
||||
@@ -142,15 +138,7 @@ OversampleRate RecordView::get_oversample_rate(uint32_t sample_rate) {
|
||||
if (file_type == FileType::WAV)
|
||||
return OversampleRate::None;
|
||||
|
||||
auto rate = ::get_oversample_rate(sample_rate);
|
||||
|
||||
// Currently proc_capture only supports /8, /16, /32 for decimation.
|
||||
if (rate < OversampleRate::x8) // clipping while /4 is not implemented yet (it will be used >1Mhz onwards when available)
|
||||
rate = OversampleRate::x8;
|
||||
else if (rate > OversampleRate::x64) // clipping while /128 is not implemented yet , (but it is not necessary for 12k5)
|
||||
rate = OversampleRate::x64;
|
||||
|
||||
return rate;
|
||||
return ::get_oversample_rate(sample_rate);
|
||||
}
|
||||
|
||||
// Setter for datetime and frequency filename
|
||||
|
||||
@@ -444,6 +444,13 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PPOC pocsag)
|
||||
|
||||
### POCSAG2 RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_pocsag2.cpp
|
||||
)
|
||||
DeclareTargets(PPO2 pocsag2)
|
||||
|
||||
### RDS
|
||||
|
||||
set(MODE_CPPSRC
|
||||
|
||||
@@ -69,29 +69,22 @@ class BlockDecimator {
|
||||
|
||||
set_input_sampling_rate(src.sampling_rate);
|
||||
|
||||
while (src_i < src.count) {
|
||||
for (size_t src_i = 0; src_i < src.count; src_i += factor()) {
|
||||
buffer[dst_i++] = src.p[src_i];
|
||||
if (dst_i == buffer.size()) {
|
||||
callback({buffer.data(), buffer.size(), output_sampling_rate()});
|
||||
reset_state();
|
||||
dst_i = 0;
|
||||
}
|
||||
|
||||
src_i += factor();
|
||||
}
|
||||
|
||||
src_i -= src.count;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<T, N> buffer{};
|
||||
uint32_t input_sampling_rate_{0};
|
||||
size_t factor_{1};
|
||||
size_t src_i{0};
|
||||
size_t dst_i{0};
|
||||
|
||||
void reset_state() {
|
||||
src_i = 0;
|
||||
dst_i = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,8 +202,8 @@ buffer_c16_t FIRC8xR16x24FS4Decim4::execute(
|
||||
uint32_t* const d = static_cast<uint32_t*>(__builtin_assume_aligned(dst.p, 4));
|
||||
|
||||
const auto k = output_scale;
|
||||
|
||||
const size_t count = src.count / decimation_factor;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const vec4_s8* const in = static_cast<const vec4_s8*>(__builtin_assume_aligned(&src.p[i * decimation_factor], 4));
|
||||
|
||||
|
||||
@@ -23,19 +23,26 @@
|
||||
#define __DSP_DECIMATE_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "dsp_types.hpp"
|
||||
|
||||
#include "simd.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
namespace dsp {
|
||||
namespace decimate {
|
||||
|
||||
/* "Saturating" scalars used by decimators to scale either
|
||||
* 8 or 16 bit complex values into 32 bit complex values.
|
||||
* Some of the decimators accept a scale factor as part of
|
||||
* configuration, which is then passed to scale_round_and_pack. */
|
||||
// c8_to_c32_sat_scalar == 2^25. 2^25 * 2^7 (signed C8 Max) == 2^32.
|
||||
constexpr int32_t c8_to_c32_sat_scalar = 0x2000000;
|
||||
// c16_to_c32_sat_scalar == 2^17. 2^17 * 2^15 (signed C16 Max) == 2^32.
|
||||
constexpr int32_t c16_to_c32_sat_scalar = 0x20000;
|
||||
|
||||
class Complex8DecimateBy2CIC3 {
|
||||
public:
|
||||
buffer_c16_t execute(
|
||||
@@ -100,7 +107,7 @@ class FIRC8xR16x24FS4Decim4 {
|
||||
|
||||
void configure(
|
||||
const std::array<tap_t, taps_count>& taps,
|
||||
const int32_t scale,
|
||||
const int32_t scale = c8_to_c32_sat_scalar,
|
||||
const Shift shift = Shift::Down);
|
||||
|
||||
buffer_c16_t execute(
|
||||
@@ -128,7 +135,7 @@ class FIRC8xR16x24FS4Decim8 {
|
||||
|
||||
void configure(
|
||||
const std::array<tap_t, taps_count>& taps,
|
||||
const int32_t scale,
|
||||
const int32_t scale = c8_to_c32_sat_scalar,
|
||||
const Shift shift = Shift::Down);
|
||||
|
||||
buffer_c16_t execute(
|
||||
@@ -151,7 +158,7 @@ class FIRC16xR16x16Decim2 {
|
||||
|
||||
void configure(
|
||||
const std::array<tap_t, taps_count>& taps,
|
||||
const int32_t scale);
|
||||
const int32_t scale = c16_to_c32_sat_scalar);
|
||||
|
||||
buffer_c16_t execute(
|
||||
const buffer_c16_t& src,
|
||||
@@ -173,7 +180,7 @@ class FIRC16xR16x32Decim8 {
|
||||
|
||||
void configure(
|
||||
const std::array<tap_t, taps_count>& taps,
|
||||
const int32_t scale);
|
||||
const int32_t scale = c16_to_c32_sat_scalar);
|
||||
|
||||
buffer_c16_t execute(
|
||||
const buffer_c16_t& src,
|
||||
|
||||
@@ -29,24 +29,29 @@ bool FMSquelch::execute(const buffer_f32_t& audio) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: No hard-coded array size.
|
||||
std::array<float, N> squelch_energy_buffer;
|
||||
const buffer_f32_t squelch_energy{
|
||||
squelch_energy_buffer.data(),
|
||||
squelch_energy_buffer.size()};
|
||||
// TODO: alloca temp buffer, assert audio.count
|
||||
std::array<float, 32> squelch_energy_buffer;
|
||||
const buffer_f32_t squelch_energy{squelch_energy_buffer.data(), audio.count};
|
||||
non_audio_hpf.execute(audio, squelch_energy);
|
||||
|
||||
// "Non-audio" implies "noise" here. Find the loudest noise sample.
|
||||
float non_audio_max_squared = 0;
|
||||
for (const auto sample : squelch_energy_buffer) {
|
||||
const float sample_squared = sample * sample;
|
||||
if (sample_squared > non_audio_max_squared) {
|
||||
for (size_t i = 0; i < squelch_energy.count; ++i) {
|
||||
auto sample = squelch_energy.p[i];
|
||||
float sample_squared = sample * sample;
|
||||
|
||||
if (sample_squared > non_audio_max_squared)
|
||||
non_audio_max_squared = sample_squared;
|
||||
}
|
||||
}
|
||||
|
||||
// Is the noise less than the threshold?
|
||||
return (non_audio_max_squared < threshold_squared);
|
||||
}
|
||||
|
||||
void FMSquelch::set_threshold(const float new_value) {
|
||||
threshold_squared = new_value * new_value;
|
||||
}
|
||||
|
||||
bool FMSquelch::enabled() const {
|
||||
return threshold_squared > 0.0;
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
|
||||
class FMSquelch {
|
||||
public:
|
||||
/* Check if noise level is lower than threshold.
|
||||
* Returns true if noise is below threshold. */
|
||||
bool execute(const buffer_f32_t& audio);
|
||||
|
||||
void set_threshold(const float new_value);
|
||||
bool enabled() const;
|
||||
|
||||
private:
|
||||
static constexpr size_t N = 32;
|
||||
float threshold_squared{0.0f};
|
||||
|
||||
IIRBiquadFilter non_audio_hpf{non_audio_hpf_config};
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
#include "event_m4.hpp"
|
||||
|
||||
ACARSProcessor::ACARSProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
packet.clear();
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@ void AFSKRxProcessor::configure(const AFSKRxConfigureMessage& message) {
|
||||
|
||||
const size_t demod_input_fs = channel_filter_output_fs;*/
|
||||
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod.configure(audio_fs, 5000);
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
#include "event_m4.hpp"
|
||||
|
||||
AISProcessor::AISProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) {
|
||||
constexpr size_t channel_filter_input_fs = decim_2_output_fs;
|
||||
// const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor;
|
||||
|
||||
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||
decim_1.configure(message.decim_1_filter.taps, 131072);
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
decim_2.configure(message.decim_2_filter.taps, decim_2_decimation_factor);
|
||||
channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor);
|
||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||
|
||||
@@ -223,8 +223,8 @@ void APRSRxProcessor::capture_config(const CaptureConfigMessage& message) {
|
||||
}
|
||||
|
||||
void APRSRxProcessor::configure(const APRSRxConfigureMessage& message) {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod.configure(audio_fs, 5000);
|
||||
|
||||
|
||||
@@ -276,8 +276,8 @@ void BTLERxProcessor::on_message(const Message* const message) {
|
||||
|
||||
void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {
|
||||
(void)message; // avoid warning
|
||||
decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_200k_wfm_decim_0.taps);
|
||||
decim_1.configure(taps_200k_wfm_decim_1.taps);
|
||||
demod.configure(audio_fs, 5000);
|
||||
|
||||
configured = true;
|
||||
|
||||
@@ -26,48 +26,32 @@
|
||||
#include "event_m4.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace dsp::decimate;
|
||||
|
||||
CaptureProcessor::CaptureProcessor() {
|
||||
decim_0_4.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/8)
|
||||
decim_0_8.configure(taps_200k_decim_0.taps, 33554432); // to be used with decim1 (/2), then total two stages decim (/16)
|
||||
decim_0_8_180k.configure(taps_180k_wfm_decim_0.taps, 33554432); // to be used alone - no additional decim1 (/2), then total single stage decim (/8)
|
||||
|
||||
decim_1_2.configure(taps_200k_decim_1.taps, 131072);
|
||||
decim_1_8.configure(taps_16k0_decim_1.taps, 131072); // tentative decim1 /8 and taps, pending to be optimized.
|
||||
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void CaptureProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
const auto decim_0_out = decim_0_execute(buffer, dst_buffer); // selectable 3 possible decim_0, (/4. /8 200k soft filter , /8 180k sharp )
|
||||
const auto decim_1_out = decim_1_execute(decim_0_out, dst_buffer); // selectable 3 possible decim_1, (/8. /2 200k or bypassed /1 )
|
||||
|
||||
/* this code was valid when we had only 2 decim1 cases.
|
||||
const auto decim_1_out = baseband_fs < 4800'000
|
||||
? decim_1_2.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
// ? decim_1_8.execute(decim_0_out, dst_buffer) // < 600khz double decim. stage , means 500khz and lower bit rates.
|
||||
: decim_0_out; // >= 600khz single decim. stage , means 600khz and upper bit rates.
|
||||
|
||||
} */
|
||||
|
||||
const auto& decimator_out = decim_1_out;
|
||||
const auto& channel = decimator_out;
|
||||
auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
auto out_buffer = decim_1.execute(decim_0_out, dst_buffer);
|
||||
|
||||
if (stream) {
|
||||
const size_t bytes_to_write = sizeof(*decimator_out.p) * decimator_out.count;
|
||||
const size_t written = stream->write(decimator_out.p, bytes_to_write);
|
||||
const size_t bytes_to_write = sizeof(*out_buffer.p) * out_buffer.count;
|
||||
const size_t written = stream->write(out_buffer.p, bytes_to_write);
|
||||
if (written != bytes_to_write) {
|
||||
// TODO eventually report error somewhere
|
||||
// TODO: Send an error message to the app?
|
||||
}
|
||||
}
|
||||
|
||||
feed_channel_stats(channel);
|
||||
feed_channel_stats(out_buffer);
|
||||
|
||||
spectrum_samples += channel.count;
|
||||
spectrum_samples += out_buffer.count;
|
||||
if (spectrum_samples >= spectrum_interval_samples) {
|
||||
spectrum_samples -= spectrum_interval_samples;
|
||||
channel_spectrum.feed(channel, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||
channel_spectrum.feed(out_buffer, channel_filter_low_f,
|
||||
channel_filter_high_f, channel_filter_transition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,120 +76,83 @@ void CaptureProcessor::on_message(const Message* const message) {
|
||||
}
|
||||
|
||||
void CaptureProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
|
||||
baseband_fs = message.sample_rate * toUType(message.oversample_rate);
|
||||
oversample_rate = message.oversample_rate;
|
||||
const auto sample_rate = message.sample_rate;
|
||||
|
||||
// The actual sample rate is the requested rate * the oversample rate.
|
||||
// See oversample.hpp for more details on oversampling.
|
||||
baseband_fs = sample_rate * toUType(message.oversample_rate);
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
|
||||
// Current fw , we are using only 2 decim_0 modes, /4 , /8
|
||||
auto decim_0_factor = oversample_rate == OversampleRate::x8
|
||||
? decim_0_4.decimation_factor
|
||||
: decim_0_8.decimation_factor;
|
||||
// TODO: Do we need to use the taps that the decimators get configured with?
|
||||
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * sample_rate;
|
||||
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * sample_rate;
|
||||
channel_filter_transition = taps_200k_decim_1.transition_normalized * sample_rate;
|
||||
|
||||
size_t decim_0_output_fs = baseband_fs / decim_0_factor;
|
||||
size_t decim_1_input_fs = decim_0_output_fs;
|
||||
// Compute the scalar that corrects the oversample_rate to be x8 when computing
|
||||
// the spectrum update interval. The original implementation only supported x8.
|
||||
// TODO: Why is this needed here but not in proc_replay? There must be some other
|
||||
// assumption about x8 oversampling in some component that makes this necessary.
|
||||
const auto oversample_correction = toUType(message.oversample_rate) / 8.0;
|
||||
|
||||
// The spectrum update interval controls how often the waterfall is fed new samples.
|
||||
spectrum_interval_samples = sample_rate / (spectrum_rate_hz * oversample_correction);
|
||||
spectrum_samples = 0;
|
||||
|
||||
// For high sample rates, the M4 is busy collecting samples so the
|
||||
// waterfall runs slower. Reduce the update interval so it runs faster.
|
||||
// NB: Trade off: looks nicer, but more frequent updates == more CPU.
|
||||
if (sample_rate >= 1'500'000)
|
||||
spectrum_interval_samples /= (sample_rate / 750'000);
|
||||
|
||||
switch (message.oversample_rate) {
|
||||
case OversampleRate::x4:
|
||||
// M4 can't handle 2 decimation passes for sample rates needing x4.
|
||||
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
|
||||
decim_1.set<NoopDecim>();
|
||||
break;
|
||||
|
||||
size_t decim_1_factor;
|
||||
switch (oversample_rate) { // we are using 3 decim_1 modes, /1 , /2 , /8
|
||||
case OversampleRate::x8:
|
||||
if (baseband_fs < 4800'000) {
|
||||
decim_1_factor = decim_1_2.decimation_factor; // /8 = /4x2
|
||||
// M4 can't handle 2 decimation passes for sample rates <= 600k.
|
||||
if (message.sample_rate < 600'000) {
|
||||
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
|
||||
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
|
||||
} else {
|
||||
decim_1_factor = 2 * 1; // 600khz and onwards, single decim /8 = /8x1 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
|
||||
// Using 180k taps to provide better filtering with a single pass.
|
||||
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
|
||||
decim_1.set<NoopDecim>();
|
||||
}
|
||||
break;
|
||||
|
||||
case OversampleRate::x16:
|
||||
decim_1_factor = 2 * decim_1_2.decimation_factor; // /16 = /8x2 (we applied additional *2 correction to increase waterfall spped >=600k and smooth & avoid abnormal motion >1M5 )
|
||||
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
|
||||
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
|
||||
break;
|
||||
|
||||
case OversampleRate::x32:
|
||||
decim_1_factor = 2 * decim_1_8.decimation_factor; // /32 = /4x8 (we applied additional *2 correction to speed up waterfall, no effect to scale spectrum)
|
||||
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
|
||||
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
|
||||
break;
|
||||
|
||||
case OversampleRate::x64:
|
||||
decim_1_factor = 8 * decim_1_8.decimation_factor; // /64 = /8x8 (we applied additional *8 correction to speed up waterfall, no effect to scale spectrum)
|
||||
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
|
||||
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
|
||||
break;
|
||||
|
||||
default:
|
||||
decim_1_factor = 2; // just default initial value to remove compile warning.
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
auto decim_1_factor = oversample_rate == OversampleRate::x32 // that was ok, when we had only 2 oversampling x8 , x16
|
||||
? decim_1_8.decimation_factor
|
||||
: decim_1_2.decimation_factor;
|
||||
|
||||
*/
|
||||
size_t decim_1_output_fs = decim_1_input_fs / decim_1_factor;
|
||||
|
||||
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_transition = taps_200k_decim_1.transition_normalized * decim_1_input_fs;
|
||||
|
||||
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
|
||||
spectrum_samples = 0;
|
||||
}
|
||||
|
||||
void CaptureProcessor::capture_config(const CaptureConfigMessage& message) {
|
||||
if (message.config) {
|
||||
if (message.config)
|
||||
stream = std::make_unique<StreamInput>(message.config);
|
||||
} else {
|
||||
else
|
||||
stream.reset();
|
||||
}
|
||||
}
|
||||
|
||||
buffer_c16_t CaptureProcessor::decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
|
||||
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
|
||||
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
|
||||
} else {
|
||||
return decim_0_8_180k.execute(src, dst); // decim_0 /8 with single decim stage
|
||||
}
|
||||
|
||||
case OversampleRate::x16:
|
||||
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
|
||||
|
||||
case OversampleRate::x32:
|
||||
return decim_0_4.execute(src, dst); // decim_0 , /4 with double decim stage
|
||||
|
||||
case OversampleRate::x64:
|
||||
return decim_0_8.execute(src, dst); // decim_0 , /8 with double decim stage
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
buffer_c16_t CaptureProcessor::decim_1_execute(const buffer_c16_t& src, const buffer_c16_t& dst) {
|
||||
switch (oversample_rate) {
|
||||
case OversampleRate::x8: // we can get /8 by two means , decim0 (/4) + decim1 (/2) . or just decim0 (/8)
|
||||
if (baseband_fs < 4800'000) { // 600khz (600k x 8)
|
||||
return decim_1_2.execute(src, dst);
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
|
||||
case OversampleRate::x16:
|
||||
return decim_1_2.execute(src, dst); // total decim /16 = /8x2, applied to 100khz and 150khz
|
||||
|
||||
case OversampleRate::x32:
|
||||
return decim_1_8.execute(src, dst); // total decim /32 = /4x8, appled to 75k , 50k, 32k
|
||||
|
||||
case OversampleRate::x64:
|
||||
return decim_1_8.execute(src, dst); // total decim /64 = /8x8, appled to 16k and 12k5
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled OversampleRate");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<CaptureProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,56 @@
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
|
||||
/* A decimator that just returns the source buffer. */
|
||||
class NoopDecim {
|
||||
public:
|
||||
static constexpr int decimation_factor = 1;
|
||||
|
||||
template <typename Buffer>
|
||||
Buffer execute(const Buffer& src, const Buffer&) {
|
||||
// TODO: should this copy to 'dst'?
|
||||
return {src.p, src.count, src.sampling_rate};
|
||||
}
|
||||
};
|
||||
|
||||
/* Decimator wrapper that can hold one of a set of decimators and dispatch at runtime. */
|
||||
template <typename... Args>
|
||||
class MultiDecimator {
|
||||
public:
|
||||
/* Dispatches to the underlying type's execute. */
|
||||
template <typename Source, typename Destination>
|
||||
Destination execute(
|
||||
const Source& src,
|
||||
const Destination& dst) {
|
||||
return std::visit(
|
||||
[&src, &dst](auto&& arg) -> Destination {
|
||||
return arg.execute(src, dst);
|
||||
},
|
||||
decimator_);
|
||||
}
|
||||
|
||||
size_t decimation_factor() const {
|
||||
return std::visit(
|
||||
[](auto&& arg) -> size_t {
|
||||
return arg.decimation_factor;
|
||||
},
|
||||
decimator_);
|
||||
}
|
||||
|
||||
/* Sets this decimator to a new instance of the specified decimator type.
|
||||
* NB: The instance is returned by-ref so 'configure' can easily be called. */
|
||||
template <typename Decimator>
|
||||
Decimator& set() {
|
||||
decimator_ = Decimator{};
|
||||
return std::get<Decimator>(decimator_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<Args...> decimator_{};
|
||||
};
|
||||
|
||||
class CaptureProcessor : public BasebandProcessor {
|
||||
public:
|
||||
@@ -50,15 +100,16 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
/* NB: There are two decimation passes: 0 and 1. In pass 0, one of
|
||||
* the following will be selected based on the oversample rate.
|
||||
* use decim_0_4 for an overall decimation factor of 8.
|
||||
* use decim_0_8 for an overall decimation factor of 16. */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0_4{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_8_180k{};
|
||||
dsp::decimate::FIRC16xR16x16Decim2 decim_1_2{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1_8{};
|
||||
/* The actual type will be configured depending on the sample rate. */
|
||||
MultiDecimator<
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4,
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8>
|
||||
decim_0{};
|
||||
MultiDecimator<
|
||||
dsp::decimate::FIRC16xR16x16Decim2,
|
||||
dsp::decimate::FIRC16xR16x32Decim8,
|
||||
NoopDecim>
|
||||
decim_1{};
|
||||
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
@@ -69,7 +120,6 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
SpectrumCollector channel_spectrum{};
|
||||
size_t spectrum_interval_samples = 0;
|
||||
size_t spectrum_samples = 0;
|
||||
OversampleRate oversample_rate{OversampleRate::x8};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
@@ -78,12 +128,6 @@ class CaptureProcessor : public BasebandProcessor {
|
||||
|
||||
void sample_rate_config(const SampleRateConfigMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
/* Dispatch to the correct decim_0 based on oversample rate. */
|
||||
buffer_c16_t decim_0_execute(const buffer_c8_t& src, const buffer_c16_t& dst);
|
||||
|
||||
/* Dispatch to the correct decim_1 based on oversample rate. */
|
||||
buffer_c16_t decim_1_execute(const buffer_c16_t& src, const buffer_c16_t& dst);
|
||||
};
|
||||
|
||||
#endif /*__PROC_CAPTURE_HPP__*/
|
||||
|
||||
@@ -144,8 +144,8 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) {
|
||||
|
||||
const size_t demod_input_fs = channel_filter_output_fs;
|
||||
|
||||
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||
decim_1.configure(message.decim_1_filter.taps, 131072);
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
channel_filter.configure(message.channel_filter.taps, message.channel_decimation);
|
||||
demod.configure(demod_input_fs, message.deviation);
|
||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||
|
||||
@@ -243,8 +243,8 @@ void NRFRxProcessor::on_message(const Message* const message) {
|
||||
|
||||
void NRFRxProcessor::configure(const NRFRxConfigureMessage& message) {
|
||||
(void)message; // avoir unused warning
|
||||
decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_200k_wfm_decim_0.taps);
|
||||
decim_1.configure(taps_200k_wfm_decim_1.taps);
|
||||
demod.configure(audio_fs, 5000);
|
||||
|
||||
configured = true;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2016 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -24,16 +25,15 @@
|
||||
|
||||
#include "proc_pocsag.hpp"
|
||||
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
|
||||
// This is called at 1500Hz
|
||||
|
||||
if (!configured) return;
|
||||
|
||||
// Get 24kHz audio
|
||||
@@ -41,11 +41,36 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
smooth.Process(audio.p, audio.count); // Smooth the data to make decoding more accurate
|
||||
audio_output.write(audio);
|
||||
|
||||
// If squelching, check for audio before smoothing because smoothing
|
||||
// causes the squelch noise detection to fail. Likely because squelch
|
||||
// looks for HF noise and smoothing is basically a lowpass filter.
|
||||
// NB: Squelch in this processor is only for the the audio output.
|
||||
// Squelching will likely drop data "noise" and break processing.
|
||||
if (squelch_.enabled()) {
|
||||
bool has_audio = squelch_.execute(audio);
|
||||
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
|
||||
}
|
||||
|
||||
smooth.Process(audio.p, audio.count);
|
||||
processDemodulatedSamples(audio.p, 16);
|
||||
extractFrames();
|
||||
|
||||
samples_processed += buffer.count;
|
||||
if (samples_processed >= stat_update_threshold) {
|
||||
send_stats();
|
||||
samples_processed = 0;
|
||||
}
|
||||
|
||||
// Clear the output before sending to audio chip.
|
||||
// Only clear the audio buffer when there hasn't been any audio for a while.
|
||||
if (squelch_.enabled() && squelch_history == 0) {
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
audio.p[i] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
audio_output.write(audio);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
@@ -71,8 +96,20 @@ int POCSAGProcessor::OnDataFrame(int len, int baud) {
|
||||
}
|
||||
|
||||
void POCSAGProcessor::on_message(const Message* const message) {
|
||||
if (message->id == Message::ID::POCSAGConfigure)
|
||||
configure();
|
||||
switch (message->id) {
|
||||
case Message::ID::POCSAGConfigure:
|
||||
configure();
|
||||
break;
|
||||
|
||||
case Message::ID::NBFMConfigure: {
|
||||
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
|
||||
squelch_.set_threshold(config->squelch_level / 100.0);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGProcessor::configure() {
|
||||
@@ -87,16 +124,16 @@ void POCSAGProcessor::configure() {
|
||||
|
||||
const size_t demod_input_fs = channel_filter_output_fs;
|
||||
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
|
||||
|
||||
// Smoothing should be roughly sample rate over max baud
|
||||
// 24k / 3.2k is 7.5
|
||||
// 24k / 3.2k = 7.5
|
||||
smooth.SetSize(8);
|
||||
|
||||
// TODO: support squelch?
|
||||
// audio_output.configure(message.audio_hpf_config, message.audio_deemph_config, (float)message.squelch_level / 100.0);
|
||||
// Don't have audio process the stream.
|
||||
audio_output.configure(false);
|
||||
|
||||
// Set up the frame extraction, limits of baud
|
||||
@@ -106,6 +143,11 @@ void POCSAGProcessor::configure() {
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void POCSAGProcessor::send_stats() const {
|
||||
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync, getRate());
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Frame extractraction methods
|
||||
// -----------------------------
|
||||
@@ -480,7 +522,7 @@ int POCSAGProcessor::getNoOfBits() {
|
||||
// ====================================================================
|
||||
//
|
||||
// ====================================================================
|
||||
uint32_t POCSAGProcessor::getRate() {
|
||||
uint32_t POCSAGProcessor::getRate() const {
|
||||
return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -106,10 +107,10 @@ class SmoothVals {
|
||||
|
||||
// Use a rolling smoothed value while processing the buffer
|
||||
for (int buffPos = 0; buffPos < numVals; ++buffPos) {
|
||||
m_pos = (m_pos + 1); // Increment the position in the stored values
|
||||
m_pos++;
|
||||
if (m_pos >= m_size) {
|
||||
m_pos = 0;
|
||||
} // loop if reached the end of the stored values
|
||||
}
|
||||
|
||||
m_sumVal -= (CalcType)m_lastVals[m_pos]; // Subtract the oldest value
|
||||
m_lastVals[m_pos] = valBuff[buffPos]; // Store the new value
|
||||
@@ -136,6 +137,9 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr uint8_t stat_update_interval = 10;
|
||||
static constexpr uint32_t stat_update_threshold =
|
||||
baseband_fs / stat_update_interval;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
@@ -157,7 +161,10 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
bool configured = false;
|
||||
pocsag::POCSAGPacket packet{};
|
||||
|
||||
uint32_t samples_processed = 0;
|
||||
|
||||
void configure();
|
||||
void send_stats() const;
|
||||
|
||||
// ----------------------------------------
|
||||
// Frame extractraction methods and members
|
||||
@@ -180,7 +187,7 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
short getBit();
|
||||
|
||||
int getNoOfBits();
|
||||
uint32_t getRate();
|
||||
uint32_t getRate() const;
|
||||
|
||||
uint32_t m_averageSymbolLen_1024{0};
|
||||
uint32_t m_lastStableSymbolLen_1024{0};
|
||||
@@ -215,6 +222,9 @@ class POCSAGProcessor : public BasebandProcessor {
|
||||
int m_numCode{0};
|
||||
bool m_inverted{false};
|
||||
|
||||
FMSquelch squelch_{};
|
||||
uint64_t squelch_history = 0;
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
* Copyright (C) 1996 Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu)
|
||||
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* 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 "proc_pocsag2.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
/* Count of bits that differ between the two values. */
|
||||
uint8_t differ_bit_count(uint32_t left, uint32_t right) {
|
||||
uint32_t diff = 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
|
||||
|
||||
/* AudioNormalizer ***************************************/
|
||||
|
||||
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
|
||||
// Decay min/max every second (@24kHz).
|
||||
if (counter_ >= 24'000) {
|
||||
// 90% decay factor seems to work well.
|
||||
// This keeps large transients from wrecking the filter.
|
||||
max_ *= 0.9f;
|
||||
min_ *= 0.9f;
|
||||
counter_ = 0;
|
||||
calculate_thresholds();
|
||||
}
|
||||
|
||||
counter_ += audio.count;
|
||||
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
auto& val = audio.p[i];
|
||||
|
||||
if (val > max_) {
|
||||
max_ = val;
|
||||
calculate_thresholds();
|
||||
}
|
||||
if (val < min_) {
|
||||
min_ = val;
|
||||
calculate_thresholds();
|
||||
}
|
||||
|
||||
if (val >= t_hi_)
|
||||
val = 1.0f;
|
||||
else if (val <= t_lo_)
|
||||
val = -1.0f;
|
||||
else
|
||||
val = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioNormalizer::calculate_thresholds() {
|
||||
auto center = (max_ + min_) / 2.0f;
|
||||
auto range = (max_ - min_) / 2.0f;
|
||||
|
||||
// 10% off center force either +/-1.0f.
|
||||
// Higher == larger dead zone.
|
||||
// Lower == more false positives.
|
||||
auto threshold = range * 0.1;
|
||||
t_hi_ = center + threshold;
|
||||
t_lo_ = center - threshold;
|
||||
}
|
||||
|
||||
/* BitQueue **********************************************/
|
||||
|
||||
void BitQueue::push(bool bit) {
|
||||
data_ = (data_ << 1) | (bit ? 1 : 0);
|
||||
if (count_ < max_size_) ++count_;
|
||||
}
|
||||
|
||||
bool BitQueue::pop() {
|
||||
if (count_ == 0) return false;
|
||||
|
||||
--count_;
|
||||
return (data_ & (1 << count_)) != 0;
|
||||
}
|
||||
|
||||
void BitQueue::reset() {
|
||||
data_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
uint8_t BitQueue::size() const {
|
||||
return count_;
|
||||
}
|
||||
|
||||
uint32_t BitQueue::data() const {
|
||||
return data_;
|
||||
}
|
||||
|
||||
/* BitExtractor ******************************************/
|
||||
|
||||
void BitExtractor::extract_bits(const buffer_f32_t& audio) {
|
||||
// Assumes input has been normalized +/- 1.0f.
|
||||
|
||||
for (size_t i = 0; i < audio.count; ++i) {
|
||||
sample_ = audio.p[i];
|
||||
++sample_index_;
|
||||
|
||||
// There's a transition when both sides of the XOR are the
|
||||
// same which will result in a the overall value being 0.
|
||||
bool is_transition = ((last_sample_ < 0) ^ (sample_ >= 0)) == 0;
|
||||
if (is_transition) {
|
||||
if (handle_transition())
|
||||
bad_transitions_ = 0;
|
||||
else
|
||||
++bad_transitions_;
|
||||
|
||||
// Too many bad transitions? Reset.
|
||||
if (bad_transitions_ > bad_transition_reset_threshold)
|
||||
reset();
|
||||
}
|
||||
|
||||
// Time to push the next bit?
|
||||
if (sample_index_ >= next_bit_center_) {
|
||||
// Use the two most recent samples for the bit value.
|
||||
auto val = (sample_ + last_sample_) / 2.0;
|
||||
bits_.push(val < 0); // NB: '1' is negative.
|
||||
|
||||
if (current_rate_)
|
||||
next_bit_center_ += current_rate_->bit_length;
|
||||
}
|
||||
|
||||
last_sample_ = sample_;
|
||||
}
|
||||
}
|
||||
|
||||
void BitExtractor::configure(uint32_t sample_rate) {
|
||||
sample_rate_ = sample_rate;
|
||||
min_valid_length_ = UINT16_MAX;
|
||||
|
||||
// Build the baud rate info table based on the sample rate.
|
||||
for (auto& info : known_rates_) {
|
||||
info.bit_length = sample_rate / info.baud_rate;
|
||||
|
||||
// Allow for 20% deviation.
|
||||
info.min_bit_length = 0.80 * info.bit_length;
|
||||
info.max_bit_length = 1.20 * info.bit_length;
|
||||
|
||||
if (info.min_bit_length < min_valid_length_)
|
||||
min_valid_length_ = info.min_bit_length;
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void BitExtractor::reset() {
|
||||
current_rate_ = nullptr;
|
||||
rate_misses_ = 0;
|
||||
|
||||
sample_ = 0.0;
|
||||
last_sample_ = 0.0;
|
||||
next_bit_center_ = 0.0;
|
||||
|
||||
sample_index_ = 0;
|
||||
last_transition_index_ = 0;
|
||||
bad_transitions_ = 0;
|
||||
}
|
||||
|
||||
uint16_t BitExtractor::baud_rate() const {
|
||||
return current_rate_ ? current_rate_->baud_rate : 0;
|
||||
}
|
||||
|
||||
bool BitExtractor::handle_transition() {
|
||||
auto length = sample_index_ - last_transition_index_;
|
||||
last_transition_index_ = sample_index_;
|
||||
|
||||
// Length is too short, ignore this.
|
||||
if (length <= min_valid_length_) return false;
|
||||
|
||||
// TODO: should the following be "bad" or "rate misses"?
|
||||
// Is length a multiple of the current rate's bit length?
|
||||
uint16_t bit_count = 0;
|
||||
if (!count_bits(length, bit_count)) return false;
|
||||
|
||||
// Does the bit length correspond to a known rate?
|
||||
auto bit_length = length / static_cast<float>(bit_count);
|
||||
auto rate = get_baud_info(bit_length);
|
||||
if (!rate) return false;
|
||||
|
||||
// Set current rate if it hasn't been set yet.
|
||||
if (!current_rate_)
|
||||
current_rate_ = rate;
|
||||
|
||||
// Maybe current rate isn't the best rate?
|
||||
auto rate_miss = rate != current_rate_;
|
||||
if (rate_miss) {
|
||||
++rate_misses_;
|
||||
|
||||
// Lots of rate misses, try another rate.
|
||||
if (rate_misses_ > rate_miss_reset_threshold) {
|
||||
current_rate_ = rate;
|
||||
rate_misses_ = 0;
|
||||
}
|
||||
} else {
|
||||
// Transition is aligned with the current rate, predict next bit.
|
||||
auto half_bit = current_rate_->bit_length / 2.0;
|
||||
next_bit_center_ = sample_index_ + half_bit;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BitExtractor::count_bits(uint32_t length, uint16_t& bit_count) {
|
||||
bit_count = 0;
|
||||
|
||||
// No rate yet, assume one valid bit. Downstream will deal with it.
|
||||
if (!current_rate_) {
|
||||
bit_count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// How many bits span the specified length?
|
||||
float exact_bits = length / current_rate_->bit_length;
|
||||
|
||||
// < 1 bit, current rate is probably too low.
|
||||
if (exact_bits < 0.80) return false;
|
||||
|
||||
// Round to the nearest # of bits and determine how
|
||||
// well the current rate fits the data.
|
||||
float round_bits = std::round(exact_bits);
|
||||
float error = std::abs(exact_bits - round_bits) / exact_bits;
|
||||
|
||||
// Good transition are w/in 15% of current rate estimate.
|
||||
bit_count = round_bits;
|
||||
return error < 0.15;
|
||||
}
|
||||
|
||||
const BitExtractor::BaudInfo* BitExtractor::get_baud_info(float bit_length) const {
|
||||
// NB: This assumes known_rates_ are ordered slowest first.
|
||||
for (const auto& info : known_rates_) {
|
||||
if (bit_length >= info.min_bit_length &&
|
||||
bit_length <= info.max_bit_length) {
|
||||
return &info;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* CodewordExtractor *************************************/
|
||||
|
||||
void CodewordExtractor::process_bits() {
|
||||
// Process all of the bits in the bits queue.
|
||||
while (bits_.size() > 0) {
|
||||
take_one_bit();
|
||||
|
||||
// Wait until data_ is full.
|
||||
if (bit_count_ < data_bit_count)
|
||||
continue;
|
||||
|
||||
// Wait for the sync frame.
|
||||
if (!has_sync_) {
|
||||
if (differ_bit_count(data_, sync_codeword) <= 2)
|
||||
handle_sync(/*inverted=*/false);
|
||||
else if (differ_bit_count(data_, ~sync_codeword) <= 2)
|
||||
handle_sync(/*inverted=*/true);
|
||||
continue;
|
||||
}
|
||||
|
||||
save_current_codeword();
|
||||
|
||||
if (word_count_ == pocsag::batch_size)
|
||||
handle_batch_complete();
|
||||
}
|
||||
}
|
||||
|
||||
void CodewordExtractor::flush() {
|
||||
// Don't bother flushing if there's no pending data.
|
||||
if (word_count_ == 0) return;
|
||||
|
||||
pad_idle();
|
||||
handle_batch_complete();
|
||||
}
|
||||
|
||||
void CodewordExtractor::reset() {
|
||||
clear_data_bits();
|
||||
has_sync_ = false;
|
||||
inverted_ = false;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::clear_data_bits() {
|
||||
data_ = 0;
|
||||
bit_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::take_one_bit() {
|
||||
data_ = (data_ << 1) | bits_.pop();
|
||||
if (bit_count_ < data_bit_count)
|
||||
++bit_count_;
|
||||
}
|
||||
|
||||
void CodewordExtractor::handle_sync(bool inverted) {
|
||||
clear_data_bits();
|
||||
has_sync_ = true;
|
||||
inverted_ = inverted;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::save_current_codeword() {
|
||||
batch_[word_count_++] = inverted_ ? ~data_ : data_;
|
||||
clear_data_bits();
|
||||
}
|
||||
|
||||
void CodewordExtractor::handle_batch_complete() {
|
||||
on_batch_(*this);
|
||||
has_sync_ = false;
|
||||
word_count_ = 0;
|
||||
}
|
||||
|
||||
void CodewordExtractor::pad_idle() {
|
||||
while (word_count_ < pocsag::batch_size)
|
||||
batch_[word_count_++] = idle_codeword;
|
||||
}
|
||||
|
||||
/* POCSAGProcessor ***************************************/
|
||||
|
||||
void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
// buffer has 2048 samples
|
||||
// decim0 out: 2048/8 = 256 samples
|
||||
// decim1 out: 256/8 = 32 samples
|
||||
// channel out: 32/2 = 16 samples
|
||||
|
||||
// Get 24kHz audio
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
|
||||
// Check if there's any signal in the audio buffer.
|
||||
bool has_audio = squelch.execute(audio);
|
||||
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
|
||||
|
||||
// Has there been any signal recently?
|
||||
if (squelch_history == 0) {
|
||||
// No recent signal, flush and prepare for next message.
|
||||
if (word_extractor.current() > 0) {
|
||||
flush();
|
||||
reset();
|
||||
send_stats();
|
||||
}
|
||||
|
||||
// Clear the audio stream before sending.
|
||||
for (size_t i = 0; i < audio.count; ++i)
|
||||
audio.p[i] = 0.0;
|
||||
|
||||
audio_output.write(audio);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out high-frequency noise then normalize.
|
||||
lpf.execute_in_place(audio);
|
||||
normalizer.execute_in_place(audio);
|
||||
audio_output.write(audio);
|
||||
|
||||
// Decode the messages from the audio.
|
||||
bit_extractor.extract_bits(audio);
|
||||
word_extractor.process_bits();
|
||||
|
||||
// Update the status.
|
||||
samples_processed += buffer.count;
|
||||
if (samples_processed >= stat_update_threshold) {
|
||||
send_stats();
|
||||
samples_processed -= stat_update_threshold;
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGProcessor::on_message(const Message* const message) {
|
||||
switch (message->id) {
|
||||
case Message::ID::POCSAGConfigure:
|
||||
configure();
|
||||
break;
|
||||
|
||||
case Message::ID::NBFMConfigure: {
|
||||
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
|
||||
squelch.set_threshold(config->squelch_level / 99.0);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGProcessor::configure() {
|
||||
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
|
||||
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
|
||||
constexpr size_t channel_filter_output_fs = decim_1_output_fs / 2;
|
||||
constexpr size_t demod_input_fs = channel_filter_output_fs;
|
||||
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
|
||||
|
||||
// Don't process the audio stream.
|
||||
audio_output.configure(false);
|
||||
|
||||
bit_extractor.configure(demod_input_fs);
|
||||
|
||||
// Set ready to process data.
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void POCSAGProcessor::flush() {
|
||||
word_extractor.flush();
|
||||
}
|
||||
|
||||
void POCSAGProcessor::reset() {
|
||||
bits.reset();
|
||||
bit_extractor.reset();
|
||||
word_extractor.reset();
|
||||
samples_processed = 0;
|
||||
}
|
||||
|
||||
void POCSAGProcessor::send_stats() const {
|
||||
POCSAGStatsMessage message(
|
||||
word_extractor.current(), word_extractor.count(),
|
||||
word_extractor.has_sync(), bit_extractor.baud_rate());
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
void POCSAGProcessor::send_packet() {
|
||||
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
||||
packet.set_timestamp(Timestamp::now());
|
||||
packet.set_bitrate(bit_extractor.baud_rate());
|
||||
packet.set(word_extractor.batch());
|
||||
|
||||
POCSAGPacketMessage message(packet);
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
/* main **************************************************/
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright (C) 1996 Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu)
|
||||
* Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com)
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_POCSAG2_H__
|
||||
#define __PROC_POCSAG2_H__
|
||||
|
||||
/* https://www.aaroncake.net/schoolpage/pocsag.htm */
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "dsp_iir_config.hpp"
|
||||
#include "message.hpp"
|
||||
#include "pocsag.hpp"
|
||||
#include "pocsag_packet.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
/* Normalizes audio stream to +/-1.0f */
|
||||
class AudioNormalizer {
|
||||
public:
|
||||
void execute_in_place(const buffer_f32_t& audio);
|
||||
|
||||
private:
|
||||
void calculate_thresholds();
|
||||
|
||||
uint32_t counter_ = 0;
|
||||
float min_ = 99.0f;
|
||||
float max_ = -99.0f;
|
||||
float t_hi_ = 1.0;
|
||||
float t_lo_ = 1.0;
|
||||
};
|
||||
|
||||
/* FIFO wrapper over a uint32_t's bits. */
|
||||
class BitQueue {
|
||||
public:
|
||||
void push(bool bit);
|
||||
bool pop();
|
||||
void reset();
|
||||
uint8_t size() const;
|
||||
uint32_t data() const;
|
||||
|
||||
private:
|
||||
uint32_t data_ = 0;
|
||||
uint8_t count_ = 0;
|
||||
|
||||
static constexpr uint8_t max_size_ = sizeof(data_) * 8;
|
||||
};
|
||||
|
||||
/* Extracts bits and bitrate from audio stream. */
|
||||
class BitExtractor {
|
||||
public:
|
||||
BitExtractor(BitQueue& bits)
|
||||
: bits_{bits} {}
|
||||
|
||||
void extract_bits(const buffer_f32_t& audio);
|
||||
void configure(uint32_t sample_rate);
|
||||
void reset();
|
||||
|
||||
uint16_t baud_rate() const;
|
||||
|
||||
private:
|
||||
/* Number of rate misses that would cause a rate update. */
|
||||
static constexpr uint8_t rate_miss_reset_threshold = 5;
|
||||
|
||||
/* Number of rate misses that would cause a rate update. */
|
||||
static constexpr uint8_t bad_transition_reset_threshold = 10;
|
||||
|
||||
struct BaudInfo {
|
||||
uint16_t baud_rate = 0;
|
||||
float bit_length = 0.0;
|
||||
float min_bit_length = 0.0;
|
||||
float max_bit_length = 0.0;
|
||||
};
|
||||
|
||||
/* Handle a transition, returns true if "good". */
|
||||
bool handle_transition();
|
||||
|
||||
/* Count the number of bits the length represents.
|
||||
* Returns true if valid given the current baud rate. */
|
||||
bool count_bits(uint32_t length, uint16_t& bit_count);
|
||||
|
||||
/* Gets the best baud info associated with the specified bit length. */
|
||||
const BaudInfo* get_baud_info(float bit_length) const;
|
||||
|
||||
std::array<BaudInfo, 3> known_rates_{
|
||||
BaudInfo{512},
|
||||
BaudInfo{1200},
|
||||
BaudInfo{2400}};
|
||||
|
||||
BitQueue& bits_;
|
||||
|
||||
uint32_t sample_rate_ = 0;
|
||||
uint16_t min_valid_length_ = 0;
|
||||
const BaudInfo* current_rate_ = nullptr;
|
||||
uint8_t rate_misses_ = 0;
|
||||
|
||||
float sample_ = 0.0;
|
||||
float last_sample_ = 0.0;
|
||||
float next_bit_center_ = 0.0;
|
||||
|
||||
uint32_t sample_index_ = 0;
|
||||
uint32_t last_transition_index_ = 0;
|
||||
uint32_t bad_transitions_ = 0;
|
||||
};
|
||||
|
||||
/* Extracts codeword batches from the BitQueue. */
|
||||
class CodewordExtractor {
|
||||
public:
|
||||
using batch_t = pocsag::batch_t;
|
||||
using batch_handler_t = std::function<void(CodewordExtractor&)>;
|
||||
|
||||
CodewordExtractor(BitQueue& bits, batch_handler_t on_batch)
|
||||
: bits_{bits}, on_batch_{on_batch} {}
|
||||
|
||||
/* Process the BitQueue to extract codeword batches. */
|
||||
void process_bits();
|
||||
|
||||
/* Pad then send any pending frames. */
|
||||
void flush();
|
||||
|
||||
/* Completely reset to prepare for a new message. */
|
||||
void reset();
|
||||
|
||||
/* Gets the underlying batch array. */
|
||||
const batch_t& batch() const { return batch_; }
|
||||
|
||||
/* Gets in-progress codeword. */
|
||||
uint32_t current() const { return data_; }
|
||||
|
||||
/* Gets the count of completed codewords. */
|
||||
uint8_t count() const { return word_count_; }
|
||||
|
||||
/* Returns true if the batch has as sync frame. */
|
||||
bool has_sync() const { return has_sync_; }
|
||||
|
||||
private:
|
||||
/* Sync frame codeword. */
|
||||
static constexpr uint32_t sync_codeword = 0x7cd215d8;
|
||||
|
||||
/* Idle codeword used to pad a 16 codeword "batch". */
|
||||
static constexpr uint32_t idle_codeword = 0x7a89c197;
|
||||
|
||||
/* Number of bits in 'data_' member. */
|
||||
static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8;
|
||||
|
||||
/* Clears data_ and bit_count_ to prepare for next codeword. */
|
||||
void clear_data_bits();
|
||||
|
||||
/* Pop a bit off the queue and add it to data_. */
|
||||
void take_one_bit();
|
||||
|
||||
/* Handles receiving the sync frame codeword, start of batch. */
|
||||
void handle_sync(bool inverted);
|
||||
|
||||
/* Saves the current codeword in data_ to the batch. */
|
||||
void save_current_codeword();
|
||||
|
||||
/* Sends the batch to the handler, resets for next batch. */
|
||||
void handle_batch_complete();
|
||||
|
||||
/* Fill the rest of the batch with 'idle' codewords. */
|
||||
void pad_idle();
|
||||
|
||||
BitQueue& bits_;
|
||||
batch_handler_t on_batch_{};
|
||||
|
||||
/* When true, sync frame has been received. */
|
||||
bool has_sync_ = false;
|
||||
|
||||
/* When true, bit vales are flipped in the codewords. */
|
||||
bool inverted_ = false;
|
||||
|
||||
uint32_t data_ = 0;
|
||||
uint8_t bit_count_ = 0;
|
||||
uint8_t word_count_ = 0;
|
||||
batch_t batch_{};
|
||||
};
|
||||
|
||||
/* Processes POCSAG signal into codeword batches. */
|
||||
class POCSAGProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr uint8_t stat_update_interval = 10;
|
||||
static constexpr uint32_t stat_update_threshold =
|
||||
baseband_fs / stat_update_interval;
|
||||
|
||||
void configure();
|
||||
void flush();
|
||||
void reset();
|
||||
void send_stats() const;
|
||||
void send_packet();
|
||||
|
||||
/* Set once app is ready to receive messages. */
|
||||
bool configured = false;
|
||||
|
||||
/* Buffer for decimated IQ data. */
|
||||
std::array<complex16_t, 256> dst{};
|
||||
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
|
||||
|
||||
/* Buffer for demodulated audio. */
|
||||
std::array<float, 16> audio{};
|
||||
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
|
||||
|
||||
/* Decimate to 48kHz. */
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
/* Filter to 24kHz and demodulate. */
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
dsp::demodulate::FM demod{};
|
||||
|
||||
/* Squelch to ignore noise. */
|
||||
FMSquelch squelch{};
|
||||
uint64_t squelch_history = 0;
|
||||
|
||||
/* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
|
||||
* nicely into the transition band of this 1800Hz filter.
|
||||
* scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
|
||||
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
|
||||
{1.00000000f, -1.34896775f, 0.51398189f}}};
|
||||
|
||||
/* Attempts to de-noise and normalize signal. */
|
||||
AudioNormalizer normalizer{};
|
||||
|
||||
/* Handles writing audio stream to hardware. */
|
||||
AudioOutput audio_output{};
|
||||
|
||||
/* Holds the data sent to the app. */
|
||||
pocsag::POCSAGPacket packet{};
|
||||
|
||||
/* Used to keep track of how many samples were processed
|
||||
* between status update messages. */
|
||||
uint32_t samples_processed = 0;
|
||||
|
||||
BitQueue bits{};
|
||||
|
||||
/* Processes audio into bits. */
|
||||
BitExtractor bit_extractor{bits};
|
||||
|
||||
/* Processes bits into codewords. */
|
||||
CodewordExtractor word_extractor{
|
||||
bits, [this](CodewordExtractor&) {
|
||||
send_packet();
|
||||
}};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_POCSAG2_H__*/
|
||||
@@ -69,7 +69,7 @@ void ReplayProcessor::execute(const buffer_c8_t& buffer) {
|
||||
chDbgPanic("Output not div.");
|
||||
|
||||
// Is the input smaple buffer big enough?
|
||||
if (samples_to_read > iq_buffer.size())
|
||||
if (samples_to_read > iq_buffer.count)
|
||||
chDbgPanic("IQ buf ovf.");
|
||||
#endif
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ReplayProcessor : public BasebandProcessor {
|
||||
static constexpr auto spectrum_rate_hz = 50.0f;
|
||||
|
||||
// Holds the read IQ data chunk from the file to send.
|
||||
std::array<complex16_t, 256> iq{};
|
||||
std::array<complex16_t, 512> iq{};
|
||||
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
#include "audio_output.hpp"
|
||||
|
||||
SondeProcessor::SondeProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
|
||||
audio_output.configure(false);
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
#include "event_m4.hpp"
|
||||
|
||||
TestProcessor::TestProcessor() {
|
||||
decim_0.configure(taps_11k0_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_11k0_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
#include "event_m4.hpp"
|
||||
|
||||
TPMSProcessor::TPMSProcessor() {
|
||||
decim_0.configure(taps_200k_decim_0.taps, 33554432);
|
||||
decim_1.configure(taps_200k_decim_1.taps, 131072);
|
||||
decim_0.configure(taps_200k_decim_0.taps);
|
||||
decim_1.configure(taps_200k_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -165,8 +165,8 @@ void WidebandFMAudio::configure(const WFMConfigureMessage& message) {
|
||||
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
|
||||
spectrum_samples = 0;
|
||||
|
||||
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||
decim_1.configure(message.decim_1_filter.taps, 131072);
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
channel_filter_low_f = message.decim_1_filter.low_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_high_f = message.decim_1_filter.high_frequency_normalized * decim_1_input_fs;
|
||||
channel_filter_transition = message.decim_1_filter.transition_normalized * decim_1_input_fs;
|
||||
|
||||
@@ -36,24 +36,12 @@ struct complex<int8_t> {
|
||||
typedef int8_t value_type;
|
||||
typedef uint16_t rep_type;
|
||||
|
||||
// constexpr complex(
|
||||
// rep_type r
|
||||
// ) : _rep { r }
|
||||
// {
|
||||
// }
|
||||
|
||||
constexpr complex(
|
||||
int8_t re = 0,
|
||||
int8_t im = 0)
|
||||
: _v{re, im} {
|
||||
}
|
||||
|
||||
// constexpr complex(
|
||||
// const complex& o
|
||||
// ) : _rep { o._rep }
|
||||
// {
|
||||
// }
|
||||
|
||||
constexpr int8_t real() const { return _v[0]; }
|
||||
constexpr int8_t imag() const { return _v[1]; }
|
||||
|
||||
@@ -77,24 +65,12 @@ struct complex<int16_t> {
|
||||
typedef int16_t value_type;
|
||||
typedef uint32_t rep_type;
|
||||
|
||||
// constexpr complex(
|
||||
// rep_type r
|
||||
// ) : _rep { r }
|
||||
// {
|
||||
// }
|
||||
|
||||
constexpr complex(
|
||||
int16_t re = 0,
|
||||
int16_t im = 0)
|
||||
: _v{re, im} {
|
||||
}
|
||||
|
||||
// constexpr complex(
|
||||
// const complex& o
|
||||
// ) : _rep { o._rep }
|
||||
// {
|
||||
// }
|
||||
|
||||
constexpr int16_t real() const { return _v[0]; }
|
||||
constexpr int16_t imag() const { return _v[1]; }
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ constexpr iir_biquad_config_t audio_48k_hpf_300hz_config{
|
||||
// scipy.signal.butter(2, 300 / 12000.0, 'highpass', analog=False)
|
||||
constexpr iir_biquad_config_t audio_24k_hpf_300hz_config{
|
||||
{0.94597686f, -1.89195371f, 0.94597686f},
|
||||
{1.00000000f, -1.88903308f, 0.89487434f}
|
||||
|
||||
};
|
||||
{1.00000000f, -1.88903308f, 0.89487434f}};
|
||||
|
||||
// scipy.signal.butter(2, 30 / 12000.0, 'highpass', analog=False)
|
||||
constexpr iir_biquad_config_t audio_24k_hpf_30hz_config{
|
||||
|
||||
@@ -344,12 +344,21 @@ class POCSAGPacketMessage : public Message {
|
||||
class POCSAGStatsMessage : public Message {
|
||||
public:
|
||||
constexpr POCSAGStatsMessage(
|
||||
uint32_t current_bits,
|
||||
uint8_t current_frames,
|
||||
bool has_sync,
|
||||
uint16_t baud_rate)
|
||||
: Message{ID::POCSAGStats},
|
||||
baud_rate_{baud_rate} {
|
||||
current_bits{current_bits},
|
||||
current_frames{current_frames},
|
||||
has_sync{has_sync},
|
||||
baud_rate{baud_rate} {
|
||||
}
|
||||
|
||||
uint16_t baud_rate_;
|
||||
uint32_t current_bits = 0;
|
||||
uint8_t current_frames = 0;
|
||||
bool has_sync = false;
|
||||
uint16_t baud_rate = 0;
|
||||
};
|
||||
|
||||
class ACARSPacketMessage : public Message {
|
||||
@@ -816,8 +825,8 @@ enum class OversampleRate : uint8_t {
|
||||
None = 1,
|
||||
x1 = None,
|
||||
|
||||
// 4x would make sense to have, but need to ensure it doesn't
|
||||
// overrun the IQ read buffer in proc_replay.
|
||||
/* Oversample rate of 4 times the sample rate. */
|
||||
x4 = 4,
|
||||
|
||||
/* Oversample rate of 8 times the sample rate. */
|
||||
x8 = 8,
|
||||
@@ -830,9 +839,6 @@ enum class OversampleRate : uint8_t {
|
||||
|
||||
/* Oversample rate of 64 times the sample rate. */
|
||||
x64 = 64,
|
||||
|
||||
/* Oversample rate of 128 times the sample rate. */
|
||||
x128 = 128,
|
||||
};
|
||||
|
||||
class SampleRateConfigMessage : public Message {
|
||||
|
||||
@@ -42,26 +42,27 @@
|
||||
* In testing, a minimum rate of 400kHz seems to the functional minimum.
|
||||
*
|
||||
* There are several different concepts or terms related to Capture and Replay,
|
||||
* (1) oversampling (x8, x16 ,...) / decimation (/8, /16...)
|
||||
* In Capture App , when ADC can not handle directly a requiered low sample rates ,
|
||||
* we need to apply oversampling (x8. x16 ex) , getting more real samples than needed) by "x_number" ,
|
||||
* and later to write it to SD card with the proper needed real sample rate , we apply Decimation , "/ number" .
|
||||
* (1) Oversampling (x8, x16, ...) and Decimation (/8, /16...)
|
||||
* In Capture App, the ADC can not directly handle low sample rates.
|
||||
* We need to apply oversampling (x8, x16, ...), collecting more "x number" additional real samples.
|
||||
* To write it to SD card with the desired sample rate, we apply Decimation ("/ number").
|
||||
*
|
||||
* (2) up-sampling or re-escale or interpolation. (x8, x16, ...)
|
||||
* In Replay-list App, when we got too low bit rate data for Hackrf ,
|
||||
* we need to upsampling or interpolate or resampling to increase those low bit rates
|
||||
* to a proper sample rate higher than the min. to be able to be transmitted by Hackrf.
|
||||
* (2) Up-sampling or re-scale or interpolation. (x8, x16, ...).
|
||||
* In Replay-list App, when the bit rate data is too low for HackRF.
|
||||
* We need to upsample or interpolate to increase those low bit rates
|
||||
* to a rate higher than the hardware nminimum to be transmitted by Hackrf.
|
||||
*/
|
||||
|
||||
/* Gets the oversample rate for a given sample rate.
|
||||
* The oversample rate is used to increase the sample rate to improve SNR and quality.
|
||||
* This is also used as the interpolation rate when replaying captures. */
|
||||
inline OversampleRate get_oversample_rate(uint32_t sample_rate) {
|
||||
if (sample_rate < 30'000) return OversampleRate::x64; // 25k, 16k, 12k5.
|
||||
if (sample_rate < 80'000) return OversampleRate::x32; // 75k, 50k, 32k.
|
||||
if (sample_rate < 250'000) return OversampleRate::x16; // 100k and 150k.
|
||||
if (sample_rate < 25'000) return OversampleRate::x64; // 12k5, 16k.
|
||||
if (sample_rate < 80'000) return OversampleRate::x32; // 25k, 32k, 50k, 75k.
|
||||
if (sample_rate < 250'000) return OversampleRate::x16; // 100k, 150k.
|
||||
if (sample_rate < 1'250'000) return OversampleRate::x8; // 250k, 500k, 600k, 750k, 1Mhz.
|
||||
|
||||
return OversampleRate::x8; // 250k .. 1Mhz, that decim x8 , is already applied.(OVerSampling and decim OK)
|
||||
return OversampleRate::x4; // Top range (1.25Mhz ... 5.5Mhz).
|
||||
}
|
||||
|
||||
/* Gets the actual sample rate for a given sample rate.
|
||||
|
||||
+77
-94
@@ -225,27 +225,14 @@ void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t fun
|
||||
} while (char_idx < message_size);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
inline int bitsDiff(unsigned long left, unsigned long right) {
|
||||
unsigned long xord = left ^ right;
|
||||
int count = 0;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
if ((xord & 0x01) != 0) ++count;
|
||||
xord = xord >> 1;
|
||||
}
|
||||
return (count);
|
||||
// ----------------------------------------------------------------------------
|
||||
// EccContainer
|
||||
// ----------------------------------------------------------------------------
|
||||
EccContainer::EccContainer() {
|
||||
setup_ecc();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
static uint32_t ecs[32]; /* error correction sequence */
|
||||
static uint32_t bch[1025];
|
||||
static int eccSetup = 0;
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
void setupecc() {
|
||||
void EccContainer::setup_ecc() {
|
||||
unsigned int srr = 0x3b4;
|
||||
unsigned int i, n, j, k;
|
||||
|
||||
@@ -308,36 +295,24 @@ void setupecc() {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------------
|
||||
inline int errorCorrection(uint32_t* val) {
|
||||
// Set up the tables the first time
|
||||
if (eccSetup == 0) {
|
||||
setupecc();
|
||||
eccSetup = 1;
|
||||
}
|
||||
|
||||
int EccContainer::error_correct(uint32_t& val) {
|
||||
int i, synd, errl, acc, pari, ecc, b1, b2;
|
||||
|
||||
errl = 0;
|
||||
pari = 0;
|
||||
|
||||
/* run through error detection and correction routine */
|
||||
|
||||
// for (i=0; i<=20; i++)
|
||||
ecc = 0;
|
||||
for (i = 31; i >= 11; --i) {
|
||||
if ((*val & (1 << i))) {
|
||||
if (val & (1 << i)) {
|
||||
ecc = ecc ^ ecs[31 - i];
|
||||
pari = pari ^ 0x01;
|
||||
}
|
||||
}
|
||||
|
||||
// for (i=21; i<=30; i++)
|
||||
acc = 0;
|
||||
for (i = 10; i >= 1; --i) {
|
||||
acc = acc << 1;
|
||||
if ((*val & (1 << i))) {
|
||||
if (val & (1 << i)) {
|
||||
acc = acc ^ 0x01;
|
||||
}
|
||||
}
|
||||
@@ -355,12 +330,12 @@ inline int errorCorrection(uint32_t* val) {
|
||||
b2 = b2 & 0x1f;
|
||||
|
||||
if (b2 != 0x1f) {
|
||||
*val ^= 0x01 << (31 - b2);
|
||||
val ^= 0x01 << (31 - b2);
|
||||
ecc = ecc ^ ecs[b2];
|
||||
}
|
||||
|
||||
if (b1 != 0x1f) {
|
||||
*val ^= 0x01 << (31 - b1);
|
||||
val ^= 0x01 << (31 - b1);
|
||||
ecc = ecc ^ ecs[b1];
|
||||
}
|
||||
|
||||
@@ -377,78 +352,86 @@ inline int errorCorrection(uint32_t* val) {
|
||||
return errl;
|
||||
}
|
||||
|
||||
void pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState* const state) {
|
||||
int errors = 0;
|
||||
uint32_t codeword;
|
||||
char ascii_char;
|
||||
std::string output_text = "";
|
||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
||||
constexpr uint8_t codeword_max = 16;
|
||||
state.output.clear();
|
||||
|
||||
state->out_type = EMPTY;
|
||||
while (state.codeword_index < codeword_max) {
|
||||
auto codeword = batch[state.codeword_index];
|
||||
bool is_address = (codeword & 0x80000000U) == 0;
|
||||
|
||||
// For each codeword...
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
codeword = batch[i];
|
||||
// Error correct twice. First time to fix any errors it can,
|
||||
// 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);
|
||||
|
||||
errorCorrection(&codeword);
|
||||
errors = errorCorrection(&codeword);
|
||||
switch (state.mode) {
|
||||
case STATE_CLEAR:
|
||||
if (is_address && codeword != POCSAG_IDLEWORD) {
|
||||
state.function = (codeword >> 11) & 3;
|
||||
state.address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
|
||||
state.mode = STATE_HAVE_ADDRESS;
|
||||
state.out_type = ADDRESS;
|
||||
state.errors = error_count;
|
||||
|
||||
if (!(codeword & 0x80000000U)) {
|
||||
// Address codeword
|
||||
if (state->mode == STATE_CLEAR) {
|
||||
// if (codeword != POCSAG_IDLEWORD) {
|
||||
if (!(bitsDiff(codeword, POCSAG_IDLEWORD) < 1)) {
|
||||
state->function = (codeword >> 11) & 3;
|
||||
state->address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
|
||||
state->mode = STATE_HAVE_ADDRESS;
|
||||
state->out_type = ADDRESS;
|
||||
state->errors = errors;
|
||||
|
||||
state->ascii_idx = 0;
|
||||
state->ascii_data = 0;
|
||||
state.ascii_idx = 0;
|
||||
state.ascii_data = 0;
|
||||
} else if (codeword == POCSAG_IDLEWORD) {
|
||||
state.out_type = IDLE;
|
||||
}
|
||||
} else {
|
||||
state->mode = STATE_CLEAR; // New address = new message
|
||||
}
|
||||
} else {
|
||||
state->errors += errors;
|
||||
// Message codeword
|
||||
if (state->mode == STATE_HAVE_ADDRESS) {
|
||||
// First message codeword: complete address
|
||||
state->address |= (i >> 1); // Add in the 3 LSBs (frame #)
|
||||
state->mode = STATE_GETTING_MSG;
|
||||
}
|
||||
break;
|
||||
|
||||
state->out_type = MESSAGE;
|
||||
case STATE_HAVE_ADDRESS:
|
||||
if (is_address) {
|
||||
// Got another address, return the current state.
|
||||
state.mode = STATE_CLEAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
state->ascii_data |= ((codeword >> 11) & 0xFFFFF); // Get 20 message bits
|
||||
state->ascii_idx += 20;
|
||||
// First message codeword, complete the address.
|
||||
state.address |= (state.codeword_index >> 1); // Add in the 3 LSBs (frame #).
|
||||
state.mode = STATE_GETTING_MSG;
|
||||
[[fallthrough]];
|
||||
|
||||
// Raw 20 bits to 7 bit reversed ASCII
|
||||
while (state->ascii_idx >= 7) {
|
||||
state->ascii_idx -= 7;
|
||||
ascii_char = ((state->ascii_data) >> (state->ascii_idx)) & 0x7F;
|
||||
case STATE_GETTING_MSG:
|
||||
if (is_address) {
|
||||
// Codeword isn't a message, return the current state.
|
||||
state.mode = STATE_CLEAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bottom's up
|
||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
|
||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
|
||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> *7654321
|
||||
state.out_type = MESSAGE;
|
||||
state.errors += error_count;
|
||||
state.ascii_data |= (codeword >> 11) & 0xFFFFF; // Get 20 message bits.
|
||||
state.ascii_idx += 20;
|
||||
|
||||
// Translate non-printable chars
|
||||
if ((ascii_char < 32) || (ascii_char > 126)) {
|
||||
// output_text += "[" + to_string_dec_uint(ascii_char) + "]";
|
||||
output_text += ".";
|
||||
} else
|
||||
output_text += ascii_char;
|
||||
}
|
||||
// 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) {
|
||||
state.ascii_idx -= 7;
|
||||
char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F;
|
||||
|
||||
state->ascii_data <<= 20; // Remaining bits are for next time...
|
||||
// Reverse the bits. (TODO: __RBIT?)
|
||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
|
||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
|
||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210
|
||||
|
||||
// Translate non-printable chars. TODO: Leave CRLF?
|
||||
if (ascii_char < 32 || ascii_char > 126)
|
||||
state.output += ".";
|
||||
else
|
||||
state.output += ascii_char;
|
||||
}
|
||||
|
||||
state.ascii_data <<= 20; // Remaining bits are for next iteration...
|
||||
break;
|
||||
}
|
||||
|
||||
state.codeword_index++;
|
||||
}
|
||||
|
||||
state->output = output_text;
|
||||
|
||||
if (state->mode == STATE_HAVE_ADDRESS)
|
||||
state->mode = STATE_CLEAR;
|
||||
return false;
|
||||
}
|
||||
|
||||
} /* namespace pocsag */
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
namespace pocsag {
|
||||
|
||||
// Todo: these enums suck, make a better decode_batch
|
||||
// TODO: these enums suck, make a better decode_batch
|
||||
|
||||
enum Mode : uint32_t {
|
||||
STATE_CLEAR,
|
||||
@@ -45,6 +45,7 @@ enum Mode : uint32_t {
|
||||
|
||||
enum OutputType : uint32_t {
|
||||
EMPTY,
|
||||
IDLE,
|
||||
ADDRESS,
|
||||
MESSAGE
|
||||
};
|
||||
@@ -55,22 +56,46 @@ enum MessageType : uint32_t {
|
||||
ALPHANUMERIC
|
||||
};
|
||||
|
||||
/* Holds error correction arrays. This allows the arrays to
|
||||
* be freed when POCSAG is not running, saving ~4kb of RAM. */
|
||||
class EccContainer {
|
||||
public:
|
||||
EccContainer();
|
||||
|
||||
EccContainer(const EccContainer&) = delete;
|
||||
EccContainer(EccContainer&&) = delete;
|
||||
EccContainer& operator=(const EccContainer&) = delete;
|
||||
EccContainer& operator=(EccContainer&&) = delete;
|
||||
|
||||
int error_correct(uint32_t& val);
|
||||
|
||||
private:
|
||||
void setup_ecc();
|
||||
|
||||
/* error correction sequence */
|
||||
uint32_t ecs[32];
|
||||
uint32_t bch[1025];
|
||||
};
|
||||
|
||||
struct POCSAGState {
|
||||
uint32_t function;
|
||||
uint32_t address;
|
||||
EccContainer* ecc = nullptr;
|
||||
uint8_t codeword_index = 0;
|
||||
uint32_t function = 0;
|
||||
uint32_t address = 0;
|
||||
Mode mode = STATE_CLEAR;
|
||||
OutputType out_type = EMPTY;
|
||||
uint32_t ascii_data;
|
||||
uint32_t ascii_idx;
|
||||
uint32_t errors;
|
||||
std::string output;
|
||||
uint32_t ascii_data = 0;
|
||||
uint32_t ascii_idx = 0;
|
||||
uint32_t errors = 0;
|
||||
std::string output{};
|
||||
};
|
||||
|
||||
const pocsag::BitRate pocsag_bitrates[4] = {
|
||||
pocsag::BitRate::FSK512,
|
||||
pocsag::BitRate::FSK1200,
|
||||
pocsag::BitRate::FSK2400,
|
||||
pocsag::BitRate::FSK3200};
|
||||
pocsag::BitRate::FSK3200,
|
||||
};
|
||||
|
||||
std::string bitrate_str(BitRate bitrate);
|
||||
std::string flag_str(PacketFlag packetflag);
|
||||
@@ -78,7 +103,9 @@ std::string flag_str(PacketFlag packetflag);
|
||||
void insert_BCH(BCHCode& BCH_code, uint32_t* codeword);
|
||||
uint32_t get_digit_code(char code);
|
||||
void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t function, const std::string message, const uint32_t address, std::vector<uint32_t>& codewords);
|
||||
void pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState* const state);
|
||||
|
||||
// Returns true if the batch has more to process.
|
||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state);
|
||||
|
||||
} /* namespace pocsag */
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ enum PacketFlag : uint32_t {
|
||||
TOO_LONG
|
||||
};
|
||||
|
||||
/* Number of codewords in a batch. */
|
||||
constexpr uint8_t batch_size = 16;
|
||||
using batch_t = std::array<uint32_t, batch_size>;
|
||||
|
||||
class POCSAGPacket {
|
||||
public:
|
||||
void set_timestamp(const Timestamp& value) {
|
||||
@@ -55,16 +59,20 @@ class POCSAGPacket {
|
||||
return timestamp_;
|
||||
}
|
||||
|
||||
void set(const size_t index, const uint32_t data) {
|
||||
if (index < 16)
|
||||
void set(size_t index, uint32_t data) {
|
||||
if (index < batch_size)
|
||||
codewords[index] = data;
|
||||
}
|
||||
|
||||
uint32_t operator[](const size_t index) const {
|
||||
return (index < 16) ? codewords[index] : 0;
|
||||
void set(const batch_t& batch) {
|
||||
codewords = batch;
|
||||
}
|
||||
|
||||
void set_bitrate(const uint16_t bitrate) {
|
||||
uint32_t operator[](size_t index) const {
|
||||
return (index < batch_size) ? codewords[index] : 0;
|
||||
}
|
||||
|
||||
void set_bitrate(uint16_t bitrate) {
|
||||
bitrate_ = bitrate;
|
||||
}
|
||||
|
||||
@@ -72,7 +80,7 @@ class POCSAGPacket {
|
||||
return bitrate_;
|
||||
}
|
||||
|
||||
void set_flag(const PacketFlag flag) {
|
||||
void set_flag(PacketFlag flag) {
|
||||
flag_ = flag;
|
||||
}
|
||||
|
||||
@@ -89,7 +97,7 @@ class POCSAGPacket {
|
||||
private:
|
||||
uint16_t bitrate_{0};
|
||||
PacketFlag flag_{NORMAL};
|
||||
std::array<uint32_t, 16> codewords{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
batch_t codewords{};
|
||||
Timestamp timestamp_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ static_assert(sizeof(ui_config2_t) == sizeof(uint32_t));
|
||||
struct misc_config_t {
|
||||
bool mute_audio : 1;
|
||||
bool disable_speaker : 1;
|
||||
bool UNUSED_2 : 1;
|
||||
bool config_disable_external_tcxo : 1;
|
||||
bool UNUSED_3 : 1;
|
||||
bool UNUSED_4 : 1;
|
||||
bool UNUSED_5 : 1;
|
||||
@@ -226,6 +226,9 @@ struct data_t {
|
||||
// Additional UI settings.
|
||||
ui_config2_t ui_config2;
|
||||
|
||||
// recovery mode magic value storage
|
||||
uint32_t config_mode_storage;
|
||||
|
||||
constexpr data_t()
|
||||
: structure_version(data_structure_version_enum::VERSION_CURRENT),
|
||||
target_frequency(target_frequency_reset_value),
|
||||
@@ -273,7 +276,8 @@ struct data_t {
|
||||
UNUSED_8(0),
|
||||
headphone_volume_cb(-600),
|
||||
misc_config(),
|
||||
ui_config2() {
|
||||
ui_config2(),
|
||||
config_mode_storage(CONFIG_MODE_NORMAL_VALUE) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -363,6 +367,7 @@ void defaults() {
|
||||
|
||||
set_config_backlight_timer(backlight_config_t{});
|
||||
set_config_splash(true);
|
||||
set_config_disable_external_tcxo(false);
|
||||
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
|
||||
set_config_speaker_disable(true); // Disable AK4951 speaker by default (in case of OpenSourceSDRLab H2)
|
||||
|
||||
@@ -380,6 +385,10 @@ void defaults() {
|
||||
|
||||
void init() {
|
||||
const auto switches_state = get_switches_state();
|
||||
|
||||
// ignore for valid check
|
||||
auto config_mode_backup = config_mode_storage();
|
||||
set_config_mode_storage(CONFIG_MODE_NORMAL_VALUE);
|
||||
if (!(switches_state[(size_t)ui::KeyEvent::Left] && switches_state[(size_t)ui::KeyEvent::Right]) && backup_ram->is_valid()) {
|
||||
// Copy valid persistent data into cache.
|
||||
cached_backup_ram = *backup_ram;
|
||||
@@ -395,6 +404,7 @@ void init() {
|
||||
// Copy defaults into cache.
|
||||
defaults();
|
||||
}
|
||||
set_config_mode_storage(config_mode_backup);
|
||||
}
|
||||
|
||||
void persist() {
|
||||
@@ -550,6 +560,10 @@ bool config_speaker_disable() {
|
||||
return data->misc_config.disable_speaker;
|
||||
}
|
||||
|
||||
bool config_disable_external_tcxo() {
|
||||
return data->misc_config.config_disable_external_tcxo;
|
||||
}
|
||||
|
||||
bool stealth_mode() {
|
||||
return data->ui_config.stealth_mode;
|
||||
}
|
||||
@@ -611,6 +625,10 @@ void set_config_speaker_disable(bool v) {
|
||||
data->misc_config.disable_speaker = v;
|
||||
}
|
||||
|
||||
void set_config_disable_external_tcxo(bool v) {
|
||||
data->misc_config.config_disable_external_tcxo = v;
|
||||
}
|
||||
|
||||
void set_stealth_mode(bool v) {
|
||||
data->ui_config.stealth_mode = v;
|
||||
}
|
||||
@@ -840,6 +858,16 @@ void set_encoder_dial_sensitivity(uint8_t v) {
|
||||
data->encoder_dial_sensitivity = v;
|
||||
}
|
||||
|
||||
// Recovery mode magic value storage
|
||||
static data_t* data_direct_access = reinterpret_cast<data_t*>(memory::map::backup_ram.base());
|
||||
|
||||
uint32_t config_mode_storage() {
|
||||
return data_direct_access->config_mode_storage;
|
||||
}
|
||||
void set_config_mode_storage(uint32_t v) {
|
||||
data_direct_access->config_mode_storage = v;
|
||||
}
|
||||
|
||||
// PMem to sdcard settings
|
||||
|
||||
bool should_use_sdcard_for_pmem() {
|
||||
@@ -965,6 +993,8 @@ bool debug_dump() {
|
||||
// misc_config bits
|
||||
pmem_dump_file.write_line("misc_config config_audio_mute: " + to_string_dec_int(config_audio_mute()));
|
||||
pmem_dump_file.write_line("misc_config config_speaker_disable: " + to_string_dec_int(config_speaker_disable()));
|
||||
pmem_dump_file.write_line("ui_config config_disable_external_tcxo: " + to_string_dec_uint(config_disable_external_tcxo()));
|
||||
|
||||
// receiver_model
|
||||
pmem_dump_file.write_line("\n[Receiver Model]");
|
||||
pmem_dump_file.write_line("target_frequency: " + to_string_dec_uint(receiver_model.target_frequency()));
|
||||
|
||||
@@ -173,6 +173,7 @@ void set_stealth_mode(const bool v);
|
||||
uint8_t config_cpld();
|
||||
void set_config_cpld(uint8_t i);
|
||||
|
||||
bool config_disable_external_tcxo();
|
||||
bool config_splash();
|
||||
bool config_converter();
|
||||
bool config_updown_converter();
|
||||
@@ -193,6 +194,7 @@ void set_gui_return_icon(bool v);
|
||||
void set_load_app_settings(bool v);
|
||||
void set_save_app_settings(bool v);
|
||||
void set_show_bigger_qr_code(bool v);
|
||||
void set_config_disable_external_tcxo(bool v);
|
||||
void set_config_splash(bool v);
|
||||
bool config_converter();
|
||||
bool config_updown_converter();
|
||||
@@ -218,6 +220,11 @@ void set_disable_touchscreen(bool v);
|
||||
uint8_t config_encoder_dial_sensitivity();
|
||||
void set_encoder_dial_sensitivity(uint8_t v);
|
||||
|
||||
#define CONFIG_MODE_GUARD_VALUE 2001
|
||||
#define CONFIG_MODE_NORMAL_VALUE 1999
|
||||
uint32_t config_mode_storage();
|
||||
void set_config_mode_storage(uint32_t v);
|
||||
|
||||
uint32_t pocsag_last_address();
|
||||
void set_pocsag_last_address(uint32_t address);
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'};
|
||||
constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'};
|
||||
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
|
||||
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
|
||||
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};
|
||||
constexpr image_tag_t image_tag_sonde{'P', 'S', 'O', 'N'};
|
||||
constexpr image_tag_t image_tag_tpms{'P', 'T', 'P', 'M'};
|
||||
constexpr image_tag_t image_tag_wfm_audio{'P', 'W', 'F', 'M'};
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
namespace ui {
|
||||
|
||||
// CGA palette
|
||||
// Index into this table should match STR_COLOR_ escape string in ui.hpp
|
||||
Color term_colors[16] = {
|
||||
Color::black(),
|
||||
Color::dark_blue(),
|
||||
|
||||
@@ -26,6 +26,25 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
// Escape sequences for colored text; second character is index into term_colors[]
|
||||
#define STR_COLOR_BLACK "\x1B\x00"
|
||||
#define STR_COLOR_DARK_BLUE "\x1B\x01"
|
||||
#define STR_COLOR_DARK_GREEN "\x1B\x02"
|
||||
#define STR_COLOR_DARK_CYAN "\x1B\x03"
|
||||
#define STR_COLOR_DARK_RED "\x1B\x04"
|
||||
#define STR_COLOR_DARK_MAGENTA "\x1B\x05"
|
||||
#define STR_COLOR_DARK_YELLOW "\x1B\x06"
|
||||
#define STR_COLOR_LIGHT_GREY "\x1B\x07"
|
||||
#define STR_COLOR_DARK_GREY "\x1B\x08"
|
||||
#define STR_COLOR_BLUE "\x1B\x09"
|
||||
#define STR_COLOR_GREEN "\x1B\x0A"
|
||||
#define STR_COLOR_CYAN "\x1B\x0B"
|
||||
#define STR_COLOR_RED "\x1B\x0C"
|
||||
#define STR_COLOR_MAGENTA "\x1B\x0D"
|
||||
#define STR_COLOR_YELLOW "\x1B\x0E"
|
||||
#define STR_COLOR_WHITE "\x1B\x0F"
|
||||
#define STR_COLOR_FOREGROUND "\x1B\x10"
|
||||
|
||||
#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0)
|
||||
|
||||
using Coord = int16_t;
|
||||
|
||||
@@ -57,8 +57,8 @@ int Painter::draw_string(
|
||||
|
||||
for (auto c : text) {
|
||||
if (escape) {
|
||||
if (c <= 15)
|
||||
pen = term_colors[c & 15];
|
||||
if (c < std::size(term_colors))
|
||||
pen = term_colors[(uint8_t)c];
|
||||
else
|
||||
pen = foreground;
|
||||
escape = false;
|
||||
|
||||
@@ -617,7 +617,7 @@ void Console::write(std::string message) {
|
||||
|
||||
for (const auto c : message) {
|
||||
if (escape) {
|
||||
if (c <= 15)
|
||||
if (c < std::size(term_colors))
|
||||
pen_color = term_colors[(uint8_t)c];
|
||||
else
|
||||
pen_color = s.foreground;
|
||||
|
||||
@@ -50,8 +50,46 @@ TEST_CASE("ifft successfully calculates dc on zero frequency") {
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
TEST_CASE("ifft successfully calculates sine of quarter the sample rate") {
|
||||
uint32_t fft_width = 8;
|
||||
complex16_t* v = new complex16_t[fft_width];
|
||||
complex16_t* tmp = new complex16_t[fft_width];
|
||||
|
||||
v[0] = {0, 0};
|
||||
v[1] = {0, 0};
|
||||
v[2] = {1024, 0}; // sample rate /4 bin
|
||||
v[3] = {0, 0};
|
||||
v[4] = {0, 0};
|
||||
v[5] = {0, 0};
|
||||
v[6] = {0, 0};
|
||||
v[7] = {0, 0};
|
||||
|
||||
ifft<complex16_t>(v, fft_width, tmp);
|
||||
|
||||
CHECK(v[0].real() == 1024);
|
||||
CHECK(v[1].real() == 0);
|
||||
CHECK(v[2].real() == -1024);
|
||||
CHECK(v[3].real() == 0);
|
||||
CHECK(v[4].real() == 1024);
|
||||
CHECK(v[5].real() == 0);
|
||||
CHECK(v[6].real() == -1024);
|
||||
CHECK(v[7].real() == 0);
|
||||
|
||||
CHECK(v[0].imag() == 0);
|
||||
CHECK(v[1].imag() == 1024);
|
||||
CHECK(v[2].imag() == 0);
|
||||
CHECK(v[3].imag() == -1024);
|
||||
CHECK(v[4].imag() == 0);
|
||||
CHECK(v[5].imag() == 1024);
|
||||
CHECK(v[6].imag() == 0);
|
||||
CHECK(v[7].imag() == -1024);
|
||||
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
TEST_CASE("ifft successfully calculates pure sine of half the sample rate") {
|
||||
@@ -82,6 +120,6 @@ TEST_CASE("ifft successfully calculates pure sine of half the sample rate") {
|
||||
for (uint32_t i = 0; i < fft_width; i++)
|
||||
CHECK(v[i].imag() == 0);
|
||||
|
||||
free(v);
|
||||
free(tmp);
|
||||
delete[] v;
|
||||
delete[] tmp;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user