From 43a5163b77d0ff832afb905f9625f8ae0e58a611 Mon Sep 17 00:00:00 2001 From: Totoo Date: Tue, 18 Nov 2025 12:07:40 +0100 Subject: [PATCH 01/13] prevent sonde pos to change to 0 when the frame is invalid. fixes #2862 (#2869) --- firmware/application/apps/ui_sonde.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/firmware/application/apps/ui_sonde.cpp b/firmware/application/apps/ui_sonde.cpp index f5aa030ac..ff927076b 100644 --- a/firmware/application/apps/ui_sonde.cpp +++ b/firmware/application/apps/ui_sonde.cpp @@ -174,10 +174,11 @@ void SondeView::on_packet(const sonde::Packet& packet) { gps_info = packet.get_GPS_data(); - geopos.set_altitude(gps_info.alt); - geopos.set_lat(gps_info.lat); - geopos.set_lon(gps_info.lon); - + if (gps_info.lat != 0 && gps_info.lon != 0) { // only update when valid, to prevent flashing + geopos.set_altitude(gps_info.alt); + geopos.set_lat(gps_info.lat); + geopos.set_lon(gps_info.lon); + } if (logger && logging) { logger->on_packet(packet); } From c01597baf2e83add9b7be1c569b2ba07961bebd2 Mon Sep 17 00:00:00 2001 From: Totoo Date: Tue, 18 Nov 2025 14:05:27 +0100 Subject: [PATCH 02/13] Pocsag manual baud option (#2870) * baseband part of manual baud control of pocsag * added manual pocsag baud to ui, to fix #2546 * fix adult toys settings filename --- firmware/application/apps/pocsag_app.cpp | 7 ++++-- firmware/application/apps/pocsag_app.hpp | 11 +++++++++ firmware/application/baseband_api.cpp | 4 ++-- firmware/application/baseband_api.hpp | 2 +- .../ui_adult_toys_controller.hpp | 2 +- firmware/baseband/proc_pocsag2.cpp | 24 +++++++++++++++---- firmware/baseband/proc_pocsag2.hpp | 5 ++-- firmware/common/message.hpp | 5 ++-- 8 files changed, 45 insertions(+), 15 deletions(-) diff --git a/firmware/application/apps/pocsag_app.cpp b/firmware/application/apps/pocsag_app.cpp index c89e37960..f2a122d35 100644 --- a/firmware/application/apps/pocsag_app.cpp +++ b/firmware/application/apps/pocsag_app.cpp @@ -56,6 +56,7 @@ POCSAGSettingsView::POCSAGSettingsView( : settings_{settings} { add_children( {&labels, + &opt_baud_rate, &check_log, &check_log_raw, &check_small_font, @@ -65,6 +66,7 @@ POCSAGSettingsView::POCSAGSettingsView( &field_filter_address, &button_save}); + opt_baud_rate.set_by_value(settings_.baud_rate); check_log.set_value(settings_.enable_logging); check_log_raw.set_value(settings_.enable_raw_log); check_small_font.set_value(settings_.enable_small_font); @@ -81,7 +83,7 @@ POCSAGSettingsView::POCSAGSettingsView( settings_.hide_addr_only = check_hide_addr_only.value(); settings_.filter_mode = opt_filter_mode.selected_index_value(); settings_.filter_address = field_filter_address.to_integer(); - + settings_.baud_rate = opt_baud_rate.selected_index_value(); nav.pop(); }; } @@ -142,7 +144,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav) audio::output::start(); receiver_model.enable(); - baseband::set_pocsag(); + baseband::set_pocsag((int8_t)settings_.baud_rate); } void POCSAGAppView::focus() { @@ -182,6 +184,7 @@ void POCSAGAppView::refresh_ui() { btn_text = "Filter Last"; break; } + baseband::set_pocsag((int8_t)settings_.baud_rate); button_filter_last.set_text(btn_text); } diff --git a/firmware/application/apps/pocsag_app.hpp b/firmware/application/apps/pocsag_app.hpp index 9b5dd4d92..e5527b1f0 100644 --- a/firmware/application/apps/pocsag_app.hpp +++ b/firmware/application/apps/pocsag_app.hpp @@ -126,6 +126,7 @@ struct POCSAGSettings { bool hide_bad_data = false; bool hide_addr_only = false; uint8_t filter_mode = false; + int32_t baud_rate = -1; uint32_t filter_address = 0; }; @@ -139,7 +140,16 @@ class POCSAGSettingsView : public View { private: POCSAGSettings& settings_; + OptionsField opt_baud_rate{ + {8 * 8, 0 * 16}, + 4, + {{"Auto", -1}, + {" 512", 0}, + {"1200", 1}, + {"2400", 2}}}; + Labels labels{ + {{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground}, {{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground}, }; @@ -221,6 +231,7 @@ class POCSAGAppView : public View { {"filter_address"sv, &settings_.filter_address}, {"hide_bad_data"sv, &settings_.hide_bad_data}, {"hide_addr_only"sv, &settings_.hide_addr_only}, + {"baud_rate"sv, &settings_.baud_rate}, }}; void refresh_ui(); diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 2655f37f9..49c9c35fc 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -288,8 +288,8 @@ void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, send_message(&message); } -void set_pocsag() { - const POCSAGConfigureMessage message{}; +void set_pocsag(int8_t baud_config) { + const POCSAGConfigureMessage message{baud_config}; send_message(&message); } diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 84d5f292a..3f2e7d972 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -91,7 +91,7 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t void set_ook_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint8_t repeat, const uint32_t pause_symbols, const uint8_t de_bruijn_length = 0); void kill_ook(); void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint32_t shift, const uint32_t progress_notice); -void set_pocsag(); +void set_pocsag(int8_t baud_config = -1); void set_adsb(); void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed); void set_rds_data(const uint16_t message_length); diff --git a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.hpp b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.hpp index 7a8a087b9..fecb737df 100644 --- a/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.hpp +++ b/firmware/application/external/adult_toys_controller/ui_adult_toys_controller.hpp @@ -104,7 +104,7 @@ class AdultToysView : public ui::View { /*short_ui*/ true}; app_settings::SettingsManager settings_{ - "Adult Toys", app_settings::Mode::TX}; + "tx_adult_toys", app_settings::Mode::TX}; OptionsField options_target{ {UI_POS_X(6), UI_POS_Y(1)}, diff --git a/firmware/baseband/proc_pocsag2.cpp b/firmware/baseband/proc_pocsag2.cpp index 65dd7b096..590a5dda7 100644 --- a/firmware/baseband/proc_pocsag2.cpp +++ b/firmware/baseband/proc_pocsag2.cpp @@ -159,13 +159,27 @@ void BitExtractor::configure(uint32_t sample_rate) { // without needing to know exact transition boundaries. for (auto& rate : known_rates_) rate.sample_interval = sample_rate / (2.0 * rate.baud_rate); + + if (baud_config_ >= 0 && baud_config_ < static_cast(known_rates_.size())) { + current_rate_ = &known_rates_[baud_config_]; + } else { + current_rate_ = nullptr; + } } void BitExtractor::reset() { - current_rate_ = nullptr; - for (auto& rate : known_rates_) rate.reset(); + + if (baud_config_ >= 0 && baud_config_ < static_cast(known_rates_.size())) { + current_rate_ = &known_rates_[baud_config_]; + } else { + current_rate_ = nullptr; + } +} + +void BitExtractor::set_baud_config(int8_t baud_config) { + baud_config_ = baud_config; } uint16_t BitExtractor::baud_rate() const { @@ -352,7 +366,7 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) { void POCSAGProcessor::on_message(const Message* const message) { switch (message->id) { case Message::ID::POCSAGConfigure: - configure(); + configure(reinterpret_cast(message)->baud_config); break; case Message::ID::NBFMConfigure: { @@ -370,7 +384,7 @@ void POCSAGProcessor::on_message(const Message* const message) { } } -void POCSAGProcessor::configure() { +void POCSAGProcessor::configure(int8_t baud_config) { 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; @@ -383,7 +397,7 @@ void POCSAGProcessor::configure() { // Don't process the audio stream. audio_output.configure(false); - + bit_extractor.set_baud_config(baud_config); bit_extractor.configure(demod_input_fs); // Set ready to process data. diff --git a/firmware/baseband/proc_pocsag2.hpp b/firmware/baseband/proc_pocsag2.hpp index 9215e7fd3..23fe536a9 100644 --- a/firmware/baseband/proc_pocsag2.hpp +++ b/firmware/baseband/proc_pocsag2.hpp @@ -84,6 +84,7 @@ class BitExtractor { void extract_bits(const buffer_f32_t& audio); void configure(uint32_t sample_rate); void reset(); + void set_baud_config(int8_t baud_config); uint16_t baud_rate() const; private: @@ -117,7 +118,7 @@ class BitExtractor { RateInfo{2400}}; BitQueue& bits_; - + int8_t baud_config_ = -1; uint32_t sample_rate_ = 0; RateInfo* current_rate_ = nullptr; }; @@ -207,7 +208,7 @@ class POCSAGProcessor : public BasebandProcessor { static constexpr uint32_t stat_update_threshold = baseband_fs / stat_update_interval; - void configure(); + void configure(int8_t baud_config = -1); void flush(); void reset(); void send_stats() const; diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index ae6f33bb7..74692613d 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -1187,9 +1187,10 @@ class FSKRxConfigureMessage : public Message { class POCSAGConfigureMessage : public Message { public: - constexpr POCSAGConfigureMessage() - : Message{ID::POCSAGConfigure} { + constexpr POCSAGConfigureMessage(int8_t baud_config = -1) + : Message{ID::POCSAGConfigure}, baud_config(baud_config) { } + int8_t baud_config; //-1 auto, 0=512,1=1200,2=2400 }; class APRSPacketMessage : public Message { From 6b02ba6e5dc1572b1fde21fa699d1ba7720d38b6 Mon Sep 17 00:00:00 2001 From: Totoo Date: Fri, 21 Nov 2025 09:30:01 +0100 Subject: [PATCH 03/13] Faster osm (#2874) Much faster OSM map handler --- firmware/application/ui/ui_geomap.cpp | 22 ++++++--- firmware/common/bmpfile.cpp | 47 ++++++++++++++++++++ firmware/common/bmpfile.hpp | 1 + firmware/standalone/common/ui/bmpfile.cpp | 49 ++++++++++++++++++++- firmware/standalone/common/ui/bmpfile.hpp | 1 + firmware/standalone/common/ui/ui_geomap.cpp | 22 ++++++--- firmware/standalone/pacman/main.cpp | 1 + 7 files changed, 128 insertions(+), 15 deletions(-) diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index a7cfd70a0..59c8a9f2a 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -454,14 +454,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int return false; } std::vector line(clip_w); - for (int y = 0; y < clip_h; ++y) { - int source_row = src_y + y; - int dest_row = dest_y + y; - bmp.seek(src_x, source_row); - for (int x = 0; x < clip_w; ++x) { - bmp.read_next_px(line[x], true); + if (bmp.is_bottomup()) { + for (int y = clip_h - 1; y >= 0; --y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + bmp.read_next_px_cnt(line.data(), clip_w, false); + display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); + } + } else { + for (int y = 0; y < clip_h; ++y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + bmp.read_next_px_cnt(line.data(), clip_w, false); + display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); } - display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); } return true; } diff --git a/firmware/common/bmpfile.cpp b/firmware/common/bmpfile.cpp index bc8e51340..4ef17aed0 100644 --- a/firmware/common/bmpfile.cpp +++ b/firmware/common/bmpfile.cpp @@ -200,6 +200,53 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { return true; } +bool BMPFile::read_next_px_cnt(ui::Color* px, uint32_t count, bool seek) { + if (!is_opened) return false; + size_t bytesneeded = byte_per_px * count; + while (bytesneeded > 0) { // read in batches + size_t currusedbytes = bytesneeded > 512 ? 170 * byte_per_px : bytesneeded; // don't mind this magic number. + uint8_t buffer[currusedbytes]; + auto res = bmpimage.read(buffer, currusedbytes); + if (res.is_error()) return false; + for (uint32_t i = 0; i < currusedbytes; i += byte_per_px, px++) { + switch (type) { + case 5: { + // ARGB1555 + uint16_t val = buffer[i] | (buffer[i + 1] << 8); + // Extract components + //*a = (val >> 15) & 0x01; // 1-bit alpha + uint8_t r = (val >> 10) & 0x1F; // 5-bit red + uint8_t g = (val >> 5) & 0x1F; // 5-bit green + uint8_t b = (val)&0x1F; // 5-bit blue + // expand + r = (r << 3) | (r >> 2); + g = (g << 3) | (g >> 2); + b = (b << 3) | (b >> 2); + *px = ui::Color(r, g, b); + break; + } + case 2: // 32 + *px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]); + break; + + case 4: { // 8-bit + // uint8_t index = buffer[0]; + // px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR + // px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette + break; + } + case 1: // 24 + default: + *px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]); + break; + } + } + bytesneeded -= currusedbytes; + } + if (seek) advance_curr_px(count); + return true; +} + // if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower. void BMPFile::set_bg_color(ui::Color background) { bg = background; diff --git a/firmware/common/bmpfile.hpp b/firmware/common/bmpfile.hpp index f3709bd23..f0d6c1b0f 100644 --- a/firmware/common/bmpfile.hpp +++ b/firmware/common/bmpfile.hpp @@ -42,6 +42,7 @@ class BMPFile { uint32_t getbpr() { return byte_per_row; }; bool read_next_px(ui::Color& px, bool seek); + bool read_next_px_cnt(ui::Color* px, uint32_t count, bool seek); bool write_next_px(ui::Color& px); uint32_t get_real_height(); uint32_t get_width(); diff --git a/firmware/standalone/common/ui/bmpfile.cpp b/firmware/standalone/common/ui/bmpfile.cpp index bc8e51340..c0f129fce 100644 --- a/firmware/standalone/common/ui/bmpfile.cpp +++ b/firmware/standalone/common/ui/bmpfile.cpp @@ -173,7 +173,7 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { //*a = (val >> 15) & 0x01; // 1-bit alpha uint8_t r = (val >> 10) & 0x1F; // 5-bit red uint8_t g = (val >> 5) & 0x1F; // 5-bit green - uint8_t b = (val)&0x1F; // 5-bit blue + uint8_t b = (val) & 0x1F; // 5-bit blue // expand r = (r << 3) | (r >> 2); g = (g << 3) | (g >> 2); @@ -200,6 +200,53 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { return true; } +bool BMPFile::read_next_px_cnt(ui::Color* px, uint32_t count, bool seek) { + if (!is_opened) return false; + size_t bytesneeded = byte_per_px * count; + while (bytesneeded > 0) { // read in batches + size_t currusedbytes = bytesneeded > 256 ? 85 * byte_per_px : bytesneeded; // don't mind this magic number. + uint8_t buffer[currusedbytes]; + auto res = bmpimage.read(buffer, currusedbytes); + if (res.is_error()) return false; + for (uint32_t i = 0; i < currusedbytes; i += byte_per_px, px++) { + switch (type) { + case 5: { + // ARGB1555 + uint16_t val = buffer[i] | (buffer[i + 1] << 8); + // Extract components + //*a = (val >> 15) & 0x01; // 1-bit alpha + uint8_t r = (val >> 10) & 0x1F; // 5-bit red + uint8_t g = (val >> 5) & 0x1F; // 5-bit green + uint8_t b = (val) & 0x1F; // 5-bit blue + // expand + r = (r << 3) | (r >> 2); + g = (g << 3) | (g >> 2); + b = (b << 3) | (b >> 2); + *px = ui::Color(r, g, b); + break; + } + case 2: // 32 + *px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]); + break; + + case 4: { // 8-bit + // uint8_t index = buffer[0]; + // px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR + // px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette + break; + } + case 1: // 24 + default: + *px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]); + break; + } + } + bytesneeded -= currusedbytes; + } + if (seek) advance_curr_px(count); + return true; +} + // if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower. void BMPFile::set_bg_color(ui::Color background) { bg = background; diff --git a/firmware/standalone/common/ui/bmpfile.hpp b/firmware/standalone/common/ui/bmpfile.hpp index f3709bd23..f0d6c1b0f 100644 --- a/firmware/standalone/common/ui/bmpfile.hpp +++ b/firmware/standalone/common/ui/bmpfile.hpp @@ -42,6 +42,7 @@ class BMPFile { uint32_t getbpr() { return byte_per_row; }; bool read_next_px(ui::Color& px, bool seek); + bool read_next_px_cnt(ui::Color* px, uint32_t count, bool seek); bool write_next_px(ui::Color& px); uint32_t get_real_height(); uint32_t get_width(); diff --git a/firmware/standalone/common/ui/ui_geomap.cpp b/firmware/standalone/common/ui/ui_geomap.cpp index c44857b1c..3f1fd9abd 100644 --- a/firmware/standalone/common/ui/ui_geomap.cpp +++ b/firmware/standalone/common/ui/ui_geomap.cpp @@ -451,14 +451,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int return false; } std::vector line(clip_w); - for (int y = 0; y < clip_h; ++y) { - int source_row = src_y + y; - int dest_row = dest_y + y; - bmp.seek(src_x, source_row); - for (int x = 0; x < clip_w; ++x) { - bmp.read_next_px(line[x], true); + if (bmp.is_bottomup()) { + for (int y = clip_h - 1; y >= 0; --y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + bmp.read_next_px_cnt(line.data(), clip_w, false); + painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); + } + } else { + for (int y = 0; y < clip_h; ++y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + bmp.read_next_px_cnt(line.data(), clip_w, false); + painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); } - painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); } return true; } diff --git a/firmware/standalone/pacman/main.cpp b/firmware/standalone/pacman/main.cpp index d8dd88479..65832b4aa 100644 --- a/firmware/standalone/pacman/main.cpp +++ b/firmware/standalone/pacman/main.cpp @@ -25,6 +25,7 @@ bool notouch(int, int, uint32_t) { // do nothing + return false; } void nothing() { // do nothing From caac5e1041d9dba9667126c54deed6a18d858ea4 Mon Sep 17 00:00:00 2001 From: Totoo Date: Wed, 26 Nov 2025 19:13:21 +0100 Subject: [PATCH 04/13] Sonde + map (#2879) * Update geomap view position on GPS data reception * Refactor timestamp and temperature/humidity display formatting in SondeView * Implement battery voltage reading for Meteomodem M20 * Enhance GPS data validation and update OSM zoom handling - Introduced a new method to validate GPS data in the Packet structure. - Updated the SondeView to use the new GPS validation method. - Modified GeoMap to improve handling of OSM zoom levels and ensure consistent usage of real zoom values. - Adjusted battery voltage calculation for Meteomodem M20 to ensure correct scaling. * Add serial number extraction for Meteomodem M20 support * Add support for Meteomodem M20 temperature and humidity readings * Update log file naming to include timestamp in SondeView * Add pressure reading support for Meteomodem M20 and update UI * Update SondeView UI layout and enhance Meteomodem M20 packet handling * Add vertical speed calculation and display to SondeView * Fix set_fsk function parameter type for samplesPerSymbol --- firmware/application/apps/ui_sonde.cpp | 39 ++++++-- firmware/application/apps/ui_sonde.hpp | 60 +++++++----- firmware/application/baseband_api.hpp | 2 +- firmware/application/rtc_time.cpp | 27 ++++++ firmware/application/rtc_time.hpp | 3 + firmware/application/ui/ui_geomap.cpp | 23 +++-- firmware/application/ui/ui_geomap.hpp | 5 +- firmware/common/sonde_packet.cpp | 128 ++++++++++++++++++++++++- firmware/common/sonde_packet.hpp | 85 ++++++++++++++-- 9 files changed, 316 insertions(+), 56 deletions(-) diff --git a/firmware/application/apps/ui_sonde.cpp b/firmware/application/apps/ui_sonde.cpp index ff927076b..ae97d925d 100644 --- a/firmware/application/apps/ui_sonde.cpp +++ b/firmware/application/apps/ui_sonde.cpp @@ -30,11 +30,10 @@ #include "portapack.hpp" #include #include +#include "rtc_time.hpp" using namespace portapack; namespace pmem = portapack::persistent_memory; - -#include "string_format.hpp" #include "complex.hpp" void SondeLogger::on_packet(const sonde::Packet& packet) { @@ -65,6 +64,8 @@ SondeView::SondeView(NavigationView& nav) &text_frame, &text_temp, &text_humid, + &text_press, + &text_vspeed, &geopos, &button_see_qr, &button_see_map}); @@ -107,7 +108,7 @@ SondeView::SondeView(NavigationView& nav) logger = std::make_unique(); if (logger) - logger->append(logs_dir / u"SONDE.TXT"); + logger->append(logs_dir / u"SONDE_" + to_string_timestamp(rtc_time::now()) + u".TXT"); if (pmem::beep_on_packets()) { audio::set_rate(audio::Rate::Hz_24000); @@ -155,7 +156,7 @@ void SondeView::on_packet(const sonde::Packet& packet) { sonde_id = packet.serial_number(); // used also as tag on the geomap text_serial.set(sonde_id); - text_timestamp.set(to_string_timestamp(packet.received_at())); + text_timestamp.set(to_string_datetime(packet.received_at(), TimeFormat::YMDHMS)); text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 2) + "V"); @@ -163,21 +164,41 @@ void SondeView::on_packet(const sonde::Packet& packet) { temp_humid_info = packet.get_temp_humid(); if (temp_humid_info.humid != 0) { - double decimals = abs(get_decimals(temp_humid_info.humid, 10, true)); - text_humid.set(to_string_dec_int((int)temp_humid_info.humid) + "." + to_string_dec_uint(decimals, 1) + "%"); + text_humid.set(to_string_decimal(temp_humid_info.humid, 1) + "%"); } if (temp_humid_info.temp != 0) { - double decimals = abs(get_decimals(temp_humid_info.temp, 10, true)); - text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C); + text_temp.set(to_string_decimal(temp_humid_info.temp, 1) + STR_DEGREES_C); + } + + if (packet.get_pressure() != 0) { + text_press.set(to_string_decimal(packet.get_pressure(), 1) + " hPa"); } gps_info = packet.get_GPS_data(); - if (gps_info.lat != 0 && gps_info.lon != 0) { // only update when valid, to prevent flashing + if (last_timestamp_update_ != 0 && last_altitude_ != 0) { + // calculate speeds + float vspeed = 0; + time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at()); + int32_t time_diff = (currpackettime - last_timestamp_update_); + if (time_diff >= 10) { // update only every 10 seconds + vspeed = (static_cast(gps_info.alt) - static_cast(last_altitude_)) / (float)time_diff; + last_timestamp_update_ = currpackettime; + last_altitude_ = gps_info.alt; + text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s"); + } + } else { // save first valid packet time + altitude + last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at()); + last_altitude_ = geopos.altitude(); + } + if (gps_info.is_valid()) { // only update when valid, to prevent flashing geopos.set_altitude(gps_info.alt); geopos.set_lat(gps_info.lat); geopos.set_lon(gps_info.lon); + if (geomap_view_) { + geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0); + } } if (logger && logging) { logger->on_packet(packet); diff --git a/firmware/application/apps/ui_sonde.hpp b/firmware/application/apps/ui_sonde.hpp index 11f4b942d..2f4a3f354 100644 --- a/firmware/application/apps/ui_sonde.hpp +++ b/firmware/application/apps/ui_sonde.hpp @@ -30,6 +30,7 @@ #include "ui_rssi.hpp" #include "ui_qrcode.hpp" #include "ui_geomap.hpp" +#include "string_format.hpp" #include "event_m0.hpp" @@ -94,32 +95,33 @@ class SondeView : public View { // AudioOutput audio_output { }; Labels labels{ - {{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground}, - {{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground}, - {{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground}, - {{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground}, - {{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}}; + {{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}}; RxFrequencyField field_frequency{ - {UI_POS_X(0), 0 * 8}, + {UI_POS_X(0), UI_POS_Y(0)}, nav_}; RFAmpField field_rf_amp{ - {13 * 8, UI_POS_Y(0)}}; - + {UI_POS_X(13), UI_POS_Y(0)}}; LNAGainField field_lna{ - {15 * 8, UI_POS_Y(0)}}; + {UI_POS_X(15), UI_POS_Y(0)}}; VGAGainField field_vga{ - {18 * 8, UI_POS_Y(0)}}; + {UI_POS_X(18), UI_POS_Y(0)}}; RSSI rssi{ - {21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}}; + {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}}; Channel channel{ - {21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4}, + {UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4}, }; AudioVolumeField field_volume{ @@ -136,47 +138,57 @@ class SondeView : public View { "CRC"}; Text text_signature{ - {9 * 8, 2 * 16, 10 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, "..."}; Text text_serial{ - {9 * 8, 3 * 16, 11 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, "..."}; Text text_timestamp{ - {9 * 8, 4 * 16, 11 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)}, "..."}; Text text_voltage{ - {9 * 8, 5 * 16, 10 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "..."}; Text text_frame{ - {9 * 8, 6 * 16, 10 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "..."}; Text text_temp{ - {9 * 8, 7 * 16, 10 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "..."}; Text text_humid{ - {9 * 8, 8 * 16, 10 * 8, 16}, + {UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, + "..."}; + + Text text_press{ + {UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, + "..."}; + + Text text_vspeed{ + {UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "..."}; GeoPos geopos{ - {0, 12 * 16}, + {UI_POS_X(0), UI_POS_Y(12)}, GeoPos::alt_unit::METERS, GeoPos::spd_unit::HIDDEN}; Button button_see_qr{ - {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16}, + {UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "See QR"}; Button button_see_map{ - {UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16}, + {UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "See on map"}; GeoMapView* geomap_view_{nullptr}; + time_t last_timestamp_update_{0}; + uint32_t last_altitude_{0}; MessageHandlerRegistration message_handler_packet{ Message::ID::SondePacket, diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 3f2e7d972..c642cc1c7 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -80,7 +80,7 @@ void set_pitch_rssi(int32_t avg, bool enabled); void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count); void kill_afsk(); void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word); -void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes); +void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes); void set_aprs(const uint32_t baudrate); void set_btlerx(uint8_t channel_number); diff --git a/firmware/application/rtc_time.cpp b/firmware/application/rtc_time.cpp index 68e8824df..55f241a5f 100644 --- a/firmware/application/rtc_time.cpp +++ b/firmware/application/rtc_time.cpp @@ -265,4 +265,31 @@ uint8_t day_of_week(uint16_t year, uint8_t month, uint8_t day) { return (day - 1 + (13 * m / 5) + y + (y / 4) - (y / 100) + (y / 400)) % 7; } +bool isLeap(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + +time_t rtcToUnixUTC(const rtc::RTC& rtc) { + const uint8_t daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint16_t y = rtc.year(); + uint8_t m = rtc.month(); + uint8_t d = rtc.day(); + uint32_t totalDays = 0; + for (int i = 1970; i < y; i++) { + totalDays += isLeap(i) ? 366 : 365; + } + for (int i = 1; i < m; i++) { + totalDays += daysOfMonth[i]; + if (i == 2 && isLeap(y)) { + totalDays++; + } + } + totalDays += (d - 1); + time_t totalSeconds = totalDays * 86400; // 24 * 60 * 60 + totalSeconds += rtc.hour() * 3600; + totalSeconds += rtc.minute() * 60; + totalSeconds += rtc.second(); + return totalSeconds; +} + } /* namespace rtc_time */ diff --git a/firmware/application/rtc_time.hpp b/firmware/application/rtc_time.hpp index 207b2a100..19c126db5 100644 --- a/firmware/application/rtc_time.hpp +++ b/firmware/application/rtc_time.hpp @@ -56,6 +56,9 @@ bool leap_year(uint16_t year); uint16_t day_of_year(uint16_t year, uint8_t month, uint8_t day); uint16_t day_of_year_of_nth_weekday(uint16_t year, uint8_t month, uint8_t n, uint8_t weekday); +bool isLeap(int year); +time_t rtcToUnixUTC(const rtc::RTC& rtc); + } /* namespace rtc_time */ #endif /*__RTC_TIME_H__*/ diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 59c8a9f2a..fca90d717 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -199,7 +199,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { } } map_osm_zoom++; - if (has_osm) set_osm_max_zoom(); + if (has_osm) set_osm_max_zoom(true); } else if (delta < 0) { if (map_zoom > -MAX_MAP_ZOOM_OUT) { if (map_zoom == 1) { @@ -212,6 +212,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { } } if (map_osm_zoom > 0) map_osm_zoom--; + if (has_osm) set_osm_max_zoom(true); } else { return false; } @@ -299,8 +300,8 @@ ui::Point GeoMap::item_rect_pixel(GeoMarker& item) { return {(int16_t)x, (int16_t)y}; } // osm calculation - double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py; - double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px; + double y = lat_to_pixel_y_tile(item.lat, map_osm_real_zoom) - viewport_top_left_py; + double x = lon_to_pixel_x_tile(item.lon, map_osm_real_zoom) - viewport_top_left_px; return {(int16_t)x, (int16_t)y}; } @@ -327,7 +328,7 @@ int GeoMap::lat2tile(double lat, int zoom) { return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom)); } -void GeoMap::set_osm_max_zoom() { +void GeoMap::set_osm_max_zoom(bool changeboth) { if (map_osm_zoom > 20) map_osm_zoom = 20; for (uint8_t i = map_osm_zoom; i > 0; i--) { int tile_x = lon2tile(lon_, i); @@ -335,11 +336,13 @@ void GeoMap::set_osm_max_zoom() { std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp"; std::filesystem::path file_path(filename); if (file_exists(file_path)) { - map_osm_zoom = i; + map_osm_real_zoom = i; + if (changeboth) map_osm_zoom = i; return; } } - map_osm_zoom = 0; // should not happen + if (changeboth) map_osm_zoom = 0; // should not happen + map_osm_real_zoom = 0; // should not happen } // checks if the tile file presents or not. to determine if we got osm or not @@ -530,8 +533,8 @@ void GeoMap::paint(Painter& painter) { } else { // display osm tiles // Convert center GPS to a global pixel coordinate - double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom); - double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom); + double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom); + double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom); // Find the top-left corner of the screen (viewport) in global pixel coordinates viewport_top_left_px = global_center_px - (r.width() / 2.0); @@ -560,7 +563,7 @@ void GeoMap::paint(Painter& painter) { // For the first tile (x=0, y=0), this will be the negative offset. int draw_pos_x = round(render_offset_x + x * TILE_SIZE); int draw_pos_y = round(render_offset_y + y * TILE_SIZE); - if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) { + if (!draw_osm_file(map_osm_real_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) { // already blanked it. } } @@ -623,7 +626,7 @@ bool GeoMap::on_touch(const TouchEvent event) { on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false); } else { p = event.point - screen_rect().location(); - on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true); + on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_real_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_real_zoom), true); } return true; } diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 414474b21..38ebd0ca8 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -255,7 +255,7 @@ class GeoMap : public Widget { void map_read_line_bin(ui::Color* buffer, uint16_t pixels); // open street map related uint8_t find_osm_file_tile(); - void set_osm_max_zoom(); + void set_osm_max_zoom(bool changeboth = false); bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y); int lon2tile(double lon, int zoom); int lat2tile(double lat, int zoom); @@ -263,7 +263,8 @@ class GeoMap : public Widget { double lat_to_pixel_y_tile(double lat, int zoom); double tile_pixel_x_to_lon(int x, int zoom); double tile_pixel_y_to_lat(int y, int zoom); - uint8_t map_osm_zoom{3}; + uint8_t map_osm_zoom{5}; + uint8_t map_osm_real_zoom{5}; double viewport_top_left_px = 0; double viewport_top_left_py = 0; diff --git a/firmware/common/sonde_packet.cpp b/firmware/common/sonde_packet.cpp index 0878f51f9..8bcb73779 100644 --- a/firmware/common/sonde_packet.cpp +++ b/firmware/common/sonde_packet.cpp @@ -64,7 +64,7 @@ Packet::Packet( type_ = Type::Meteomodem_M10; else if (id_byte == 0x648F) type_ = Type::Meteomodem_M2K2; - else if (id_byte == 0x4520) // https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c + else if (id_byte == 0x4520 || id_byte == 0x4320) // https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c type_ = Type::Meteomodem_M20; } } @@ -145,7 +145,7 @@ uint32_t Packet::battery_voltage() const { if (type_ == Type::Meteomodem_M10) return (reader_bi_m.read(69 * 8, 8) + (reader_bi_m.read(70 * 8, 8) << 8)) * 1000 / 150; else if (type_ == Type::Meteomodem_M20) { - return 0; // NOT SUPPPORTED YET + return reader_bi_m.read(0x26 * 8, 8) * (3.3f / 255.0) * 1000; // based on https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c } else if (type_ == Type::Meteomodem_M2K2) return reader_bi_m.read(69 * 8, 8) * 66; // Actually 65.8 else if (type_ == Type::Vaisala_RS41_SG) { @@ -160,10 +160,46 @@ uint32_t Packet::frame() const { if (type_ == Type::Vaisala_RS41_SG) { uint32_t frame_number = vaisala_descramble(pos_FrameNb) | (vaisala_descramble(pos_FrameNb + 1) << 8); return frame_number; + } else if (type_ == Type::Meteomodem_M20) { + return reader_bi_m.read(0x15 * 8, 8); } else { return 0; // Unknown } } +uint8_t Packet::getFwVerM20() const { + size_t pos_fw = 0x43; + int flen = reader_bi_m.read(0, 8); + if (flen != 0x45) { + int auxLen = flen - 0x45; + if (auxLen < 0) { + pos_fw = flen - 2; + } + } + return reader_bi_m.read(pos_fw, 8); +} + +float Packet::get_pressure() const { + float pressure = 0.0f; + if (type_ == Type::Meteomodem_M20) { + float hPa = 0.0f; + uint32_t val = ((uint32_t)reader_bi_m.read(0x25 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x24 * 8, 8); // cf. DF9DQ + uint8_t p0 = 0x00; + uint8_t fwVer = getFwVerM20(); + if (fwVer >= 0x07) { // SPI1_P[0] + p0 = reader_bi_m.read(0x16 * 8, 8); + } + val = (val << 8) | p0; + + if (val > 0) { + hPa = val / (float)(16 * 256); // 4096=0x1000 + } + if (hPa > 2560.0f) { // val > 0xA00000 + hPa = -1.0f; + } + pressure = hPa; + } + return pressure; +} temp_humid Packet::get_temp_humid() const { temp_humid result; @@ -331,6 +367,78 @@ temp_humid Packet::get_temp_humid() const { result.humid = rh; } } + + if (type_ == Type::Meteomodem_M20) { + float p0 = 1.07303516e-03, + p1 = 2.41296733e-04, + p2 = 2.26744154e-06, + p3 = 6.52855181e-08; + float Rs[3] = {12.1e3, 36.5e3, 475.0e3}; // bias/series + float Rp[3] = {1e20, 330.0e3, 2000.0e3}; // parallel, Rp[0]=inf + uint8_t scT = 0; // {0,1,2}, range/scale voltage divider + uint16_t ADC_RT; // ADC12 + // ui16_t Tcal[2]; + + float x, R; + float T = 0; // T/Kelvin + uint32_t b2 = reader_bi_m.read(0x5 * 8, 8); + uint32_t b1 = reader_bi_m.read(0x4 * 8, 8); + ADC_RT = (b2 << 8) | b1; + if (ADC_RT > 8191) { + scT = 2; + ADC_RT -= 8192; + } else if (ADC_RT > 4095) { + scT = 1; + ADC_RT -= 4096; + } else { + scT = 0; + } // also if (ADC_RT>>12)&3 == 3 + // ADC12 , 4096 = 1<<12, max: 4095 + x = (4095.0 - ADC_RT) / ADC_RT; // (Vcc-Vout)/Vout = Vcc/Vout - 1 + R = Rs[scT] / (x - Rs[scT] / Rp[scT]); + if (R > 0) T = 1.0 / (p0 + p1 * log(R) + p2 * log(R) * log(R) + p3 * log(R) * log(R) * log(R)); + if (T - 273.15 < -120.0 || T - 273.15 > 60.0) T = 0; // T < -120C, T > 60C invalid + result.temp = T - 273.15; // celsius + + // humidity + // humi helper tntc2: + float Rsq = 22.1e3; // P5.6=Vcc + float R25 = 2.2e3; // 0.119e3; //2.2e3; + float b = 3650.0; // B/Kelvin + float T25 = 25.0 + 273.15; // T0=25C, R0=R25=5k + // -> Steinhart-Hart coefficients (polyfit): + T = 0.0; // T/Kelvin + uint16_t ADC_ntc0; // M10: ADC12 P6.4(A4) + float xq, Rq; + uint32_t bq2 = reader_bi_m.read(0x7 * 8, 8); + uint32_t bq1 = reader_bi_m.read(0x6 * 8, 8); + ADC_ntc0 = (bq2 << 8) | bq1; // M10: 0x40,0x3F + xq = (4095.0 - ADC_ntc0) / ADC_ntc0; // (Vcc-Vout)/Vout + Rq = Rsq / xq; + if (Rq > 0) T = 1.0 / (1.0 / T25 + 1.0 / b * log(Rq / R25)); + + // really the humidity + float TU = T - 273.15; + float RH = -1.0f; + float xqq; + + uint16_t humval = ((uint32_t)reader_bi_m.read(0x03 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x02 * 8, 8); + uint16_t rh_cal = ((uint32_t)reader_bi_m.read(0x30 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x2F * 8, 8); + float humidityCalibration = 6.4e8f / (rh_cal + 80000.0f); + xqq = (humval + 80000.0f) * humidityCalibration * (1.0f - 5.8e-4f * (TU - 25.0f)); + xqq = 4.16e9f / xqq; + xqq = 10.087f * xqq * xqq * xqq - 211.62f * xqq * xqq + 1388.2f * xqq - 2797.0f; + RH = -1.0f; + if (humval < 48000) { + if (xqq > -20.0f && xqq < 120.f) { + RH = xqq; + if (RH < 0.0f) RH = 0.0f; + if (RH > 100.0f) RH = 100.0f; + } + } + result.humid = RH; + } + return result; } @@ -378,6 +486,10 @@ std::string Packet::serial_number() const { } } return serial_id; + } else if (type_ == Type::Meteomodem_M20) { + // Inspired by https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c + uint32_t sn = reader_bi_m.read(0x12 * 8, 8) | (reader_bi_m.read(0x13 * 8, 8) << 8) | (reader_bi_m.read(0x14 * 8, 8) << 16); + return to_string_dec_uint(sn); // Serial is 3 bytes at byte #12 } else { return "?"; } @@ -404,11 +516,23 @@ bool Packet::crc_ok() const { return crc_ok_M10(); case Type::Vaisala_RS41_SG: return crc_ok_RS41(); + case Type::Meteomodem_M20: + return check_ok_M20(); default: return true; // euquiq: it was false, but if no crc routine, then no way to check } } +bool Packet::check_ok_M20() const { + uint8_t b1 = reader_bi_m.read(0, 8); + uint8_t b2 = reader_bi_m.read(8, 8); + if ((b1 != 0x45 && b1 != 0x43) || b2 != 0x20) + return false; + if (packet_.size() / 8 < b1) + return false; + return true; +} + // each data block has a 2 byte header, data, and 2 byte tail: // 1st byte: block ID // 2nd byte: data length (without header or tail) diff --git a/firmware/common/sonde_packet.hpp b/firmware/common/sonde_packet.hpp index f5475df7b..bbe5d04fa 100644 --- a/firmware/common/sonde_packet.hpp +++ b/firmware/common/sonde_packet.hpp @@ -36,6 +36,16 @@ struct GPS_data { uint32_t alt{0}; float lat{0}; float lon{0}; + + bool is_valid() const { + if (lat >= -0.01 && lat <= 0.01 && lon >= -0.01 && lon <= 0.01) + return false; + if (lat < -90.0 || lat > 90.0) + return false; + if (lon < -180.0 || lon > 180.0) + return false; + return true; + } }; struct temp_humid { @@ -68,21 +78,79 @@ class Packet { GPS_data get_GPS_data() const; uint32_t frame() const; temp_humid get_temp_humid() const; + float get_pressure() const; FormattedSymbols symbols_formatted() const; bool crc_ok() const; private: + uint8_t getFwVerM20() const; static constexpr uint8_t vaisala_mask[64] = { - 0x96, 0x83, 0x3E, 0x51, 0xB1, 0x49, 0x08, 0x98, - 0x32, 0x05, 0x59, 0x0E, 0xF9, 0x44, 0xC6, 0x26, - 0x21, 0x60, 0xC2, 0xEA, 0x79, 0x5D, 0x6D, 0xA1, - 0x54, 0x69, 0x47, 0x0C, 0xDC, 0xE8, 0x5C, 0xF1, - 0xF7, 0x76, 0x82, 0x7F, 0x07, 0x99, 0xA2, 0x2C, - 0x93, 0x7C, 0x30, 0x63, 0xF5, 0x10, 0x2E, 0x61, - 0xD0, 0xBC, 0xB4, 0xB6, 0x06, 0xAA, 0xF4, 0x23, - 0x78, 0x6E, 0x3B, 0xAE, 0xBF, 0x7B, 0x4C, 0xC1}; + 0x96, + 0x83, + 0x3E, + 0x51, + 0xB1, + 0x49, + 0x08, + 0x98, + 0x32, + 0x05, + 0x59, + 0x0E, + 0xF9, + 0x44, + 0xC6, + 0x26, + 0x21, + 0x60, + 0xC2, + 0xEA, + 0x79, + 0x5D, + 0x6D, + 0xA1, + 0x54, + 0x69, + 0x47, + 0x0C, + 0xDC, + 0xE8, + 0x5C, + 0xF1, + 0xF7, + 0x76, + 0x82, + 0x7F, + 0x07, + 0x99, + 0xA2, + 0x2C, + 0x93, + 0x7C, + 0x30, + 0x63, + 0xF5, + 0x10, + 0x2E, + 0x61, + 0xD0, + 0xBC, + 0xB4, + 0xB6, + 0x06, + 0xAA, + 0xF4, + 0x23, + 0x78, + 0x6E, + 0x3B, + 0xAE, + 0xBF, + 0x7B, + 0x4C, + 0xC1}; GPS_data ecef_to_gps() const; @@ -97,6 +165,7 @@ class Packet { bool crc_ok_M10() const; bool crc_ok_RS41() const; + bool check_ok_M20() const; bool crc16rs41(uint32_t field_start) const; }; From 4129d57c09033eaabd441fc01e43fb368961b342 Mon Sep 17 00:00:00 2001 From: zxkmm Date: Sat, 6 Dec 2025 23:34:10 +0800 Subject: [PATCH 05/13] fix dead link (#2882) * fix dead link * submodule --- firmware/application/apps/ui_about_simple.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/application/apps/ui_about_simple.cpp b/firmware/application/apps/ui_about_simple.cpp index 953999bbc..86e188bf1 100644 --- a/firmware/application/apps/ui_about_simple.cpp +++ b/firmware/application/apps/ui_about_simple.cpp @@ -10,7 +10,7 @@ namespace ui { constexpr std::string_view mayhem_information_list[] = { "#****** Mayhem Community ******", " ", - " https://discord.mayhem.app", + " https://discord.hackrf.app", " ", "#**** List of contributors ****", " ", From 106e56abc31b0545e9fa5d9e560155861272abd3 Mon Sep 17 00:00:00 2001 From: Tim Elfelt Date: Fri, 12 Dec 2025 10:43:51 -0600 Subject: [PATCH 06/13] 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> --- firmware/application/baseband_api.cpp | 5 + firmware/application/baseband_api.hpp | 1 + firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 6 + firmware/application/external/flex/main.cpp | 84 ++ .../application/external/flex_rx/main.cpp | 83 ++ .../external/flex_rx/ui_flex_rx.cpp | 74 ++ .../external/flex_rx/ui_flex_rx.hpp | 88 ++ firmware/baseband/CMakeLists.txt | 7 + firmware/baseband/proc_flex.cpp | 810 ++++++++++++++++++ firmware/baseband/proc_flex.hpp | 177 ++++ firmware/common/flex_defs.hpp | 34 + firmware/common/message.hpp | 50 ++ firmware/common/spi_image.hpp | 1 + 14 files changed, 1425 insertions(+) create mode 100644 firmware/application/external/flex/main.cpp create mode 100644 firmware/application/external/flex_rx/main.cpp create mode 100644 firmware/application/external/flex_rx/ui_flex_rx.cpp create mode 100644 firmware/application/external/flex_rx/ui_flex_rx.hpp create mode 100644 firmware/baseband/proc_flex.cpp create mode 100644 firmware/baseband/proc_flex.hpp create mode 100644 firmware/common/flex_defs.hpp 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'}; From 472571b1edbb46b8e9bfdd3f101e0223a593b318 Mon Sep 17 00:00:00 2001 From: RocketGod <57732082+RocketGod-git@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:40:15 -0800 Subject: [PATCH 07/13] Refactor FLEX RX UI to add color cycling and message log (#2885) Replaces the console with a scrollable menu view for displaying FLEX messages, adds support for cycling text colors, and implements message logging with line wrapping and persistence. Also refactors frequency handling for persistence and updates UI element layout and initialization. --- .../external/flex_rx/ui_flex_rx.cpp | 109 ++++++++++++++++-- .../external/flex_rx/ui_flex_rx.hpp | 72 ++++++++---- 2 files changed, 151 insertions(+), 30 deletions(-) diff --git a/firmware/application/external/flex_rx/ui_flex_rx.cpp b/firmware/application/external/flex_rx/ui_flex_rx.cpp index 8bbe8c6bd..e04a688df 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.cpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -4,7 +4,6 @@ #include "portapack_persistent_memory.hpp" #include "string_format.hpp" #include "memory_map.hpp" -#include "usb_serial_asyncmsg.hpp" using namespace portapack; @@ -12,26 +11,41 @@ namespace ui::external_app::flex_rx { FlexAppView::FlexAppView(NavigationView& nav) : nav_{nav} { + // Load baseband image for FLEX decoding baseband::run_prepared_image(portapack::memory::map::m4_code.base()); add_children({&field_frequency, &field_rf_amp, &field_lna, &field_vga, + &button_color, &rssi, - &console}); + &menu_view}); - field_frequency.set_value(receiver_model.target_frequency()); + // Restore saved frequency + field_frequency.set_value(frequency_value); + receiver_model.set_target_frequency(frequency_value); + + // Frequency change callback field_frequency.updated = [this](rf::Frequency f) { update_freq(f); }; + // Color button cycles through available colors + button_color.on_select = [this](Button&) { + cycle_color(); + }; + + // Configure receiver receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); receiver_model.enable(); receiver_model.set_squelch_level(0); + // Initialize FLEX baseband baseband::set_flex_config(); + + log_message("FLEX RX Ready"); } FlexAppView::~FlexAppView() { @@ -43,32 +57,111 @@ void FlexAppView::focus() { field_frequency.focus(); } +// Cycle to next text color and refresh display +void FlexAppView::cycle_color() { + current_color_index = (current_color_index + 1) % text_colors.size(); + rebuild_menu(); +} + +// Rebuild entire menu with current color +void FlexAppView::rebuild_menu() { + menu_view.clear(); + Color current_color = text_colors[current_color_index]; + for (const auto& msg : log_messages) { + menu_view.add_item({msg, + current_color, + nullptr, + [](KeyEvent) {}}); + } + if (menu_view.item_count() > 0) { + menu_view.set_highlighted(menu_view.item_count() - 1); + } +} + +// Add message to log with automatic line wrapping +void FlexAppView::log_message(const std::string& message) { + // Calculate characters per line based on screen width (8 pixels per char) + const size_t chars_per_line = screen_width / 8; + Color current_color = text_colors[current_color_index]; + + std::string remaining = message; + bool first_line = true; + bool needs_rebuild = false; + size_t lines_added = 0; + + // Split message into screen-width chunks + while (!remaining.empty()) { + std::string line; + if (remaining.length() <= chars_per_line) { + line = remaining; + remaining.clear(); + } else { + line = remaining.substr(0, chars_per_line); + remaining = remaining.substr(chars_per_line); + } + + // Indent continuation lines + if (!first_line) { + line = " " + line; + } + first_line = false; + + // Remove oldest line if at limit + if (log_messages.size() >= MAX_LOG_LINES) { + log_messages.erase(log_messages.begin()); + needs_rebuild = true; + } + + log_messages.push_back(line); + lines_added++; + } + + // Either rebuild all or just add new lines + if (needs_rebuild) { + rebuild_menu(); + } else { + size_t start_idx = log_messages.size() - lines_added; + for (size_t i = start_idx; i < log_messages.size(); i++) { + menu_view.add_item({log_messages[i], + current_color, + nullptr, + [](KeyEvent) {}}); + } + if (menu_view.item_count() > 0) { + menu_view.set_highlighted(menu_view.item_count() - 1); + } + } +} + +// Update frequency and save for persistence void FlexAppView::update_freq(rf::Frequency f) { + frequency_value = f; receiver_model.set_target_frequency(f); } +// Handle decoded FLEX packet from baseband void FlexAppView::on_packet(const FlexPacketMessage* message) { - std::string text = ""; - - text += "FLEX "; + std::string 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); + log_message(text); } +// Handle stats message (currently unused) void FlexAppView::on_stats(const FlexStatsMessage* /* message */) { } +// Handle debug message from baseband 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); + log_message(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 index f3d6f7d42..034633487 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.hpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -6,14 +6,12 @@ #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 +#include namespace ui::external_app::flex_rx { @@ -27,37 +25,70 @@ class FlexAppView : public View { 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 + // Saved settings + rf::Frequency frequency_value{931740000}; // Default FLEX frequency + uint32_t current_color_index{0}; // Current text color selection + + // Available text colors for message display + static constexpr std::array text_colors = {{Color::green(), + Color::white(), + Color::cyan(), + Color::magenta(), + Color::yellow(), + Color::blue(), + Color::red()}}; + + RxRadioState radio_state_{}; + + // Message log settings + static constexpr size_t MAX_LOG_LINES = 32; // Limit to prevent memory issues + std::vector log_messages{}; // Stored log lines + + // Helper methods + void log_message(const std::string& message); // Add message with word wrap + void rebuild_menu(); // Rebuild menu after color change or overflow + void update_freq(rf::Frequency f); // Update tuned frequency + void cycle_color(); // Cycle through text colors + + // UI Elements - Row 0, dynamically positioned RxFrequencyField field_frequency{ - {0 * 8, 0 * 16}, + {UI_POS_X(0), UI_POS_Y(0)}, nav_}; RFAmpField field_rf_amp{ - {13 * 8, 0 * 16}}; + {UI_POS_X(13), UI_POS_Y(0)}}; LNAGainField field_lna{ - {15 * 8, 0 * 16}}; + {UI_POS_X(15), UI_POS_Y(0)}}; VGAGainField field_vga{ - {18 * 8, 0 * 16}}; + {UI_POS_X(18), UI_POS_Y(0)}}; + + // Color cycle button + Button button_color{ + {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, + "COLOR"}; RSSI rssi{ - {21 * 8, 0, 6 * 8, 4}}; + {UI_POS_X(26), 0, UI_POS_WIDTH(4), 4}}; - // Console - starts at row 1, extends to bottom of screen - Console console{ - {0, 1 * 16, screen_width, screen_height - 1 * 16}}; + // Message display area - scrollable menu view + MenuView menu_view{ + {0, 1 * 16, screen_width, screen_height - 1 * 16}, + true}; - // Logic + // Persistent settings manager + app_settings::SettingsManager settings_{ + "rx_flex", + app_settings::Mode::RX, + {{"frequency", &frequency_value}, + {"color_index", ¤t_color_index}}}; + + // Message handlers void on_packet(const FlexPacketMessage* message); void on_stats(const FlexStatsMessage* message); void on_debug(const FlexDebugMessage* message); - // Message Handlers + // Message handler registrations MessageHandlerRegistration message_handler_packet{ Message::ID::FlexPacket, [this](const Message* const p) { @@ -78,9 +109,6 @@ class FlexAppView : public View { const auto message = *static_cast(p); this->on_debug(&message); }}; - - // Config - void update_freq(rf::Frequency f); }; } // namespace ui::external_app::flex_rx From 7e8ad83537f95bea35bdde418eab4e1e98e9372a Mon Sep 17 00:00:00 2001 From: gullradriel <3157857+gullradriel@users.noreply.github.com> Date: Sun, 14 Dec 2025 11:26:21 +0100 Subject: [PATCH 08/13] Update clang format to 18 (#2891) * move to clang-format-18 * clang-format-18 indendation --- .github/workflows/check_formatting.yml | 1 + firmware/application/apps/ais_app.hpp | 2 +- firmware/application/apps/ble_comm_app.hpp | 2 +- firmware/application/apps/ble_rx_app.hpp | 2 +- firmware/application/apps/ble_tx_app.hpp | 2 +- firmware/application/apps/ui_recon.cpp | 4 ++-- firmware/application/clock_manager.cpp | 18 ++++++------------ firmware/application/debug.cpp | 9 +++------ firmware/application/debug.hpp | 3 +-- firmware/application/external/ert/ert_app.hpp | 2 +- .../application/external/tetris/ui_tetris.cpp | 3 +-- .../application/external/tpmsrx/tpms_app.hpp | 2 +- firmware/application/file.hpp | 2 +- firmware/application/hw/si5351.hpp | 3 +-- firmware/application/portapack.cpp | 9 +++------ firmware/baseband/baseband_processor.hpp | 2 +- firmware/baseband/debug.hpp | 3 +-- firmware/baseband/fxpt_atan2.cpp | 2 +- firmware/baseband/proc_ook.cpp | 3 +-- firmware/baseband/sd_over_usb/scsi.c | 6 ++---- firmware/baseband/sd_over_usb/sd_over_usb.c | 3 +-- firmware/common/bmpfile.cpp | 4 ++-- firmware/common/i2cdevmanager.hpp | 2 +- firmware/common/manchester.hpp | 2 +- firmware/common/message_queue.hpp | 3 +-- firmware/common/random.hpp | 4 ++-- firmware/common/sstv.hpp | 2 +- firmware/common/ui.hpp | 18 +++++++++--------- firmware/common/ui_painter.hpp | 2 +- firmware/common/wm8731.hpp | 4 ++-- format-code.sh | 2 +- 31 files changed, 53 insertions(+), 73 deletions(-) diff --git a/.github/workflows/check_formatting.yml b/.github/workflows/check_formatting.yml index 95d404f4e..95f714913 100644 --- a/.github/workflows/check_formatting.yml +++ b/.github/workflows/check_formatting.yml @@ -19,5 +19,6 @@ jobs: - name: clang-format Check uses: jidicula/clang-format-action@v4.11.0 with: + clang-format-version: '18' check-path: ${{ matrix.path }} fallback-style: Chromium diff --git a/firmware/application/apps/ais_app.hpp b/firmware/application/apps/ais_app.hpp index 59a8325db..22566de6e 100644 --- a/firmware/application/apps/ais_app.hpp +++ b/firmware/application/apps/ais_app.hpp @@ -159,7 +159,7 @@ class AISAppView : public View { ~AISAppView(); void set_parent_rect(const Rect new_parent_rect) override; - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/apps/ble_comm_app.hpp b/firmware/application/apps/ble_comm_app.hpp index 76107dd77..62ef2ec36 100644 --- a/firmware/application/apps/ble_comm_app.hpp +++ b/firmware/application/apps/ble_comm_app.hpp @@ -59,7 +59,7 @@ class BLECommView : public View { ~BLECommView(); void set_parent_rect(const Rect new_parent_rect) override; - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/apps/ble_rx_app.hpp b/firmware/application/apps/ble_rx_app.hpp index 99b4abd21..5c4ac8c14 100644 --- a/firmware/application/apps/ble_rx_app.hpp +++ b/firmware/application/apps/ble_rx_app.hpp @@ -209,7 +209,7 @@ class BLERxView : public View { ~BLERxView(); void set_parent_rect(const Rect new_parent_rect) override; - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/apps/ble_tx_app.hpp b/firmware/application/apps/ble_tx_app.hpp index 7ef4f3b5d..bf05d4dc3 100644 --- a/firmware/application/apps/ble_tx_app.hpp +++ b/firmware/application/apps/ble_tx_app.hpp @@ -105,7 +105,7 @@ class BLETxView : public View { ~BLETxView(); void set_parent_rect(const Rect new_parent_rect) override; - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/apps/ui_recon.cpp b/firmware/application/apps/ui_recon.cpp index 3126c9a29..255b73bb4 100644 --- a/firmware/application/apps/ui_recon.cpp +++ b/firmware/application/apps/ui_recon.cpp @@ -1086,8 +1086,8 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) { if (stepper < 0) stepper++; if (stepper > 0) stepper--; } // if( recon || stepper != 0 || index_stepper != 0 ) - } // if (frequency_list.size() > 0 ) - } /* on_statistics_updates */ + } // if (frequency_list.size() > 0 ) + } /* on_statistics_updates */ } handle_retune(); recon_redraw(); diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index a9e6fa96d..391a687d1 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -244,8 +244,7 @@ static void portapack_tcxo_enable() { /* Delay >10ms at 96MHz clock speed for reference oscillator to start. */ /* Delay an additional 1ms (arbitrary) for the clock generator to detect a signal. */ volatile uint32_t delay = 240000 + 24000; - while (delay--) - ; + while (delay--); } static void portapack_tcxo_disable() { @@ -329,8 +328,7 @@ void ClockManager::init_clock_generator() { : (ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20; - while ((clock_generator.device_status() & device_status_mask) != 0) - ; + while ((clock_generator.device_status() & device_status_mask) != 0); clock_generator.set_clock_control( clock_generator_output_mcu_clkin, @@ -377,8 +375,7 @@ ClockManager::Reference ClockManager::choose_reference() { if (hackrf_r9) { gpio_r9_clkin_en.write(1); volatile uint32_t delay = 240000 + 24000; - while (delay--) - ; + while (delay--); } const auto detected_reference = detect_reference_source(); @@ -513,8 +510,7 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se void ClockManager::wait_For_frequency_monitor_measurement_done() { // FREQ_MON mechanism fails to finish if there's no clock present on selected input?! - while (LPC_CGU->FREQ_MON.MEAS == 1) - ; + while (LPC_CGU->FREQ_MON.MEAS == 1); } uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() { @@ -559,8 +555,7 @@ void ClockManager::start_audio_pll() { }); cgu::pll0audio::power_up(); - while (!cgu::pll0audio::is_locked()) - ; + while (!cgu::pll0audio::is_locked()); cgu::pll0audio::clock_enable(); set_base_audio_clock_divider(1); @@ -577,8 +572,7 @@ void ClockManager::set_base_audio_clock_divider(const size_t divisor) { void ClockManager::stop_audio_pll() { cgu::pll0audio::clock_disable(); cgu::pll0audio::power_down(); - while (cgu::pll0audio::is_locked()) - ; + while (cgu::pll0audio::is_locked()); } void ClockManager::enable_clock_output(bool enable) { diff --git a/firmware/application/debug.cpp b/firmware/application/debug.cpp index f15538b29..74568a902 100644 --- a/firmware/application/debug.cpp +++ b/firmware/application/debug.cpp @@ -139,13 +139,11 @@ void runtime_error(uint8_t source) { led.off(); // wait for DFU button release if pressed, so we don't immediately jump into stack dump - while (swizzled_switches() & (1 << (int)Switch::Dfu)) - ; + while (swizzled_switches() & (1 << (int)Switch::Dfu)); while (true) { volatile size_t n = 1000000U; - while (n--) - ; + while (n--); led.toggle(); // Stack dump will cover entire screen, so wait for DFU button press to attempt it @@ -225,8 +223,7 @@ void draw_stack_dump() { // 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))) - ; + 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}, *Theme::getInstance()->bg_darkest_small, "Use UP/DOWN key"); diff --git a/firmware/application/debug.hpp b/firmware/application/debug.hpp index a44880ece..b3e52521a 100644 --- a/firmware/application/debug.hpp +++ b/firmware/application/debug.hpp @@ -41,8 +41,7 @@ extern uint32_t __process_stack_end__; inline uint32_t get_free_stack_space() { uint32_t* p; - for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++) - ; + for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++); auto stack_space_left = p - &__process_stack_base__; return stack_space_left; diff --git a/firmware/application/external/ert/ert_app.hpp b/firmware/application/external/ert/ert_app.hpp index 95d8183fa..df09b9e2b 100644 --- a/firmware/application/external/ert/ert_app.hpp +++ b/firmware/application/external/ert/ert_app.hpp @@ -118,7 +118,7 @@ class ERTAppView : public View { // Prevent painting of region covered entirely by a child. // TODO: Add flag to View that specifies view does not need to be cleared before painting. - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/external/tetris/ui_tetris.cpp b/firmware/application/external/tetris/ui_tetris.cpp index 34990c81a..3d8886955 100644 --- a/firmware/application/external/tetris/ui_tetris.cpp +++ b/firmware/application/external/tetris/ui_tetris.cpp @@ -598,8 +598,7 @@ void pause_game() { joystick.detach(); locate((INFO_LEFT + SCREEN_WIDTH) / 2 - 25, 200); printf("PAUSED"); - while ((get_switches_state().to_ulong() & 0x10) == 0) - ; + while ((get_switches_state().to_ulong() & 0x10) == 0); fillrect(INFO_LEFT, 195, SCREEN_WIDTH, 210, Black); joystick.attach(&ReadJoystickForFigure, 0.3); game.attach(&PlayGame, delays[level]); diff --git a/firmware/application/external/tpmsrx/tpms_app.hpp b/firmware/application/external/tpmsrx/tpms_app.hpp index 9a6a4bd2b..432aa8123 100644 --- a/firmware/application/external/tpmsrx/tpms_app.hpp +++ b/firmware/application/external/tpmsrx/tpms_app.hpp @@ -99,7 +99,7 @@ class TPMSAppView : public View { // Prevent painting of region covered entirely by a child. // TODO: Add flag to View that specifies view does not need to be cleared before painting. - void paint(Painter&) override{}; + void paint(Painter&) override {}; void focus() override; diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index 3a9e1d522..45c2cfc68 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -315,7 +315,7 @@ class File { template using Result = Result; - File(){}; + File() {}; ~File(); File(File&& other) { diff --git a/firmware/application/hw/si5351.hpp b/firmware/application/hw/si5351.hpp index 8c3c03c55..1768a8a2f 100644 --- a/firmware/application/hw/si5351.hpp +++ b/firmware/application/hw/si5351.hpp @@ -361,8 +361,7 @@ class Si5351 { } void wait_for_device_ready() { - while (device_status() & 0x80) - ; + while (device_status() & 0x80); } bool plla_loss_of_signal() { diff --git a/firmware/application/portapack.cpp b/firmware/application/portapack.cpp index 16ab3af8f..7ec63ac0c 100644 --- a/firmware/application/portapack.cpp +++ b/firmware/application/portapack.cpp @@ -322,8 +322,7 @@ static void shutdown_base() { }); cgu::pll1::enable(); - while (!cgu::pll1::is_locked()) - ; + while (!cgu::pll1::is_locked()); set_clock_config(clock_config_pll1_boot); @@ -361,15 +360,13 @@ static void set_cpu_clock_speed() { }); cgu::pll1::enable(); - while (!cgu::pll1::is_locked()) - ; + while (!cgu::pll1::is_locked()); set_clock_config(clock_config_pll1_step); /* Delay >50us at 90-110MHz clock speed */ volatile uint32_t delay = 1400; - while (delay--) - ; + while (delay--); set_clock_config(clock_config_pll1); diff --git a/firmware/baseband/baseband_processor.hpp b/firmware/baseband/baseband_processor.hpp index 7f8ea6f04..b9c9ccc67 100644 --- a/firmware/baseband/baseband_processor.hpp +++ b/firmware/baseband/baseband_processor.hpp @@ -34,7 +34,7 @@ class BasebandProcessor { virtual void execute(const buffer_c8_t& buffer) = 0; - virtual void on_message(const Message* const){}; + virtual void on_message(const Message* const) {}; protected: void feed_channel_stats(const buffer_c16_t& channel); diff --git a/firmware/baseband/debug.hpp b/firmware/baseband/debug.hpp index e217539cd..e228d15cc 100644 --- a/firmware/baseband/debug.hpp +++ b/firmware/baseband/debug.hpp @@ -30,8 +30,7 @@ extern uint32_t __process_stack_end__; inline uint32_t get_free_stack_space() { uint32_t* p; - for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++) - ; + for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++); auto stack_space_left = p - &__process_stack_base__; return stack_space_left; diff --git a/firmware/baseband/fxpt_atan2.cpp b/firmware/baseband/fxpt_atan2.cpp index 0ca1678c4..734b15bd3 100644 --- a/firmware/baseband/fxpt_atan2.cpp +++ b/firmware/baseband/fxpt_atan2.cpp @@ -76,7 +76,7 @@ static inline int16_t q15_mul(const int16_t j, const int16_t k) { return intermediate >> 15; #elif 0 // biased rounding return (intermediate + 0x4000) >> 15; -#else // unbiased rounding +#else // unbiased rounding return (intermediate + ((intermediate & 0x7FFF) == 0x4000 ? 0 : 0x4000)) >> 15; #endif } diff --git a/firmware/baseband/proc_ook.cpp b/firmware/baseband/proc_ook.cpp index 863982982..c231421a3 100644 --- a/firmware/baseband/proc_ook.cpp +++ b/firmware/baseband/proc_ook.cpp @@ -108,8 +108,7 @@ inline size_t OOKProcessor::duval_algo_step() { for (unsigned int j = 0; j < w - idx; j++) v[idx + j] = v[j]; - for (idx = w; (idx > 0) && (v[idx - 1] >= duval_symbols - 1); idx--) - ; + for (idx = w; (idx > 0) && (v[idx - 1] >= duval_symbols - 1); idx--); if (idx) v[idx - 1]++; diff --git a/firmware/baseband/sd_over_usb/scsi.c b/firmware/baseband/sd_over_usb/scsi.c index c20e0aa08..7867a261d 100644 --- a/firmware/baseband/sd_over_usb/scsi.c +++ b/firmware/baseband/sd_over_usb/scsi.c @@ -45,8 +45,7 @@ void usb_send_bulk(void* const data, const uint32_t maximum_length) { usb_bulk_block_cb, NULL); - while (!usb_bulk_block_done) - ; + while (!usb_bulk_block_done); } void usb_receive_bulk(void* const data, const uint32_t maximum_length) { @@ -59,8 +58,7 @@ void usb_receive_bulk(void* const data, const uint32_t maximum_length) { usb_bulk_block_cb, NULL); - while (!usb_bulk_block_done) - ; + while (!usb_bulk_block_done); } void usb_send_csw(msd_cbw_t* msd_cbw_data, uint8_t status) { diff --git a/firmware/baseband/sd_over_usb/sd_over_usb.c b/firmware/baseband/sd_over_usb/sd_over_usb.c index 5f4a0d9b5..9d516203a 100644 --- a/firmware/baseband/sd_over_usb/sd_over_usb.c +++ b/firmware/baseband/sd_over_usb/sd_over_usb.c @@ -118,8 +118,7 @@ void usb_transfer(void) { scsi_bulk_transfer_complete, NULL); - while (!transfer_complete) - ; + while (!transfer_complete); msd_cbw_t* msd_cbw_data = (msd_cbw_t*)&usb_bulk_buffer[0x4000]; diff --git a/firmware/common/bmpfile.cpp b/firmware/common/bmpfile.cpp index 4ef17aed0..edc6ac5ed 100644 --- a/firmware/common/bmpfile.cpp +++ b/firmware/common/bmpfile.cpp @@ -173,7 +173,7 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { //*a = (val >> 15) & 0x01; // 1-bit alpha uint8_t r = (val >> 10) & 0x1F; // 5-bit red uint8_t g = (val >> 5) & 0x1F; // 5-bit green - uint8_t b = (val)&0x1F; // 5-bit blue + uint8_t b = (val) & 0x1F; // 5-bit blue // expand r = (r << 3) | (r >> 2); g = (g << 3) | (g >> 2); @@ -217,7 +217,7 @@ bool BMPFile::read_next_px_cnt(ui::Color* px, uint32_t count, bool seek) { //*a = (val >> 15) & 0x01; // 1-bit alpha uint8_t r = (val >> 10) & 0x1F; // 5-bit red uint8_t g = (val >> 5) & 0x1F; // 5-bit green - uint8_t b = (val)&0x1F; // 5-bit blue + uint8_t b = (val) & 0x1F; // 5-bit blue // expand r = (r << 3) | (r >> 2); g = (g << 3) | (g >> 2); diff --git a/firmware/common/i2cdevmanager.hpp b/firmware/common/i2cdevmanager.hpp index 203598dbc..617ee8706 100644 --- a/firmware/common/i2cdevmanager.hpp +++ b/firmware/common/i2cdevmanager.hpp @@ -40,7 +40,7 @@ namespace i2cdev { // The device class. You'll derive your from this. Override init() and update(); class I2cDev { public: - virtual ~I2cDev(){}; + virtual ~I2cDev() {}; virtual bool init(uint8_t addr) = 0; // returns true if it is that that device we are looking for. virtual void update() = 0; // override this, and you'll be able to query your device and broadcast the result to the system diff --git a/firmware/common/manchester.hpp b/firmware/common/manchester.hpp index f95318acb..c1de4e95e 100644 --- a/firmware/common/manchester.hpp +++ b/firmware/common/manchester.hpp @@ -47,7 +47,7 @@ class ManchesterBase { virtual size_t symbols_count() const; - virtual ~ManchesterBase(){}; + virtual ~ManchesterBase() {}; protected: const baseband::Packet& packet; diff --git a/firmware/common/message_queue.hpp b/firmware/common/message_queue.hpp index edef2ef7b..f96a95e59 100644 --- a/firmware/common/message_queue.hpp +++ b/firmware/common/message_queue.hpp @@ -55,8 +55,7 @@ class MessageQueue { const bool result = push(message); if (result) { // TODO: More graceful method of waiting for empty? Maybe sleep for a bit? - while (!is_empty()) - ; + while (!is_empty()); } return result; } diff --git a/firmware/common/random.hpp b/firmware/common/random.hpp index a31583c8d..120afbcdf 100644 --- a/firmware/common/random.hpp +++ b/firmware/common/random.hpp @@ -28,8 +28,8 @@ #define MATRIX_A 0x9908b0dfUL /* constant vector a */ #define UMASK 0x80000000UL /* most significant w-r bits */ #define LMASK 0x7fffffffUL /* least significant r bits */ -#define MIXBITS(u, v) (((u)&UMASK) | ((v)&LMASK)) -#define TWIST(u, v) ((MIXBITS(u, v) >> 1) ^ ((v)&1UL ? MATRIX_A : 0UL)) +#define MIXBITS(u, v) (((u) & UMASK) | ((v) & LMASK)) +#define TWIST(u, v) ((MIXBITS(u, v) >> 1) ^ ((v) & 1UL ? MATRIX_A : 0UL)) /* initializes state[N] with a seed */ extern void init_genrand(unsigned long s); diff --git a/firmware/common/sstv.hpp b/firmware/common/sstv.hpp index ab10bbff5..7a43a4257 100644 --- a/firmware/common/sstv.hpp +++ b/firmware/common/sstv.hpp @@ -28,7 +28,7 @@ namespace sstv { #define SSTV_SAMPLERATE 3072000 #define SSTV_DELTA_COEF ((1ULL << 32) / SSTV_SAMPLERATE) -#define SSTV_F2D(f) (uint32_t)((f)*SSTV_DELTA_COEF) +#define SSTV_F2D(f) (uint32_t)((f) * SSTV_DELTA_COEF) #define SSTV_MS2S(d) (uint32_t)((d) / 1000.0 * (float)SSTV_SAMPLERATE) #define SSTV_VIS_SS SSTV_F2D(1200) diff --git a/firmware/common/ui.hpp b/firmware/common/ui.hpp index ff6fc1371..578087cf9 100644 --- a/firmware/common/ui.hpp +++ b/firmware/common/ui.hpp @@ -36,29 +36,29 @@ namespace ui { // default font width #define UI_POS_DEFAULT_WIDTH 8 // px position of the linenum-th character (Y) -#define UI_POS_Y(linenum) ((int)((linenum)*UI_POS_DEFAULT_HEIGHT)) +#define UI_POS_Y(linenum) ((int)((linenum) * UI_POS_DEFAULT_HEIGHT)) // px position of the linenum-th character from the bottom of the screen (Y) (please calculate the +1 line top-bar to it too if that is visible!) -#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum)*UI_POS_DEFAULT_HEIGHT)) +#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum) * UI_POS_DEFAULT_HEIGHT)) // px position of the linenum-th character from the left of the screen (X) -#define UI_POS_X(charnum) ((int)((charnum)*UI_POS_DEFAULT_WIDTH)) +#define UI_POS_X(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH)) // px position of the linenum-th character from the right of the screen (X) -#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH))) +#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH))) // px position of the left character from the center of the screen (X) (for N character wide string) -#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum)*UI_POS_DEFAULT_WIDTH / 2))) +#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum) * UI_POS_DEFAULT_WIDTH / 2))) // px position of the currcol in a table with colnum number of columns, where one coloumn is charnum characters wide maximum #define UI_POS_X_TABLE(colnum, currcol) ((currcol) * (screen_width / (colnum))) // px width of N characters -#define UI_POS_WIDTH(charnum) ((int)((charnum)*UI_POS_DEFAULT_WIDTH)) +#define UI_POS_WIDTH(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH)) // px width of the screen #define UI_POS_MAXWIDTH (screen_width) // px height of N line -#define UI_POS_HEIGHT(linecount) ((int)((linecount)*UI_POS_DEFAULT_HEIGHT)) +#define UI_POS_HEIGHT(linecount) ((int)((linecount) * UI_POS_DEFAULT_HEIGHT)) // px height of the screen's percent #define UI_POS_HEIGHT_PERCENT(percent) ((int)(screen_height * (percent) / 100)) // remaining px from the linenum-th line to the bottom of the screen. (please calculate the +1 line top-bar to it too if that is visible!) -#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum)*UI_POS_DEFAULT_HEIGHT))) +#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum) * UI_POS_DEFAULT_HEIGHT))) // remaining px from the charnum-th character to the right of the screen -#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH))) +#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH))) // px width of the screen's percent #define UI_POS_WIDTH_PERCENT(percent) ((int)(screen_width * (percent) / 100)) // px width of the screen diff --git a/firmware/common/ui_painter.hpp b/firmware/common/ui_painter.hpp index d628da278..ca6efa9c4 100644 --- a/firmware/common/ui_painter.hpp +++ b/firmware/common/ui_painter.hpp @@ -65,7 +65,7 @@ class Widget; class Painter { public: - Painter(){}; + Painter() {}; Painter(const Painter&) = delete; Painter(Painter&&) = delete; diff --git a/firmware/common/wm8731.hpp b/firmware/common/wm8731.hpp index 85dfb1e38..ad353f589 100644 --- a/firmware/common/wm8731.hpp +++ b/firmware/common/wm8731.hpp @@ -343,8 +343,8 @@ class WM8731 : public audio::Codec { headphone_mute(); } - void speaker_enable(){}; - void speaker_disable(){}; + void speaker_enable() {}; + void speaker_disable() {}; bool speaker_disable_supported() const override { return false; } diff --git a/format-code.sh b/format-code.sh index ab2f21b04..b0e9bb507 100755 --- a/format-code.sh +++ b/format-code.sh @@ -1,2 +1,2 @@ #!/bin/sh -find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-13 -style=file -i +find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-18 -style=file -i From c53adfc7657fed92917864e46ee898e584b9591a Mon Sep 17 00:00:00 2001 From: RocketGod <57732082+RocketGod-git@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:08:39 -0800 Subject: [PATCH 09/13] Refactor FLEX RX UI to use console for messages (#2890) --- firmware/application/external/flex/main.cpp | 84 ----------- .../external/flex_rx/ui_flex_rx.cpp | 130 +++++++----------- .../external/flex_rx/ui_flex_rx.hpp | 41 ++---- 3 files changed, 57 insertions(+), 198 deletions(-) delete mode 100644 firmware/application/external/flex/main.cpp diff --git a/firmware/application/external/flex/main.cpp b/firmware/application/external/flex/main.cpp deleted file mode 100644 index 64ed2a07b..000000000 --- a/firmware/application/external/flex/main.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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/ui_flex_rx.cpp b/firmware/application/external/flex_rx/ui_flex_rx.cpp index e04a688df..ccc7410d5 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.cpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -18,9 +18,8 @@ FlexAppView::FlexAppView(NavigationView& nav) &field_rf_amp, &field_lna, &field_vga, - &button_color, &rssi, - &menu_view}); + &console}); // Restore saved frequency field_frequency.set_value(frequency_value); @@ -31,11 +30,6 @@ FlexAppView::FlexAppView(NavigationView& nav) update_freq(f); }; - // Color button cycles through available colors - button_color.on_select = [this](Button&) { - cycle_color(); - }; - // Configure receiver receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); @@ -45,7 +39,7 @@ FlexAppView::FlexAppView(NavigationView& nav) // Initialize FLEX baseband baseband::set_flex_config(); - log_message("FLEX RX Ready"); + console.writeln("Ready"); } FlexAppView::~FlexAppView() { @@ -57,79 +51,53 @@ void FlexAppView::focus() { field_frequency.focus(); } -// Cycle to next text color and refresh display -void FlexAppView::cycle_color() { - current_color_index = (current_color_index + 1) % text_colors.size(); - rebuild_menu(); -} - -// Rebuild entire menu with current color -void FlexAppView::rebuild_menu() { - menu_view.clear(); - Color current_color = text_colors[current_color_index]; - for (const auto& msg : log_messages) { - menu_view.add_item({msg, - current_color, - nullptr, - [](KeyEvent) {}}); - } - if (menu_view.item_count() > 0) { - menu_view.set_highlighted(menu_view.item_count() - 1); +// Redraw all messages to console +void FlexAppView::redraw_console() { + console.clear(true); + bool first = true; + for (const auto& msg : messages) { + if (!first) { + console.writeln(""); // Blank line between messages + } + first = false; + console.writeln(msg); } } // Add message to log with automatic line wrapping void FlexAppView::log_message(const std::string& message) { - // Calculate characters per line based on screen width (8 pixels per char) const size_t chars_per_line = screen_width / 8; - Color current_color = text_colors[current_color_index]; + // Console height accounts for status bar and controls row + const size_t console_lines = (screen_height - 2 * 16) / 16; - std::string remaining = message; - bool first_line = true; - bool needs_rebuild = false; - size_t lines_added = 0; + messages.push_back(message); - // Split message into screen-width chunks - while (!remaining.empty()) { - std::string line; - if (remaining.length() <= chars_per_line) { - line = remaining; - remaining.clear(); - } else { - line = remaining.substr(0, chars_per_line); - remaining = remaining.substr(chars_per_line); - } - - // Indent continuation lines - if (!first_line) { - line = " " + line; - } - first_line = false; - - // Remove oldest line if at limit - if (log_messages.size() >= MAX_LOG_LINES) { - log_messages.erase(log_messages.begin()); - needs_rebuild = true; - } - - log_messages.push_back(line); - lines_added++; + // Calculate total lines used (messages + blank lines between them) + size_t total_lines = 0; + for (size_t i = 0; i < messages.size(); i++) { + if (i > 0) total_lines++; // Count blank line separator + size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line; + if (msg_lines == 0) msg_lines = 1; + total_lines += msg_lines; } - // Either rebuild all or just add new lines - if (needs_rebuild) { - rebuild_menu(); + // If console would overflow, remove oldest messages and redraw + if (total_lines > console_lines) { + while (total_lines > console_lines && !messages.empty()) { + const auto& oldest = messages.front(); + size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line; + if (oldest_lines == 0) oldest_lines = 1; + total_lines -= oldest_lines; + if (messages.size() > 1) total_lines--; // Remove separator line too + messages.erase(messages.begin()); + } + redraw_console(); } else { - size_t start_idx = log_messages.size() - lines_added; - for (size_t i = start_idx; i < log_messages.size(); i++) { - menu_view.add_item({log_messages[i], - current_color, - nullptr, - [](KeyEvent) {}}); - } - if (menu_view.item_count() > 0) { - menu_view.set_highlighted(menu_view.item_count() - 1); + // Just append new message + if (messages.size() > 1) { + console.writeln(""); // Blank line before new message } + console.writeln(message); } } @@ -141,27 +109,21 @@ void FlexAppView::update_freq(rf::Frequency f) { // Handle decoded FLEX packet from baseband void FlexAppView::on_packet(const FlexPacketMessage* message) { - std::string text = "FLEX "; - text += to_string_dec_uint(message->packet.bitrate); - text += " "; - text += to_string_dec_uint(message->packet.capcode); - text += ": "; - text += message->packet.message; - - log_message(text); + log_message(message->packet.message); } // Handle stats message (currently unused) -void FlexAppView::on_stats(const FlexStatsMessage* /* message */) { +void FlexAppView::on_stats(const FlexStatsMessage*) { } -// Handle debug message from baseband +// Debug handler - uncomment to see baseband debug messages 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); - log_message(text); + (void)message; // Suppress unused parameter warning + // std::string text = "DBG: "; + // text += message->text; + // text += " " + to_string_hex(message->val1, 8); + // text += " " + to_string_hex(message->val2, 8); + // log_message(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 index 034633487..7f182878a 100644 --- a/firmware/application/external/flex_rx/ui_flex_rx.hpp +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -11,7 +11,6 @@ #include #include -#include namespace ui::external_app::flex_rx { @@ -28,28 +27,17 @@ class FlexAppView : public View { // Saved settings rf::Frequency frequency_value{931740000}; // Default FLEX frequency - uint32_t current_color_index{0}; // Current text color selection - - // Available text colors for message display - static constexpr std::array text_colors = {{Color::green(), - Color::white(), - Color::cyan(), - Color::magenta(), - Color::yellow(), - Color::blue(), - Color::red()}}; RxRadioState radio_state_{}; - // Message log settings - static constexpr size_t MAX_LOG_LINES = 32; // Limit to prevent memory issues - std::vector log_messages{}; // Stored log lines + // Message storage for console redraw + static constexpr size_t MAX_MESSAGES = 20; + std::vector messages{}; // Helper methods - void log_message(const std::string& message); // Add message with word wrap - void rebuild_menu(); // Rebuild menu after color change or overflow - void update_freq(rf::Frequency f); // Update tuned frequency - void cycle_color(); // Cycle through text colors + void log_message(const std::string& message); + void redraw_console(); + void update_freq(rf::Frequency f); // UI Elements - Row 0, dynamically positioned RxFrequencyField field_frequency{ @@ -63,25 +51,18 @@ class FlexAppView : public View { VGAGainField field_vga{ {UI_POS_X(18), UI_POS_Y(0)}}; - // Color cycle button - Button button_color{ - {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, - "COLOR"}; - RSSI rssi{ - {UI_POS_X(26), 0, UI_POS_WIDTH(4), 4}}; + {UI_POS_X(21), 0, UI_POS_WIDTH(9), 4}}; - // Message display area - scrollable menu view - MenuView menu_view{ - {0, 1 * 16, screen_width, screen_height - 1 * 16}, - true}; + // Message display area (below controls, account for status bar) + Console console{ + {0, 1 * 16, screen_width, screen_height - 2 * 16}}; // Persistent settings manager app_settings::SettingsManager settings_{ "rx_flex", app_settings::Mode::RX, - {{"frequency", &frequency_value}, - {"color_index", ¤t_color_index}}}; + {{"frequency", &frequency_value}}}; // Message handlers void on_packet(const FlexPacketMessage* message); From f71f19e71960dd4dcb526d10d3fe553ad8325c10 Mon Sep 17 00:00:00 2001 From: StarVore Labs <105611837+StarVore@users.noreply.github.com> Date: Wed, 17 Dec 2025 07:14:19 -0500 Subject: [PATCH 10/13] Addition of SSTV RX application (#2888) --- firmware/application/baseband_api.cpp | 13 + firmware/application/baseband_api.hpp | 2 + firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 + firmware/application/external/sstvrx/main.cpp | 88 +++ .../application/external/sstvrx/ui_sstvrx.cpp | 510 +++++++++++++ .../application/external/sstvrx/ui_sstvrx.hpp | 196 +++++ firmware/baseband/CMakeLists.txt | 7 + firmware/baseband/proc_sstvrx.cpp | 719 ++++++++++++++++++ firmware/baseband/proc_sstvrx.hpp | 175 +++++ firmware/common/message.hpp | 60 ++ firmware/common/spi_image.hpp | 1 + 12 files changed, 1783 insertions(+) create mode 100644 firmware/application/external/sstvrx/main.cpp create mode 100644 firmware/application/external/sstvrx/ui_sstvrx.cpp create mode 100644 firmware/application/external/sstvrx/ui_sstvrx.hpp create mode 100644 firmware/baseband/proc_sstvrx.cpp create mode 100644 firmware/baseband/proc_sstvrx.hpp diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index c5fdd1dd5..7152b7430 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -147,6 +147,19 @@ void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration) { send_message(&message); } +void set_sstvrx_data(const uint8_t code) { + const SSTVRXConfigureMessage message{ + code}; + send_message(&message); +} + +void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant) { + const SSTVRXPhaseSlantMessage message{ + phase, + slant}; + send_message(&message); +} + void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) { const AFSKRxConfigureMessage message{ baudrate, diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index c95b612c3..86d06604c 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -74,6 +74,8 @@ void set_tone(const uint32_t index, const uint32_t delta, const uint32_t duratio void set_tones_config(const uint32_t bw, const uint32_t pre_silence, const uint16_t tone_count, const bool dual_tone, const bool audio_out); void kill_tone(); void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration); +void set_sstvrx_data(const uint8_t code); +void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant); void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, uint8_t bits_per_sample, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled); void set_fifo_data(const int8_t* data); void set_pitch_rssi(int32_t avg, bool enabled); diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index f7789d7b8..32a404141 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -99,6 +99,10 @@ set(EXTCPPSRC external/sstvtx/main.cpp external/sstvtx/ui_sstvtx.cpp + #sstvrx + external/sstvrx/main.cpp + external/sstvrx/ui_sstvrx.cpp + #random 464 bytes. external/random_password/main.cpp external/random_password/ui_random_password.cpp @@ -294,6 +298,7 @@ set(EXTAPPLIST adsbtx morse_tx sstvtx + sstvrx random_password # acars_rx --not working wefax_rx diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 946086db7..a6f681ed5 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -87,6 +87,7 @@ MEMORY 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 + ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k } @@ -477,5 +478,11 @@ SECTIONS KEEP(*(.external_app.app_flex_rx.application_information)); *(*ui*external_app*flex_rx*); } > ram_external_app_flex_rx + + .external_app_sstvrx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_sstvrx.application_information)); + *(*ui*external_app*sstvrx*); + } > ram_external_app_sstvrx } diff --git a/firmware/application/external/sstvrx/main.cpp b/firmware/application/external/sstvrx/main.cpp new file mode 100644 index 000000000..ffcdb459a --- /dev/null +++ b/firmware/application/external/sstvrx/main.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 StarVore Labs + * + * 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_sstvrx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::sstvrx { + +void initialize_app(NavigationView& nav) { + nav.push(); +} + +} // namespace ui::external_app::sstvrx + +extern "C" { + +// Az alkalmazás információ C-linkage-ként, hogy a firmware hívhassa +__attribute__((section(".external_app.app_sstvrx.application_information"), used)) +application_information_t _application_information_sstvrx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::sstvrx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "SSTV RX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0xFE, + 0x7F, + 0x03, + 0xC0, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0xAB, + 0xCA, + 0x53, + 0xD5, + 0x03, + 0xC0, + 0xFF, + 0xFF, + 0xFB, + 0xD7, + 0xFE, + 0x7F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'S', 'R', 'X'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; + +} // extern "C" \ No newline at end of file diff --git a/firmware/application/external/sstvrx/ui_sstvrx.cpp b/firmware/application/external/sstvrx/ui_sstvrx.cpp new file mode 100644 index 000000000..165d25822 --- /dev/null +++ b/firmware/application/external/sstvrx/ui_sstvrx.cpp @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2025 StarVore Labs + * + * 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_sstvrx.hpp" + +#include "portapack_persistent_memory.hpp" +#include "portapack.hpp" +#include "hackrf_hal.hpp" +#include "file_path.hpp" +#include "message.hpp" +#include +#include +#include +#include +#include + +using namespace portapack; +using namespace modems; +using namespace ui; + +#if SSTVRX_ENABLE_LOGGER +#define SSTVRX_LOG_INFO(msg) \ + do { \ + if (logger) { \ + logger->log_info(msg); \ + } \ + } while (0) +#define SSTVRX_LOG_ERROR(msg) \ + do { \ + if (logger) { \ + logger->log_error(msg); \ + } \ + } while (0) +#else +#define SSTVRX_LOG_INFO(msg) \ + do { \ + (void)sizeof(msg); \ + } while (0) +#define SSTVRX_LOG_ERROR(msg) \ + do { \ + (void)sizeof(msg); \ + } while (0) +#endif + +// SSTV RX View Implementation +namespace ui::external_app::sstvrx { + +static_assert(sizeof(shared_memory.bb_data.data) == 512, + "SSTV shared buffer size mismatch"); + +#if SSTVRX_ENABLE_LOGGER +void SstvRxLogger::log_error(const std::string& error_message) { + log_file.write_entry(rtc_time::now(), "ERROR: " + error_message); +} + +void SstvRxLogger::log_info(const std::string& info_message) { + log_file.write_entry(rtc_time::now(), "INFO: " + info_message); +} +#endif + +SstvRxView::SstvRxView(ui::NavigationView& nav) + : nav_(nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + DISPLAY_HEIGHT = screen_height - SSTV_IMG_START_ROW * 16 - 16; + DISPLAY_WIDTH = screen_width; + add_children({&field_rf_amp, + &field_lna, + &field_vga, + &rssi, + &channel, + &field_frequency, + &field_volume, + &audio, + &start_stop_btn, + &options_mode, + &field_phase, + &field_slant, + &labels, + &text_calibration}); + + // Initialize audio with proper rate for SSTV + audio::set_rate(audio::Rate::Hz_48000); + audio::output::start(); + + // Configure receiver with optimal settings for SSTV + // NOTE: Do NOT set modulation mode - SSTV uses a custom baseband processor + // Standard sampling rate (3.072MHz) with wide bandwidth to capture full SSTV audio spectrum + // SSTV uses 1200-2300 Hz tones, so we need wide baseband to avoid distortion + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); // Standard wideband setting + receiver_model.set_squelch_level(1); + receiver_model.set_hidden_offset(0); // No offset needed + + // Field values will be set in on_show() to ensure proper initialization + + using option_t = std::pair; + using options_t = std::vector; + options_t mode_options; + uint32_t c; + + // Start/Stop button handler - toggles between start and stop + start_stop_btn.on_select = [this](Button&) { + start_stop_btn.focus(); + on_start_stop(); + }; + + // Initialize frequency field from settings or use default + if (settings_.loaded() && settings_.raw().rx_frequency != 0) { + field_frequency.set_value(settings_.raw().rx_frequency); + } else if (field_frequency.value() == 0) { + field_frequency.set_value(145800000); // Default to 145.800 MHz (ISS) + } + field_frequency.set_step(25000); + + // Populate mode list + for (c = 0; c < SSTV_MODES_NB; c++) + mode_options.emplace_back(sstv_modes[c].name, c); + options_mode.set_options(mode_options); + + options_mode.on_change = [this](size_t i, int32_t) { + this->on_mode_changed(i); + }; + options_mode.set_selected_index(1); // Scottie 2 + on_mode_changed(1); + + // Initialize phase and slant controls from loaded settings + field_phase.set_value(phase_adjustment); + field_phase.on_change = [this](int32_t v) { + phase_adjustment = v; + if (is_receiving) { + baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment); + } else if (max_received_line > 0) { + // Auto-redraw when adjusting after reception + redraw_image(); + } + }; + + field_slant.set_value(slant_adjustment); + field_slant.on_change = [this](int32_t v) { + slant_adjustment = v; + if (is_receiving) { + baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment); + } else if (max_received_line > 0) { + // Auto-redraw when adjusting after reception + redraw_image(); + } + }; + +#if SSTVRX_ENABLE_LOGGER + logger = std::make_unique(); + if (logger) { + logger->append(logs_dir / "SSTVRX.txt"); + logger->log_info("----------SSTV RX Started----------"); + } +#endif +} + +// Destructor: Ensure reception is stopped +SstvRxView::~SstvRxView() { + is_receiving = true; + on_stop(); + baseband::shutdown(); + SSTVRX_LOG_INFO("SSTV RX Stopped"); +} + +void SstvRxView::on_show() { + // Update field values from receiver model to reflect loaded settings + field_lna.set_value(receiver_model.lna()); + field_vga.set_value(receiver_model.vga()); + field_rf_amp.set_value(receiver_model.rf_amp()); + field_volume.set_value(receiver_model.normalized_headphone_volume()); +} + +void SstvRxView::focus() { + field_frequency.focus(); +} + +// Combined start/stop handler - toggles based on current state +void SstvRxView::on_start_stop() { + if (is_receiving) { + // Currently receiving - stop it + on_stop(); + start_stop_btn.set_text("Start RX"); + } else { + // Currently stopped - start reception + SSTVRX_LOG_INFO("Starting SSTV RX Reception"); + start_audio(); + start_stop_btn.set_text("Stop RX"); + } +} + +// Stop NFM audio reception +void SstvRxView::on_stop() { + SSTVRX_LOG_INFO("Stopping SSTV RX Reception"); + if (is_receiving) { + // Stop in reverse order of start + receiver_model.disable(); + audio::output::stop(); + + // Reset state + is_receiving = false; + + // Close image file if still open + bmp.close(); + + pending_line_valid = false; + pending_chunk_mask = 0; + std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0); + *reinterpret_cast(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0; + + SSTVRX_LOG_INFO("SSTV RX Reception Stopped"); + } else { + SSTVRX_LOG_ERROR("SSTV RX Reception Not Running"); + } +} + +// Start NFM audio reception +void SstvRxView::start_audio() { + SSTVRX_LOG_INFO("Configuring SSTV RX Audio Reception"); + + // Configure the baseband processor with VIS code + if (rx_sstv_mode) { + baseband::set_sstvrx_data(rx_sstv_mode->vis_code); + SSTVRX_LOG_INFO("Sent VIS code to processor: " + to_string_dec_uint(rx_sstv_mode->vis_code)); + } + + // Send phase and slant adjustments + baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment); + + // Initialize audio path + audio::output::stop(); + audio::set_rate(audio::Rate::Hz_48000); + + // Set audio routing and volume + // audio::output::start(); + + // Clear display area and reset line counter + portapack::display.fill_rectangle( + {0, SSTV_IMG_START_ROW * 16, DISPLAY_WIDTH, DISPLAY_HEIGHT}, + {0, 0, 0}); + line_num = 0; + file_line_num = 0; + max_received_line = 0; + pending_line_valid = false; + pending_chunk_mask = 0; + std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0); + *reinterpret_cast(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0; + + // Clear calibration display + text_calibration.set("Calibrating..."); + + // Initialize new image file + current_line_rx = 0; + auto timestamp = to_string_timestamp(rtc_time::now()); + auto dir_error = ensure_directory(sstv_dir / "RX"); + if (!dir_error.ok()) { + SSTVRX_LOG_ERROR("Failed to create directory: SSTV/RX"); + } + current_image_path = sstv_dir / ("RX/SSTV_" + timestamp + ".bmp"); + auto ok = bmp.create(current_image_path, IMAGE_WIDTH, 1); + if (!ok) { + SSTVRX_LOG_ERROR("Failed to create file: " + current_image_path.string()); + bmp.close(); + } + + // Start audio output + audio::output::start(); + audio::headphone::set_volume(persistent_memory::headphone_volume()); + + // Keep ReceiverModel in capture mode so it doesn't override our custom baseband/audio configuration. + receiver_model.set_modulation(ReceiverModel::Mode::Capture); + + // Enable receiver last + receiver_model.enable(); + is_receiving = true; + SSTVRX_LOG_INFO("SSTV RX Started"); +} + +void SstvRxView::on_mode_changed(const size_t index) { + rx_sstv_mode = &sstv_modes[index]; +} + +void SstvRxView::write_line_to_file(uint16_t line_num, const uint8_t* rgb_line) { + (void)line_num; + if (!bmp.is_loaded()) return; + // Ensure BMP height is sufficient + if (bmp.get_real_height() <= file_line_num) { + bmp.expand_y(file_line_num + 1); + } + bmp.seek(0, file_line_num); + + // Write RGB data in BGR order + for (uint16_t x = 0; x < IMAGE_WIDTH; x++) { + uint8_t r = rgb_line[x * 3 + 0]; + uint8_t g = rgb_line[x * 3 + 1]; + uint8_t b = rgb_line[x * 3 + 2]; + Color px(g, b, r); + bmp.write_next_px(px); + } + file_line_num++; +} + +void SstvRxView::update_display(uint16_t current_line, const uint8_t* rgb_line) { + if (current_line >= IMAGE_HEIGHT) return; + + // Reset line counter if we reach the bottom of display + if (line_num >= DISPLAY_HEIGHT) { + line_num = 0; + } + + // Scale line to display width + for (uint16_t x = 0; x < DISPLAY_WIDTH; x++) { + // Scale x coordinate + uint16_t src_x = (x * IMAGE_WIDTH) / DISPLAY_WIDTH; + if (src_x >= IMAGE_WIDTH) continue; + + // Get RGB values from interleaved data [R,G,B,R,G,B,...] + uint8_t r = rgb_line[src_x * 3 + 0]; + uint8_t g = rgb_line[src_x * 3 + 1]; + uint8_t b = rgb_line[src_x * 3 + 2]; + // Display uses BGR order like BMP format + // line_buffer[x] = Color(b, r, g); + line_buffer[x] = Color(g, b, r); + } + + // Render the line at the current position + portapack::display.render_line( + {0, line_num + SSTV_IMG_START_ROW * 16}, + DISPLAY_WIDTH, + line_buffer); + + // Increment line counter + line_num++; +} +void SstvRxView::redraw_image() { + // Disabled: Post-reception redraw requires 245KB buffer which exceeds M0 memory + // Phase and slant adjustments must be set before reception starts +} + +void SstvRxView::on_progress(uint16_t line, uint16_t total_lines) { + if (!is_receiving) { + if (line < 0xFFF0) { + *reinterpret_cast(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0; + } + return; + } + + // Handle debug messages + if (line == 0xFFFF) { + SSTVRX_LOG_ERROR("Processor not configured"); + return; + } + if (line == 0xFFFE) { + SSTVRX_LOG_INFO("Sync pulse duration: " + to_string_dec_uint(total_lines) + " samples"); + return; + } + if (line == 0xFFFD) { + SSTVRX_LOG_INFO("Sync detected, count=" + to_string_dec_uint(total_lines)); + text_calibration.set("Syncs: " + to_string_dec_uint(total_lines)); + return; + } + if (line == 0xFFFC) { + SSTVRX_LOG_INFO("Expected interval: " + to_string_dec_uint(total_lines) + " samples"); + return; + } + if (line == 0xFFFB) { + SSTVRX_LOG_INFO("Actual interval: " + to_string_dec_uint(total_lines) + " samples"); + return; + } + if (line == 0xFFFA) { + SSTVRX_LOG_INFO("SYNC TIMEOUT after " + to_string_dec_uint(total_lines) + " samples - continuing without sync"); + return; + } + if (line == 0xFFF9) { + SSTVRX_LOG_INFO("Detected frequency at sync: " + to_string_dec_uint(total_lines) + " Hz"); + return; + } + if (line == 0xFFF8) { + SSTVRX_LOG_INFO("OUTLIER REJECTED: interval=" + to_string_dec_uint(total_lines) + " samples (expected 10000-15000)"); + return; + } + if (line == 0xFFF7) { + SSTVRX_LOG_INFO("PRE-RECORD: sync_history_count=" + to_string_dec_uint(total_lines)); + return; + } + if (line == 0xFFF6) { + SSTVRX_LOG_INFO("MAX_SYNC_HISTORY EXCEEDED: count=" + to_string_dec_uint(total_lines)); + return; + } + if (line == 0xFFF5) { + // Decode rejection reason bit flags: 0x1=interval, 0x2=freq, 0x4=duration + std::string reasons = ""; + if (total_lines & 0x1) reasons += "interval "; + if (total_lines & 0x2) reasons += "freq "; + if (total_lines & 0x4) reasons += "duration "; + if (reasons.empty()) reasons = "unknown"; + SSTVRX_LOG_INFO("FALSE SYNC PAIR REJECTED on Line 0: " + reasons); + return; + } + if (line == 0xFFF4) { + SSTVRX_LOG_INFO("STARTING LINE 0 DECODE after sync_sample_count=" + to_string_dec_uint(total_lines)); + return; + } + + std::array chunk{}; + memcpy(chunk.data(), shared_memory.bb_data.data, chunk.size()); + *reinterpret_cast(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0; + + const uint16_t line_num_encoded = chunk[0] | (chunk[1] << 8); + const bool is_second_chunk = (line_num_encoded & 1) == 1; + const uint16_t actual_line_num = line_num_encoded / 2; + + if (actual_line_num >= IMAGE_HEIGHT) { + return; + } + + const bool multi_chunk_line = PIXELS_PER_LINE > MAX_CHUNK_PIXELS; + if (!pending_line_valid || pending_line_number != actual_line_num) { + pending_line_number = actual_line_num; + pending_line_valid = true; + pending_chunk_mask = 0; + std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0); + } + + const uint16_t chunk_pixels = multi_chunk_line + ? (is_second_chunk ? (PIXELS_PER_LINE - MAX_CHUNK_PIXELS) : MAX_CHUNK_PIXELS) + : PIXELS_PER_LINE; + const uint16_t dest_pixel_offset = (multi_chunk_line && is_second_chunk) ? MAX_CHUNK_PIXELS : 0; + const uint16_t chunk_bytes = chunk_pixels * 3; + const uint16_t max_copy_bytes = static_cast(chunk.size() > CHUNK_HEADER_BYTES ? (chunk.size() - CHUNK_HEADER_BYTES) : 0); + const uint16_t bytes_to_copy = (chunk_bytes < max_copy_bytes) ? chunk_bytes : max_copy_bytes; + const uint16_t dest_byte_offset = dest_pixel_offset * 3; + + if (bytes_to_copy > 0 && (dest_byte_offset + bytes_to_copy) <= pending_line_rgb.size()) { + memcpy(pending_line_rgb.data() + dest_byte_offset, chunk.data() + CHUNK_HEADER_BYTES, bytes_to_copy); + } + + const uint8_t chunk_bit = (multi_chunk_line && is_second_chunk) ? 0x2 : 0x1; + pending_chunk_mask |= chunk_bit; + const uint8_t required_mask = multi_chunk_line ? 0x3 : 0x1; + if (pending_chunk_mask != required_mask) { + return; + } + + pending_line_valid = false; + pending_chunk_mask = 0; + current_line_rx = actual_line_num; + if (actual_line_num < IMAGE_HEIGHT) { + write_line_to_file(actual_line_num, pending_line_rgb.data()); + update_display(actual_line_num, pending_line_rgb.data()); + max_received_line = max_received_line > (actual_line_num + 1) ? max_received_line : (actual_line_num + 1); + } + +#if SSTVRX_ENABLE_LOGGER + if (logger && (actual_line_num % 10 == 0)) { + logger->log_info("Line " + to_string_dec_uint(actual_line_num) + "/" + to_string_dec_uint(total_lines)); + } +#endif + + // if (actual_line_num >= (total_lines - 1)) { //don't auto finish image upon end, user need to manually stop. this method is not reliable enough. + // finish_image(); + // } +} + +void SstvRxView::finish_image() { + bmp.close(); + SSTVRX_LOG_INFO("Image completed: " + current_image_path.string()); +} + +void SstvRxView::on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count) { + if (!is_receiving) return; + + // Display calibration suggestions to the user + if (sync_count >= 4) { + std::string cal_text = "Try Slant=" + to_string_dec_int(suggested_slant); + text_calibration.set(cal_text); + text_calibration.set_dirty(); + + // Don't auto-apply yet - just suggest for now until we verify the values are correct + // User can manually adjust if needed + + // Log the suggestion + SSTVRX_LOG_INFO("Calibration suggestion: phase=" + to_string_dec_int(suggested_phase) + + " slant=" + to_string_dec_int(suggested_slant) + + " (from " + to_string_dec_uint(sync_count) + " syncs)"); + } else { + // Log that we received calibration but not enough syncs yet + SSTVRX_LOG_INFO("Calibration received: " + to_string_dec_uint(sync_count) + " syncs (need 4+)"); + } +} + +} // namespace ui::external_app::sstvrx \ No newline at end of file diff --git a/firmware/application/external/sstvrx/ui_sstvrx.hpp b/firmware/application/external/sstvrx/ui_sstvrx.hpp new file mode 100644 index 000000000..6c7ccfbfb --- /dev/null +++ b/firmware/application/external/sstvrx/ui_sstvrx.hpp @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 StarVore Labs + * + * 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 __SSTVRX_H__ +#define __SSTVRX_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_receiver.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "ui_freqman.hpp" +#include "ui_channel.hpp" +#include "baseband_api.hpp" +#include "event_m0.hpp" +#include "message.hpp" +#include "sstv.hpp" +#include "file.hpp" +#include "bmpfile.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "oversample.hpp" +#include "string_format.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "audio.hpp" +#include "portapack.hpp" +#include +#include + +#ifndef SSTVRX_ENABLE_LOGGER +#define SSTVRX_ENABLE_LOGGER 0 +#endif + +using namespace sstv; + +namespace ui::external_app::sstvrx { + +#define FMR_BTNGRID_TOP 60 + +#if SSTVRX_ENABLE_LOGGER +class SstvRxLogger { + public: + Optional append(const std::filesystem::path& filename) { + return log_file.append(filename); + } + + void log_error(const std::string& error_message); + void log_info(const std::string& info_message); + + private: + LogFile log_file{}; +}; +#endif + +class SstvRxView : public ui::View { + public: + SstvRxView(ui::NavigationView& nav); + SstvRxView& operator=(const SstvRxView&) = delete; + SstvRxView(const SstvRxView&) = delete; + ~SstvRxView(); + + std::string title() { return "SSTV RX"; } + void focus() override; + void on_show() override; + + private: + ui::NavigationView& nav_; +#if SSTVRX_ENABLE_LOGGER + std::unique_ptr logger{}; +#endif + + // Phase and slant adjustments (runtime only, not persisted) + int16_t phase_adjustment{0}; // Horizontal offset in pixels (-50 to +50) + int16_t slant_adjustment{0}; // Timing adjustment in 0.1% units (-100 to +100) + + // Settings must be declared before UI controls + app_settings::SettingsManager settings_{ + "rx_sstv", + app_settings::Mode::RX}; + + ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio; + AudioSpectrum* audio_spectrum_data{nullptr}; + int16_t audio_spectrum[128]{0}; + RxRadioState radio_state_{}; + audio::Rate audio_sampling_rate = audio::Rate::Hz_48000; + uint8_t radio_bw = 0; + bool is_receiving = false; + const sstv_mode* rx_sstv_mode{}; + + // Image data storage - only store current line to save memory + static constexpr uint16_t IMAGE_WIDTH = 320; + static constexpr uint16_t IMAGE_HEIGHT = 256; + static constexpr uint16_t PIXELS_PER_LINE = 320; + uint16_t DISPLAY_WIDTH = 240; // Scaled display width + uint16_t DISPLAY_HEIGHT = 192; // Scaled display height + static constexpr uint16_t SSTV_IMG_START_ROW = 7; // Start drawing at row 7 (after controls) + static constexpr size_t SHARED_BUFFER_BYTES = 512; + static constexpr size_t CHUNK_FLAG_INDEX = SHARED_BUFFER_BYTES - 1; + static constexpr size_t CHUNK_HEADER_BYTES = 2; + static constexpr size_t CHUNK_COPY_BYTES = CHUNK_FLAG_INDEX; // Exclude flag byte + static constexpr uint16_t MAX_CHUNK_PIXELS = (CHUNK_COPY_BYTES - CHUNK_HEADER_BYTES) / 3; + + uint16_t current_line_rx{0}; + BMPFile bmp{}; + std::filesystem::path current_image_path{}; + ui::Color line_buffer[320]; + uint16_t line_num{0}, file_line_num{0}; + std::array pending_line_rgb{}; + uint16_t pending_line_number{0}; + uint8_t pending_chunk_mask{0}; + bool pending_line_valid{false}; + + // Note: Post-reception phase/slant adjustment disabled due to M0 memory constraints + // The 245KB image buffer exceeds available heap memory + uint16_t max_received_line{0}; + + MessageHandlerRegistration message_handler_progress{ + Message::ID::SSTVRXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_progress(message.line, message.total_lines); + }}; + + MessageHandlerRegistration message_handler_calibration{ + Message::ID::SSTVRXCalibration, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_calibration(message.suggested_phase, message.suggested_slant, message.sync_count); + }}; + + // UI Elements + RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}}; + LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}}; + VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}}; + + RSSI rssi{{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}}; + Channel channel{{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}}; + RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_}; + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; + + OptionsField options_mode{{UI_POS_X(6), UI_POS_Y(1)}, 16, {}}; + + NumberField field_phase{{UI_POS_X(4), UI_POS_Y(2)}, 3, {-50, 50}, 1, ' '}; + NumberField field_slant{{UI_POS_X(13), UI_POS_Y(2)}, 4, {-100, 100}, 1, ' '}; + + Labels labels{ + {{UI_POS_X(1), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(1), UI_POS_Y(2)}, "Ph:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(8), UI_POS_Y(2)}, "Slnt:", Theme::getInstance()->fg_light->foreground}}; + + Audio audio{{UI_POS_X(21), 10, UI_POS_WIDTH(6), 4}}; + ui::Button start_stop_btn{{UI_POS_X_RIGHT(12), UI_POS_Y(3), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Start RX"}; + // ui::Button redraw_btn{{16 * 8, UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "Redraw"}; + + // Calibration suggestion display + Text text_calibration{ + {UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH(17), UI_POS_HEIGHT(1)}, + "Calib: N/A"}; + + void on_audio_spectrum(); + void update_display(uint16_t line_num, const uint8_t* rgb_line); + void redraw_image(); // Disabled due to memory constraints + void start_audio(); + void on_start_stop(); // Combined start/stop handler + void on_stop(); + void on_mode_changed(const size_t index); + void on_progress(uint16_t line, uint16_t total_lines); + void on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count); + void write_bmp_header(); + void write_line_to_file(uint16_t line_num, const uint8_t* rgb_line); + void finish_image(); +}; + +} // namespace ui::external_app::sstvrx + +#endif // __SSTVRX_H__ \ No newline at end of file diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 9c8cb2bed..5d967f1fb 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -651,6 +651,13 @@ set(MODE_CPPSRC ) DeclareTargets(PSTX sstvtx) +### SSTV RX + +set(MODE_CPPSRC + proc_sstvrx.cpp +) +DeclareTargets(PSRX sstvrx) + ### TPMS set(MODE_CPPSRC diff --git a/firmware/baseband/proc_sstvrx.cpp b/firmware/baseband/proc_sstvrx.cpp new file mode 100644 index 000000000..5e97e5118 --- /dev/null +++ b/firmware/baseband/proc_sstvrx.cpp @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2025 StarVore Labs + * + * 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_sstvrx.hpp" +#include "event_m4.hpp" +#include "portapack_shared_memory.hpp" +#include "audio_dma.hpp" +#include "sine_table_int8.hpp" +#include "fxpt_atan2.hpp" +#include "message.hpp" + +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace { +constexpr size_t sstv_shared_buffer_bytes = sizeof(shared_memory.bb_data.data); +constexpr size_t sstv_chunk_flag_index = sstv_shared_buffer_bytes - 1; // Reserve last byte as ownership flag +constexpr size_t sstv_chunk_header_bytes = 2; +constexpr size_t sstv_chunk_copy_bytes = sstv_shared_buffer_bytes - 1; // Bytes copied to M0 (excludes flag) +constexpr uint16_t sstv_max_chunk_pixels = (sstv_chunk_copy_bytes - sstv_chunk_header_bytes) / 3; + +inline volatile uint8_t& chunk_flag() { + return *reinterpret_cast(&shared_memory.bb_data.data[sstv_chunk_flag_index]); +} + +inline void wait_for_chunk_slot() { + while (chunk_flag() != 0) { + __asm__ volatile("nop"); + } +} + +inline void mark_chunk_ready() { + chunk_flag() = 1; +} + +inline const sstv_mode* find_mode_by_vis_code(const uint8_t vis_code) { + for (const auto& mode : sstv_modes) { + if (mode.vis_code == vis_code) { + return &mode; + } + } + return nullptr; +} + +inline std::array color_order_for_mode(const sstv_mode& mode) { + switch (mode.color_sequence) { + case SSTV_COLOR_RGB: + return {0, 1, 2}; + case SSTV_COLOR_GBR: + return {1, 2, 0}; + default: + return {0, 1, 2}; + } +} +} // namespace + +void SSTVRXProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) { + // Just return silently if not configured + return; + } + + // Decimation chain (same as NFM) + 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 = channel_filter.execute(decim_1_out, dst_buffer); + feed_channel_stats(channel); + // FM demodulation and audio processing + // Demodulator outputs 24kHz audio after channel filter decimation + auto audio = demod.execute(channel, work_audio_buffer); + + // Feed audio samples to output and use for frequency estimation + audio_output.write(audio); + + // Process each audio sample for SSTV decoding + // audio is buffer_s16_t, so audio.p[i] is int16_t + for (size_t i = 0; i < audio.count; i++) { + // Get int16 audio sample directly (no float conversion needed) + int32_t audio_sample = audio.p[i]; + + // Increment global sample counter for calibration + global_sample_count++; + + // Estimate frequency using Goertzel algorithm on the audio tones + estimate_frequency_goertzel(audio_sample); + + // Process based on current state + switch (state) { + case STATE_SYNC_SEARCH: + // Before Line 0: wait for initial sync pulses to establish timing + if (current_line == 0) { + detect_sync(current_freq); + } + // After Line 0 started: we're at end of a line, waiting for next sync + // Just wait - the sync will be detected and we'll transition to separator + else { + detect_sync(current_freq); + } + break; + + case STATE_VIS_DECODE: + // VIS code detection not implemented yet + // Skip directly to separator wait + state = STATE_SEPARATOR; + sample_count = 0; + break; + + case STATE_SEPARATOR: + // Wait for separator/porch tone to finish before resuming pixels + sample_count++; + if (separator_target == 0 || sample_count >= separator_target) { + sample_count = 0; + state = STATE_IMAGE_DATA; + } + break; + + case STATE_IMAGE_DATA: + // Process pixels continuously + process_pixel_sample(current_freq); + break; + } + } +} + +// Estimate frequency from audio samples using Goertzel algorithm +void SSTVRXProcessor::estimate_frequency_goertzel(int32_t audio_sample) { + // Normalize sample to float [-1.0, 1.0] + float sample = audio_sample / 32768.0f; + + // Update Goertzel filters for each target frequency + for (int f = 0; f < 4; f++) { + float Q0 = goertzel_coeff[f] * goertzel_Q1[f] - goertzel_Q2[f] + sample; + goertzel_Q2[f] = goertzel_Q1[f]; + goertzel_Q1[f] = Q0; + } + + goertzel_count++; + + // Calculate magnitudes every N samples + if (goertzel_count >= GOERTZEL_N) { + float magnitudes[4]; + + for (int f = 0; f < 4; f++) { + // Calculate magnitude^2 (we don't need sqrt for comparison) + magnitudes[f] = goertzel_Q1[f] * goertzel_Q1[f] + + goertzel_Q2[f] * goertzel_Q2[f] - + goertzel_Q1[f] * goertzel_Q2[f] * goertzel_coeff[f]; + + // Reset for next block + goertzel_Q1[f] = 0; + goertzel_Q2[f] = 0; + } + + // Find which frequency has the strongest response + int max_idx = 0; + float max_mag = magnitudes[0]; + for (int f = 1; f < 4; f++) { + if (magnitudes[f] > max_mag) { + max_mag = magnitudes[f]; + max_idx = f; + } + } + + // Map index to frequency + // 0=1200Hz, 1=1500Hz, 2=1900Hz, 3=2300Hz + const int freqs[4] = {1200, 1500, 1900, 2300}; + + // Check if we have a strong enough signal + // Lowered threshold for weak signals (SSTV often has low audio levels) + if (max_mag > 0.001f) { // Very low threshold - accept weak signals + int freq_est = freqs[max_idx]; + + // Improved linear interpolation between bins + if (max_idx > 0 && magnitudes[max_idx - 1] > 0.0005f) { + float ratio = magnitudes[max_idx - 1] / max_mag; + if (ratio > 0.2f) { + freq_est -= (int)((freqs[max_idx] - freqs[max_idx - 1]) * ratio * 0.5f); + } + } + if (max_idx < 3 && magnitudes[max_idx + 1] > 0.0005f) { + float ratio = magnitudes[max_idx + 1] / max_mag; + if (ratio > 0.2f) { + freq_est += (int)((freqs[max_idx + 1] - freqs[max_idx]) * ratio * 0.5f); + } + } + + // Light smoothing to reduce noise while maintaining responsiveness + current_freq = (current_freq + freq_est) / 2; + } else { + // Signal too weak - don't update frequency (keeps last valid estimate) + // This prevents spurious detections from noise + } + + goertzel_count = 0; + } +} + +// Convert frequency to pixel value (0-255) +int32_t SSTVRXProcessor::freq_to_pixel(int32_t freq) { + // SSTV standard: 1500 Hz = black (0), 2300 Hz = white (255) + if (freq < FREQ_BLACK) freq = FREQ_BLACK; + if (freq > FREQ_WHITE) freq = FREQ_WHITE; + + // Linear mapping + int32_t pixel = ((freq - FREQ_BLACK) * 255) / (FREQ_WHITE - FREQ_BLACK); + + if (pixel < 0) pixel = 0; + if (pixel > 255) pixel = 255; + + return pixel; +} + +// Detect horizontal sync pulses +void SSTVRXProcessor::detect_sync(int32_t freq) { + // Sync pulse is 1200 Hz for ~9ms + const int32_t sync_tolerance = 150; // Hz - tolerance for sync detection + + // Check for sync frequency (1200 Hz ± 150 Hz) + if (freq > (FREQ_SYNC - sync_tolerance) && freq < (FREQ_SYNC + sync_tolerance)) { + sync_sample_count++; + in_sync = true; + } else { + // Not sync frequency - check if we just finished a valid sync + // Require at least 1/3 of expected sync duration (more lenient for weak signals) + if (in_sync && sync_sample_count >= (samples_per_sync / 3)) { + // Valid sync pulse detected - always record it for timing tracking + // Debug: log current history count before recording + SSTVRXProgressMessage pre_count_msg{0xFFF7, sync_history_count}; + shared_memory.application_queue.push(pre_count_msg); + + if (sync_history_count < MAX_SYNC_HISTORY) { + sync_positions[sync_history_count] = global_sample_count; + sync_history_count++; + + // Send debug message with sync count + SSTVRXProgressMessage sync_debug{0xFFFD, sync_history_count}; + shared_memory.application_queue.push(sync_debug); + + // Check if this sync should be used for calibration (reject outliers) + bool use_for_calibration = true; + if (sync_history_count > 1) { + uint32_t interval = sync_positions[sync_history_count - 1] - sync_positions[sync_history_count - 2]; + const uint32_t nominal_interval = compute_nominal_line_interval(); + if (nominal_interval == 0) { + use_for_calibration = false; + } else { + const uint32_t tolerance = nominal_interval / 4; + const uint32_t min_interval = (nominal_interval > tolerance) ? (nominal_interval - tolerance) : 0; + const uint32_t max_interval = nominal_interval + tolerance; + if (interval < min_interval || interval > max_interval) { + use_for_calibration = false; // Don't use this sync for calibration + // Debug: Send outlier rejection message (use 0xFFF8 for interval value) + SSTVRXProgressMessage outlier_msg{0xFFF8, (uint16_t)(interval & 0xFFFF)}; + shared_memory.application_queue.push(outlier_msg); + } + } + } + + // Calculate calibration after collecting enough syncs for accuracy + // Wait for 8 syncs to get better statistics, then update every 8 syncs + if (use_for_calibration && sync_history_count >= 8 && pixel_time_frac != 0.0f && sync_history_count % 8 == 0) { + calculate_calibration(); + } + } else { + // Debug: MAX_SYNC_HISTORY exceeded + SSTVRXProgressMessage max_reached_msg{0xFFF6, sync_history_count}; + shared_memory.application_queue.push(max_reached_msg); + } + + // Debug: Send sync detection info with timing data + // Also send current frequency estimate for debugging + SSTVRXProgressMessage debug_msg{0xFFFE, (uint16_t)sync_sample_count}; + shared_memory.application_queue.push(debug_msg); + + // Send frequency estimate for debugging (use 0xFFF9) + SSTVRXProgressMessage freq_msg{0xFFF9, (uint16_t)current_freq}; + shared_memory.application_queue.push(freq_msg); + + bool ready_for_line = false; + if (waiting_for_first_line) { + if (sync_history_count >= 2) { + waiting_for_first_line = false; + ready_for_line = true; + SSTVRXProgressMessage start_msg{0xFFF4, static_cast(sync_sample_count)}; + shared_memory.application_queue.push(start_msg); + } + } else if (state == STATE_SYNC_SEARCH) { + ready_for_line = true; + } + + if (ready_for_line) { + begin_line_after_sync(); + } + // else: Line 0 without enough syncs, or mid-image but not in SYNC_SEARCH - just track the sync + } + in_sync = false; + sync_sample_count = 0; + } +} + +// Calculate phase and slant calibration from sync timing +void SSTVRXProcessor::calculate_calibration() { + if (sync_history_count < 2 || pixel_time_frac == 0.0f) return; + + expected_sync_interval = compute_nominal_line_interval(); + if (expected_sync_interval == 0) { + return; + } + + // Send debug info about expected interval + SSTVRXProgressMessage debug_interval{0xFFFC, (uint16_t)(expected_sync_interval & 0xFFFF)}; + shared_memory.application_queue.push(debug_interval); + + // Calculate average timing error (slant) from recent intervals + // Use last 8 intervals for more responsive calibration, but filter outliers + int32_t total_timing_error = 0; + uint32_t last_interval = 0; + uint16_t start_idx = (sync_history_count > 8) ? (sync_history_count - 8) : 1; + uint16_t interval_count = 0; + + for (uint16_t i = start_idx; i < sync_history_count; i++) { + uint32_t actual_interval = sync_positions[i] - sync_positions[i - 1]; + last_interval = actual_interval; + + // Filter out outliers: reject intervals >20% off expected value + // These are likely missed syncs, not actual timing drift + int32_t timing_error = (int32_t)actual_interval - (int32_t)expected_sync_interval; + int32_t max_deviation = (int32_t)expected_sync_interval / 5; // 20% threshold + + // Only include intervals within ±20% of expected + if (timing_error >= -max_deviation && timing_error <= max_deviation) { + total_timing_error += timing_error; + interval_count++; + } + } + + // Send debug info about last actual interval + SSTVRXProgressMessage debug_actual{0xFFFB, (uint16_t)(last_interval & 0xFFFF)}; + shared_memory.application_queue.push(debug_actual); + + if (interval_count == 0) return; // Safety check - no valid intervals + + // Average error per line + int32_t avg_error = total_timing_error / interval_count; + + // Convert to slant adjustment (0.1% units) + // Error in samples / expected_sync_interval = fractional error + // Multiply by 1000 to get 0.1% units + int16_t suggested_slant = (int16_t)(((int64_t)avg_error * 1000) / expected_sync_interval); + + // Clamp to reasonable range (±10% = ±100 in 0.1% units) + if (suggested_slant > 100) suggested_slant = 100; + if (suggested_slant < -100) suggested_slant = -100; + + // Phase is harder to detect automatically without knowing absolute position + // For now, we only suggest slant correction + int16_t suggested_phase = 0; + + // Send calibration suggestion + SSTVRXCalibrationMessage cal_msg{suggested_phase, suggested_slant, sync_history_count}; + shared_memory.application_queue.push(cal_msg); +} + +uint32_t SSTVRXProcessor::compute_nominal_line_interval() const { + const uint32_t channel_sections = (channel_count > 0) ? channel_count : 1U; + const uint32_t gap_sections = (samples_per_gap == 0) + ? 0U + : ((active_mode && active_mode->gaps) ? channel_sections : 1U); + const float samples_per_channel_f = pixel_time_frac * static_cast(PIXELS_PER_LINE); + const float rounded_channel = std::round(samples_per_channel_f); + const uint32_t samples_per_channel = static_cast(std::max(1.0f, rounded_channel)); + const uint32_t total_channel_samples = samples_per_channel * channel_sections; + const uint32_t total_gap_samples = samples_per_gap * gap_sections; + return samples_per_sync + total_gap_samples + total_channel_samples; +} + +// Process pixel samples during image data state +void SSTVRXProcessor::process_pixel_sample(int32_t freq) { + // Accumulate frequency samples for averaging + pixel_accumulator += freq; + pixel_sample_count++; + + // Advance pixel phase (1.0 per sample, adjusted by slant) + pixel_phase += slant_factor; + + // Check if we've accumulated enough samples for one or more pixels + // pixel_time_frac is the number of audio samples per pixel for the current mode + // Use a loop to handle cases where pixel_phase exceeds pixel_time_frac by more than one pixel + while (pixel_phase >= pixel_time_frac && pixel_index < PIXELS_PER_LINE) { + // Pixel complete - calculate average frequency + // Prevent division by zero + int32_t avg_freq; + if (pixel_sample_count > 0) { + avg_freq = pixel_accumulator / pixel_sample_count; + } else { + avg_freq = freq; // Use current frequency if no samples accumulated + } + + // Convert to pixel value + uint8_t pixel_value = freq_to_pixel(avg_freq); + + // Apply phase offset (horizontal shift) and clamp to prevent out-of-bounds writes + // Clamping prevents pixels from wrapping around and causing duplication + int32_t adjusted_pixel_index = (int32_t)pixel_index + phase_offset; + if (adjusted_pixel_index < 0) { + adjusted_pixel_index = 0; + } else if (adjusted_pixel_index >= PIXELS_PER_LINE) { + adjusted_pixel_index = PIXELS_PER_LINE - 1; + } + + store_pixel_value(channel_index, static_cast(adjusted_pixel_index), pixel_value); + + pixel_index++; + // Reset accumulator for next pixel + // If this is not the last pixel in the loop, subsequent pixels will use current sample + pixel_accumulator = freq; + pixel_sample_count = 1; + pixel_phase -= pixel_time_frac; // Keep fractional part for next pixel + + // Check if we finished a color channel + if (pixel_index >= PIXELS_PER_LINE) { + pixel_index = 0; + + const bool last_channel = ((channel_index + 1) >= channel_count); + if (last_channel) { + process_line(); + channel_index = 0; + state = STATE_SYNC_SEARCH; + sync_sample_count = 0; + in_sync = false; + reset_pixel_state(); + break; + } else { + channel_index++; + reset_pixel_state(); + if (channel_gap_samples > 0) { + start_gap(channel_gap_samples); + } else { + state = STATE_IMAGE_DATA; + } + break; + } + } + } +} + +void SSTVRXProcessor::process_line() { + if (current_line >= mode_total_lines) current_line = 1; // reset, maybe a new image + if (mode_total_lines == 0) return; // not set + + const uint16_t first_chunk_pixels = (PIXELS_PER_LINE < sstv_max_chunk_pixels) ? PIXELS_PER_LINE : sstv_max_chunk_pixels; + const uint16_t remaining_pixels = (PIXELS_PER_LINE > sstv_max_chunk_pixels) ? (PIXELS_PER_LINE - sstv_max_chunk_pixels) : 0; + + auto write_chunk = [&](const uint16_t encoded_line, const uint16_t start_pixel, const uint16_t pixel_count) { + if (pixel_count == 0) { + return; + } + + wait_for_chunk_slot(); + + uint8_t* data_ptr = shared_memory.bb_data.data; + data_ptr[0] = encoded_line & 0xFF; + data_ptr[1] = (encoded_line >> 8) & 0xFF; + + for (uint16_t i = 0; i < pixel_count; i++) { + const uint16_t src_idx = start_pixel + i; + const size_t dst = sstv_chunk_header_bytes + i * 3; + data_ptr[dst + 0] = line_buffer_r[src_idx]; + data_ptr[dst + 1] = line_buffer_g[src_idx]; + data_ptr[dst + 2] = line_buffer_b[src_idx]; + } + + mark_chunk_ready(); + SSTVRXProgressMessage progress_message{encoded_line, mode_total_lines}; + shared_memory.application_queue.push(progress_message); + }; + + write_chunk(static_cast(current_line * 2), 0, first_chunk_pixels); + + if (remaining_pixels) { + write_chunk(static_cast(current_line * 2 + 1), first_chunk_pixels, remaining_pixels); + } + + current_line++; +} + +void SSTVRXProcessor::on_message(const Message* const msg) { + switch (msg->id) { + case Message::ID::CaptureConfig: + capture_config(*reinterpret_cast(msg)); + break; + + case Message::ID::SSTVRXPhaseSlant: { + const auto message = *reinterpret_cast(msg); + phase_offset = message.phase; + slant_rate = message.slant; + // Convert slant from 0.1% units to a multiplier + // slant_rate of +10 = +1% faster = multiply by 1.01 + slant_factor = 1.0f + (slant_rate / 1000.0f); + break; + } + + case Message::ID::SSTVRXConfigure: { + const auto message = *reinterpret_cast(msg); + vis_code = message.code; + + active_mode = find_mode_by_vis_code(message.code); + if (!active_mode) { + configured = false; + SSTVRXProgressMessage error_msg{0xFFFF, 0}; + shared_memory.application_queue.push(error_msg); + break; + } + if (active_mode->pixels != PIXELS_PER_LINE) { + configured = false; + SSTVRXProgressMessage error_msg{0xFFFF, 0}; + shared_memory.application_queue.push(error_msg); + break; + } + mode_total_lines = active_mode->lines; + if (mode_total_lines == 0) { + mode_total_lines = 1; + } + channel_count = static_cast(active_mode->color ? 3U : 1U); + if (channel_count == 0) { + channel_count = 1; + } + color_order = color_order_for_mode(*active_mode); + waiting_for_first_line = true; + + // Configure decimation chain using NFM filters (narrower than WFMAM) + decim_0.configure(taps_11k0_decim_0.taps); // NFM decim0 filter + decim_1.configure(taps_11k0_decim_1.taps); // NFM decim1 filter + channel_filter.configure(taps_11k0_channel.taps, 1); // Keep 48kHz audio for better pixel resolution + + // Calculate filter parameters + const size_t decim_0_input_fs = baseband_fs; + const size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor; + const size_t decim_1_input_fs = decim_0_output_fs; + const size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor; + const size_t channel_filter_output_fs = decim_1_output_fs; // Final rate: 48kHz + + // Configure demodulator for SSTV - use moderate NFM deviation + // SSTV needs wider deviation than voice NFM to capture 1200-2300 Hz tone range + demod.configure(channel_filter_output_fs, 7500); // 7.5kHz deviation (wider for SSTV tones) + // No audio filter needed - we want clean SSTV tones without filtering + // Enable audio output for monitoring with passthrough filters + audio_output.configure(iir_config_passthrough, iir_config_passthrough, 0.0f); + + // Initialize Goertzel coefficients for 24kHz sample rate + // coeff = 2 * cos(2 * PI * freq / sample_rate) + const float sample_rate = static_cast(channel_filter_output_fs); + const float target_freqs[4] = {1200.0f, 1500.0f, 1900.0f, 2300.0f}; + for (int f = 0; f < 4; f++) { + float k = (GOERTZEL_N * target_freqs[f]) / sample_rate; + float omega = (2.0f * M_PI * k) / GOERTZEL_N; + goertzel_coeff[f] = 2.0f * cosf(omega); + goertzel_Q1[f] = 0; + goertzel_Q2[f] = 0; + } + goertzel_count = 0; + + // Initialize state variables + current_freq = 1200; // Default to sync frequency + configured = true; + current_line = 0; + sample_count = 0; + pixel_index = 0; + channel_index = 0; + pixel_accumulator = 0; + pixel_sample_count = 0; + sync_sample_count = 0; + in_sync = false; + state = STATE_SYNC_SEARCH; + separator_target = 0; + clear_line_buffers(); + + // Reset frequency offset calibration + freq_offset = 0; + freq_offset_calibrated = false; + sync_freq_accumulator = 0; + sync_freq_count = 0; + + // Reset sync history for calibration + sync_history_count = 0; + memset(sync_positions, 0, sizeof(sync_positions)); + + // Translate SSTV timing constants (expressed for 3.072MHz TX) to 48kHz RX domain + const float conversion = sample_rate / static_cast(SSTV_SAMPLERATE); + pixel_time_frac = static_cast(active_mode->samples_per_pixel) * conversion; + if (pixel_time_frac < 1.0f) { + pixel_time_frac = 1.0f; + } + samples_per_pixel = static_cast(pixel_time_frac + 0.5f); + + const auto convert_interval = [conversion](uint32_t value) -> uint32_t { + const float samples = static_cast(value) * conversion; + const float rounded = std::round(samples); + const float clamped = std::max(1.0f, rounded); + return static_cast(clamped); + }; + + samples_per_sync = convert_interval(active_mode->samples_per_sync); + samples_per_gap = convert_interval(active_mode->samples_per_gap); + channel_gap_samples = active_mode->gaps ? samples_per_gap : 0; + pixel_phase = 0.0f; + reset_pixel_state(); + shared_memory.bb_data.data[sstv_chunk_flag_index] = 0; + + break; + } + + default: + break; + } +} + +void SSTVRXProcessor::reset_pixel_state() { + pixel_accumulator = 0; + pixel_sample_count = 0; + pixel_phase = 0.0f; +} + +void SSTVRXProcessor::start_gap(const uint32_t duration) { + reset_pixel_state(); + separator_target = duration; + sample_count = 0; + if (duration == 0) { + state = STATE_IMAGE_DATA; + } else { + state = STATE_SEPARATOR; + } +} + +void SSTVRXProcessor::clear_line_buffers() { + std::fill_n(line_buffer_r, PIXELS_PER_LINE, uint8_t{0}); + std::fill_n(line_buffer_g, PIXELS_PER_LINE, uint8_t{0}); + std::fill_n(line_buffer_b, PIXELS_PER_LINE, uint8_t{0}); +} + +void SSTVRXProcessor::begin_line_after_sync() { + pixel_index = 0; + channel_index = 0; + clear_line_buffers(); + start_gap(samples_per_gap); +} + +void SSTVRXProcessor::store_pixel_value(const uint32_t channel, const uint16_t pixel, const uint8_t value) { + if (!active_mode) { + return; + } + + if (!active_mode->color) { + line_buffer_r[pixel] = value; + line_buffer_g[pixel] = value; + line_buffer_b[pixel] = value; + return; + } + + if (channel >= channel_count || channel >= color_order.size()) { + return; + } + + switch (color_order[channel]) { + case 0: + line_buffer_r[pixel] = value; + break; + case 1: + line_buffer_g[pixel] = value; + break; + case 2: + line_buffer_b[pixel] = value; + break; + default: + break; + } +} + +void SSTVRXProcessor::capture_config(const CaptureConfigMessage& message) { + if (message.config) { + audio_output.set_stream(std::make_unique(message.config)); + } else { + audio_output.set_stream(nullptr); + } +} + +int main() { + // Initialize audio DMA + audio::dma::init_audio_out(); + + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_sstvrx.hpp b/firmware/baseband/proc_sstvrx.hpp new file mode 100644 index 000000000..8483547fe --- /dev/null +++ b/firmware/baseband/proc_sstvrx.hpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2025 StarVore Labs + * + * 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_SSTV_RX__ +#define __PROC_SSTV_RX__ + +#include "portapack_shared_memory.hpp" +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "sstv.hpp" + +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" +#include "dsp_iir.hpp" +#include "audio_output.hpp" + +#include + +using namespace sstv; + +class SSTVRXProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const p) override; + + private: + enum state_t { + STATE_SYNC_SEARCH = 0, + STATE_VIS_DECODE, + STATE_SEPARATOR, // Wait for separator pulse (1500Hz) + STATE_IMAGE_DATA + }; + + static constexpr uint32_t MAX_SAMPLES_PER_LINE = 4096; + static constexpr uint16_t PIXELS_PER_LINE = 320; + + // Frequency ranges for SSTV (in Hz) + static constexpr int32_t FREQ_BLACK = 1500; + static constexpr int32_t FREQ_WHITE = 2300; + static constexpr int32_t FREQ_SYNC = 1200; + static constexpr int32_t FREQ_VIS_BIT0 = 1300; + static constexpr int32_t FREQ_VIS_BIT1 = 1100; + + state_t state{STATE_SYNC_SEARCH}; + bool configured{false}; + uint8_t vis_code{0}; + const sstv_mode* active_mode{nullptr}; + uint16_t mode_total_lines{256}; + + static constexpr size_t baseband_fs = 3072000; + + // DSP chain components (using NFM-style decimation for SSTV) + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; // Decimate by 8 (NFM style) + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; // Decimate by 8 + dsp::decimate::FIRAndDecimateComplex channel_filter{}; // Decimate by 2 -> 24kHz + dsp::demodulate::FM demod{}; // FM demodulator + AudioOutput audio_output{}; + + // Buffers + std::array dst{}; + const buffer_c16_t dst_buffer{ + dst.data(), + dst.size()}; + // work_audio_buffer and dst_buffer use the same data pointer + const buffer_s16_t work_audio_buffer{ + (int16_t*)dst.data(), + sizeof(dst) / sizeof(int16_t)}; + + // State variables for Goertzel frequency estimation + int32_t current_freq{1200}; // Current frequency in Hz + + // Goertzel filters for SSTV frequencies; configured at runtime (48kHz today) + // We'll detect 1200Hz, 1500Hz, 1900Hz, and 2300Hz + // Larger block size = better frequency discrimination but slower response + // At 48kHz: 48 samples ≈ 1ms, a little over one cycle of a 1200Hz tone + static constexpr size_t GOERTZEL_N = 48; // Increased from 16 for better accuracy + float goertzel_Q1[4]{0, 0, 0, 0}; + float goertzel_Q2[4]{0, 0, 0, 0}; + float goertzel_coeff[4]; // Calculated in configure + size_t goertzel_count{0}; + + // Line decoding state + uint8_t line_buffer_r[PIXELS_PER_LINE]; + uint8_t line_buffer_g[PIXELS_PER_LINE]; + uint8_t line_buffer_b[PIXELS_PER_LINE]; + + uint32_t sample_count{0}; + uint32_t pixel_index{0}; + uint32_t channel_index{0}; + uint8_t channel_count{3}; + std::array color_order{{1, 2, 0}}; + uint16_t current_line{0}; + bool waiting_for_first_line{true}; + + // Pixel accumulation for averaging + int32_t pixel_accumulator{0}; + uint32_t pixel_sample_count{0}; + + // Fractional pixel timing for accuracy + float pixel_time_frac{0.0f}; // Fractional samples per pixel + float pixel_phase{0.0f}; // Accumulated phase for current pixel + + // Phase and slant adjustments + int16_t phase_offset{0}; // Horizontal offset in pixels + int16_t slant_rate{0}; // Timing adjustment in 0.1% units + float slant_factor{1.0f}; // Calculated slant multiplier + + // Timing parameters (will be set based on mode) + uint32_t samples_per_pixel{7}; // Integer part for quick checks + uint32_t samples_per_sync{216}; // 9ms + uint32_t samples_per_gap{36}; // Gap after sync or between channels + uint32_t channel_gap_samples{36}; // Separators between color sections + uint32_t separator_target{0}; + + // Sync detection + uint32_t sync_sample_count{0}; + bool in_sync{false}; + int32_t sync_freq_sum{0}; // Accumulated frequency during sync pulse + uint32_t sync_freq_samples{0}; // Number of samples in sync pulse for averaging + + // Sync pulse timing tracking for auto-calibration + static constexpr uint32_t MAX_SYNC_HISTORY = 256; // Track all syncs in image + uint32_t sync_positions[MAX_SYNC_HISTORY]; // Sample positions when sync detected + uint16_t sync_history_count{0}; + uint32_t expected_sync_interval{0}; // Expected samples between syncs + int32_t accumulated_phase_error{0}; // Accumulated phase offset in samples + int32_t accumulated_slant_error{0}; // Accumulated timing drift + uint32_t global_sample_count{0}; // Never-reset counter for timing calibration + + // Frequency offset compensation (auto-calibrated from sync pulses) + int32_t freq_offset{0}; + bool freq_offset_calibrated{false}; + int32_t sync_freq_accumulator{0}; + uint32_t sync_freq_count{0}; + + // Helper functions + int32_t freq_to_pixel(int32_t freq); + void process_pixel_sample(int32_t freq); + void process_line(); + void detect_sync(int32_t freq); + void calculate_calibration(); + uint32_t compute_nominal_line_interval() const; + void estimate_frequency_goertzel(int32_t audio_sample); + void capture_config(const CaptureConfigMessage& message); + void reset_pixel_state(); + void start_gap(uint32_t duration); + void begin_line_after_sync(); + void clear_line_buffers(); + void store_pixel_value(uint32_t channel, uint16_t pixel, uint8_t value); + + RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest}; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; +}; + +#endif \ No newline at end of file diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 1301c27a2..c277d50cc 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -141,6 +141,10 @@ class Message { FlexStats = 83, FlexConfigure = 84, FlexDebug = 85, + SSTVRXConfigure = 86, + SSTVRXProgress = 87, + SSTVRXPhaseSlant = 88, + SSTVRXCalibration = 89, MAX }; @@ -1144,6 +1148,62 @@ class SSTVConfigureMessage : public Message { const uint32_t pixel_duration; }; +class SSTVRXConfigureMessage : public Message { + public: + constexpr SSTVRXConfigureMessage( + const uint8_t code) + : Message{id : ID::SSTVRXConfigure}, + code(code) { + } + + const uint8_t code; +}; + +class SSTVRXProgressMessage : public Message { + public: + constexpr SSTVRXProgressMessage( + const uint16_t line, + const uint16_t total_lines) + : Message{ID::SSTVRXProgress}, + line(line), + total_lines(total_lines) { + } + + const uint16_t line; + const uint16_t total_lines; +}; + +class SSTVRXPhaseSlantMessage : public Message { + public: + constexpr SSTVRXPhaseSlantMessage( + const int16_t phase, + const int16_t slant) + : Message{ID::SSTVRXPhaseSlant}, + phase(phase), + slant(slant) { + } + + const int16_t phase; + const int16_t slant; +}; + +class SSTVRXCalibrationMessage : public Message { + public: + constexpr SSTVRXCalibrationMessage( + const int16_t suggested_phase, + const int16_t suggested_slant, + const uint16_t sync_count) + : Message{ID::SSTVRXCalibration}, + suggested_phase(suggested_phase), + suggested_slant(suggested_slant), + sync_count(sync_count) { + } + + const int16_t suggested_phase; // Suggested phase correction in pixels + const int16_t suggested_slant; // Suggested slant correction in 0.1% units + const uint16_t sync_count; // Number of syncs analyzed +}; + class FSKConfigureMessage : public Message { public: constexpr FSKConfigureMessage( diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 1b5591e92..6e10a87c8 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -122,6 +122,7 @@ constexpr image_tag_t image_tag_subghzd{'P', 'S', 'G', 'D'}; constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'}; constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'}; constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'}; +constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'}; constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'}; From f86d3e51f198910710835a675d9b639782a6eff8 Mon Sep 17 00:00:00 2001 From: Totoo Date: Sun, 21 Dec 2025 11:21:55 +0100 Subject: [PATCH 11/13] Subcar (#2893) --- firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 9 + .../application/external/subcarrx/main.cpp | 81 ++++ .../external/subcarrx/ui_subcar.cpp | 459 ++++++++++++++++++ .../external/subcarrx/ui_subcar.hpp | 222 +++++++++ firmware/baseband/CMakeLists.txt | 7 + firmware/baseband/fprotos/c-bmw_v0.hpp | 169 +++++++ firmware/baseband/fprotos/c-fiat_v0.hpp | 204 ++++++++ firmware/baseband/fprotos/c-ford_v0.hpp | 150 ++++++ firmware/baseband/fprotos/c-kia_v0.hpp | 121 +++++ firmware/baseband/fprotos/c-kia_v1.hpp | 192 ++++++++ firmware/baseband/fprotos/c-kia_v2.hpp | 167 +++++++ firmware/baseband/fprotos/c-kia_v3v4.hpp | 185 +++++++ firmware/baseband/fprotos/c-kia_v5.hpp | 176 +++++++ firmware/baseband/fprotos/c-subaru.hpp | 172 +++++++ firmware/baseband/fprotos/c-suzuki.hpp | 120 +++++ firmware/baseband/fprotos/c-vw.hpp | 236 +++++++++ firmware/baseband/fprotos/subcarbase.hpp | 54 +++ firmware/baseband/fprotos/subcarprotos.hpp | 75 +++ firmware/baseband/fprotos/subcartypes.hpp | 30 ++ firmware/baseband/proc_protoview.cpp | 2 +- firmware/baseband/proc_protoview.hpp | 2 +- firmware/baseband/proc_subcar.cpp | 206 ++++++++ firmware/baseband/proc_subcar.hpp | 98 ++++ firmware/baseband/proc_subghzd.hpp | 4 +- firmware/common/message.hpp | 20 + firmware/common/spi_image.hpp | 1 + 27 files changed, 3163 insertions(+), 4 deletions(-) create mode 100644 firmware/application/external/subcarrx/main.cpp create mode 100644 firmware/application/external/subcarrx/ui_subcar.cpp create mode 100644 firmware/application/external/subcarrx/ui_subcar.hpp create mode 100644 firmware/baseband/fprotos/c-bmw_v0.hpp create mode 100644 firmware/baseband/fprotos/c-fiat_v0.hpp create mode 100644 firmware/baseband/fprotos/c-ford_v0.hpp create mode 100644 firmware/baseband/fprotos/c-kia_v0.hpp create mode 100644 firmware/baseband/fprotos/c-kia_v1.hpp create mode 100644 firmware/baseband/fprotos/c-kia_v2.hpp create mode 100644 firmware/baseband/fprotos/c-kia_v3v4.hpp create mode 100644 firmware/baseband/fprotos/c-kia_v5.hpp create mode 100644 firmware/baseband/fprotos/c-subaru.hpp create mode 100644 firmware/baseband/fprotos/c-suzuki.hpp create mode 100644 firmware/baseband/fprotos/c-vw.hpp create mode 100644 firmware/baseband/fprotos/subcarbase.hpp create mode 100644 firmware/baseband/fprotos/subcarprotos.hpp create mode 100644 firmware/baseband/fprotos/subcartypes.hpp create mode 100644 firmware/baseband/proc_subcar.cpp create mode 100644 firmware/baseband/proc_subcar.hpp diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 32a404141..b0b33d87d 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -272,6 +272,10 @@ set(EXTCPPSRC #flex_rx external/flex_rx/main.cpp external/flex_rx/ui_flex_rx.cpp + + #subcarrx + external/subcarrx/main.cpp + external/subcarrx/ui_subcar.cpp ) set(EXTAPPLIST @@ -340,4 +344,5 @@ set(EXTAPPLIST morse_practice adult_toys_controller flex_rx + subcarrx ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index a6f681ed5..6ee5cc178 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -88,6 +88,7 @@ MEMORY ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k + ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k } @@ -484,5 +485,13 @@ SECTIONS KEEP(*(.external_app.app_sstvrx.application_information)); *(*ui*external_app*sstvrx*); } > ram_external_app_sstvrx + + .external_app_subcarrx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_subcarrx.application_information)); + *(*ui*external_app*subcarrx*); + } > ram_external_app_subcarrx + + } diff --git a/firmware/application/external/subcarrx/main.cpp b/firmware/application/external/subcarrx/main.cpp new file mode 100644 index 000000000..20b0fc0f9 --- /dev/null +++ b/firmware/application/external/subcarrx/main.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2026 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_subcar.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::subcarrx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::subcarrx +extern "C" { + +__attribute__((section(".external_app.app_subcarrx.application_information"), used)) application_information_t _application_information_subcarrx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::subcarrx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "SubCar", + /*.bitmap_data = */ { + 0xC0, + 0x03, + 0xE0, + 0x07, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0xE0, + 0x07, + 0xC0, + 0x03, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x07, + 0x80, + 0x03, + 0x80, + 0x07, + 0x80, + 0x01, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'S', 'C', 'D'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/subcarrx/ui_subcar.cpp b/firmware/application/external/subcarrx/ui_subcar.cpp new file mode 100644 index 000000000..cfd6bdef0 --- /dev/null +++ b/firmware/application/external/subcarrx/ui_subcar.cpp @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2026 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_subcar.hpp" +#include "audio.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "file_path.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; +using namespace ui; + +namespace ui::external_app::subcarrx { + +std::string SubCarRecentEntry::to_csv() { + std::string csv = ";"; + csv += SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)sensorType); + csv += ";" + to_string_dec_uint(bits) + ";"; + csv += to_string_hex(data, 64 / 4) + ";" + to_string_hex(data2, 64 / 4); + return csv; +} + +void SubCarLogger::log_data(SubCarRecentEntry& data) { + log_file.write_entry(data.to_csv()); +} + +void SubCarRecentEntryDetailView::update_data() { + // process protocol data + parseProtocol(); + // set text elements + text_type.set(SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry_.sensorType)); + + text_id.set("0x" + to_string_hex(serial)); + if (entry_.bits > 0) console.writeln("Bits: " + to_string_dec_uint(entry_.bits)); + if (!btn.empty()) console.writeln("Btn: " + btn); + if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt)); + if (entry_.data != 0) console.writeln("Data : " + to_string_hex(entry_.data)); + if (entry_.data2 != 0) console.writeln("Data2: " + to_string_hex(entry_.data2)); +} + +SubCarRecentEntryDetailView::SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry) + : nav_{nav}, + entry_{entry} { + add_children({&button_done, + &text_type, + &text_id, + &console, + &labels}); + + button_done.on_select = [&nav](const ui::Button&) { + nav.pop(); + }; + update_data(); +} + +void SubCarRecentEntryDetailView::focus() { + button_done.focus(); +} + +void SubCarView::focus() { + field_frequency.focus(); +} + +SubCarView::SubCarView(NavigationView& nav) + : nav_{nav} { + add_children({&rssi, + &channel, + &field_rf_amp, + &field_lna, + &field_vga, + &field_frequency, + &button_clear_list, + &check_log, + &labels, + &recent_entries_view}); + + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + logger = std::make_unique(); + + button_clear_list.on_select = [this](Button&) { + recent.clear(); + recent_entries_view.set_dirty(); + }; + field_frequency.set_step(10000); + check_log.on_select = [this](Checkbox&, bool v) { + logging = v; + if (logger && logging) { + logger->append(logs_dir.string() + "/SubCarLOG_" + to_string_timestamp(rtc_time::now()) + ".CSV"); + logger->write_header(); + } + }; + check_log.set_value(logging); + const Rect content_rect{0, header_height, screen_width, screen_height - header_height}; + recent_entries_view.set_parent_rect(content_rect); + recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) { + nav_.push(entry); + }; + baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am + receiver_model.enable(); + signal_token_tick_second = rtc_time::signal_tick_second += [this]() { + on_tick_second(); + }; +} + +void SubCarView::on_tick_second() { + for (auto& entry : recent) { + entry.inc_age(1); + } + recent_entries_view.set_dirty(); +} + +void SubCarView::on_data(const SubCarDataMessage* data) { + SubCarRecentEntry key{data->sensorType, data->data, data->data2, data->bits}; + if (logger && logging) { + logger->log_data(key); + } + auto matching_recent = find(recent, key.key()); + if (matching_recent != std::end(recent)) { + // Found within. Move to front of list, increment counter. + (*matching_recent).reset_age(); + recent.push_front(*matching_recent); + recent.erase(matching_recent); + } else { + recent.emplace_front(key); + truncate_entries(recent, 64); + } + recent_entries_view.set_dirty(); +} + +SubCarView::~SubCarView() { + rtc_time::signal_tick_second -= signal_token_tick_second; + receiver_model.disable(); + baseband::shutdown(); +} + +const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) { + switch (type) { + case FPC_SUZUKI: + return "Suzuki"; + case FPC_VW: + return "VW"; + case FPC_SUBARU: + return "Subaru"; + case FPC_KIAV5: + return "Kia V5"; + case FPC_KIAV3V4: + return "Kia V3/V4"; + case FPC_KIAV2: + return "Kia V2"; + case FPC_KIAV1: + return "Kia V1"; + case FPC_KIAV0: + return "Kia V0"; + case FPC_FORDV0: + return "Ford V0"; + case FPC_FIATV0: + return "Fiat V0"; + case FPC_BMWV0: + return "BMW V0"; + + case FPC_Invalid: + default: + return "Unknown"; + } +} + +std::string SubCarView::pad_string_with_spaces(int snakes) { + std::string paddedStr(snakes, ' '); + return paddedStr; +} + +void SubCarView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +void subaru_decode_count(const uint8_t* KB, uint16_t* count) { + uint8_t lo = 0; + if ((KB[4] & 0x40) == 0) + lo |= 0x01; + if ((KB[4] & 0x80) == 0) + lo |= 0x02; + if ((KB[5] & 0x01) == 0) + lo |= 0x04; + if ((KB[5] & 0x02) == 0) + lo |= 0x08; + if ((KB[6] & 0x01) == 0) + lo |= 0x10; + if ((KB[6] & 0x02) == 0) + lo |= 0x20; + if ((KB[5] & 0x40) == 0) + lo |= 0x40; + if ((KB[5] & 0x80) == 0) + lo |= 0x80; + + uint8_t REG_SH1 = (KB[7] << 4) & 0xF0; + if (KB[5] & 0x04) + REG_SH1 |= 0x04; + if (KB[5] & 0x08) + REG_SH1 |= 0x08; + if (KB[6] & 0x80) + REG_SH1 |= 0x02; + if (KB[6] & 0x40) + REG_SH1 |= 0x01; + + uint8_t REG_SH2 = ((KB[6] << 2) & 0xF0) | ((KB[7] >> 4) & 0x0F); + + uint8_t SER0 = KB[3]; + uint8_t SER1 = KB[1]; + uint8_t SER2 = KB[2]; + + uint8_t total_rot = 4 + lo; + for (uint8_t i = 0; i < total_rot; ++i) { + uint8_t t_bit = (SER0 >> 7) & 1; + SER0 = ((SER0 << 1) & 0xFE) | ((SER1 >> 7) & 1); + SER1 = ((SER1 << 1) & 0xFE) | ((SER2 >> 7) & 1); + SER2 = ((SER2 << 1) & 0xFE) | t_bit; + } + + uint8_t T1 = SER1 ^ REG_SH1; + uint8_t T2 = SER2 ^ REG_SH2; + + uint8_t hi = 0; + if ((T1 & 0x10) == 0) + hi |= 0x04; + if ((T1 & 0x20) == 0) + hi |= 0x08; + if ((T2 & 0x80) == 0) + hi |= 0x02; + if ((T2 & 0x40) == 0) + hi |= 0x01; + if ((T1 & 0x01) == 0) + hi |= 0x40; + if ((T1 & 0x02) == 0) + hi |= 0x80; + if ((T2 & 0x08) == 0) + hi |= 0x20; + if ((T2 & 0x04) == 0) + hi |= 0x10; + *count = ((hi << 8) | lo) & 0xFFFF; +} + +void SubCarRecentEntryDetailView::parseProtocol() { + btn = ""; + cnt = SD_NO_CNT; + serial = 0; + + if (entry_.sensorType == FPC_Invalid) return; + + if (entry_.sensorType == FPC_SUZUKI) { + uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12); + serial = serial_button >> 4; + uint8_t buttonid = serial_button & 0xF; + cnt = (entry_.data >> 44) & 0xFFFF; + btn = to_string_dec_uint(buttonid); + return; + } + if (entry_.sensorType == FPC_VW) { + // uint32_t key_high = (entry_.data >> 32) & 0xFFFFFFFF; + uint32_t key_low = entry_.data & 0xFFFFFFFF; + serial = key_low; // trimmed to 32 bits for VW + uint8_t check = entry_.data2 & 0xFF; + uint8_t btnid = (check >> 4) & 0xF; + switch (btnid) { + case 0x1: + btn = "UNLOCK"; + break; + case 0x2: + btn = "LOCK"; + break; + case 0x3: + btn = "Un+Lk"; + break; + case 0x4: + btn = "TRUNK"; + break; + case 0x5: + btn = "Un+Tr"; + break; + case 0x6: + btn = "Lk+Tr"; + break; + case 0x7: + btn = "Un+Lk+Tr"; + break; + case 0x8: + btn = "PANIC"; + break; + default: + btn = "Unknown"; + break; + } + } + if (entry_.sensorType == FPC_SUBARU) { + uint8_t* data_bytes = (uint8_t*)entry_.data; + serial = ((uint32_t)data_bytes[1] << 16) | ((uint32_t)data_bytes[2] << 8) | data_bytes[3]; + uint8_t button = data_bytes[0] & 0x0F; + btn = to_string_dec_uint(button); + uint16_t cnttmp = 0; + subaru_decode_count(data_bytes, &cnttmp); + cnt = cnttmp; + } + + if (entry_.sensorType == FPC_KIAV5) { + serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1); + uint8_t button = (entry_.data >> 61) & 0x07; + btn = to_string_dec_uint(button); + cnt = (uint16_t)(entry_.data & 0xFFFF); + } + + if (entry_.sensorType == FPC_KIAV3V4) { + // not decrypted! + serial = SD_NO_SERIAL; //(uint32_t)entry_.data; + // uint8_t button = entry_.data2 & 0xFF; + btn = "?"; // to_string_dec_uint(button); + } + + if (entry_.sensorType == FPC_KIAV2) { + serial = (uint32_t)((entry_.data >> 20) & 0xFFFFFFFF); + uint8_t button = (uint8_t)((entry_.data >> 16) & 0x0F); + uint16_t raw_count = (uint16_t)((entry_.data >> 4) & 0xFFF); + cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF; + btn = to_string_dec_uint(button); + } + + if (entry_.sensorType == FPC_KIAV1) { + serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF); + uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF); + cnt = (uint8_t)((entry_.data >> 8) & 0xFF); + btn = to_string_dec_uint(button); + } + + if (entry_.sensorType == FPC_KIAV0) { + serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF); + uint8_t button = (entry_.data >> 8) & 0x0F; + cnt = (entry_.data >> 40) & 0xFFFF; + btn = to_string_dec_uint(button); + } + if (entry_.sensorType == FPC_FORDV0) { + uint8_t buf[13] = {0}; + + for (int i = 0; i < 8; ++i) { + buf[i] = (uint8_t)(entry_.data >> (56 - i * 8)); + } + + buf[8] = (uint8_t)(entry_.data2 >> 8); + buf[9] = (uint8_t)(entry_.data2 & 0xFF); + uint8_t tmp = buf[8]; + uint8_t parity = 0; + uint8_t parity_any = (tmp != 0); + while (tmp) { + parity ^= (tmp & 1); + tmp >>= 1; + } + buf[11] = parity_any ? parity : 0; + uint8_t xor_byte; + uint8_t limit; + if (buf[11]) { + xor_byte = buf[7]; + limit = 7; + } else { + xor_byte = buf[6]; + limit = 6; + } + + for (int idx = 1; idx < limit; ++idx) { + buf[idx] ^= xor_byte; + } + + if (buf[11] == 0) { + buf[7] ^= xor_byte; + } + + uint8_t orig_b7 = buf[7]; + buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55); + uint8_t mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55); + buf[12] = mixed; + buf[6] = mixed; + + uint32_t serial_le = ((uint32_t)buf[1]) | + ((uint32_t)buf[2] << 8) | + ((uint32_t)buf[3] << 16) | + ((uint32_t)buf[4] << 24); + + serial = ((serial_le & 0xFF) << 24) | + (((serial_le >> 8) & 0xFF) << 16) | + (((serial_le >> 16) & 0xFF) << 8) | + ((serial_le >> 24) & 0xFF); + + uint8_t button = (buf[5] >> 4) & 0x0F; + + cnt = ((buf[5] & 0x0F) << 16) | + (buf[6] << 8) | + buf[7]; + btn = to_string_dec_uint(button); + } + + if (entry_.sensorType == FPC_FIATV0) { + serial = (uint32_t)(entry_.data & 0xFFFFFFFF); + cnt = (uint32_t)((entry_.data >> 32) & 0xFFFFFFFF); + uint8_t button = (uint8_t)(entry_.data2 & 0xFF); + btn = to_string_dec_uint(button); + } + + if (entry_.sensorType == FPC_BMWV0) { + serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF); + uint8_t button = (entry_.data >> 8) & 0x0F; + cnt = (entry_.data >> 40) & 0xFFFF; + btn = to_string_dec_uint(button); + } + + return; +} + +} // namespace ui::external_app::subcarrx + +namespace ui { + +template <> +void RecentEntriesTable::draw( + const Entry& entry, + const Rect& target_rect, + Painter& painter, + const Style& style, + ui::RecentEntriesColumns& columns) { + std::string line{}; + line.reserve(30); + + line = ui::external_app::subcarrx::SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry.sensorType); + line = line + " " + to_string_hex(entry.data << 32); + line.resize(columns.at(0).second, ' '); + std::string ageStr = to_string_dec_uint(entry.age); + std::string bitsStr = to_string_dec_uint(entry.bits); + line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(5 - bitsStr.length()) + bitsStr; + line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(4 - ageStr.length()) + ageStr; + + line.resize(target_rect.width() / 8, ' '); + painter.draw_string(target_rect.location(), style, line); +} + +} // namespace ui \ No newline at end of file diff --git a/firmware/application/external/subcarrx/ui_subcar.hpp b/firmware/application/external/subcarrx/ui_subcar.hpp new file mode 100644 index 000000000..a309fffa1 --- /dev/null +++ b/firmware/application/external/subcarrx/ui_subcar.hpp @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2026 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. + */ +/* + This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside. +*/ + +#ifndef __UI_SubCar_H__ +#define __UI_SubCar_H__ + +#define SD_NO_SERIAL 0xFFFFFFFF +#define SD_NO_BTN 0xFF +#define SD_NO_CNT 0xFF + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "utility.hpp" +#include "log_file.hpp" +#include "recent_entries.hpp" + +#include "../../baseband/fprotos/subcartypes.hpp" + +using namespace ui; + +namespace ui::external_app::subcarrx { + +struct SubCarRecentEntry { + using Key = uint64_t; + static constexpr Key invalid_key = 0x0fffffff; + uint8_t sensorType = FPC_Invalid; + uint16_t bits = 0; + uint16_t age = 0; // updated on each seconds, show how long the signal was last seen + uint64_t data = 0; + uint64_t data2 = 0; + SubCarRecentEntry() {} + SubCarRecentEntry( + uint8_t sensorType, + uint64_t data = 0, + uint64_t data2 = 0, + uint16_t bits = 0) + : sensorType{sensorType}, + bits{bits}, + data{data}, + data2{data2} { + } + Key key() const { + return (data ^ ((static_cast(sensorType) & 0xFF) << 0)); // should be optimized... + } + void inc_age(int delta) { + if (UINT16_MAX - delta > age) age += delta; + } + void reset_age() { + age = 0; + } + + std::string to_csv(); +}; + +class SubCarLogger { + public: + Optional append(const std::filesystem::path& filename) { + return log_file.append(filename); + } + + void log_data(SubCarRecentEntry& data); + void write_header() { + log_file.write_entry(";Type; Bits; Data;"); + } + + private: + LogFile log_file{}; +}; +using SubCarRecentEntries = RecentEntries; +using SubCarRecentEntriesView = RecentEntriesView; + +class SubCarView : public View { + public: + SubCarView(NavigationView& nav); + ~SubCarView(); + + void focus() override; + + std::string title() const override { return "SubCar"; }; + static const char* getSensorTypeName(FPROTO_SUBCAR_SENSOR type); + static std::string pad_string_with_spaces(int snakes); + + private: + void on_tick_second(); + void on_data(const SubCarDataMessage* data); + + NavigationView& nav_; + RxRadioState radio_state_{ + 433'920'000 /* frequency */, + 1'750'000 /* bandwidth */, + 4'000'000 /* sampling rate */, + ReceiverModel::Mode::AMAudio}; + bool logging = false; + app_settings::SettingsManager settings_{ + "rx_subcar", + app_settings::Mode::RX, + { + {"log"sv, &logging}, + }}; + + SubCarRecentEntries recent{}; + + RFAmpField field_rf_amp{ + {13 * 8, UI_POS_Y(0)}}; + LNAGainField field_lna{ + {15 * 8, UI_POS_Y(0)}}; + VGAGainField field_vga{ + {18 * 8, UI_POS_Y(0)}}; + RSSI rssi{ + {21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}}; + Channel channel{ + {21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4}, + }; + RxFrequencyField field_frequency{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + + SignalToken signal_token_tick_second{}; + + Button button_clear_list{ + {0, 16, 7 * 8, 32}, + "Clear"}; + + Checkbox check_log{ + {10 * 8, 18}, + 3, + "Log", + true}; + + Labels labels{ + {{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground}, + }; + + static constexpr auto header_height = 3 * 16; + + std::unique_ptr logger{}; + + ui::RecentEntriesColumns columns{{ + {"Type", 0}, + {"Bits", 4}, + {"Age", 3}, + }}; + SubCarRecentEntriesView recent_entries_view{columns, recent}; + + void on_freqchg(int64_t freq); + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::SubCarData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_data(message); + }}; +}; + +class SubCarRecentEntryDetailView : public View { + public: + SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry); + + void update_data(); + void focus() override; + + private: + NavigationView& nav_; + SubCarRecentEntry entry_{}; + + uint32_t serial = 0; + std::string btn = ""; + uint32_t cnt = SD_NO_CNT; + + Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"}; + Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"}; + + Console console{ + {0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground}, + }; + + Button button_done{ + {screen_width - 96 - 4, screen_height - 32 - 12, 96, 32}, + "Done"}; + + void parseProtocol(); +}; + +} // namespace ui::external_app::subcarrx + +#endif /*__UI_SubCar_H__*/ diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 5d967f1fb..5a92c17ed 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -704,6 +704,13 @@ set(MODE_CPPSRC ) DeclareTargets(PATX audio_tx) +### SubCar Decoders + +set(MODE_CPPSRC + proc_subcar.cpp +) +DeclareTargets(PSCD subcar) + ### HackRF "factory" firmware diff --git a/firmware/baseband/fprotos/c-bmw_v0.hpp b/firmware/baseband/fprotos/c-bmw_v0.hpp new file mode 100644 index 000000000..2be444701 --- /dev/null +++ b/firmware/baseband/fprotos/c-bmw_v0.hpp @@ -0,0 +1,169 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + BMWDecoderStepReset = 0, + BMWDecoderStepCheckPreambula, + BMWDecoderStepSaveDuration, + BMWDecoderStepCheckDuration, +} BMWDecoderStep; + +class FProtoSubCarBMWV0 : public FProtoSubCarBase { + public: + FProtoSubCarBMWV0() { + sensorType = FPC_BMWV0; + te_short = 350; + te_long = 700; + te_delta = 120; + min_count_bit_for_found = 61; + } + uint8_t subghz_protocol_bmw_crc8(uint8_t* data, size_t len) { + uint8_t crc = 0x00; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80) + crc = (uint8_t)((crc << 1) ^ 0x31); + else + crc <<= 1; + } + } + return crc; + } + + uint16_t subghz_protocol_bmw_crc16(uint8_t* data, size_t len) { + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= ((uint16_t)data[i] << 8); + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x8000) + crc = (crc << 1) ^ 0x1021; + else + crc <<= 1; + } + } + return crc; + } + void subghz_protocol_decoder_bmw_reset_internal() { + decode_data = 0; + decode_count_bit = 0; + decode_data2 = 0; + parser_step = BMWDecoderStepReset; + header_count = 0; + crc_type = 0; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case BMWDecoderStepReset: + if (level && (DURATION_DIFF(duration, te_short) < + te_delta)) { + parser_step = BMWDecoderStepCheckPreambula; + te_last = duration; + header_count = 0; + decode_data = 0; + decode_count_bit = 0; + } + break; + + case BMWDecoderStepCheckPreambula: + if (level) { + if ((DURATION_DIFF(duration, te_short) < + te_delta) || + (DURATION_DIFF(duration, te_long) < + te_delta)) { + te_last = duration; + } else { + parser_step = BMWDecoderStepReset; + } + } else if ( + (DURATION_DIFF(duration, te_short) < + te_delta) && + (DURATION_DIFF(te_last, te_short) < + te_delta)) { + header_count++; + } else if ( + (DURATION_DIFF(duration, te_long) < + te_delta) && + (DURATION_DIFF(te_last, te_long) < + te_delta)) { + if (header_count > 15) { + parser_step = BMWDecoderStepSaveDuration; + decode_data = 0ULL; + decode_count_bit = 0; + } else { + parser_step = BMWDecoderStepReset; + } + } else { + parser_step = BMWDecoderStepReset; + } + break; + + case BMWDecoderStepSaveDuration: + if (level) { + if (duration >= + (te_long + te_delta * 2UL)) { + if (decode_count_bit >= + min_count_bit_for_found) { + // instance->generic.data = decode_data; + data_count_bit = decode_count_bit; + + // Perform CRC check with both CRC8 and CRC16 + uint8_t* raw_bytes = (uint8_t*)decode_data; + size_t raw_len = (decode_count_bit + 7) / 8; + uint8_t crc8 = subghz_protocol_bmw_crc8(raw_bytes, raw_len - 1); + if (crc8 == raw_bytes[raw_len - 1]) { + crc_type = 8; + } else { + uint16_t crc16 = subghz_protocol_bmw_crc16(raw_bytes, raw_len - 2); + uint16_t rx_crc16 = (raw_bytes[raw_len - 2] << 8) | raw_bytes[raw_len - 1]; + if (crc16 == rx_crc16) { + crc_type = 16; + } else { + crc_type = 0; // invalid + } + } + + if (crc_type != 0 && callback) { + callback(this); + } + } + subghz_protocol_decoder_bmw_reset_internal(); + } else { + te_last = duration; + parser_step = BMWDecoderStepCheckDuration; + } + } else { + parser_step = BMWDecoderStepReset; + } + break; + + case BMWDecoderStepCheckDuration: + if (!level) { + if ((DURATION_DIFF(te_last, te_short) < + te_delta) && + (DURATION_DIFF(duration, te_short) < + te_delta)) { + subghz_protocol_blocks_add_bit(0); + parser_step = BMWDecoderStepSaveDuration; + } else if ( + (DURATION_DIFF(te_last, te_long) < + te_delta) && + (DURATION_DIFF(duration, te_long) < + te_delta)) { + subghz_protocol_blocks_add_bit(1); + parser_step = BMWDecoderStepSaveDuration; + } else { + parser_step = BMWDecoderStepReset; + } + } else { + parser_step = BMWDecoderStepReset; + } + break; + } + } + + uint16_t header_count = 0; + uint8_t crc_type = 0; +}; diff --git a/firmware/baseband/fprotos/c-fiat_v0.hpp b/firmware/baseband/fprotos/c-fiat_v0.hpp new file mode 100644 index 000000000..589cfff79 --- /dev/null +++ b/firmware/baseband/fprotos/c-fiat_v0.hpp @@ -0,0 +1,204 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + FiatV0DecoderStepReset = 0, + FiatV0DecoderStepPreamble = 1, + FiatV0DecoderStepData = 2, +} FiatV0DecoderStep; + +class FProtoSubCarFiatV0 : public FProtoSubCarBase { + public: + FProtoSubCarFiatV0() { + sensorType = FPC_FIATV0; + te_short = 200; + te_long = 400; + te_delta = 100; + min_count_bit_for_found = 64; + } + + void feed(bool level, uint32_t duration) { + uint32_t gap_threshold = 800; + uint32_t diff; + switch (decoder_state) { + case FiatV0DecoderStepReset: + if (!level) { + return; + } + if (duration < te_short) { + diff = te_short - duration; + } else { + diff = duration - te_short; + } + if (diff < te_delta) { + data_low = 0; + data_high = 0; + decoder_state = FiatV0DecoderStepPreamble; + te_last = duration; + preamble_count = 0; + bit_count = 0; + FProtoGeneral::manchester_advance( + manchester_state, + ManchesterEventReset, + &manchester_state, + NULL); + } + break; + case FiatV0DecoderStepPreamble: + if (level) { + return; + } + if (duration < te_short) { + diff = te_short - duration; + if (diff < te_delta) { + preamble_count++; + te_last = duration; + if (preamble_count >= 0x96) { + if (duration < gap_threshold) { + diff = gap_threshold - duration; + } else { + diff = duration - gap_threshold; + } + if (diff < te_delta) { + decoder_state = FiatV0DecoderStepData; + preamble_count = 0; + data_low = 0; + data_high = 0; + bit_count = 0; + te_last = duration; + return; + } + } + } else { + decoder_state = FiatV0DecoderStepReset; + if (preamble_count >= 0x96) { + if (duration < gap_threshold) { + diff = gap_threshold - duration; + } else { + diff = duration - gap_threshold; + } + if (diff < te_delta) { + decoder_state = FiatV0DecoderStepData; + preamble_count = 0; + data_low = 0; + data_high = 0; + bit_count = 0; + te_last = duration; + return; + } + } + } + } else { + diff = duration - te_short; + if (diff < te_delta) { + preamble_count++; + te_last = duration; + } else { + decoder_state = FiatV0DecoderStepReset; + } + if (preamble_count >= 0x96) { + if (duration >= 799) { + diff = duration - gap_threshold; + } else { + diff = gap_threshold - duration; + } + if (diff < te_delta) { + decoder_state = FiatV0DecoderStepData; + preamble_count = 0; + data_low = 0; + data_high = 0; + bit_count = 0; + te_last = duration; + return; + } + } + } + break; + case FiatV0DecoderStepData: + ManchesterEvent event = ManchesterEventReset; + if (duration < te_short) { + diff = te_short - duration; + if (diff < te_delta) { + event = level ? ManchesterEventShortLow : ManchesterEventShortHigh; + } + } else { + diff = duration - te_short; + if (diff < te_delta) { + event = level ? ManchesterEventShortLow : ManchesterEventShortHigh; + } else { + if (duration < te_long) { + diff = te_long - duration; + } else { + diff = duration - te_long; + } + if (diff < te_delta) { + event = level ? ManchesterEventLongLow : ManchesterEventLongHigh; + } + } + } + + if (event != ManchesterEventReset) { + bool data_bit_bool; + if (FProtoGeneral::manchester_advance( + manchester_state, + event, + &manchester_state, + &data_bit_bool)) { + uint32_t new_bit = data_bit_bool ? 1 : 0; + + uint32_t carry = (data_low >> 31) & 1; + data_low = (data_low << 1) | new_bit; + data_high = (data_high << 1) | carry; + + bit_count++; + + if (bit_count == 0x40) { + fix = data_low; + hop = data_high; + data_low = 0; + data_high = 0; + } + + if (bit_count > 0x46) { + final_count = bit_count; + + endbyte = (uint8_t)data_low; + /* + generic.data = ((uint64_t)hop << 32) | fix; + generic.data_count_bit = 64; + generic.serial = fix; + generic.btn = endbyte; // still exported as btn for UI compatibility + generic.cnt = hop; + */ + decode_data = ((uint64_t)hop << 32) | fix; // this is my own data passer, not the original + decode_data2 = endbyte; + data_count_bit = 64; + if (callback) { + callback(this); + } + + data_low = 0; + data_high = 0; + bit_count = 0; + decoder_state = FiatV0DecoderStepReset; + } + } + } + te_last = duration; + break; + } + } + + ManchesterState manchester_state = ManchesterStateMid1; + uint8_t decoder_state = 0; + uint16_t preamble_count = 0; + uint32_t data_low = 0; + uint32_t data_high = 0; + uint8_t bit_count = 0; + uint32_t hop = 0; + uint32_t fix = 0; + uint8_t endbyte = 0; + uint8_t final_count = 0; + uint32_t te_last = 0; +}; diff --git a/firmware/baseband/fprotos/c-ford_v0.hpp b/firmware/baseband/fprotos/c-ford_v0.hpp new file mode 100644 index 000000000..b976d9e10 --- /dev/null +++ b/firmware/baseband/fprotos/c-ford_v0.hpp @@ -0,0 +1,150 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + FordV0DecoderStepReset = 0, + FordV0DecoderStepPreamble, + FordV0DecoderStepPreambleCheck, + FordV0DecoderStepGap, + FordV0DecoderStepData, +} FordV0DecoderStep; + +class FProtoSubCarFordV0 : public FProtoSubCarBase { + public: + FProtoSubCarFordV0() { + sensorType = FPC_FORDV0; + te_short = 250; + te_long = 500; + te_delta = 100; + min_count_bit_for_found = 64; + } + void ford_v0_add_bit(bool bit) { + uint32_t low = (uint32_t)data_low; + data_low = (data_low << 1) | (bit ? 1 : 0); + data_high = (data_high << 1) | ((low >> 31) & 1); + bit_count++; + } + + bool ford_v0_process_data() { + if (bit_count == 64) { + uint64_t combined = ((uint64_t)data_high << 32) | data_low; + key1 = ~combined; + data_low = 0; + data_high = 0; + return false; + } + + if (bit_count == 80) { + uint16_t key2_raw = (uint16_t)(data_low & 0xFFFF); + uint16_t key2 = ~key2_raw; + decode_data = key1; + decode_data2 = key2; + // decode_ford_v0(key1, key2, &serial, &button, &count); + return true; + } + + return false; + } + + void feed(bool level, uint32_t duration) { + uint32_t gap_threshold = 3500; + + switch (parser_step) { + case FordV0DecoderStepReset: + if (level && (DURATION_DIFF(duration, te_short) < te_delta)) { + data_low = 0; + data_high = 0; + parser_step = FordV0DecoderStepPreamble; + te_last = duration; + header_count = 0; + bit_count = 0; + FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL); + } + break; + + case FordV0DecoderStepPreamble: + if (!level) { + if (DURATION_DIFF(duration, te_long) < te_delta) { + te_last = duration; + parser_step = FordV0DecoderStepPreambleCheck; + } else { + parser_step = FordV0DecoderStepReset; + } + } + break; + + case FordV0DecoderStepPreambleCheck: + if (level) { + if (DURATION_DIFF(duration, te_long) < te_delta) { + header_count++; + te_last = duration; + parser_step = FordV0DecoderStepPreamble; + } else if (DURATION_DIFF(duration, te_short) < te_delta) { + parser_step = FordV0DecoderStepGap; + } else { + parser_step = FordV0DecoderStepReset; + } + } + break; + + case FordV0DecoderStepGap: + if (!level && (DURATION_DIFF(duration, gap_threshold) < 250)) { + data_low = 1; + data_high = 0; + bit_count = 1; + parser_step = FordV0DecoderStepData; + } else if (!level && duration > gap_threshold + 250) { + parser_step = FordV0DecoderStepReset; + } + break; + + case FordV0DecoderStepData: { + ManchesterEvent event; + + if (DURATION_DIFF(duration, te_short) < te_delta) { + event = level ? ManchesterEventShortLow : ManchesterEventShortHigh; + } else if (DURATION_DIFF(duration, te_long) < te_delta) { + event = level ? ManchesterEventLongLow : ManchesterEventLongHigh; + } else { + parser_step = FordV0DecoderStepReset; + break; + } + + bool data_bit; + if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) { + ford_v0_add_bit(data_bit); + if (ford_v0_process_data()) { + /* instance->generic.data = instance->key1; + instance->generic.data_count_bit = 64; + instance->generic.serial = instance->serial; + instance->generic.btn = instance->button; + instance->generic.cnt = instance->count; + */ + if (callback) { + callback(this); + } + + data_low = 0; + data_high = 0; + bit_count = 0; + parser_step = FordV0DecoderStepReset; + } + } + + te_last = duration; + break; + } + } + } + + ManchesterState manchester_state = ManchesterStateMid1; + + uint64_t data_low = 0; + uint64_t data_high = 0; + uint8_t bit_count = 0; + + uint16_t header_count = 0; + uint64_t key1 = 0; + uint16_t key2 = 0; +}; diff --git a/firmware/baseband/fprotos/c-kia_v0.hpp b/firmware/baseband/fprotos/c-kia_v0.hpp new file mode 100644 index 000000000..7b2bf1a1b --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v0.hpp @@ -0,0 +1,121 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + KIADecoderStepReset = 0, + KIADecoderStepCheckPreambula, + KIADecoderStepSaveDuration, + KIADecoderStepCheckDuration, +} KIADecoderStep; + +class FProtoSubCarKiaV0 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV0() { + sensorType = FPC_KIAV0; + te_short = 250; + te_long = 500; + te_delta = 100; + min_count_bit_for_found = 61; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case KIADecoderStepReset: + if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) { + parser_step = KIADecoderStepCheckPreambula; + te_last = duration; + header_count = 0; + } + break; + case KIADecoderStepCheckPreambula: + if (level) { + if ((DURATION_DIFF(duration, te_short) < te_delta) || + (DURATION_DIFF(duration, te_long) < te_delta)) { + te_last = duration; + } else { + parser_step = KIADecoderStepReset; + } + } else if ( + (DURATION_DIFF(duration, te_short) < te_delta) && + (DURATION_DIFF(te_last, te_short) < te_delta)) { + header_count++; + break; + } else if ( + (DURATION_DIFF(duration, te_long) < te_delta) && + (DURATION_DIFF(te_last, te_long) < te_delta)) { + if (header_count > 15) { + parser_step = KIADecoderStepSaveDuration; + decode_data = 0; + decode_count_bit = 1; + subghz_protocol_blocks_add_bit(1); + // FURI_LOG_I(TAG, "Starting data decode after %u header pulses", header_count); + } else { + parser_step = KIADecoderStepReset; + } + } else { + parser_step = KIADecoderStepReset; + } + break; + case KIADecoderStepSaveDuration: + if (level) { + if (duration >= + (te_long + te_delta * 2UL)) { + // Signal ended too early! + // FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration); + + parser_step = KIADecoderStepReset; + if (decode_count_bit == min_count_bit_for_found) { + // instance->generic.data = decode_data; + data_count_bit = decode_count_bit; + if (callback) + callback(this); + } else { + // FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit); + } + decode_data = 0; + decode_count_bit = 0; + break; + } else { + te_last = duration; + parser_step = KIADecoderStepCheckDuration; + } + } else { + parser_step = KIADecoderStepReset; + } + break; + case KIADecoderStepCheckDuration: + if (!level) { + if ((DURATION_DIFF(te_last, te_short) < te_delta) && + (DURATION_DIFF(duration, te_short) < te_delta)) { + subghz_protocol_blocks_add_bit(0); + if (decode_count_bit % 10 == 0) { + // FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit); + } + parser_step = KIADecoderStepSaveDuration; + } else if ( + (DURATION_DIFF(te_last, te_long) < te_delta) && + (DURATION_DIFF(duration, te_long) < te_delta)) { + subghz_protocol_blocks_add_bit(1); + if (decode_count_bit % 10 == 0) { + // FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit); + } + parser_step = KIADecoderStepSaveDuration; + } else { + // FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration); + parser_step = KIADecoderStepReset; + } + } else { + parser_step = KIADecoderStepReset; + } + break; + } + } + + bool is_running = false; + size_t preamble_count = 0; + size_t data_bit_index = 0; + uint8_t last_bit = 0; + bool send_high = false; + uint16_t header_count = 0; +}; diff --git a/firmware/baseband/fprotos/c-kia_v1.hpp b/firmware/baseband/fprotos/c-kia_v1.hpp new file mode 100644 index 000000000..6c5850855 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v1.hpp @@ -0,0 +1,192 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + KiaV1DecoderStepReset = 0, + KiaV1DecoderStepCheckPreamble, + KiaV1DecoderStepFoundShortLow, + KiaV1DecoderStepCollectRawBits, +} KiaV1DecoderStep; + +class FProtoSubCarKiaV1 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV1() { + sensorType = FPC_KIAV1; + te_short = 800; + te_long = 1600; + te_delta = 200; + min_count_bit_for_found = 56; + } + void kia_v1_add_raw_bit(bool bit) { + if (raw_bit_count < 192) { + uint16_t byte_idx = raw_bit_count / 8; + uint8_t bit_idx = 7 - (raw_bit_count % 8); + if (bit) { + raw_bits[byte_idx] |= (1 << bit_idx); + } else { + raw_bits[byte_idx] &= ~(1 << bit_idx); + } + raw_bit_count++; + } + } + inline bool kia_v1_get_raw_bit(uint16_t idx) { + uint16_t byte_idx = idx / 8; + uint8_t bit_idx = 7 - (idx % 8); + return (raw_bits[byte_idx] >> bit_idx) & 1; + } + bool kia_v1_manchester_decode() { + if (raw_bit_count < 113) { + // FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count); + return false; + } + // Try different offsets to find best alignment (RTL-433 uses -1 bit offset) + uint16_t best_bits = 0; + uint64_t best_data = 0; + // uint16_t best_offset = 0; + + for (uint16_t offset = 0; offset < 8; offset++) { + uint64_t data = 0; + uint16_t decoded_bits = 0; + + for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) { + bool bit1 = kia_v1_get_raw_bit(i); + bool bit2 = kia_v1_get_raw_bit(i + 1); + + uint8_t two_bits = (bit1 << 1) | bit2; + + // V1 uses: 10=1, 01=0 + if (two_bits == 0x02) { // 10 = decoded 1 + data = (data << 1) | 1; + decoded_bits++; + } else if (two_bits == 0x01) { // 01 = decoded 0 + data = (data << 1); + decoded_bits++; + } else { + break; + } + } + + if (decoded_bits > best_bits) { + best_bits = decoded_bits; + best_data = data; + // best_offset = offset; + } + } + + // FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data); + + decode_data = best_data; + decode_count_bit = best_bits; + + return best_bits >= min_count_bit_for_found; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case KiaV1DecoderStepReset: + // Preamble 0xCCCCCCCD produces alternating LONG pulses + if ((level) && (DURATION_DIFF(duration, te_long) < + te_delta)) { + parser_step = KiaV1DecoderStepCheckPreamble; + te_last = duration; + header_count = 1; + } + break; + + case KiaV1DecoderStepCheckPreamble: + if (level) { + if (DURATION_DIFF(duration, te_long) < + te_delta) { + te_last = duration; + header_count++; + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta) { + te_last = duration; + } else { + parser_step = KiaV1DecoderStepReset; + } + } else { + // LOW pulse + if (DURATION_DIFF(duration, te_long) < + te_delta) { + header_count++; + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta) { + // Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H) + if (header_count > 12) { + parser_step = KiaV1DecoderStepFoundShortLow; + } + } else { + parser_step = KiaV1DecoderStepReset; + } + } + break; + + case KiaV1DecoderStepFoundShortLow: + // Expecting SHORT HIGH to complete sync + if (level && (DURATION_DIFF(duration, te_short) < + te_delta)) { + // FURI_LOG_I(TAG, "Sync! hdr=%u", header_count); + parser_step = KiaV1DecoderStepCollectRawBits; + raw_bit_count = 0; + memset(raw_bits, 0, sizeof(raw_bits)); + // Add the sync short HIGH as first raw bit + kia_v1_add_raw_bit(true); + } else { + parser_step = KiaV1DecoderStepReset; + } + break; + + case KiaV1DecoderStepCollectRawBits: + if (duration > 2400) { + // FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count); + + if (kia_v1_manchester_decode()) { + // instance->generic.data = decode_data; + data_count_bit = decode_count_bit; + + // Extract fields from 56-bit data per RTL-433: + // Serial: bits 55-24 (32 bits) + // Btn: bits 23-16 (8 bits) + // Count: bits 15-8 (8 bits) + // CRC: bits 7-0 (8 bits) + // instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF); + // instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF); + // instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF); + + if (callback) + callback(this); + } + + parser_step = KiaV1DecoderStepReset; + break; + } + + int num_bits = 0; + if (DURATION_DIFF(duration, te_short) < + te_delta) { + num_bits = 1; + } else if ( + DURATION_DIFF(duration, te_long) < + te_delta) { + num_bits = 2; + } else { + parser_step = KiaV1DecoderStepReset; + break; + } + + for (int i = 0; i < num_bits; i++) { + kia_v1_add_raw_bit(level); + } + + break; + } + } + + uint8_t raw_bits[24]{0}; + uint16_t raw_bit_count = 0; + uint16_t header_count = 0; +}; diff --git a/firmware/baseband/fprotos/c-kia_v2.hpp b/firmware/baseband/fprotos/c-kia_v2.hpp new file mode 100644 index 000000000..2f86d5013 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v2.hpp @@ -0,0 +1,167 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + KiaV2DecoderStepReset = 0, + KiaV2DecoderStepCheckPreamble, + KiaV2DecoderStepCollectRawBits, +} KiaV2DecoderStep; + +class FProtoSubCarKiaV2 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV2() { + sensorType = FPC_KIAV2; + te_short = 500; + te_long = 1000; + te_delta = 160; + min_count_bit_for_found = 51; + } + + void kia_v2_add_raw_bit(bool bit) { + if (raw_bit_count < 160) { + uint16_t byte_idx = raw_bit_count / 8; + uint8_t bit_idx = 7 - (raw_bit_count % 8); + if (bit) { + raw_bits[byte_idx] |= (1 << bit_idx); + } else { + raw_bits[byte_idx] &= ~(1 << bit_idx); + } + raw_bit_count++; + } + } + inline bool kia_v2_get_raw_bit(uint16_t idx) { + uint16_t byte_idx = idx / 8; + uint8_t bit_idx = 7 - (idx % 8); + return (raw_bits[byte_idx] >> bit_idx) & 1; + } + bool kia_v2_manchester_decode() { + if (raw_bit_count < 100) { + return false; + } + + uint16_t best_bits = 0; + uint64_t best_data = 0; + + for (uint16_t offset = 0; offset < 8; offset++) { + uint64_t data = 0; + uint16_t decoded_bits = 0; + + for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) { + bool bit1 = kia_v2_get_raw_bit(i); + bool bit2 = kia_v2_get_raw_bit(i + 1); + + uint8_t two_bits = (bit1 << 1) | bit2; + + if (two_bits == 0x02) { + data = (data << 1) | 1; + decoded_bits++; + } else if (two_bits == 0x01) { + data = (data << 1); + decoded_bits++; + } else { + break; + } + } + + if (decoded_bits > best_bits) { + best_bits = decoded_bits; + best_data = data; + } + } + + decode_data = best_data; + decode_count_bit = best_bits; + + return best_bits >= min_count_bit_for_found; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case KiaV2DecoderStepReset: + if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) { + parser_step = KiaV2DecoderStepCheckPreamble; + te_last = duration; + header_count = 1; + } + break; + + case KiaV2DecoderStepCheckPreamble: + if (level) { + if (DURATION_DIFF(duration, te_long) < + te_delta) { + te_last = duration; + header_count++; + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta) { + te_last = duration; + } else { + parser_step = KiaV2DecoderStepReset; + } + } else { + if (DURATION_DIFF(duration, te_long) < + te_delta) { + header_count++; + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta) { + if (header_count > 10 && + DURATION_DIFF(te_last, te_short) < + te_delta) { + parser_step = KiaV2DecoderStepCollectRawBits; + raw_bit_count = 0; + memset(raw_bits, 0, sizeof(raw_bits)); + } + } else { + parser_step = KiaV2DecoderStepReset; + } + } + break; + + case KiaV2DecoderStepCollectRawBits: + if (duration > 1500) { + if (kia_v2_manchester_decode()) { + /*data = decode_data; + data_count_bit = decode_count_bit; + + serial = (uint32_t)((data >> 20) & 0xFFFFFFFF); + btn = (uint8_t)((data >> 16) & 0x0F); + + uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF); + cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF; + */ + data_count_bit = decode_count_bit; + if (callback) + callback(this); + } + + parser_step = KiaV2DecoderStepReset; + break; + } + + int num_bits = 0; + if (DURATION_DIFF(duration, te_short) < + te_delta) { + num_bits = 1; + } else if ( + DURATION_DIFF(duration, te_long) < + te_delta) { + num_bits = 2; + } else { + parser_step = KiaV2DecoderStepReset; + break; + } + + for (int i = 0; i < num_bits; i++) { + kia_v2_add_raw_bit(level); + } + + break; + } + } + + uint8_t raw_bits[20]{0}; + uint16_t raw_bit_count = 0; + uint16_t header_count = 0; +}; diff --git a/firmware/baseband/fprotos/c-kia_v3v4.hpp b/firmware/baseband/fprotos/c-kia_v3v4.hpp new file mode 100644 index 000000000..bede831c1 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v3v4.hpp @@ -0,0 +1,185 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + KiaV3V4DecoderStepReset = 0, + KiaV3V4DecoderStepCheckPreamble, + KiaV3V4DecoderStepCollectRawBits, +} KiaV3V4DecoderStep; + +class FProtoSubCarKiaV3V4 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV3V4() { + sensorType = FPC_KIAV3V4; + te_short = 400; + te_long = 800; + te_delta = 150; + min_count_bit_for_found = 64; + } + uint8_t reverse8(uint8_t byte) { + byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4; + byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2; + byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1; + return byte; + } + void kia_v3_v4_add_raw_bit(bool bit) { + if (raw_bit_count < 256) { + uint16_t byte_idx = raw_bit_count / 8; + uint8_t bit_idx = 7 - (raw_bit_count % 8); + if (bit) { + raw_bits[byte_idx] |= (1 << bit_idx); + } else { + raw_bits[byte_idx] &= ~(1 << bit_idx); + } + raw_bit_count++; + } + } + bool kia_v3_v4_process_buffer() { + if (raw_bit_count < 64) { + return false; + } + + uint8_t* b = raw_bits; + + // For V3-style (long LOW sync), data is inverted + if (is_v3_sync) { + uint16_t num_bytes = (raw_bit_count + 7) / 8; + for (uint16_t i = 0; i < num_bytes; i++) { + b[i] = ~b[i]; + } + } + + // Extract fields + // uint32_t encrypted = ((uint32_t)reverse8(b[3]) << 24) | ((uint32_t)reverse8(b[2]) << 16) | ((uint32_t)reverse8(b[1]) << 8) | (uint32_t)reverse8(b[0]); + uint32_t serial = ((uint32_t)reverse8(b[7] & 0xF0) << 24) | ((uint32_t)reverse8(b[6]) << 16) | ((uint32_t)reverse8(b[5]) << 8) | (uint32_t)reverse8(b[4]); + + uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4; + decode_data = serial; + decode_count_bit = 64; + decode_data2 = btn; + data_count_bit = decode_count_bit; + if (callback) + callback(this); + // uint8_t our_serial_lsb = serial & 0xFF; + + // Decrypt --skipped, no keeloq decoding + /* uint32_t decrypted = keeloq_common_decrypt(encrypted, kia_mf_key); + uint8_t dec_btn = (decrypted >> 28) & 0x0F; + uint8_t dec_serial_lsb = (decrypted >> 16) & 0xFF; + + // Validate + if (dec_btn != btn || dec_serial_lsb != our_serial_lsb) { + return false; + } + + // Valid decode - version determined by sync type + instance->encrypted = encrypted; + instance->decrypted = decrypted; + instance->generic.serial = serial; + instance->generic.btn = btn; + instance->generic.cnt = decrypted & 0xFFFF; + instance->version = is_v3_sync ? 1 : 0; + + uint64_t key_data = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) | ((uint64_t)b[2] << 40) | + ((uint64_t)b[3] << 32) | ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) | + ((uint64_t)b[6] << 8) | (uint64_t)b[7]; + instance->generic.data = key_data; + instance->generic.data_count_bit = 64; + */ + return true; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case KiaV3V4DecoderStepReset: + if (level && DURATION_DIFF(duration, te_short) < + te_delta) { + parser_step = KiaV3V4DecoderStepCheckPreamble; + te_last = duration; + header_count = 1; + } + break; + + case KiaV3V4DecoderStepCheckPreamble: + if (level) { + if (DURATION_DIFF(duration, te_short) < + te_delta) { + te_last = duration; + } else if (duration > 1000 && duration < 1500) { + // V4 style: Sync is LONG HIGH + if (header_count >= 8) { + parser_step = KiaV3V4DecoderStepCollectRawBits; + raw_bit_count = 0; + is_v3_sync = false; + memset(raw_bits, 0, sizeof(raw_bits)); + } else { + parser_step = KiaV3V4DecoderStepReset; + } + } else { + parser_step = KiaV3V4DecoderStepReset; + } + } else { + if (duration > 1000 && duration < 1500) { + // V3 style: Sync is LONG LOW + if (header_count >= 8) { + parser_step = KiaV3V4DecoderStepCollectRawBits; + raw_bit_count = 0; + is_v3_sync = true; + memset(raw_bits, 0, sizeof(raw_bits)); + } else { + parser_step = KiaV3V4DecoderStepReset; + } + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta && + DURATION_DIFF(te_last, te_short) < + te_delta) { + header_count++; + } else if (duration > 1500) { + parser_step = KiaV3V4DecoderStepReset; + } + } + break; + + case KiaV3V4DecoderStepCollectRawBits: + if (level) { + if (duration > 1000 && duration < 1500) { + // Next sync pulse (V4 style) - end this packet + kia_v3_v4_process_buffer(); + parser_step = KiaV3V4DecoderStepReset; + } else if ( + DURATION_DIFF(duration, te_short) < + te_delta) { + kia_v3_v4_add_raw_bit(false); + } else if ( + DURATION_DIFF(duration, te_long) < + te_delta) { + kia_v3_v4_add_raw_bit(true); + } else { + parser_step = KiaV3V4DecoderStepReset; + } + } else { + if (duration > 1000 && duration < 1500) { + // Next sync pulse (V3 style) - end this packet + kia_v3_v4_process_buffer(); + parser_step = KiaV3V4DecoderStepReset; + } else if (duration > 1500) { + // Long gap - end of transmission + kia_v3_v4_process_buffer(); + parser_step = KiaV3V4DecoderStepReset; + } + } + break; + } + } + + bool is_v3_sync = false; // true = V3 (long LOW sync), false = V4 (long HIGH sync) + uint8_t version = 0; // 0 = V4, 1 = V3 + uint8_t raw_bits[32]{0}; + uint16_t raw_bit_count = 0; + uint16_t header_count = 0; + + // uint32_t encrypted; + // uint32_t decrypted; +}; diff --git a/firmware/baseband/fprotos/c-kia_v5.hpp b/firmware/baseband/fprotos/c-kia_v5.hpp new file mode 100644 index 000000000..d62d70dc6 --- /dev/null +++ b/firmware/baseband/fprotos/c-kia_v5.hpp @@ -0,0 +1,176 @@ +#pragma once +#include "subcarbase.hpp" +#include +typedef enum { + KiaV5DecoderStepReset = 0, + KiaV5DecoderStepCheckPreamble, + KiaV5DecoderStepCollectRawBits, +} KiaV5DecoderStep; + +class FProtoSubCarKiaV5 : public FProtoSubCarBase { + public: + FProtoSubCarKiaV5() { + sensorType = FPC_KIAV5; + te_short = 400; + te_long = 800; + te_delta = 150; + min_count_bit_for_found = 64; + } + + inline bool kia_v5_get_raw_bit(uint16_t idx) { + uint16_t byte_idx = idx / 8; + uint8_t bit_idx = 7 - (idx % 8); + return (raw_bits[byte_idx] >> bit_idx) & 1; + } + + void kia_v5_add_raw_bit(bool bit) { + if (raw_bit_count < 256) { + uint16_t byte_idx = raw_bit_count / 8; + uint8_t bit_idx = 7 - (raw_bit_count % 8); + if (bit) { + raw_bits[byte_idx] |= (1 << bit_idx); + } else { + raw_bits[byte_idx] &= ~(1 << bit_idx); + } + raw_bit_count++; + } + } + + bool kia_v5_manchester_decode() { + if (raw_bit_count < 130) { + return false; + } + + decode_data = 0; + decode_count_bit = 0; + + // Start at offset 2 for proper Manchester alignment + const uint16_t start_bit = 2; + + for (uint16_t i = start_bit; + i + 1 < raw_bit_count && decode_count_bit < 64; + i += 2) { + bool bit1 = kia_v5_get_raw_bit(i); + bool bit2 = kia_v5_get_raw_bit(i + 1); + + uint8_t two_bits = (bit1 << 1) | bit2; + + if (two_bits == 0x01) { // 01 = decoded 1 + decode_data = (decode_data << 1) | 1; + decode_count_bit++; + } else if (two_bits == 0x02) { // 10 = decoded 0 + decode_data = (decode_data << 1); + decode_count_bit++; + } else { + break; + } + } + return decode_count_bit >= min_count_bit_for_found; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case KiaV5DecoderStepReset: + if ((level) && (DURATION_DIFF(duration, te_short) < + te_delta)) { + parser_step = KiaV5DecoderStepCheckPreamble; + te_last = duration; + header_count = 1; + } + break; + + case KiaV5DecoderStepCheckPreamble: + if (level) { + if ((DURATION_DIFF(duration, te_short) < + te_delta) || + (DURATION_DIFF(duration, te_long) < + te_delta)) { + te_last = duration; + } else { + parser_step = KiaV5DecoderStepReset; + } + } else { + if ((DURATION_DIFF(duration, te_short) < + te_delta) && + (DURATION_DIFF(te_last, te_short) < + te_delta)) { + header_count++; + } else if ( + (DURATION_DIFF(duration, te_long) < + te_delta) && + (DURATION_DIFF(te_last, te_short) < + te_delta)) { + if (header_count > 40) { + parser_step = KiaV5DecoderStepCollectRawBits; + raw_bit_count = 0; + memset(raw_bits, 0, sizeof(raw_bits)); + } else { + header_count++; + } + } else if ( + DURATION_DIFF(te_last, te_long) < + te_delta) { + header_count++; + } else { + parser_step = KiaV5DecoderStepReset; + } + } + break; + + case KiaV5DecoderStepCollectRawBits: + if (duration > 1200) { + if (kia_v5_manchester_decode()) { + // generic.data = decode_data; + // generic.data_count_bit = decode_count_bit; + data_count_bit = decode_count_bit; + // Compute yek (bit-reverse each byte) + uint64_t yek = 0; + for (int i = 0; i < 8; i++) { + uint8_t byte = (decode_data >> (i * 8)) & 0xFF; + uint8_t reversed = 0; + for (int b = 0; b < 8; b++) { + if (byte & (1 << b)) + reversed |= (1 << (7 - b)); + } + yek |= ((uint64_t)reversed << ((7 - i) * 8)); + } + decode_data = yek; + + // Shift serial right by 1 to correct alignment + // generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1); + // generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too + // generic.cnt = (uint16_t)(yek & 0xFFFF); + + if (callback) + callback(this); + } + + parser_step = KiaV5DecoderStepReset; + break; + } + + int num_bits = 0; + if (DURATION_DIFF(duration, te_short) < + te_delta) { + num_bits = 1; + } else if ( + DURATION_DIFF(duration, te_long) < + te_delta) { + num_bits = 2; + } else { + parser_step = KiaV5DecoderStepReset; + break; + } + + for (int i = 0; i < num_bits; i++) { + kia_v5_add_raw_bit(level); + } + + break; + } + } + + uint8_t raw_bits[32]{}; + uint16_t raw_bit_count = 0; + uint16_t header_count = 0; +}; diff --git a/firmware/baseband/fprotos/c-subaru.hpp b/firmware/baseband/fprotos/c-subaru.hpp new file mode 100644 index 000000000..bfc4bc50f --- /dev/null +++ b/firmware/baseband/fprotos/c-subaru.hpp @@ -0,0 +1,172 @@ +#pragma once +#include "subcarbase.hpp" +#include + +typedef enum { + SubaruDecoderStepReset = 0, + SubaruDecoderStepCheckPreamble, + SubaruDecoderStepFoundGap, + SubaruDecoderStepFoundSync, + SubaruDecoderStepSaveDuration, + SubaruDecoderStepCheckDuration, +} SubaruDecoderStep; + +class FProtoSubCarSubaru : public FProtoSubCarBase { + public: + FProtoSubCarSubaru() { + sensorType = FPC_SUBARU; + te_short = 800; + te_long = 1600; + te_delta = 260; + min_count_bit_for_found = 64; + } + void subghz_protocol_decoder_subaru_reset() { + parser_step = SubaruDecoderStepReset; + te_last = 0; + header_count = 0; + bit_count = 0; + memset(data, 0, sizeof(data)); + } + + void subaru_add_bit(bool bit) { + if (bit_count < 64) { + uint8_t byte_idx = bit_count / 8; + uint8_t bit_idx = 7 - (bit_count % 8); + if (bit) { + data[byte_idx] |= (1 << bit_idx); + + } else { + data[byte_idx] &= ~(1 << bit_idx); + } + bit_count++; + } + } + + bool subaru_process_data() { + if (bit_count < 64) { + return false; + } + uint8_t* b = data; + + uint64_t key = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) | + ((uint64_t)b[2] << 40) | ((uint64_t)b[3] << 32) | + ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) | + ((uint64_t)b[6] << 8) | ((uint64_t)b[7]); + decode_data = key; + // uint32_t serial = ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3]; + // uint8_t button = b[0] & 0x0F; + // uint16_t cnt; + // subaru_decode_count(b, &cnt); + data_count_bit = decode_count_bit; + if (callback) { + callback(this); + } + return true; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case SubaruDecoderStepReset: + if (level && DURATION_DIFF(duration, te_long) < te_delta) { + parser_step = SubaruDecoderStepCheckPreamble; + te_last = duration; + header_count = 1; + } + break; + + case SubaruDecoderStepCheckPreamble: + if (!level) { + if (DURATION_DIFF(duration, te_long) < te_delta) { + header_count++; + } else if (duration > 2000 && duration < 3500) { + if (header_count > 20) { + parser_step = SubaruDecoderStepFoundGap; + } else { + parser_step = SubaruDecoderStepReset; + } + } else { + parser_step = SubaruDecoderStepReset; + } + } else { + if (DURATION_DIFF(duration, te_long) < te_delta) { + te_last = duration; + header_count++; + } else { + parser_step = SubaruDecoderStepReset; + } + } + break; + + case SubaruDecoderStepFoundGap: + if (level && duration > 2000 && duration < 3500) { + parser_step = SubaruDecoderStepFoundSync; + } else { + parser_step = SubaruDecoderStepReset; + } + break; + + case SubaruDecoderStepFoundSync: + if (!level && DURATION_DIFF(duration, te_long) < te_delta) { + parser_step = SubaruDecoderStepSaveDuration; + bit_count = 0; + memset(data, 0, sizeof(data)); + } else { + parser_step = SubaruDecoderStepReset; + } + break; + + case SubaruDecoderStepSaveDuration: + if (level) { + // HIGH pulse duration encodes the bit: + // Short HIGH (~800µs) = 1 + // Long HIGH (~1600µs) = 0 + if (DURATION_DIFF(duration, te_short) < te_delta) { + // Short HIGH = bit 1 + subaru_add_bit(true); + te_last = duration; + parser_step = SubaruDecoderStepCheckDuration; + } else if (DURATION_DIFF(duration, te_long) < te_delta) { + // Long HIGH = bit 0 + subaru_add_bit(false); + te_last = duration; + parser_step = SubaruDecoderStepCheckDuration; + } else if (duration > 3000) { + // End of transmission + if (bit_count >= 64) { + subaru_process_data(); + } + parser_step = SubaruDecoderStepReset; + } else { + parser_step = SubaruDecoderStepReset; + } + } else { + parser_step = SubaruDecoderStepReset; + } + break; + + case SubaruDecoderStepCheckDuration: + if (!level) { + // LOW pulse - just validates timing, doesn't encode bit + if (DURATION_DIFF(duration, te_short) < te_delta || + DURATION_DIFF(duration, te_long) < te_delta) { + parser_step = SubaruDecoderStepSaveDuration; + } else if (duration > 3000) { + // Gap - end of packet + if (bit_count >= 64) { + subaru_process_data(); + } + parser_step = SubaruDecoderStepReset; + } else { + parser_step = SubaruDecoderStepReset; + } + } else { + parser_step = SubaruDecoderStepReset; + } + break; + } + } + + uint16_t header_count = 0; + uint8_t data[8]; + uint8_t bit_count = 0; +}; diff --git a/firmware/baseband/fprotos/c-suzuki.hpp b/firmware/baseband/fprotos/c-suzuki.hpp new file mode 100644 index 000000000..93340df01 --- /dev/null +++ b/firmware/baseband/fprotos/c-suzuki.hpp @@ -0,0 +1,120 @@ +#pragma once +#include "subcarbase.hpp" + +#define SUZUKI_GAP_TIME 2000 +#define SUZUKI_GAP_DELTA 400 + +typedef enum { + SuzukiDecoderStepReset = 0, + SuzukiDecoderStepFoundStartPulse, + SuzukiDecoderStepSaveDuration, +} SuzukiDecoderStep; + +class FProtoSubCarSuzuki : public FProtoSubCarBase { + public: + FProtoSubCarSuzuki() { + sensorType = FPC_SUZUKI; + te_short = 250; + te_long = 500; + te_delta = 110; + min_count_bit_for_found = 64; + } + void suzuki_add_bit(uint32_t bit) { + uint32_t carry = data_low >> 31; + data_low = (data_low << 1) | bit; + data_high = (data_high << 1) | carry; + data_count_bit++; + } + void subghz_protocol_decoder_suzuki_reset() { + parser_step = SuzukiDecoderStepReset; + header_count = 0; + data_count_bit = 0; + data_low = 0; + data_high = 0; + } + + void feed(bool level, uint32_t duration) { + switch (parser_step) { + case SuzukiDecoderStepReset: + // Wait for short HIGH pulse (~250µs) to start preamble + if (!level) + return; + + if (DURATION_DIFF(duration, te_short) > te_delta) { + return; + } + + data_low = 0; + data_high = 0; + parser_step = SuzukiDecoderStepFoundStartPulse; + header_count = 0; + data_count_bit = 0; + break; + + case SuzukiDecoderStepFoundStartPulse: + if (level) { + // HIGH pulse + if (header_count < 257) { + // Still in preamble - just count + return; + } + + // After preamble, look for long HIGH to start data + if (DURATION_DIFF(duration, te_long) < te_delta) { + parser_step = SuzukiDecoderStepSaveDuration; + suzuki_add_bit(1); + } + // Ignore short HIGHs after preamble until we see a long one + } else { + // LOW pulse - count as header if short + if (DURATION_DIFF(duration, te_short) < te_delta) { + te_last = duration; + header_count++; + } else { + parser_step = SuzukiDecoderStepReset; + } + } + break; + + case SuzukiDecoderStepSaveDuration: + if (level) { + // HIGH pulse - determines bit value + // Long HIGH (~500µs) = 1, Short HIGH (~250µs) = 0 + if (DURATION_DIFF(duration, te_long) < te_delta) { + suzuki_add_bit(1); + } else if (DURATION_DIFF(duration, te_short) < te_delta) { + suzuki_add_bit(0); + } else { + parser_step = SuzukiDecoderStepReset; + } + // Stay in this state for next bit + } else { + // LOW pulse - check for gap (end of transmission) + if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) { + // Gap found - end of transmission + if (data_count_bit == 64) { + data_count_bit = 64; + decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low; + // Check manufacturer nibble (should be 0xF) + uint8_t manufacturer = (data_high >> 28) & 0xF; + if (manufacturer == 0xF) { + // Extract fields + decode_data2 = 0; // Not used + if (callback) { + callback(this); + } + } + } + parser_step = SuzukiDecoderStepReset; + } + // Short LOW pulses are ignored - stay in this state + } + break; + } + } + + uint16_t header_count = 0; + uint32_t data_high = 0; + uint32_t data_low = 0; + uint8_t data_count_bit = 0; +}; diff --git a/firmware/baseband/fprotos/c-vw.hpp b/firmware/baseband/fprotos/c-vw.hpp new file mode 100644 index 000000000..fdfbc4493 --- /dev/null +++ b/firmware/baseband/fprotos/c-vw.hpp @@ -0,0 +1,236 @@ +#pragma once +#include "subcarbase.hpp" + +typedef enum { + VwDecoderStepReset = 0, + VwDecoderStepFoundSync, + VwDecoderStepFoundStart1, + VwDecoderStepFoundStart2, + VwDecoderStepFoundStart3, + VwDecoderStepFoundData, +} VwDecoderStep; + +class FProtoSubCarVW : public FProtoSubCarBase { + public: + FProtoSubCarVW() { + sensorType = FPC_VW; + te_short = 500; + te_long = 1000; + te_delta = 130; + min_count_bit_for_found = 80; + } + uint8_t vw_get_bit_index(uint8_t bit) { + uint8_t bit_index = 0; + if (bit < 72 && bit >= 8) { + // use generic.data (bytes 1-8) + bit_index = bit - 8; + } else { + // use data_2 + if (bit >= 72) { + bit_index = bit - 64; // byte 0 = type + } + if (bit < 8) { + bit_index = bit; // byte 9 = check digit + } + bit_index |= 0x80; // mark for data_2 + } + return bit_index; + } + + void vw_add_bit(bool level) { + if (data_count_bit >= min_count_bit_for_found) { + return; + } + uint8_t bit_index_full = min_count_bit_for_found - 1 - data_count_bit; + uint8_t bit_index_masked = vw_get_bit_index(bit_index_full); + uint8_t bit_index = bit_index_masked & 0x7F; + if (bit_index_masked & 0x80) { + // use data_2 + if (level) { + decode_data2 |= (1ULL << bit_index); + } else { + decode_data2 &= ~(1ULL << bit_index); + } + } else { + // use data + if (level) { + decode_data |= (1ULL << bit_index); + } else { + decode_data &= ~(1ULL << bit_index); + } + } + + data_count_bit++; + if (data_count_bit >= min_count_bit_for_found) { + if (callback) { + callback(this); + } + } + } + + void subghz_protocol_decoder_vw_reset() { + parser_step = VwDecoderStepReset; + data_count_bit = 0; + decode_data = 0; + decode_data2 = 0; + manchester_state = ManchesterStateMid1; + } + + bool vw_manchester_advance( + ManchesterState state, + ManchesterEvent event, + ManchesterState* next_state, + bool* data) { + bool result = false; + ManchesterState new_state = ManchesterStateMid1; + + if (event == ManchesterEventReset) { + new_state = ManchesterStateMid1; + } else if (state == ManchesterStateMid0 || state == ManchesterStateMid1) { + if (event == ManchesterEventShortHigh) { + new_state = ManchesterStateStart1; + } else if (event == ManchesterEventShortLow) { + new_state = ManchesterStateStart0; + } else { + new_state = ManchesterStateMid1; + } + } else if (state == ManchesterStateStart1) { + if (event == ManchesterEventShortLow) { + new_state = ManchesterStateMid1; + result = true; + if (data) + *data = true; + } else if (event == ManchesterEventLongLow) { + new_state = ManchesterStateStart0; + result = true; + if (data) + *data = true; + } else { + new_state = ManchesterStateMid1; + } + } else if (state == ManchesterStateStart0) { + if (event == ManchesterEventShortHigh) { + new_state = ManchesterStateMid0; + result = true; + if (data) + *data = false; + } else if (event == ManchesterEventLongHigh) { + new_state = ManchesterStateStart1; + result = true; + if (data) + *data = false; + } else { + new_state = ManchesterStateMid1; + } + } + + *next_state = new_state; + return result; + } + + void feed(bool level, uint32_t duration) { + uint32_t te_med = (te_long + te_short) / 2; + uint32_t te_end = te_long * 5; + + ManchesterEvent event = ManchesterEventReset; + + switch (parser_step) { + case VwDecoderStepReset: + if (DURATION_DIFF(duration, te_short) < te_delta) { + parser_step = VwDecoderStepFoundSync; + } + break; + + case VwDecoderStepFoundSync: + if (DURATION_DIFF(duration, te_short) < te_delta) { + // Stay - sync pattern repeats ~43 times + break; + } + + if (level && DURATION_DIFF(duration, te_long) < te_delta) { + parser_step = VwDecoderStepFoundStart1; + break; + } + + parser_step = VwDecoderStepReset; + break; + + case VwDecoderStepFoundStart1: + if (!level && DURATION_DIFF(duration, te_short) < te_delta) { + parser_step = VwDecoderStepFoundStart2; + break; + } + + parser_step = VwDecoderStepReset; + break; + + case VwDecoderStepFoundStart2: + if (level && DURATION_DIFF(duration, te_med) < te_delta) { + parser_step = VwDecoderStepFoundStart3; + break; + } + + parser_step = VwDecoderStepReset; + break; + + case VwDecoderStepFoundStart3: + if (DURATION_DIFF(duration, te_med) < te_delta) { + // Stay - med pattern repeats + break; + } + + if (level && DURATION_DIFF(duration, te_short) < te_delta) { + // Start data collection + vw_manchester_advance( + manchester_state, + ManchesterEventReset, + &manchester_state, + NULL); + vw_manchester_advance( + manchester_state, + ManchesterEventShortHigh, + &manchester_state, + NULL); + data_count_bit = 0; + decode_data = 0; + decode_data2 = 0; + parser_step = VwDecoderStepFoundData; + break; + } + + parser_step = VwDecoderStepReset; + break; + + case VwDecoderStepFoundData: + if (DURATION_DIFF(duration, te_short) < te_delta) { + event = level ? ManchesterEventShortHigh : ManchesterEventShortLow; + } + + if (DURATION_DIFF(duration, te_long) < te_delta) { + event = level ? ManchesterEventLongHigh : ManchesterEventLongLow; + } + + // Last bit can be arbitrarily long + if (data_count_bit == min_count_bit_for_found - 1 && + !level && duration > te_end) { + event = ManchesterEventShortLow; + } + + if (event == ManchesterEventReset) { + subghz_protocol_decoder_vw_reset(); + } else { + bool new_level; + if (vw_manchester_advance( + manchester_state, + event, + &manchester_state, + &new_level)) { + vw_add_bit(new_level); + } + } + break; + } + } + + ManchesterState manchester_state = ManchesterStateMid1; +}; diff --git a/firmware/baseband/fprotos/subcarbase.hpp b/firmware/baseband/fprotos/subcarbase.hpp new file mode 100644 index 000000000..e44332133 --- /dev/null +++ b/firmware/baseband/fprotos/subcarbase.hpp @@ -0,0 +1,54 @@ +/* +Base class for all weather protocols. +This and most of the weather protocols uses code from Flipper XTreme codebase ( https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/lib/subghz ). Thanks for their work! +For comments in a protocol implementation check w-nexus-th.hpp +*/ + +#ifndef __FPROTO_SCARBASE_H__ +#define __FPROTO_SCARBASE_H__ + +#include "fprotogeneral.hpp" +#include "subcartypes.hpp" + +#include +// default values to indicate 'no value' + +class FProtoSubCarBase; +typedef void (*SubCarProtocolDecoderBaseRxCallback)(FProtoSubCarBase* instance); + +class FProtoSubCarBase { + public: + FProtoSubCarBase() {} + virtual ~FProtoSubCarBase() {} + virtual void feed(bool level, uint32_t duration) = 0; // need to be implemented on each protocol handler. + void setCallback(SubCarProtocolDecoderBaseRxCallback cb) { callback = cb; } // this is called when there is a hit. + + // General data holder, these will be passed + uint8_t sensorType = FPC_Invalid; + uint16_t data_count_bit = 0; + uint64_t decode_data = 0; + uint64_t decode_data2 = 0; + + protected: + // Helper functions to keep it as compatible with flipper as we can, so adding new protos will be easy. + void subghz_protocol_blocks_add_bit(uint8_t bit) { + decode_data = decode_data << 1 | bit; + decode_count_bit++; + } + + // inner logic stuff, also for flipper compatibility. + uint32_t te_short = UINT32_MAX; + uint32_t te_long = UINT32_MAX; + uint32_t te_delta = UINT32_MAX; + uint32_t min_count_bit_for_found = UINT32_MAX; + + SubCarProtocolDecoderBaseRxCallback callback = NULL; + + uint8_t parser_step = 0; + uint32_t te_last = 0; + uint32_t decode_count_bit = 0; + + // +}; + +#endif \ No newline at end of file diff --git a/firmware/baseband/fprotos/subcarprotos.hpp b/firmware/baseband/fprotos/subcarprotos.hpp new file mode 100644 index 000000000..d88e8d69f --- /dev/null +++ b/firmware/baseband/fprotos/subcarprotos.hpp @@ -0,0 +1,75 @@ +/* +This is the protocol list handler. It holds an instance of all known protocols. +So include here the .hpp, and add a new element to the protos vector in the constructor. That's all you need to do here if you wanna add a new proto. + @htotoo +*/ + +#include +#include +#include "portapack_shared_memory.hpp" + +#include "fprotolistgeneral.hpp" +#include "subcarbase.hpp" +#include "c-suzuki.hpp" +#include "c-vw.hpp" +#include "c-subaru.hpp" +#include "c-kia_v5.hpp" +#include "c-kia_v3v4.hpp" +#include "c-kia_v2.hpp" +#include "c-kia_v1.hpp" +#include "c-kia_v0.hpp" +#include "c-ford_v0.hpp" +#include "c-fiat_v0.hpp" +#include "c-bmw_v0.hpp" + +#ifndef __FPROTO_PROTOLISTCAR_H__ +#define __FPROTO_PROTOLISTCAR_H__ + +class SubCarProtos : public FProtoListGeneral { + public: + SubCarProtos(const SubCarProtos&) { SubCarProtos(); }; // won't use, but makes compiler happy + SubCarProtos& operator=(const SubCarProtos&) { return *this; } // won't use, but makes compiler happy + SubCarProtos() { + // add protos + protos[FPC_SUZUKI] = new FProtoSubCarSuzuki(); + protos[FPC_VW] = new FProtoSubCarVW(); + protos[FPC_SUBARU] = new FProtoSubCarSubaru(); + protos[FPC_KIAV5] = new FProtoSubCarKiaV5(); + protos[FPC_KIAV3V4] = new FProtoSubCarKiaV3V4(); + protos[FPC_KIAV2] = new FProtoSubCarKiaV2(); + protos[FPC_KIAV1] = new FProtoSubCarKiaV1(); + protos[FPC_KIAV0] = new FProtoSubCarKiaV0(); + protos[FPC_FORDV0] = new FProtoSubCarFordV0(); + protos[FPC_FIATV0] = new FProtoSubCarFiatV0(); + protos[FPC_BMWV0] = new FProtoSubCarBMWV0(); + + for (uint8_t i = 0; i < FPC_COUNT; ++i) { + if (protos[i] != NULL) protos[i]->setCallback(callbackTarget); + } + } + + ~SubCarProtos() { // not needed for current operation logic, but a bit more elegant :) + for (uint8_t i = 0; i < FPC_COUNT; ++i) { + if (protos[i] != NULL) { + free(protos[i]); + protos[i] = NULL; + } + } + }; + + static void callbackTarget(FProtoSubCarBase* instance) { + SubCarDataMessage packet_message{instance->sensorType, instance->data_count_bit, instance->decode_data, instance->decode_data2}; + shared_memory.application_queue.push(packet_message); + } + + void feed(bool level, uint32_t duration) { + for (uint8_t i = 0; i < FPC_COUNT; ++i) { + if (protos[i] != NULL) protos[i]->feed(level, duration); + } + } + + protected: + FProtoSubCarBase* protos[FPC_COUNT] = {NULL}; +}; + +#endif diff --git a/firmware/baseband/fprotos/subcartypes.hpp b/firmware/baseband/fprotos/subcartypes.hpp new file mode 100644 index 000000000..3b01cdbc7 --- /dev/null +++ b/firmware/baseband/fprotos/subcartypes.hpp @@ -0,0 +1,30 @@ + +#ifndef __FPROTO_SUBCARTYPES_H__ +#define __FPROTO_SUBCARTYPES_H__ + +/* +Define known protocols. +These values must be present on the protocol's constructor, like FProtoWeatherAcurite592TXR() { sensorType = FPS_ANSONIC; } +Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name. +*/ + +#define FPM_AM 0 +#define FPM_FM 1 + +enum FPROTO_SUBCAR_SENSOR : uint8_t { + FPC_Invalid = 0, + FPC_SUZUKI = 1, + FPC_VW = 2, + FPC_SUBARU = 3, + FPC_KIAV5 = 4, + FPC_KIAV3V4 = 5, + FPC_KIAV2 = 6, + FPC_KIAV1 = 7, + FPC_KIAV0 = 8, + FPC_FORDV0 = 9, + FPC_FIATV0 = 10, + FPC_BMWV0 = 11, + FPC_COUNT +}; + +#endif diff --git a/firmware/baseband/proc_protoview.cpp b/firmware/baseband/proc_protoview.cpp index 7f8abd89b..9d8b5432c 100644 --- a/firmware/baseband/proc_protoview.cpp +++ b/firmware/baseband/proc_protoview.cpp @@ -109,4 +109,4 @@ int main() { EventDispatcher event_dispatcher{std::make_unique()}; event_dispatcher.run(); return 0; -} +} \ No newline at end of file diff --git a/firmware/baseband/proc_protoview.hpp b/firmware/baseband/proc_protoview.hpp index abe56d7f8..731d09b8f 100644 --- a/firmware/baseband/proc_protoview.hpp +++ b/firmware/baseband/proc_protoview.hpp @@ -71,4 +71,4 @@ class ProtoViewProcessor : public BasebandProcessor { RSSIThread rssi_thread{}; }; -#endif /*__PROC_PROTOVIEW_H__*/ +#endif /*__PROC_PROTOVIEW_H__*/ \ No newline at end of file diff --git a/firmware/baseband/proc_subcar.cpp b/firmware/baseband/proc_subcar.cpp new file mode 100644 index 000000000..846266d99 --- /dev/null +++ b/firmware/baseband/proc_subcar.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2026 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. + */ +/* + This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside. +*/ +#include "proc_subcar.hpp" +#include "portapack_shared_memory.hpp" +#include "event_m4.hpp" + +static inline int get_quadrant(int16_t i, int16_t q) { + if (i >= 0) { + return (q >= 0) ? 0 : 3; + } else { + return (q >= 0) ? 1 : 2; + } +} + +void SubCarProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + // SR = 4Mhz , and we are decimating by /8 in total , decim1_out clock 4Mhz /8= 500khz samples/sec. + // buffer has 2048 complex i8 I,Q signed samples + // decim0 out: 2048/4 = 512 complex i16 I,Q signed samples + // decim1 out: 512/2 = 256 complex i16 I,Q signed samples + // Regarding Filters, we are re-using existing FIR filters, @4Mhz, FIR decim1 ilter, BW =+-220Khz (at -3dB's). BW = 440kHZ. + + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); // Input:2048 complex/4 (decim factor) = 512_output complex (1024 I/Q samples) + const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples) + feed_channel_stats(decim_1_out); + + // for fm + const int32_t DC_ALPHA = 5; // Auto-centering speed + int32_t buffer_rotation_sum = 0; + + for (size_t i = 0; i < decim_1_out.count; i++) { + // am + threshold = (low_estimate + high_estimate) / 2; + int32_t const hysteresis = threshold / 8; // +-12% + int16_t re = decim_1_out.p[i].real(); + int16_t im = decim_1_out.p[i].imag(); + uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im); + + mag = (mag >> 10); + int32_t const ook_low_delta = mag - low_estimate; + bool meashl = currentHiLow; + if (sig_state == STATE_IDLE) { + if (mag > (threshold + hysteresis)) { // just become high + meashl = true; + sig_state = STATE_PULSE; + numg = 0; + } else { + meashl = false; // still low + low_estimate += ook_low_delta / OOK_EST_LOW_RATIO; + low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling + // Calculate default OOK high level estimate + high_estimate = 1.35 * low_estimate; // Default is a ratio of low level + high_estimate = std::max(high_estimate, min_high_level); + high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); + } + + } else if (sig_state == STATE_PULSE) { + ++numg; + if (numg > 100) numg = 100; + if (mag < (threshold - hysteresis)) { + // check if really a bad value + if (numg < 3) { + // susp + sig_state = STATE_GAP; + } else { + numg = 0; + sig_state = STATE_GAP_START; + } + meashl = false; // low + } else { + high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO; + high_estimate = std::max(high_estimate, min_high_level); + high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL); + meashl = true; // still high + } + } else if (sig_state == STATE_GAP_START) { + ++numg; + if (mag > (threshold + hysteresis)) { // New pulse? + sig_state = STATE_PULSE; + meashl = true; + } else if (numg >= 3) { + sig_state = STATE_GAP; + meashl = false; // gap + } + } else if (sig_state == STATE_GAP) { + ++numg; + if (mag > (threshold + hysteresis)) { // New pulse? + numg = 0; + sig_state = STATE_PULSE; + meashl = true; + } else { + meashl = false; + } + } + + if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal + { + currentDuration += nsPerDecSamp; + } else { // called on change, so send the last duration and dir. + if (currentDuration >= 30'000'000) sig_state = STATE_IDLE; + if (protoList) protoList->feed(currentHiLow, currentDuration / 1000); + currentDuration = nsPerDecSamp; + currentHiLow = meashl; + } + + // fm part: -- NOT WORKING!!!! TODO FIX. AI code ;) + int current_quad = get_quadrant(re, im); + // Calculate Step (Current - Previous) + int diff = current_quad - fm_state.prev_quad; + // Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3) + // 3 -> 0 should be +1 (CCW) + // 0 -> 3 should be -1 (CW) + if (diff == -3) + diff = 1; + else if (diff == 3) + diff = -1; + // Update History + fm_state.prev_quad = current_quad; + // Accumulate Rotation + buffer_rotation_sum += diff; + } + + // fm finish: + // 3. AUTO-CENTERING (DC BLOCKER) + // Even with quadrant counting, "drift" (hand effect) makes the wheel spin + // faster or slower. We need to subtract the average speed. + // Update our "Average Speed" estimate + // Note: buffer_rotation_sum is roughly proportional to frequency. + fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA; + // Remove the drift + int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset; + // 4. LOW PASS FILTER + const int32_t LPF_ALPHA = 4; + fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA; + // 5. DECISION LOGIC + // Threshold is small now because we are counting quadrant steps. + // Max steps per buffer (256 samples) is 256. + // Typical FSK deviation might give you +/- 10 to 50 steps per buffer. + const int32_t THRESHOLD = 3; + bool new_level = fm_state.current_logic_level; + if (fm_state.smoothed_error > THRESHOLD) { + new_level = true; + } else if (fm_state.smoothed_error < -THRESHOLD) { + new_level = false; + } + // 6. TIMING OUTPUT + if (new_level == fm_state.current_logic_level) { + fm_state.buffer_count++; + } else { + // Output pulse duration + int32_t duration_us = fm_state.buffer_count * 512; + + if (duration_us > 250) { + if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us); + } + fm_state.current_logic_level = new_level; + fm_state.buffer_count = 1; + } +} + +void SubCarProcessor::on_message(const Message* const message) { + if (message->id == Message::ID::SubGhzFPRxConfigure) + configure(*reinterpret_cast(message)); +} + +void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) { + // constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused + // constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused + + baseband_fs = message.sampling_rate; + baseband_thread.set_sampling_rate(baseband_fs); + nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8 + + decim_0.configure(taps_200k_wfm_decim_0.taps); + decim_1.configure(taps_200k_wfm_decim_1.taps); + + configured = true; +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_subcar.hpp b/firmware/baseband/proc_subcar.hpp new file mode 100644 index 000000000..9d16e79ba --- /dev/null +++ b/firmware/baseband/proc_subcar.hpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2026 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. + */ + +/* + This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside. +*/ + +#ifndef __PROC_SUBCAR_H__ +#define __PROC_SUBCAR_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" +#include "message.hpp" +#include "dsp_decimate.hpp" + +#pragma GCC push_options +#pragma GCC optimize("Os") +#include "fprotos/subcarprotos.hpp" +#pragma GCC pop_options + +#define OOK_EST_HIGH_RATIO 3 // Constant for slowness of OOK high level estimator +#define OOK_EST_LOW_RATIO 5 // Constant for slowness of OOK low level (noise) estimator (very slow) +#define OOK_MAX_HIGH_LEVEL 450000 + +class SubCarProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const message) override; + + private: + enum { + STATE_IDLE = 0, + STATE_PULSE = 1, + STATE_GAP_START = 2, + STATE_GAP = 3, + } sig_state = STATE_IDLE; + uint32_t low_estimate = 100; + uint32_t high_estimate = 12000; + uint32_t min_high_level = 10; + uint8_t numg = 0; // count of matched signals to filter spikes + size_t baseband_fs = 0; // will be set later by configure message + uint32_t nsPerDecSamp = 0; + + /* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */ + std::array dst{}; // decim0 /4 , 2048/4 = 512 complex I,Q + const buffer_c16_t dst_buffer{ + dst.data(), + dst.size()}; + + /* Decimates */ + dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{}; + dsp::decimate::FIRC16xR16x16Decim2 decim_1{}; + + uint32_t currentDuration = 0; + uint32_t threshold = 0x0630; + bool currentHiLow = false; + bool configured{false}; + uint8_t mode = 0; // 0 = am, 1 = fm + + // fm part: + struct DemodFMState { + int prev_quad = 0; // Stores 0, 1, 2, or 3 + int32_t dc_offset = 0; + int32_t smoothed_error = 0; + bool current_logic_level = false; + uint32_t buffer_count = 0; + }; + DemodFMState fm_state{}; + + FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse + FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual) + void configure(const SubGhzFPRxConfigureMessage& message); + + /* 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_WEATHER_H__*/ diff --git a/firmware/baseband/proc_subghzd.hpp b/firmware/baseband/proc_subghzd.hpp index 0650b3d3b..aeef5a061 100644 --- a/firmware/baseband/proc_subghzd.hpp +++ b/firmware/baseband/proc_subghzd.hpp @@ -23,8 +23,8 @@ Creator: @htotoo */ -#ifndef __PROC_WEATHER_H__ -#define __PROC_WEATHER_H__ +#ifndef __PROC_SUBGHZD_H__ +#define __PROC_SUBGHZD_H__ #include "baseband_processor.hpp" #include "baseband_thread.hpp" diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index c277d50cc..f9b774908 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -145,6 +145,7 @@ class Message { SSTVRXProgress = 87, SSTVRXPhaseSlant = 88, SSTVRXCalibration = 89, + SubCarData = 90, MAX }; @@ -1679,4 +1680,23 @@ class FlexDebugMessage : public Message { char text[64]; }; +class SubCarDataMessage : public Message { + public: + constexpr SubCarDataMessage( + uint8_t sensorType = 0, + uint16_t bits = 0, + uint64_t data = 0, + uint64_t data2 = 0) + : Message{ID::SubCarData}, + sensorType{sensorType}, + bits{bits}, + data{data}, + data2{data2} { + } + uint8_t sensorType = 0; + uint16_t bits = 0; + uint64_t data = 0; + uint64_t data2 = 0; +}; + #endif /*__MESSAGE_H__*/ diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 6e10a87c8..6dee37495 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -119,6 +119,7 @@ constexpr image_tag_t image_tag_usb_sd{'P', 'U', 'S', 'B'}; constexpr image_tag_t image_tag_weather{'P', 'W', 'T', 'H'}; constexpr image_tag_t image_tag_subghzd{'P', 'S', 'G', 'D'}; +constexpr image_tag_t image_tag_subcar{'P', 'S', 'C', 'D'}; constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'}; constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'}; constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'}; From 2261e5398101331fcbd784ed04edc0ad0df4652c Mon Sep 17 00:00:00 2001 From: Totoo Date: Sun, 21 Dec 2025 11:38:35 +0100 Subject: [PATCH 12/13] fix bit counts (#2894) --- firmware/baseband/fprotos/c-ford_v0.hpp | 1 + firmware/baseband/fprotos/c-kia_v1.hpp | 2 +- firmware/baseband/fprotos/c-subaru.hpp | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/firmware/baseband/fprotos/c-ford_v0.hpp b/firmware/baseband/fprotos/c-ford_v0.hpp index b976d9e10..241fe2c66 100644 --- a/firmware/baseband/fprotos/c-ford_v0.hpp +++ b/firmware/baseband/fprotos/c-ford_v0.hpp @@ -121,6 +121,7 @@ class FProtoSubCarFordV0 : public FProtoSubCarBase { instance->generic.btn = instance->button; instance->generic.cnt = instance->count; */ + data_count_bit = 64; if (callback) { callback(this); } diff --git a/firmware/baseband/fprotos/c-kia_v1.hpp b/firmware/baseband/fprotos/c-kia_v1.hpp index 6c5850855..7a0202d18 100644 --- a/firmware/baseband/fprotos/c-kia_v1.hpp +++ b/firmware/baseband/fprotos/c-kia_v1.hpp @@ -146,7 +146,7 @@ class FProtoSubCarKiaV1 : public FProtoSubCarBase { if (kia_v1_manchester_decode()) { // instance->generic.data = decode_data; - data_count_bit = decode_count_bit; + data_count_bit = raw_bit_count / 8; // Extract fields from 56-bit data per RTL-433: // Serial: bits 55-24 (32 bits) diff --git a/firmware/baseband/fprotos/c-subaru.hpp b/firmware/baseband/fprotos/c-subaru.hpp index bfc4bc50f..6e1b3a5e0 100644 --- a/firmware/baseband/fprotos/c-subaru.hpp +++ b/firmware/baseband/fprotos/c-subaru.hpp @@ -57,7 +57,7 @@ class FProtoSubCarSubaru : public FProtoSubCarBase { // uint8_t button = b[0] & 0x0F; // uint16_t cnt; // subaru_decode_count(b, &cnt); - data_count_bit = decode_count_bit; + data_count_bit = bit_count; if (callback) { callback(this); } From 70ac1a03a1c97c81104e856bd8fe396a51bbdc37 Mon Sep 17 00:00:00 2001 From: gullradriel <3157857+gullradriel@users.noreply.github.com> Date: Sun, 21 Dec 2025 11:39:47 +0100 Subject: [PATCH 13/13] Make stable release (#2895) * updated submodule * updated versions --- .github/workflows/past_version.txt | 2 +- .github/workflows/version.txt | 2 +- hackrf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/past_version.txt b/.github/workflows/past_version.txt index a4b6ac3de..aaf7425f4 100644 --- a/.github/workflows/past_version.txt +++ b/.github/workflows/past_version.txt @@ -1 +1 @@ -v2.2.0 +v2.3.1 diff --git a/.github/workflows/version.txt b/.github/workflows/version.txt index aaf7425f4..f706a60d8 100644 --- a/.github/workflows/version.txt +++ b/.github/workflows/version.txt @@ -1 +1 @@ -v2.3.1 +v2.3.2 diff --git a/hackrf b/hackrf index cf6815aaf..c0b15549c 160000 --- a/hackrf +++ b/hackrf @@ -1 +1 @@ -Subproject commit cf6815aaf9c4bb11c3ad3de97721c67ddf4fcb38 +Subproject commit c0b15549cbcf18fbef5221e471335f72198f9c71