From 605c9efa0ca6aa43dd008da493e6307cb1abca0f Mon Sep 17 00:00:00 2001 From: gullradriel <3157857+gullradriel@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:26:45 +0200 Subject: [PATCH] Make detector use freqman (#3119) * make detector list a freqman file * add file loading ability, autoscan/pause, step in on pause on range, frequency value display * Update firmware/application/external/detector_rx/ui_detector_rx.hpp * Update firmware/application/external/detector_rx/ui_detector_rx.cpp * add 750 kHz step * revamp UI and add auto advance, auto scan, and only display 'RANGE SCAN...' when auto scan is on on ranges to avoid flicker of the hell. * adjust as I can the ranges as they where before * Update sdcard/FREQMAN/DETECTOR.TXT * fix instance related copilot remark --- .../external/detector_rx/ui_detector_rx.cpp | 223 ++++++++++++++---- .../external/detector_rx/ui_detector_rx.hpp | 125 +++++----- firmware/application/freqman_db.cpp | 2 + firmware/application/freqman_db.hpp | 2 + sdcard/FREQMAN/DETECTOR.TXT | 21 ++ 5 files changed, 255 insertions(+), 118 deletions(-) create mode 100644 sdcard/FREQMAN/DETECTOR.TXT diff --git a/firmware/application/external/detector_rx/ui_detector_rx.cpp b/firmware/application/external/detector_rx/ui_detector_rx.cpp index a1dd3b935..23b0b7ee9 100644 --- a/firmware/application/external/detector_rx/ui_detector_rx.cpp +++ b/firmware/application/external/detector_rx/ui_detector_rx.cpp @@ -26,6 +26,7 @@ #include "ui_freqman.hpp" #include "baseband_api.hpp" #include "file.hpp" +#include "file_path.hpp" #include "oversample.hpp" #include "ui_font_fixed_8x16.hpp" @@ -35,36 +36,112 @@ using portapack::memory::map::backup_ram; namespace ui::external_app::detector_rx { -// Function to map the value from one range to another int32_t DetectorRxView::map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh) { return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow); } void DetectorRxView::focus() { - field_lna.focus(); + button_index.focus(); } DetectorRxView::~DetectorRxView() { - // reset performance counters request to default shared_memory.request_m4_performance_counter = 0; receiver_model.disable(); audio::output::stop(); baseband::shutdown(); } -void DetectorRxView::on_timer() { - freq_index++; - if (freq_mode == 0 && freq_index >= tetra_uplink_monitoring_frequencies_hz.size()) freq_index = 0; // TETRA UP - if (freq_mode == 1 && freq_index >= lora_monitoring_frequencies_hz.size()) freq_index = 0; // Lora - if (freq_mode == 2 && freq_index >= remotes_monitoring_frequencies_hz.size()) freq_index = 0; // Remotes +std::string DetectorRxView::format_freq_mhz(int64_t freq_hz) { + int64_t mhz = freq_hz / 1000000; + int64_t khz_frac = (freq_hz % 1000000) / 1000; + return "< " + to_string_dec_uint(mhz) + "." + to_string_dec_uint(khz_frac, 3, '0') + " MHz >"; +} - if (freq_mode == 2) - freq_ = remotes_monitoring_frequencies_hz[freq_index]; - else if (freq_mode == 1) - freq_ = lora_monitoring_frequencies_hz[freq_index]; - else - freq_ = tetra_uplink_monitoring_frequencies_hz[freq_index]; - receiver_model.set_target_frequency(freq_); +void DetectorRxView::load_freqman() { + freqman_load_options options{}; + options.load_freqs = true; + options.load_ranges = true; + options.load_hamradios = false; + options.load_repeaters = false; + + if (!load_freqman_file(freq_file_stem, frequency_list, options) || frequency_list.empty()) { + button_file.set_text("No file!"); + button_index.set_text(""); + text_entry_desc.set(""); + button_freq.set_text(""); + frequency_list.clear(); + return; + } + + current_index = 0; + init_current_entry(); + button_file.set_text(freq_file_stem); +} + +void DetectorRxView::init_current_entry() { + if (frequency_list.empty()) return; + + auto& entry = *frequency_list[current_index]; + if (entry.type == freqman_type::Range) { + minfreq = entry.frequency_a; + maxfreq = entry.frequency_b; + current_freq = minfreq; + current_step = is_valid(entry.step) ? freqman_entry_get_step_value(entry.step) : DETECTOR_BW; + } else { + current_freq = entry.frequency_a; + minfreq = current_freq; + maxfreq = current_freq; + current_step = DETECTOR_BW; + } + + update_entry_display(); + update_freq_display(); + receiver_model.set_target_frequency(current_freq); +} + +void DetectorRxView::update_entry_display() { + if (frequency_list.empty()) return; + + auto& entry = *frequency_list[current_index]; + button_index.set_text( + to_string_dec_uint(current_index + 1) + "/" + + to_string_dec_uint(frequency_list.size())); + text_entry_desc.set(entry.description); +} + +void DetectorRxView::update_freq_display() { + if (auto_scan && (minfreq != maxfreq)) { + if (last_update_was_auto_range_type != 1) { + button_freq.set_text("RANGE SCAN..."); + last_update_was_auto_range_type = 1; + } + } else { + button_freq.set_text(format_freq_mhz(current_freq)); + last_update_was_auto_range_type = 0; + } +} + +void DetectorRxView::on_timer() { + if (frequency_list.empty() || !auto_scan) return; + + auto& entry = *frequency_list[current_index]; + if (entry.type == freqman_type::Range) { + current_freq += current_step; + if (current_freq > maxfreq) { + if (auto_advance) { + current_index = (current_index + 1) % frequency_list.size(); + init_current_entry(); + return; + } + current_freq = minfreq; + } + receiver_model.set_target_frequency(current_freq); + update_freq_display(); + } else if (auto_advance) { + // Single frequency: advance to next entry + current_index = (current_index + 1) % frequency_list.size(); + init_current_entry(); + } } DetectorRxView::DetectorRxView(NavigationView& nav) @@ -75,21 +152,22 @@ DetectorRxView::DetectorRxView(NavigationView& nav) &field_vga, &field_rf_amp, &field_volume, - &text_frequency, - &freq_stats_rssi, - &freq_stats_db, + &button_file, + &button_index, + &text_entry_desc, &text_beep_squelch, &field_beep_squelch, + &freq_stats_db, + &freq_stats_rssi, + &button_freq, + &button_auto_scan, + &button_auto_advance, &rssi, &rssi_graph, - &field_mode, }); - // activate vertical bar mode rssi.set_vertical_rssi(true); - freq_ = receiver_model.target_frequency(); - field_beep_squelch.set_value(beep_squelch); field_beep_squelch.on_change = [this](int32_t v) { beep_squelch = v; @@ -97,29 +175,85 @@ DetectorRxView::DetectorRxView(NavigationView& nav) rssi_graph.set_nb_columns(256); + // FILE button opens file picker + button_file.on_select = [this](Button&) { + auto open_view = nav_.push(".TXT"); + open_view->push_dir(freqman_dir); + open_view->on_changed = [this](std::filesystem::path new_file_path) { + if (new_file_path.native().find((u"/" / freqman_dir).native()) != 0) { + button_file.set_text("Invalid file"); + return; + } + freq_file_stem = new_file_path.stem().string(); + load_freqman(); + }; + }; + + // Index encoder: rotate to change entry, click does nothing + button_index.on_change = [this]() { + if (frequency_list.empty()) return; + int32_t delta = button_index.get_encoder_delta(); + button_index.set_encoder_delta(0); + if (delta == 0) return; + + // Use signed arithmetic for index wraparound to avoid unsigned promotion issues. + const int32_t list_size = static_cast(frequency_list.size()); + int32_t idx = static_cast(current_index); + if (delta > 0) { + idx = (idx + 1) % list_size; + } else { + idx = (idx - 1 + list_size) % list_size; + } + current_index = static_cast(idx); + init_current_entry(); + }; + + // Frequency encoder: rotate to step within range + button_freq.on_change = [this]() { + if (frequency_list.empty()) return; + auto& entry = *frequency_list[current_index]; + if (entry.type != freqman_type::Range) { + button_freq.set_encoder_delta(0); + return; + } + + int32_t delta = button_freq.get_encoder_delta(); + button_freq.set_encoder_delta(0); + if (delta == 0) return; + + if (delta > 0) { + current_freq += current_step; + if (current_freq > maxfreq) current_freq = minfreq; + } else { + current_freq -= current_step; + if (current_freq < minfreq) current_freq = maxfreq; + } + receiver_model.set_target_frequency(current_freq); + update_freq_display(); + }; + + // Auto-scan toggle + button_auto_scan.on_select = [this](Button&) { + auto_scan = !auto_scan; + button_auto_scan.set_text(auto_scan ? "AUTOSCAN" : "NO SCAN"); + update_freq_display(); + }; + button_auto_scan.set_text(auto_scan ? "AUTOSCAN" : "NO SCAN"); + + // Auto-advance toggle + button_auto_advance.on_select = [this](Button&) { + auto_advance = !auto_advance; + button_auto_advance.set_text(auto_advance ? "AUTOADV" : "NO ADV"); + }; + button_auto_advance.set_text(auto_advance ? "AUTOADV" : "NO ADV"); + change_mode(); rssi.set_peak(true, 3000); - // FILL STEP OPTIONS freq_stats_rssi.set_style(Theme::getInstance()->bg_darkest); freq_stats_db.set_style(Theme::getInstance()->bg_darkest); - field_mode.on_change = [this](size_t, int32_t value) { - freq_mode = value; - freq_index = 0; - switch (value) { - case 1: // Lora - text_frequency.set(" 433, 868, 915 Mhz"); - break; - case 2: // Remotes - text_frequency.set(" 433, 315 Mhz"); - break; - default: - case 0: // TETRA UP - text_frequency.set(" 380-390 Mhz"); - break; - } - }; + load_freqman(); } void DetectorRxView::on_statistics_update(const ChannelStatistics& statistics) { @@ -130,25 +264,23 @@ void DetectorRxView::on_statistics_update(const ChannelStatistics& statistics) { rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db); - // refresh db if (last_max_db != statistics.max_db) { last_max_db = statistics.max_db; freq_stats_db.set("Power: " + to_string_dec_int(statistics.max_db) + " db"); rssi.set_db(statistics.max_db); } - // refresh rssi + if (last_min_rssi != rssi_graph.get_graph_min() || last_avg_rssi != rssi_graph.get_graph_avg() || last_max_rssi != rssi_graph.get_graph_max()) { last_min_rssi = rssi_graph.get_graph_min(); last_avg_rssi = rssi_graph.get_graph_avg(); last_max_rssi = rssi_graph.get_graph_max(); - freq_stats_rssi.set("RSSI: " + to_string_dec_uint(last_min_rssi) + "/" + to_string_dec_uint(last_avg_rssi) + "/" + to_string_dec_uint(last_max_rssi)); + freq_stats_rssi.set("RSSI:" + to_string_dec_uint(last_min_rssi) + "/" + to_string_dec_uint(last_avg_rssi) + "/" + to_string_dec_uint(last_max_rssi)); } if (statistics.max_db > beep_squelch) { baseband::request_audio_beep(map(statistics.max_db, -100, 20, 400, 2600), 24000, 150); } - -} /* on_statistic_updates */ +} size_t DetectorRxView::change_mode() { audio::output::stop(); @@ -160,14 +292,13 @@ size_t DetectorRxView::change_mode() { receiver_model.set_modulation(ReceiverModel::Mode::Capture); baseband::set_sample_rate(DETECTOR_BW, get_oversample_rate(DETECTOR_BW)); - // The radio needs to know the effective sampling rate. auto actual_sampling_rate = get_actual_sample_rate(DETECTOR_BW); receiver_model.set_sampling_rate(actual_sampling_rate); receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate)); audio::set_rate(audio_sampling_rate); audio::output::start(); - receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack. + receiver_model.set_headphone_volume(receiver_model.headphone_volume()); receiver_model.enable(); diff --git a/firmware/application/external/detector_rx/ui_detector_rx.hpp b/firmware/application/external/detector_rx/ui_detector_rx.hpp index ee579e6c5..d1d565fad 100644 --- a/firmware/application/external/detector_rx/ui_detector_rx.hpp +++ b/firmware/application/external/detector_rx/ui_detector_rx.hpp @@ -29,6 +29,7 @@ #include "audio.hpp" #include "baseband_api.hpp" #include "file.hpp" +#include "freqman.hpp" #include "freqman_db.hpp" #include "portapack_persistent_memory.hpp" #include "radio_state.hpp" @@ -60,20 +61,35 @@ class DetectorRxView : public View { int32_t map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh); size_t change_mode(); void on_statistics_update(const ChannelStatistics& statistics); - void set_display_freq(int64_t freq); void on_timer(); + void load_freqman(); + void init_current_entry(); + void update_entry_display(); + void update_freq_display(); + std::string format_freq_mhz(int64_t freq_hz); + + freqman_db frequency_list{}; + size_t current_index{0}; + int64_t current_freq{0}; + int64_t minfreq{0}; + int64_t maxfreq{0}; + int32_t current_step{DETECTOR_BW}; + std::string freq_file_stem{"DETECTOR"}; + bool auto_scan{true}; + bool auto_advance{false}; + uint8_t last_update_was_auto_range_type = -1; - uint8_t freq_index = 0; - rf::Frequency freq_ = {433920000}; int32_t beep_squelch = 0; audio::Rate audio_sampling_rate = audio::Rate::Hz_48000; - uint8_t freq_mode = 0; app_settings::SettingsManager settings_{ "rx_detector", app_settings::Mode::RX, { {"beep_squelch"sv, &beep_squelch}, + {"freq_file"sv, &freq_file_stem}, + {"auto_scan"sv, &auto_scan}, + {"auto_advance"sv, &auto_advance}, }}; Labels labels{ @@ -93,41 +109,55 @@ class DetectorRxView : public View { AudioVolumeField field_volume{ {UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; - OptionsField field_mode{ - {UI_POS_X(0), UI_POS_Y(1)}, - 9, - { - {"TETRA UP", 0}, - {"Lora", 1}, - {"Remotes", 2}, - }}; + // Row 1: filename + auto advance + Button button_file{ + {UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, + ""}; - Text text_frequency{ - {UI_POS_X_RIGHT(20), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, + Button button_auto_advance{ + {UI_POS_X_RIGHT(9), UI_POS_Y(1), UI_POS_WIDTH(9), UI_POS_DEFAULT_HEIGHT}, + "NO ADV"}; + + // Row 2: index encoder + description + auto scan + ButtonWithEncoder button_index{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT}, + ""}; + + Text text_entry_desc{ + {UI_POS_X(4), UI_POS_Y(2), UI_POS_WIDTH(17), UI_POS_DEFAULT_HEIGHT}, + ""}; + + Button button_auto_scan{ + {UI_POS_X_RIGHT(9), UI_POS_Y(2), UI_POS_WIDTH(9), UI_POS_DEFAULT_HEIGHT}, + "AUTO SCAN"}; + + // Row 3: frequency encoder + bip level + ButtonWithEncoder button_freq{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, ""}; Text text_beep_squelch{ - {UI_POS_X_RIGHT(9), UI_POS_Y(2), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT}, + {UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(4), UI_POS_DEFAULT_HEIGHT}, "Bip>"}; NumberField field_beep_squelch{ - {UI_POS_X_RIGHT(5), UI_POS_Y(2)}, + {UI_POS_X_RIGHT(5), UI_POS_Y(3)}, 4, {-100, 20}, 1, ' ', }; - // RSSI: XX/XX/XXX - Text freq_stats_rssi{ - {UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, - }; - - // Power: -XXX db + // Row 4: Power + RSSI on same line Text freq_stats_db{ - {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, + {UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, }; + Text freq_stats_rssi{ + {UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, + }; + + // Row 5+: RSSI graph + vertical bar RSSIGraph rssi_graph{ {UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(6)}, }; @@ -147,55 +177,6 @@ class DetectorRxView : public View { [this](const Message* const) { this->on_timer(); }}; - - const std::vector remotes_monitoring_frequencies_hz = { - // Around 315 MHz (common for older remotes, key fobs in some regions) - // Window centered on 315 MHz, covers 314.625 - 315.375 MHz - 315000000, - - // Around 433.92 MHz (very common for remotes, sensors, key fobs globally) - // Window centered on 433.92 MHz, covers 433.545 - 434.295 MHz - 433920000, - }; - const std::vector lora_monitoring_frequencies_hz = { - // EU433 Band (Europe, typically 433.05 MHz to 434.79 MHz) - // Scanning the approximate range 433.0 MHz to 434.8 MHz with 750kHz steps - 433375000, // Covers 433.000 - 433.750 MHz - 434125000, // Covers 433.750 - 434.500 MHz (includes 433.92 MHz) - 434875000, // Covers 434.500 - 435.250 MHz (covers up to 434.79 MHz) - - // EU868 Band (Europe, typically 863 MHz to 870 MHz, specific channels around 868 MHz) - // Targeting common LoRaWAN channel groups (approx 867.0 - 868.6 MHz) with 750kHz steps - 867375000, // Covers 867.000 - 867.750 MHz - 868125000, // Covers 867.750 - 868.500 MHz - 868875000, // Covers 868.500 - 869.250 MHz (covers up to 868.6 MHz) - - // US915 Band (North America, typically 902 MHz to 928 MHz, specific channels around 915 MHz) - // Providing a few sample windows around the 915 MHz area with 750kHz steps. - // This band is wide; a full scan would require many more frequencies. - 914250000, // Covers 913.875 - 914.625 MHz - 915000000, // Covers 914.625 - 915.375 MHz (Centered on 915 MHz) - 915750000, // Covers 915.375 - 916.125 MHz - }; - const std::vector tetra_uplink_monitoring_frequencies_hz = { - // Band starts at 380,000,000 Hz, ends at 390,000,000 Hz. - // First center: 380,000,000 + 375,000 = 380,375,000 Hz - // Last center: 380,375,000 + 13 * 750,000 = 390,125,000 Hz (14 frequencies total for this band) - 380375000, // Covers 380.000 - 380.750 MHz - 381125000, // Covers 380.750 - 381.500 MHz - 381875000, // Covers 381.500 - 382.250 MHz - 382625000, // Covers 382.250 - 383.000 MHz - 383375000, // Covers 383.000 - 383.750 MHz - 384125000, // Covers 383.750 - 384.500 MHz - 384875000, // Covers 384.500 - 385.250 MHz - 385625000, // Covers 385.250 - 386.000 MHz - 386375000, // Covers 386.000 - 386.750 MHz - 387125000, // Covers 386.750 - 387.500 MHz - 387875000, // Covers 387.500 - 388.250 MHz - 388625000, // Covers 388.250 - 389.000 MHz - 389375000, // Covers 389.000 - 389.750 MHz - 390125000, // Covers 389.750 - 390.500 MHz - }; }; } // namespace ui::external_app::detector_rx diff --git a/firmware/application/freqman_db.cpp b/firmware/application/freqman_db.cpp index 615b75cf4..22be78b5d 100644 --- a/firmware/application/freqman_db.cpp +++ b/firmware/application/freqman_db.cpp @@ -134,6 +134,7 @@ options_t freqman_steps = { {"100kHz (FM2)", 100000}, {"250kHz (N2)", 250000}, {"500kHz (WFM)", 500000}, + {"750kHz ", 750000}, {"1MHz ", 1000000}, }; @@ -156,6 +157,7 @@ options_t freqman_steps_short = { {"100kHz", 100000}, {"250kHz", 250000}, {"500kHz", 500000}, + {"750kHz", 750000}, {"1MHz", 1000000}, }; diff --git a/firmware/application/freqman_db.hpp b/firmware/application/freqman_db.hpp index 2fed7a930..2fe9f28b7 100644 --- a/firmware/application/freqman_db.hpp +++ b/firmware/application/freqman_db.hpp @@ -102,6 +102,7 @@ enum class freqman_type : uint8_t { _100kHz, _250kHz, _500kHz, + _750kHz, _1MHz, Unknown, * }; @@ -130,6 +131,7 @@ enum class freqman_type : uint8_t { freqman_step_info{ freqman_step::_100kHz, "100kHz", "100kHz (FM2)", 100'000 }, freqman_step_info{ freqman_step::_250kHz, "250kHz", "250kHz (N2)", 250'000 }, freqman_step_info{ freqman_step::_500kHz, "500kHz", "500kHz (WFM)", 500'000 }, + freqman_step_info{ freqman_step::_750kHz, "750kHz", "750kHz ", 750'000 }, freqman_step_info{ freqman_step::_1MHz, "1MHz", "1MHz ", 1'000'000 }, freqman_step_info{ freqman_step::Unknown, "Unknown", "Unknown ", 0 }, * }; diff --git a/sdcard/FREQMAN/DETECTOR.TXT b/sdcard/FREQMAN/DETECTOR.TXT new file mode 100644 index 000000000..d9f7e98cc --- /dev/null +++ b/sdcard/FREQMAN/DETECTOR.TXT @@ -0,0 +1,21 @@ +# Around 315 MHz (common for older remotes, key fobs in some regions) +# Window centered on 315 MHz, covers 314.625 - 315.375 MHz +f=315000000,d=Remotes 315M + +# Around 433.92 MHz (very common for remotes, sensors, key fobs globally) +# Window centered on 433.92 MHz, covers 433.545 - 434.295 MHz +f=433920000,d=Remotes 433.92M + +# EU433 Band (Europe, typically 433.05 MHz to 434.79 MHz) +# Scanning the approximate range 433.0 MHz to 434.8 MHz with 750kHz steps +a=433375000,b=434875000,s=750kHz,d=LoRa EU433 + +# EU868 Band (Europe, typically 863 MHz to 870 MHz, specific channels around 868 MHz) +# Targeting common LoRaWAN channel groups (approx 867.0 - 868.6 MHz) with 750kHz steps +a=867375000,b=868875000,s=750kHz,d=LoRa EU868 + +# US915 Band (North America, typically 902 MHz to 928 MHz, specific channels around 915 MHz) +a=902000000,b=928250000,s=750kHz,d=LoRa US915 + +# TETRA Band starts at 380,000,000 Hz, ends at 390,000,000 Hz. +a=380375000,b=390125000,s=750kHz,d=TETRA UP