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/.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/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/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/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 ****", " ", 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/apps/ui_sonde.cpp b/firmware/application/apps/ui_sonde.cpp index f5aa030ac..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,42 @@ 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(); - geopos.set_altitude(gps_info.alt); - geopos.set_lat(gps_info.lat); - geopos.set_lon(gps_info.lon); - + 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.cpp b/firmware/application/baseband_api.cpp index 2655f37f9..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, @@ -288,8 +301,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); } @@ -328,6 +341,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 84d5f292a..86d06604c 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -74,13 +74,15 @@ 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); 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); @@ -91,7 +93,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); @@ -102,6 +104,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/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/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/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/external.cmake b/firmware/application/external/external.cmake index a32bdffbc..b0b33d87d 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 @@ -264,6 +268,14 @@ 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 + + #subcarrx + external/subcarrx/main.cpp + external/subcarrx/ui_subcar.cpp ) set(EXTAPPLIST @@ -290,6 +302,7 @@ set(EXTAPPLIST adsbtx morse_tx sstvtx + sstvrx random_password # acars_rx --not working wefax_rx @@ -330,4 +343,6 @@ set(EXTAPPLIST bht_tx morse_practice adult_toys_controller + flex_rx + subcarrx ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 0f22bd8bd..6ee5cc178 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -86,6 +86,9 @@ 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 + ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k + ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k } @@ -471,5 +474,24 @@ 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 + + .external_app_sstvrx : ALIGN(4) SUBALIGN(4) + { + 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/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..ccc7410d5 --- /dev/null +++ b/firmware/application/external/flex_rx/ui_flex_rx.cpp @@ -0,0 +1,129 @@ +#include "ui_flex_rx.hpp" + +#include "baseband_api.hpp" +#include "portapack_persistent_memory.hpp" +#include "string_format.hpp" +#include "memory_map.hpp" + +using namespace portapack; + +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, + &rssi, + &console}); + + // 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); + }; + + // 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(); + + console.writeln("Ready"); +} + +FlexAppView::~FlexAppView() { + receiver_model.disable(); + baseband::shutdown(); +} + +void FlexAppView::focus() { + field_frequency.focus(); +} + +// 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) { + const size_t chars_per_line = screen_width / 8; + // Console height accounts for status bar and controls row + const size_t console_lines = (screen_height - 2 * 16) / 16; + + messages.push_back(message); + + // 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; + } + + // 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 { + // Just append new message + if (messages.size() > 1) { + console.writeln(""); // Blank line before new message + } + console.writeln(message); + } +} + +// 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) { + log_message(message->packet.message); +} + +// Handle stats message (currently unused) +void FlexAppView::on_stats(const FlexStatsMessage*) { +} + +// Debug handler - uncomment to see baseband debug messages +void FlexAppView::on_debug(const FlexDebugMessage* message) { + (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 new file mode 100644 index 000000000..7f182878a --- /dev/null +++ b/firmware/application/external/flex_rx/ui_flex_rx.hpp @@ -0,0 +1,97 @@ +#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 "app_settings.hpp" +#include "radio_state.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_; + + // Saved settings + rf::Frequency frequency_value{931740000}; // Default FLEX frequency + + RxRadioState radio_state_{}; + + // Message storage for console redraw + static constexpr size_t MAX_MESSAGES = 20; + std::vector messages{}; + + // Helper methods + 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{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + + 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(9), 4}}; + + // 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}}}; + + // Message handlers + void on_packet(const FlexPacketMessage* message); + void on_stats(const FlexStatsMessage* message); + void on_debug(const FlexDebugMessage* message); + + // Message handler registrations + 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); + }}; +}; + +} // namespace ui::external_app::flex_rx + +#endif /*__UI_FLEX_RX_H__*/ \ No newline at end of file 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/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/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/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 a7cfd70a0..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 @@ -454,14 +457,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; } @@ -522,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); @@ -552,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. } } @@ -615,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/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 9ac499ebc..5a92c17ed 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 @@ -644,6 +651,13 @@ set(MODE_CPPSRC ) DeclareTargets(PSTX sstvtx) +### SSTV RX + +set(MODE_CPPSRC + proc_sstvrx.cpp +) +DeclareTargets(PSRX sstvrx) + ### TPMS set(MODE_CPPSRC @@ -690,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/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/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..241fe2c66 --- /dev/null +++ b/firmware/baseband/fprotos/c-ford_v0.hpp @@ -0,0 +1,151 @@ +#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; + */ + data_count_bit = 64; + 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..7a0202d18 --- /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 = raw_bit_count / 8; + + // 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..6e1b3a5e0 --- /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 = bit_count; + 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/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_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/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/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/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_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/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/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 bc8e51340..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); @@ -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/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/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.hpp b/firmware/common/message.hpp index ae6f33bb7..f9b774908 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,15 @@ class Message { NoaaAptRxImageData = 79, FSKPacket = 80, EPIRBPacket = 81, + FlexPacket = 82, + FlexStats = 83, + FlexConfigure = 84, + FlexDebug = 85, + SSTVRXConfigure = 86, + SSTVRXProgress = 87, + SSTVRXPhaseSlant = 88, + SSTVRXCalibration = 89, + SubCarData = 90, MAX }; @@ -1139,6 +1149,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( @@ -1187,9 +1253,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 { @@ -1568,4 +1635,68 @@ 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]; +}; + +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/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/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; }; diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index a4b94215c..6dee37495 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'}; @@ -118,9 +119,11 @@ 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'}; +constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'}; constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'}; 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/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 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 diff --git a/hackrf b/hackrf index cf6815aaf..c0b15549c 160000 --- a/hackrf +++ b/hackrf @@ -1 +1 @@ -Subproject commit cf6815aaf9c4bb11c3ad3de97721c67ddf4fcb38 +Subproject commit c0b15549cbcf18fbef5221e471335f72198f9c71