FLEX pager app (#2883)

* Add FLEX pager support

- Introduced a new FLEX configuration function in baseband_api.
- Added FLEX application view and associated UI elements.
- Implemented FLEX processing logic in proc_flex, including demodulation and message handling.
- Updated CMakeLists to include new FLEX source files and headers.
- Enhanced message system to support FLEX-specific messages and statistics.

This commit lays the groundwork for FLEX pager functionality, allowing for the reception and processing of FLEX messages.

* Fixed baseband and moved app to external with some other fixes.

* Format code

---------

Co-authored-by: RocketGod <57732082+RocketGod-git@users.noreply.github.com>
This commit is contained in:
Tim Elfelt
2025-12-12 10:43:51 -06:00
committed by GitHub
parent 4129d57c09
commit 106e56abc3
14 changed files with 1425 additions and 0 deletions
+5
View File
@@ -328,6 +328,11 @@ void set_noaaapt_config() {
send_message(&message);
}
void set_flex_config() {
const FlexConfigureMessage message{};
send_message(&message);
}
void set_siggen_tone(const uint32_t tone) {
const SigGenToneMessage message{
TONES_F2D(tone, TONES_SAMPLERATE)};
+1
View File
@@ -102,6 +102,7 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
void request_roger_beep();
void request_rssi_beep();
+5
View File
@@ -264,6 +264,10 @@ set(EXTCPPSRC
#adult_toys_controller 144 bytes
external/adult_toys_controller/main.cpp
external/adult_toys_controller/ui_adult_toys_controller.cpp
#flex_rx
external/flex_rx/main.cpp
external/flex_rx/ui_flex_rx.cpp
)
set(EXTAPPLIST
@@ -330,4 +334,5 @@ set(EXTAPPLIST
bht_tx
morse_practice
adult_toys_controller
flex_rx
)
+6
View File
@@ -86,6 +86,7 @@ MEMORY
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
}
@@ -471,5 +472,10 @@ SECTIONS
*(*ui*external_app*adult_toys_controller*);
} > ram_external_app_adult_toys_controller
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2025 timelf123
* with barely any help from RocketGod but I exist.
*
* 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 "ui.hpp"
#include "ui_flex_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::flex_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<FlexAppView>();
}
} // namespace ui::external_app::flex_rx
extern "C" {
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FLEX RX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0x02,
0x40,
0xFA,
0x5F,
0x02,
0x40,
0xF2,
0x4F,
0x02,
0x40,
0xE2,
0x47,
0x02,
0x40,
0xC2,
0x43,
0x02,
0x40,
0x82,
0x41,
0x02,
0x40,
0xFE,
0x7F,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::cyan().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 HTotoo
*
* 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 "ui.hpp"
#include "ui_flex_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::flex_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<FlexAppView>();
}
} // namespace ui::external_app::flex_rx
extern "C" {
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FLEX RX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0x02,
0x40,
0xFA,
0x5F,
0x02,
0x40,
0xF2,
0x4F,
0x02,
0x40,
0xE2,
0x47,
0x02,
0x40,
0xC2,
0x43,
0x02,
0x40,
0x82,
0x41,
0x02,
0x40,
0xFE,
0x7F,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+74
View File
@@ -0,0 +1,74 @@
#include "ui_flex_rx.hpp"
#include "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "memory_map.hpp"
#include "usb_serial_asyncmsg.hpp"
using namespace portapack;
namespace ui::external_app::flex_rx {
FlexAppView::FlexAppView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&field_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&console});
field_frequency.set_value(receiver_model.target_frequency());
field_frequency.updated = [this](rf::Frequency f) {
update_freq(f);
};
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
receiver_model.set_squelch_level(0);
baseband::set_flex_config();
}
FlexAppView::~FlexAppView() {
receiver_model.disable();
baseband::shutdown();
}
void FlexAppView::focus() {
field_frequency.focus();
}
void FlexAppView::update_freq(rf::Frequency f) {
receiver_model.set_target_frequency(f);
}
void FlexAppView::on_packet(const FlexPacketMessage* message) {
std::string text = "";
text += "FLEX ";
text += to_string_dec_uint(message->packet.bitrate);
text += " ";
text += to_string_dec_uint(message->packet.capcode);
text += ": ";
text += message->packet.message;
console.writeln(text);
}
void FlexAppView::on_stats(const FlexStatsMessage* /* message */) {
}
void FlexAppView::on_debug(const FlexDebugMessage* message) {
std::string text = "DBG: ";
text += message->text;
text += " " + to_string_hex(message->val1, 8);
text += " " + to_string_hex(message->val2, 8);
console.writeln(text);
}
} // namespace ui::external_app::flex_rx
+88
View File
@@ -0,0 +1,88 @@
#ifndef __UI_FLEX_RX_H__
#define __UI_FLEX_RX_H__
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_rssi.hpp"
#include "ui_spectrum.hpp"
#include "ui_record_view.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "freqman_db.hpp"
#include <string>
#include <vector>
namespace ui::external_app::flex_rx {
class FlexAppView : public View {
public:
FlexAppView(NavigationView& nav);
~FlexAppView();
void focus() override;
std::string title() const override { return "FLEX RX"; };
private:
NavigationView& nav_;
RxRadioState radio_state_{
929612500 /* frequency - common pager freq in some regions, or 931M */,
max283x::filter::bandwidth_minimum /* bandwidth */,
3072000 /* sampling rate */
};
// UI Elements - Row 0
RxFrequencyField field_frequency{
{0 * 8, 0 * 16},
nav_};
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, 0, 6 * 8, 4}};
// Console - starts at row 1, extends to bottom of screen
Console console{
{0, 1 * 16, screen_width, screen_height - 1 * 16}};
// Logic
void on_packet(const FlexPacketMessage* message);
void on_stats(const FlexStatsMessage* message);
void on_debug(const FlexDebugMessage* message);
// Message Handlers
MessageHandlerRegistration message_handler_packet{
Message::ID::FlexPacket,
[this](const Message* const p) {
const auto message = *static_cast<const FlexPacketMessage*>(p);
this->on_packet(&message);
}};
MessageHandlerRegistration message_handler_stats{
Message::ID::FlexStats,
[this](const Message* const p) {
const auto message = *static_cast<const FlexStatsMessage*>(p);
this->on_stats(&message);
}};
MessageHandlerRegistration message_handler_debug{
Message::ID::FlexDebug,
[this](const Message* const p) {
const auto message = *static_cast<const FlexDebugMessage*>(p);
this->on_debug(&message);
}};
// Config
void update_freq(rf::Frequency f);
};
} // namespace ui::external_app::flex_rx
#endif /*__UI_FLEX_RX_H__*/
+7
View File
@@ -556,6 +556,13 @@ DeclareTargets(PUSB sd_over_usb)
set(add_to_firmware FALSE)
set(MODE_FLAGS "-O3")
### FLEX RX
set(MODE_CPPSRC
proc_flex.cpp
)
DeclareTargets(PFLX flex)
### ACARS RX
set(MODE_CPPSRC
+810
View File
@@ -0,0 +1,810 @@
#include "proc_flex.hpp"
#include "event_m4.hpp"
#include "audio_dma.hpp"
#include "pocsag.hpp"
#include "dsp_fir_taps.hpp"
#include "portapack_shared_memory.hpp"
#include <cmath>
#include <cstring>
#include <cstdio> // for snprintf
// Constants from demod_flex.c
#define FREQ_SAMP 24000 // Our sample rate
#define DC_OFFSET_FILTER 0.010
#define PHASE_LOCKED_RATE 0.045
#define PHASE_UNLOCKED_RATE 0.050
#define LOCK_LEN 24
#define IDLE_THRESHOLD 0
#define DEMOD_TIMEOUT 100
#define FLEX_SYNC_MARKER 0xA6C6AAAAul
#define SLICE_THRESHOLD 0.667
// Implement EccContainer here to avoid linking pocsag.cpp which pulls in app headers
using namespace pocsag;
EccContainer::EccContainer() {
setup_ecc();
}
void EccContainer::setup_ecc() {
unsigned int srr = 0x3b4;
unsigned int i, n, j, k;
for (i = 0; i <= 20; i++) {
ecs[i] = srr;
if ((srr & 0x01) != 0)
srr = (srr >> 1) ^ 0x3B4;
else
srr = srr >> 1;
}
for (i = 0; i < 1024; i++) bch[i] = 0;
for (n = 0; n <= 20; n++) {
for (i = 0; i <= 20; i++) {
j = (i << 5) + n;
k = ecs[n] ^ ecs[i];
bch[k] = j + 0x2000;
}
}
for (n = 0; n <= 20; n++) {
k = ecs[n];
j = n + (0x1f << 5);
bch[k] = j + 0x1000;
}
for (n = 0; n <= 20; n++) {
for (i = 0; i < 10; i++) {
k = ecs[n] ^ (1 << i);
j = n + (0x1f << 5);
bch[k] = j + 0x2000;
}
}
for (n = 0; n < 10; n++) {
k = 1 << n;
bch[k] = 0x3ff + 0x1000;
}
for (n = 0; n < 10; n++) {
for (i = 0; i < 10; i++) {
if (i != n) {
k = (1 << n) ^ (1 << i);
bch[k] = 0x3ff + 0x2000;
}
}
}
}
int EccContainer::error_correct(uint32_t& val) {
int i, synd, errl, acc, pari, ecc, b1, b2;
errl = 0;
pari = 0;
ecc = 0;
for (i = 31; i >= 11; --i) {
if (val & (1 << i)) {
ecc = ecc ^ ecs[31 - i];
pari = pari ^ 0x01;
}
}
acc = 0;
for (i = 10; i >= 1; --i) {
acc = acc << 1;
if (val & (1 << i)) {
acc = acc ^ 0x01;
}
}
synd = ecc ^ acc;
errl = 0;
if (synd != 0) {
if (bch[synd] != 0) {
b1 = bch[synd] & 0x1f;
b2 = bch[synd] >> 5;
b2 = b2 & 0x1f;
if (b2 != 0x1f) {
val ^= 0x01 << (31 - b2);
ecc = ecc ^ ecs[b2];
}
if (b1 != 0x1f) {
val ^= 0x01 << (31 - b1);
ecc = ecc ^ ecs[b1];
}
errl = bch[synd] >> 12;
} else {
errl = 3;
}
if (errl == 1) pari = pari ^ 0x01;
}
if (errl == 4) errl = 3;
return errl;
}
namespace {
// Helpers
unsigned int popcount(unsigned int n) {
// Simple popcount for 32-bit integer
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
return (((n + (n >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}
uint32_t bit_reverse_32(uint32_t x) {
x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1);
x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2);
x = ((x >> 4) & 0x0F0F0F0F) | ((x & 0x0F0F0F0F) << 4);
x = ((x >> 8) & 0x00FF00FF) | ((x & 0x00FF00FF) << 8);
x = (x >> 16) | (x << 16);
return x;
}
} // namespace
void FlexProcessor::send_debug(const char* text, uint32_t v1, uint32_t v2) {
if (shared_memory.application_queue.is_empty()) return;
FlexDebugMessage message(v1, v2, text);
shared_memory.application_queue.push(message);
}
void FlexProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Heartbeat debug every ~1 second (24000Hz / 4096 buffer size * ~6)
static int debug_count = 0;
debug_count++;
if (debug_count > 1000) {
send_debug("Running", 0, 0);
debug_count = 0;
}
// Decimate and demodulate: 3.072MHz -> 24kHz
auto decim_0_out = decim_0_iq.execute(buffer, dst_buffer);
auto decim_1_out = decim_1_iq.execute(decim_0_out, dst_buffer);
auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
process_audio(audio);
}
void FlexProcessor::process_audio(const buffer_f32_t& audio) {
for (size_t i = 0; i < audio.count; ++i) {
flex_demodulate(audio.p[i]);
}
}
void FlexProcessor::flex_demodulate(double sample) {
if (build_symbol(sample) == 1) {
demodulator.nonconsec = 0;
demodulator.symbol_count++;
// modulation.symbol_rate = ... // Unused in main logic usually, just stats
/*Determine the modal symbol*/
int j;
int decmax = 0;
int modal_symbol = 0;
for (j = 0; j < 4; j++) {
if (demodulator.symcount[j] > decmax) {
modal_symbol = j;
decmax = demodulator.symcount[j];
}
}
demodulator.symcount[0] = 0;
demodulator.symcount[1] = 0;
demodulator.symcount[2] = 0;
demodulator.symcount[3] = 0;
if (demodulator.locked) {
/*Process the symbol*/
flex_sym(modal_symbol);
} else {
/*Check for lock pattern*/
/*Shift symbols into buffer, symbols are converted so that the max and min symbols map to 1 and 2 i.e each contain a single 1 */
demodulator.lock_buf = (demodulator.lock_buf << 2) | (modal_symbol ^ 0x1);
uint64_t lock_pattern = demodulator.lock_buf ^ 0x6666666666666666ull;
uint64_t lock_mask = (1ull << (2 * LOCK_LEN)) - 1;
if ((lock_pattern & lock_mask) == 0 || ((~lock_pattern) & lock_mask) == 0) {
demodulator.locked = 1;
demodulator.lock_buf = 0;
demodulator.symbol_count = 0;
demodulator.sample_count = 0;
}
}
/*Time out after X periods with no zero crossing*/
demodulator.timeout++;
if (demodulator.timeout > DEMOD_TIMEOUT) {
demodulator.locked = 0;
}
}
}
int FlexProcessor::build_symbol(double sample) {
const int64_t phase_max = 100 * demodulator.sample_freq;
const int64_t phase_rate = phase_max * demodulator.baud / demodulator.sample_freq;
const double phasepercent = 100.0 * demodulator.phase / phase_max;
demodulator.sample_count++;
/*Remove DC offset (FIR filter)*/
if (state.Current == flex::State::SYNC1) {
modulation.zero = (modulation.zero * (FREQ_SAMP * DC_OFFSET_FILTER) + sample) / ((FREQ_SAMP * DC_OFFSET_FILTER) + 1);
}
sample -= modulation.zero;
if (demodulator.locked) {
if (state.Current == flex::State::SYNC1) {
demodulator.envelope_sum += std::abs(sample);
demodulator.envelope_count++;
modulation.envelope = demodulator.envelope_sum / demodulator.envelope_count;
}
} else {
modulation.envelope = 0;
demodulator.envelope_sum = 0;
demodulator.envelope_count = 0;
demodulator.baud = 1600;
demodulator.timeout = 0;
demodulator.nonconsec = 0;
state.Current = flex::State::SYNC1;
}
/* MID 80% SYMBOL PERIOD */
if (phasepercent > 10 && phasepercent < 90) {
if (sample > 0) {
if (sample > modulation.envelope * SLICE_THRESHOLD)
demodulator.symcount[3]++;
else
demodulator.symcount[2]++;
} else {
if (sample < -modulation.envelope * SLICE_THRESHOLD)
demodulator.symcount[0]++;
else
demodulator.symcount[1]++;
}
}
/* ZERO CROSSING */
if ((demodulator.sample_last < 0 && sample >= 0) || (demodulator.sample_last >= 0 && sample < 0)) {
double phase_error = 0.0;
if (phasepercent < 50) {
phase_error = demodulator.phase;
} else {
phase_error = demodulator.phase - phase_max;
}
if (demodulator.locked) {
demodulator.phase -= phase_error * PHASE_LOCKED_RATE;
} else {
demodulator.phase -= phase_error * PHASE_UNLOCKED_RATE;
}
if (phasepercent > 10 && phasepercent < 90) {
demodulator.nonconsec++;
if (demodulator.nonconsec > 20 && demodulator.locked) {
demodulator.locked = 0;
}
} else {
demodulator.nonconsec = 0;
}
demodulator.timeout = 0;
}
demodulator.sample_last = sample;
/* END OF SYMBOL PERIOD */
demodulator.phase += phase_rate;
if (demodulator.phase > phase_max) {
demodulator.phase -= phase_max;
return 1;
} else {
return 0;
}
}
unsigned int FlexProcessor::flex_sync(unsigned char sym) {
int retval = 0;
sync.syncbuf = (sync.syncbuf << 1) | ((sym < 2) ? 1 : 0);
retval = flex_sync_check(sync.syncbuf);
if (retval != 0) {
sync.polarity = 0;
} else {
retval = flex_sync_check(~sync.syncbuf);
if (retval != 0) {
sync.polarity = 1;
}
}
return retval;
}
unsigned int FlexProcessor::flex_sync_check(uint64_t buf) {
// 64-bit FLEX sync code: AAAA:BBBBBBBB:CCCC
unsigned int marker = (buf & 0x0000FFFFFFFF0000ULL) >> 16;
unsigned short codehigh = (buf & 0xFFFF000000000000ULL) >> 48;
unsigned short codelow = ~(buf & 0x000000000000FFFFULL);
int retval = 0;
// Hamming distance check (popcount of XOR)
unsigned int diff_marker = popcount(marker ^ FLEX_SYNC_MARKER);
unsigned int diff_code = popcount(codelow ^ codehigh);
if (diff_marker < 4 && diff_code < 4) {
retval = codehigh;
} else {
retval = 0;
}
return retval;
}
void FlexProcessor::decode_mode(unsigned int sync_code) {
struct FlexModeDef {
int sync;
unsigned int baud;
unsigned int levels;
} flex_modes[] = {
{0x870C, 1600, 2},
{0xB068, 1600, 4},
{0x7B18, 3200, 2},
{0xDEA0, 3200, 4},
{0x4C7C, 3200, 4},
{0, 0, 0}};
for (int i = 0; flex_modes[i].sync != 0; i++) {
unsigned int diff = popcount((unsigned int)flex_modes[i].sync ^ sync_code);
if (diff < 4) {
sync.sync = sync_code;
sync.baud = flex_modes[i].baud;
sync.levels = flex_modes[i].levels;
return;
}
}
// Default
sync.baud = 1600;
sync.levels = 2;
}
void FlexProcessor::read_2fsk(unsigned int sym, uint32_t* dat) {
*dat = (*dat >> 1) | ((sym > 1) ? 0x80000000 : 0);
}
int FlexProcessor::bch_fix_errors(uint32_t* data_to_fix) {
// Reverse bits for EccContainer (POCSAG MSB-first expectation vs FLEX LSB-first in our representation)
uint32_t reversed = bit_reverse_32(*data_to_fix);
int result = ecc.error_correct(reversed);
if (result == 0 || result == 1 || result == 2) {
*data_to_fix = bit_reverse_32(reversed);
}
return result;
}
int FlexProcessor::decode_fiw() {
uint32_t fiw_val = fiw.rawdata;
int decode_error = bch_fix_errors(&fiw_val);
if (decode_error > 2) {
return 1;
}
fiw.checksum = fiw_val & 0xF;
fiw.cycleno = (fiw_val >> 4) & 0xF;
fiw.frameno = (fiw_val >> 8) & 0x7F;
fiw.fix3 = (fiw_val >> 15) & 0x3F;
unsigned int checksum = (fiw_val & 0xF);
checksum += ((fiw_val >> 4) & 0xF);
checksum += ((fiw_val >> 8) & 0xF);
checksum += ((fiw_val >> 12) & 0xF);
checksum += ((fiw_val >> 16) & 0xF);
checksum += ((fiw_val >> 20) & 0x01);
checksum &= 0xF;
if (checksum == 0xF) {
return 0;
} else {
return 1;
}
}
int FlexProcessor::read_data(unsigned char sym) {
int bit_a = (sym > 1);
int bit_b = 0;
if (sync.levels == 4) {
bit_b = (sym == 1) || (sym == 2);
}
if (sync.baud == 1600) {
data.phase_toggle = 0;
}
unsigned int idx = ((data.data_bit_counter >> 5) & 0xFFF8) | (data.data_bit_counter & 0x0007);
if (idx >= 88) return 0; // Boundary check
if (data.phase_toggle == 0) {
data.PhaseA.buf[idx] = (data.PhaseA.buf[idx] >> 1) | (bit_a ? 0x80000000 : 0);
data.PhaseB.buf[idx] = (data.PhaseB.buf[idx] >> 1) | (bit_b ? 0x80000000 : 0);
data.phase_toggle = 1;
if ((data.data_bit_counter & 0xFF) == 0xFF) {
if (data.PhaseA.buf[idx] == 0x00000000 || data.PhaseA.buf[idx] == 0xffffffff) data.PhaseA.idle_count++;
if (data.PhaseB.buf[idx] == 0x00000000 || data.PhaseB.buf[idx] == 0xffffffff) data.PhaseB.idle_count++;
}
} else {
data.PhaseC.buf[idx] = (data.PhaseC.buf[idx] >> 1) | (bit_a ? 0x80000000 : 0);
data.PhaseD.buf[idx] = (data.PhaseD.buf[idx] >> 1) | (bit_b ? 0x80000000 : 0);
data.phase_toggle = 0;
if ((data.data_bit_counter & 0xFF) == 0xFF) {
if (data.PhaseC.buf[idx] == 0x00000000 || data.PhaseC.buf[idx] == 0xffffffff) data.PhaseC.idle_count++;
if (data.PhaseD.buf[idx] == 0x00000000 || data.PhaseD.buf[idx] == 0xffffffff) data.PhaseD.idle_count++;
}
}
if (sync.baud == 1600 || data.phase_toggle == 0) {
data.data_bit_counter++;
}
int idle = 0;
if (sync.baud == 1600) {
if (sync.levels == 2) {
idle = (data.PhaseA.idle_count > IDLE_THRESHOLD);
} else {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseB.idle_count > IDLE_THRESHOLD));
}
} else {
if (sync.levels == 2) {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseC.idle_count > IDLE_THRESHOLD));
} else {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseB.idle_count > IDLE_THRESHOLD) && (data.PhaseC.idle_count > IDLE_THRESHOLD) && (data.PhaseD.idle_count > IDLE_THRESHOLD));
}
}
return idle;
}
void FlexProcessor::flex_sym(unsigned char sym) {
unsigned char sym_rectified;
if (sync.polarity) {
sym_rectified = 3 - sym;
} else {
sym_rectified = sym;
}
switch (state.Current) {
case flex::State::SYNC1: {
unsigned int sync_code = flex_sync(sym);
if (sync_code != 0) {
decode_mode(sync_code);
if (sync.baud != 0 && sync.levels != 0) {
state.Current = flex::State::FIW;
send_debug("SYNC1 Found", sync.baud, sync_code);
} else {
state.Current = flex::State::SYNC1;
}
} else {
state.Current = flex::State::SYNC1;
}
state.fiwcount = 0;
fiw.rawdata = 0;
break;
}
case flex::State::FIW: {
state.fiwcount++;
if (state.fiwcount >= 16) {
read_2fsk(sym_rectified, &fiw.rawdata);
}
if (state.fiwcount == 48) {
if (decode_fiw() == 0) {
state.sync2_count = 0;
demodulator.baud = sync.baud;
state.Current = flex::State::SYNC2;
send_debug("FIW OK", fiw.frameno, fiw.cycleno);
} else {
state.Current = flex::State::SYNC1;
send_debug("FIW Fail", fiw.rawdata, 0);
}
}
break;
}
case flex::State::SYNC2: {
if (++state.sync2_count == sync.baud * 25 / 1000) {
state.data_count = 0;
// Clear phase data
for (int i = 0; i < 88; i++) {
data.PhaseA.buf[i] = 0;
data.PhaseB.buf[i] = 0;
data.PhaseC.buf[i] = 0;
data.PhaseD.buf[i] = 0;
}
data.PhaseA.idle_count = 0;
data.PhaseB.idle_count = 0;
data.PhaseC.idle_count = 0;
data.PhaseD.idle_count = 0;
data.phase_toggle = 0;
data.data_bit_counter = 0;
state.Current = flex::State::DATA;
}
break;
}
case flex::State::DATA: {
int idle = read_data(sym_rectified);
if (++state.data_count == sync.baud * 1760 / 1000 || idle) {
decode_data();
demodulator.baud = 1600;
state.Current = flex::State::SYNC1;
state.data_count = 0;
}
break;
}
}
}
void FlexProcessor::decode_data() {
if (sync.baud == 1600) {
if (sync.levels == 2) {
decode_phase('A');
} else {
decode_phase('A');
decode_phase('B');
}
} else {
if (sync.levels == 2) {
decode_phase('A');
decode_phase('C');
} else {
decode_phase('A');
decode_phase('B');
decode_phase('C');
decode_phase('D');
}
}
}
void FlexProcessor::decode_phase(char PhaseNo) {
uint32_t* phaseptr = nullptr;
switch (PhaseNo) {
case 'A':
phaseptr = data.PhaseA.buf;
break;
case 'B':
phaseptr = data.PhaseB.buf;
break;
case 'C':
phaseptr = data.PhaseC.buf;
break;
case 'D':
phaseptr = data.PhaseD.buf;
break;
default:
return;
}
for (int i = 0; i < 88; i++) {
int decode_error = bch_fix_errors(&phaseptr[i]);
if (decode_error > 2) return;
phaseptr[i] &= 0x001FFFFF; // Extract message bits
}
uint32_t biw = phaseptr[0];
if (biw == 0 || biw == 0x001FFFFF) return;
int voffset = (biw >> 10) & 0x3f;
int aoffset = ((biw >> 8) & 0x03) + 1;
for (int i = aoffset; i < voffset; i++) {
int j = voffset + i - aoffset;
if (phaseptr[i] == 0x00000000 || phaseptr[i] == 0x001FFFFF) continue;
parse_capcode(phaseptr[i]);
if (decode.long_address) continue; // Skip long addresses for now
if (decode.capcode > 4297068542ll || decode.capcode < 0) continue;
uint32_t viw = phaseptr[j];
int type_val = (viw >> 4) & 0x07;
switch (type_val) {
case 0:
decode.type = flex::PageType::SECURE;
break;
case 1:
decode.type = flex::PageType::SHORT_INSTRUCTION;
break;
case 2:
decode.type = flex::PageType::TONE;
break;
case 3:
decode.type = flex::PageType::STANDARD_NUMERIC;
break;
case 4:
decode.type = flex::PageType::SPECIAL_NUMERIC;
break;
case 5:
decode.type = flex::PageType::ALPHANUMERIC;
break;
case 6:
decode.type = flex::PageType::BINARY;
break;
case 7:
decode.type = flex::PageType::NUMBERED_NUMERIC;
break;
}
int mw1 = (viw >> 7) & 0x7F;
int len = (viw >> 14) & 0x7F;
int mw2 = mw1 + (len - 1);
if (mw1 == 0 && mw2 == 0) continue;
if (decode.type == flex::PageType::TONE) mw1 = mw2 = 0;
if (decode.type == flex::PageType::ALPHANUMERIC || decode.type == flex::PageType::SECURE) {
if (mw1 > 87 || mw2 > 87) continue;
parse_alphanumeric(phaseptr, PhaseNo, mw1, mw2, 0);
} else if (decode.type == flex::PageType::STANDARD_NUMERIC || decode.type == flex::PageType::SPECIAL_NUMERIC || decode.type == flex::PageType::NUMBERED_NUMERIC) {
parse_numeric(phaseptr, PhaseNo, j);
} else if (decode.type == flex::PageType::TONE) {
parse_tone_only(phaseptr, PhaseNo, j);
} else {
// Unknown or unsupported
}
}
}
void FlexProcessor::parse_capcode(uint32_t aw1) {
decode.long_address = (aw1 < 0x008001L) || (aw1 > 0x1E0000L) || (aw1 > 0x1E7FFEL);
decode.capcode = aw1 - 0x8000;
}
void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, char, int mw1, int mw2, int) {
char message[128] = {0}; // Fixed buffer for message
int currentChar = 0;
// int frag = (phaseptr[mw1] >> 11) & 0x03;
// int cont = (phaseptr[mw1] >> 0x0A) & 0x01;
// Helper logic for fragmentation (ignored for basic display)
mw1++;
for (int i = mw1; i <= mw2; i++) {
unsigned int dw = phaseptr[i];
unsigned char ch;
// Extract chars (7-bit ASCII)
// If i > mw1 (not first word) or fragment check (simplified here)
if (i > mw1) {
ch = dw & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
}
ch = (dw >> 7) & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
ch = (dw >> 14) & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
}
message[currentChar] = '\0';
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0; // TODO extract function if available
packet.type = 5; // ALPHANUMERIC
packet.status = 0; // OK
memcpy(packet.message, message, currentChar + 1);
send_packet(packet);
}
void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) {
// Simplified numeric parsing
char message[128] = {0};
const char flex_bcd[] = "0123456789 U -][";
int w1 = phaseptr[j] >> 7;
int w2 = w1 >> 7;
w1 = w1 & 0x7f;
w2 = (w2 & 0x07) + w1;
int dw;
// Handle short vs long logic if needed (simplified)
dw = phaseptr[w1];
w1++;
w2++;
unsigned char digit = 0;
int count = 4; // Standard numeric skip
if (decode.type == flex::PageType::NUMBERED_NUMERIC)
count += 10;
else
count += 2;
int idx = 0;
for (int i = w1; i <= w2; i++) {
for (int k = 0; k < 21; k++) {
digit = (digit >> 1) & 0x0F;
if (dw & 0x01) digit ^= 0x08;
dw >>= 1;
if (--count == 0) {
if (digit != 0x0C && idx < 127) {
message[idx++] = flex_bcd[digit];
}
count = 4;
}
}
dw = phaseptr[i];
}
message[idx] = '\0';
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0;
packet.type = 3; // NUMERIC
packet.status = 0;
memcpy(packet.message, message, idx + 1);
send_packet(packet);
}
void FlexProcessor::parse_tone_only(uint32_t*, char, int) {
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0;
packet.type = 2; // TONE
packet.status = 0;
snprintf(packet.message, sizeof(packet.message), "Tone Only");
send_packet(packet);
}
void FlexProcessor::parse_unknown(uint32_t*, char, int, int) {
// Ignored
}
void FlexProcessor::on_message(const Message* const message) {
if (message->id == Message::ID::FlexConfigure) {
configure();
}
}
void FlexProcessor::configure() {
decim_0_iq.configure(taps_11k0_decim_0.taps);
decim_1_iq.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2); // Decim 2 -> 24kHz output
demod.configure(24000, 4800);
demodulator.sample_freq = 24000;
configured = true;
send_debug("Configured", 0, 0);
}
void FlexProcessor::send_packet(const flex::FlexPacket& packet) {
FlexPacketMessage message(packet);
shared_memory.application_queue.push(message);
}
void FlexProcessor::send_stats() {
// Stats
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<FlexProcessor>()};
event_dispatcher.run();
return 0;
}
+177
View File
@@ -0,0 +1,177 @@
#ifndef __PROC_FLEX_H__
#define __PROC_FLEX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "message.hpp"
#include "flex_defs.hpp"
#include "pocsag.hpp" // For EccContainer
#include <cstdint>
#include <array>
namespace flex {
enum class PageType {
SECURE,
SHORT_INSTRUCTION,
TONE,
STANDARD_NUMERIC,
SPECIAL_NUMERIC,
ALPHANUMERIC,
BINARY,
NUMBERED_NUMERIC
};
enum class State {
SYNC1,
FIW,
SYNC2,
DATA
};
struct FlexDemodParams {
unsigned int sample_freq = 24000;
double sample_last = 0.0;
int locked = 0;
int phase = 0;
unsigned int sample_count = 0;
unsigned int symbol_count = 0;
double envelope_sum = 0.0;
int envelope_count = 0;
uint64_t lock_buf = 0;
int symcount[4] = {0};
int timeout = 0;
int nonconsec = 0;
unsigned int baud = 1600;
};
struct FlexGroupHandler {
int64_t GroupCodes[17][100]; // Reduced size from 1000 to save RAM
int GroupCycle[17];
int GroupFrame[17];
};
struct FlexModulation {
double symbol_rate = 0.0;
double envelope = 0.0;
double zero = 0.0;
};
struct FlexStateInfo {
unsigned int sync2_count = 0;
unsigned int data_count = 0;
unsigned int fiwcount = 0;
State Current = State::SYNC1;
State Previous = State::SYNC1;
};
struct FlexSync {
unsigned int sync = 0;
unsigned int baud = 0;
unsigned int levels = 0;
unsigned int polarity = 0;
uint64_t syncbuf = 0;
};
struct FlexFIW {
uint32_t rawdata = 0;
unsigned int checksum = 0;
unsigned int cycleno = 0;
unsigned int frameno = 0;
unsigned int fix3 = 0;
};
struct FlexPhase {
uint32_t buf[88] = {0};
int idle_count = 0;
};
struct FlexData {
int phase_toggle = 0;
unsigned int data_bit_counter = 0;
FlexPhase PhaseA;
FlexPhase PhaseB;
FlexPhase PhaseC;
FlexPhase PhaseD;
};
struct FlexDecode {
PageType type = PageType::ALPHANUMERIC;
int long_address = 0;
int64_t capcode = 0;
};
} // namespace flex
class FlexProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
bool configured{false};
// DSP components
// 3.072MHz -> 24kHz (Decim 128)
// decim_0: 8, decim_1: 8, channel: 2. Total 128.
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_iq{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1_iq{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
// Buffers
std::array<complex16_t, 256> dst{};
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
std::array<float, 16> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
// Flex State
flex::FlexDemodParams demodulator{};
flex::FlexModulation modulation{};
flex::FlexStateInfo state{};
flex::FlexSync sync{};
flex::FlexFIW fiw{};
flex::FlexData data{};
flex::FlexDecode decode{};
flex::FlexGroupHandler group_handler{};
pocsag::EccContainer ecc{};
// Methods
void configure();
void process_audio(const buffer_f32_t& audio);
// Internal Flex logic
int build_symbol(double sample);
void flex_demodulate(double sample);
void flex_sym(unsigned char sym);
unsigned int flex_sync_check(uint64_t buf);
unsigned int flex_sync(unsigned char sym);
void decode_mode(unsigned int sync_code);
void read_2fsk(unsigned int sym, uint32_t* dat); // Changed to uint32_t*
int decode_fiw();
int read_data(unsigned char sym);
void decode_data();
void decode_phase(char PhaseNo);
int bch_fix_errors(uint32_t* data_to_fix);
// Parsing
void parse_capcode(uint32_t aw1);
void parse_alphanumeric(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2, int flex_groupmessage);
void parse_numeric(uint32_t* phaseptr, char PhaseNo, int j);
void parse_tone_only(uint32_t* phaseptr, char PhaseNo, int j);
void parse_unknown(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2);
void send_packet(const flex::FlexPacket& packet);
void send_stats();
void send_debug(const char* text, uint32_t v1, uint32_t v2);
// Threads
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
};
#endif /*__PROC_FLEX_H__*/
+34
View File
@@ -0,0 +1,34 @@
#ifndef __FLEX_DEFS_H__
#define __FLEX_DEFS_H__
#include <cstdint>
#include <array>
#include "baseband.hpp"
namespace flex {
enum class FlexMode : uint8_t {
FLEX_1600_2FSK,
FLEX_3200_2FSK,
FLEX_3200_4FSK,
FLEX_6400_4FSK
};
struct FlexStats {
uint32_t symbols_processed;
uint32_t total_frames;
uint32_t correct_frames;
};
struct FlexPacket {
uint32_t bitrate; // 1600, 3200, 6400
uint32_t capcode;
uint32_t function; // 0-3
uint32_t type; // Message type (e.g. ALN, NUM, etc - could use enum)
char message[128]; // Decoded message text
uint32_t status; // 0=OK, other=Errors
};
} /* namespace flex */
#endif /*__FLEX_DEFS_H__*/
+50
View File
@@ -35,6 +35,7 @@
#include "adsb_frame.hpp"
#include "ert_packet.hpp"
#include "pocsag_packet.hpp"
#include "flex_defs.hpp"
#include "aprs_packet.hpp"
#include "sonde_packet.hpp"
#include "tpms_packet.hpp"
@@ -136,6 +137,10 @@ class Message {
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
FlexPacket = 82,
FlexStats = 83,
FlexConfigure = 84,
FlexDebug = 85,
MAX
};
@@ -1569,4 +1574,49 @@ class NoaaAptRxImageDataMessage : public Message {
uint32_t cnt = 0;
};
class FlexPacketMessage : public Message {
public:
constexpr FlexPacketMessage(const flex::FlexPacket& packet)
: Message{ID::FlexPacket},
packet{packet} {
}
flex::FlexPacket packet;
};
class FlexStatsMessage : public Message {
public:
constexpr FlexStatsMessage(const flex::FlexStats& stats)
: Message{ID::FlexStats},
stats{stats} {
}
flex::FlexStats stats;
};
class FlexConfigureMessage : public Message {
public:
constexpr FlexConfigureMessage()
: Message{ID::FlexConfigure} {
}
};
class FlexDebugMessage : public Message {
public:
constexpr FlexDebugMessage(const uint32_t val1, const uint32_t val2, const char* msg)
: Message{ID::FlexDebug},
val1{val1},
val2{val2},
text{} {
size_t i = 0;
while (i < sizeof(text) - 1 && msg[i] != '\0') {
text[i] = msg[i];
i++;
}
text[i] = '\0';
}
uint32_t val1;
uint32_t val2;
char text[64];
};
#endif /*__MESSAGE_H__*/
+1
View File
@@ -91,6 +91,7 @@ constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
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_flex{'P', 'F', 'L', 'X'};
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'};