diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 49c9c35fc..c5fdd1dd5 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -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)}; diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index c642cc1c7..c95b612c3 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -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(); diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index a32bdffbc..f7789d7b8 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -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 ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 0f22bd8bd..946086db7 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -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 } diff --git a/firmware/application/external/flex/main.cpp b/firmware/application/external/flex/main.cpp new file mode 100644 index 000000000..64ed2a07b --- /dev/null +++ b/firmware/application/external/flex/main.cpp @@ -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(); +} +} // 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 +}; +} \ No newline at end of file diff --git a/firmware/application/external/flex_rx/main.cpp b/firmware/application/external/flex_rx/main.cpp new file mode 100644 index 000000000..6d6faa7dc --- /dev/null +++ b/firmware/application/external/flex_rx/main.cpp @@ -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(); +} +} // 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 +}; +} \ No newline at end of file diff --git a/firmware/application/external/flex_rx/ui_flex_rx.cpp b/firmware/application/external/flex_rx/ui_flex_rx.cpp new file mode 100644 index 000000000..8bbe8c6bd --- /dev/null +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -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 \ No newline at end of file diff --git a/firmware/application/external/flex_rx/ui_flex_rx.hpp b/firmware/application/external/flex_rx/ui_flex_rx.hpp new file mode 100644 index 000000000..f3d6f7d42 --- /dev/null +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -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 +#include + +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(p); + this->on_packet(&message); + }}; + + MessageHandlerRegistration message_handler_stats{ + Message::ID::FlexStats, + [this](const Message* const p) { + const auto message = *static_cast(p); + this->on_stats(&message); + }}; + + MessageHandlerRegistration message_handler_debug{ + Message::ID::FlexDebug, + [this](const Message* const p) { + const auto message = *static_cast(p); + this->on_debug(&message); + }}; + + // Config + void update_freq(rf::Frequency f); +}; + +} // namespace ui::external_app::flex_rx + +#endif /*__UI_FLEX_RX_H__*/ \ No newline at end of file diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 9ac499ebc..9c8cb2bed 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -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 diff --git a/firmware/baseband/proc_flex.cpp b/firmware/baseband/proc_flex.cpp new file mode 100644 index 000000000..d44588e5d --- /dev/null +++ b/firmware/baseband/proc_flex.cpp @@ -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 +#include +#include // 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()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_flex.hpp b/firmware/baseband/proc_flex.hpp new file mode 100644 index 000000000..3de318810 --- /dev/null +++ b/firmware/baseband/proc_flex.hpp @@ -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 +#include + +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 dst{}; + const buffer_c16_t dst_buffer{dst.data(), dst.size()}; + + std::array 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__*/ diff --git a/firmware/common/flex_defs.hpp b/firmware/common/flex_defs.hpp new file mode 100644 index 000000000..b1ff63ebd --- /dev/null +++ b/firmware/common/flex_defs.hpp @@ -0,0 +1,34 @@ +#ifndef __FLEX_DEFS_H__ +#define __FLEX_DEFS_H__ + +#include +#include +#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__*/ diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 74692613d..1301c27a2 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -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__*/ diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index a4b94215c..1b5591e92 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -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'};