From 03131c8a0cea82f6f897c7468c6b6b88b4590927 Mon Sep 17 00:00:00 2001 From: Frederic BORRY Date: Fri, 3 Apr 2026 11:35:46 +0200 Subject: [PATCH] EPIRB TX application update (#3120) --- .../external/epirb_tx/ui_epirb_tx.cpp | 197 ++++++++++++++++-- .../external/epirb_tx/ui_epirb_tx.hpp | 102 +++++++-- firmware/baseband/proc_epirb_tx.cpp | 12 +- firmware/baseband/proc_epirb_tx.hpp | 7 +- 4 files changed, 273 insertions(+), 45 deletions(-) diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.cpp b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp index fdc1a3c19..f6d027809 100644 --- a/firmware/application/external/epirb_tx/ui_epirb_tx.cpp +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp @@ -98,6 +98,39 @@ void EPIRBTXAppView::on_timer() { } } +void EPIRBTXAppView::update_bpsk_frequency() { + bool was_transmitting = false; + if (transmitting && (am_enabled || transmitting_bpsk)) { + // We need to stop transmission before changing frequency + transmitter_model.disable(); + was_transmitting = true; + } + transmitter_model.set_target_frequency(bpsk_frequency); + // Update displayed frequency + tx_view.on_show(); + if (was_transmitting) { + // Start over + start_tx(); + } +} + +void EPIRBTXAppView::update_am_transmission() { + if (am_enabled && transmitting && !transmitting_bpsk) { + // Start am transmission + // Restore am frequency + epirb_tx_message.mode_bpsk = false; + transmitter_model.set_target_frequency(am_frequency); + // Send config to baseband + baseband::set_epirb_tx_config(epirb_tx_message); + // Start transmitting + transmitter_model.enable(); + } else if (transmitting && !transmitting_bpsk) { + // Stop am transmission + transmitter_model.disable(); + tx_view.set_transmitting(false); + } +} + void EPIRBTXAppView::update_frame(bool updateConfig) { if (mode_file) { // In file mode, currently selected beacon has changed => load the new one @@ -136,17 +169,17 @@ void EPIRBTXAppView::update_frame(bool updateConfig) { } void EPIRBTXAppView::update_config() { - if (epirb_tx_message.mode_bpsk) { - // Already in BPSK mode => backup bpsk frequency - bpsk_frequency = transmitter_model.target_frequency(); - } else { + if (!epirb_tx_message.mode_bpsk) { // Previously in AM mode => restore bpsk frequency transmitter_model.set_target_frequency(bpsk_frequency); + // Update displayed frequency + tx_view.on_show(); } // Set mode to bpsk epirb_tx_message.mode_bpsk = true; + transmitting_bpsk = true; // Set pre/post count - epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms + epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.) epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms // Send config to baseband baseband::set_epirb_tx_config(epirb_tx_message); @@ -178,15 +211,12 @@ void EPIRBTXAppView::stop_tx() { void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) { (void)progress; if (done) { + transmitting_bpsk = false; if (am_enabled) { // BPSK frame sent, switch back to 121.5 AM signal epirb_tx_message.mode_bpsk = false; - // Backup bpsk frequency for next run - bpsk_frequency = transmitter_model.target_frequency(); - // Restore am frequency - transmitter_model.set_target_frequency(am_frequency); - // Send config to baseband - baseband::set_epirb_tx_config(epirb_tx_message); + // Start am transmission + update_am_transmission(); } else { // End of BPSK frame transmitter_model.disable(); @@ -262,6 +292,8 @@ EPIRBTXAppView::EPIRBTXAppView( &checkbox_am, &field_am_frequency, &checkbox_send_on_change, + &options_am_channel, + &options_bpsk_channel, &tx_view}); text_beacon.set_style(Theme::getInstance()->fg_light); @@ -279,6 +311,11 @@ EPIRBTXAppView::EPIRBTXAppView( options_mode.set_by_value(!mode_file); transmitter_model.set_target_frequency(bpsk_frequency); field_am_frequency.set_value(am_frequency); + options_am_channel.set_by_value(am_channel); + options_am_channel.set_style((am_channel == (uint8_t)AmChannel::REAL) ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest); + manual_am_frequency = am_frequency; + options_bpsk_channel.set_by_value(bpsk_channel); + manual_bpsk_frequency = bpsk_frequency; field_delay.set_value(delay); options_beacon_type.set_by_value(beacon_type); options_beacon_protocol.set_by_value(beacon_protocol); @@ -292,23 +329,23 @@ EPIRBTXAppView::EPIRBTXAppView( update_mode(); update_location(); - options_mode.on_change = [this](size_t index, OptionsField::value_t) { - mode_file = (index == 0); + options_mode.on_change = [this](size_t, OptionsField::value_t value) { + mode_file = (((BeaconMode)value) == BeaconMode::FILE); update_mode(); update_frame(); set_dirty(); }; - options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) { - beacon_params.type = (BeaconType)index; - beacon_type = index; + options_beacon_type.on_change = [this](size_t, OptionsField::value_t value) { + beacon_params.type = (BeaconType)value; + beacon_type = value; update_frame(); set_dirty(); }; - options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) { - beacon_params.protocol = (BeaconProtocol)index; - beacon_protocol = index; + options_beacon_protocol.on_change = [this](size_t, OptionsField::value_t value) { + beacon_params.protocol = (BeaconProtocol)value; + beacon_protocol = value; update_frame(); set_dirty(); }; @@ -320,6 +357,71 @@ EPIRBTXAppView::EPIRBTXAppView( set_dirty(); }; + options_am_channel.on_change = [this](size_t, OptionsField::value_t v) { + bool is_real = false; + switch ((AmChannel)v) { + case AmChannel::REAL: + is_real = true; + am_frequency = AM_REAL_FREQUENCY; + break; + case AmChannel::MANUAL: + am_frequency = manual_am_frequency; + break; + default: + v = (uint8_t)AmChannel::TEST; + // fallthrough + case AmChannel::TEST: + am_frequency = AM_TEST_FREQUENCY; + break; + } + // Actual frequency change will be done by field_am_frequency.on_change() + field_am_frequency.set_value(am_frequency); + am_channel = v; + options_am_channel.set_style(is_real ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest); + set_dirty(); + }; + + options_bpsk_channel.on_change = [this](size_t, OptionsField::value_t v) { + switch ((BpskChannel)v) { + case BpskChannel::MANUAL: + bpsk_frequency = manual_bpsk_frequency; + break; + case BpskChannel::B: + bpsk_frequency = BPSK_FREQUENCY_B; + break; + case BpskChannel::C: + bpsk_frequency = BPSK_FREQUENCY_C; + break; + case BpskChannel::F: + bpsk_frequency = BPSK_FREQUENCY_F; + break; + case BpskChannel::G: + bpsk_frequency = BPSK_FREQUENCY_G; + break; + case BpskChannel::J: + bpsk_frequency = BPSK_FREQUENCY_J; + break; + case BpskChannel::K: + bpsk_frequency = BPSK_FREQUENCY_K; + break; + case BpskChannel::N: + bpsk_frequency = BPSK_FREQUENCY_N; + break; + case BpskChannel::O: + bpsk_frequency = BPSK_FREQUENCY_O; + break; + default: + v = (uint8_t)BpskChannel::HAM; + // fallthrough + case BpskChannel::HAM: + bpsk_frequency = BPSK_FREQUENCY_HAM; + break; + } + bpsk_channel = v; + update_bpsk_frequency(); + set_dirty(); + }; + checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) { beacon_internal = v; beacon_params.is_internal = v; @@ -358,7 +460,7 @@ EPIRBTXAppView::EPIRBTXAppView( field_am_frequency.on_change = [this](rf::Frequency freq) { am_frequency = freq; - if (transmitting && !epirb_tx_message.mode_bpsk && am_enabled) + if (transmitting && !transmitting_bpsk && am_enabled) // Update transmitter frequency transmitter_model.set_target_frequency(am_frequency); }; @@ -397,6 +499,8 @@ EPIRBTXAppView::EPIRBTXAppView( checkbox_am.on_select = [this](Checkbox&, bool v) { beacon_params.has_121_5 = v; am_enabled = v; + update_am_transmission(); + // We update the additional location device data in the frame based on this if (!mode_file) update_frame(false); }; @@ -404,7 +508,21 @@ EPIRBTXAppView::EPIRBTXAppView( field_am_frequency.on_edit = [this, &nav]() { auto new_view = nav.push(field_am_frequency.value()); new_view->on_changed = [this](rf::Frequency f) { - field_am_frequency.set_value(f); + switch (f) { + case AM_REAL_FREQUENCY: + am_channel = (uint8_t)AmChannel::REAL; + break; + case AM_TEST_FREQUENCY: + am_channel = (uint8_t)AmChannel::TEST; + break; + default: + manual_am_frequency = f; + am_channel = (uint8_t)AmChannel::MANUAL; + break; + } + // Actual frequency change will be done by options_am_channel.on_change() + options_am_channel.set_by_value(am_channel); + set_dirty(); }; }; @@ -415,8 +533,43 @@ EPIRBTXAppView::EPIRBTXAppView( tx_view.on_edit_frequency = [this, &nav]() { auto new_view = nav.push(transmitter_model.target_frequency()); new_view->on_changed = [this](rf::Frequency f) { - transmitter_model.set_target_frequency(f); bpsk_frequency = f; + switch (f) { + case BPSK_FREQUENCY_HAM: + bpsk_channel = (uint8_t)BpskChannel::HAM; + break; + case BPSK_FREQUENCY_B: + bpsk_channel = (uint8_t)BpskChannel::B; + break; + case BPSK_FREQUENCY_C: + bpsk_channel = (uint8_t)BpskChannel::C; + break; + case BPSK_FREQUENCY_F: + bpsk_channel = (uint8_t)BpskChannel::F; + break; + case BPSK_FREQUENCY_G: + bpsk_channel = (uint8_t)BpskChannel::G; + break; + case BPSK_FREQUENCY_J: + bpsk_channel = (uint8_t)BpskChannel::J; + break; + case BPSK_FREQUENCY_K: + bpsk_channel = (uint8_t)BpskChannel::K; + break; + case BPSK_FREQUENCY_N: + bpsk_channel = (uint8_t)BpskChannel::N; + break; + case BPSK_FREQUENCY_O: + bpsk_channel = (uint8_t)BpskChannel::O; + break; + default: + bpsk_channel = (uint8_t)BpskChannel::MANUAL; + manual_bpsk_frequency = bpsk_frequency; + break; + } + // Actual frequency change will be done by options_bpsk_channel.on_change() + options_bpsk_channel.set_by_value(bpsk_channel); + set_dirty(); }; }; diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.hpp b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp index 132e933d7..44743895f 100644 --- a/firmware/application/external/epirb_tx/ui_epirb_tx.hpp +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp @@ -37,8 +37,26 @@ #define BEACON_HEXA_HALF_SIZE 18 #define BEACON_SIZE 18 +#define AM_TEST_FREQUENCY 121375000 +#define AM_REAL_FREQUENCY 121500000 + +#define BPSK_FREQUENCY_HAM 433025000 +#define BPSK_FREQUENCY_B 406025000 +#define BPSK_FREQUENCY_C 406028000 +#define BPSK_FREQUENCY_F 406037000 +#define BPSK_FREQUENCY_G 406040000 +#define BPSK_FREQUENCY_J 406049000 +#define BPSK_FREQUENCY_K 406052000 +#define BPSK_FREQUENCY_N 406061000 +#define BPSK_FREQUENCY_O 406064000 + namespace ui::external_app::epirb_tx { +enum class BeaconMode { + FILE = 0, + MODE_MANUAL = 1 +}; + enum class BeaconType { EPIRB = 0, ELT = 1, @@ -51,6 +69,25 @@ enum class BeaconProtocol { NATIONAL = 2 }; +enum class AmChannel { + TEST = 0, + REAL = 1, + MANUAL = 2 +}; + +enum class BpskChannel { + HAM = 0, + B = 1, + C = 2, + F = 3, + G = 4, + J = 5, + K = 6, + N = 7, + O = 8, + MANUAL = 10 +}; + struct Location { std::string locator; bool south; @@ -95,6 +132,8 @@ class EPIRBTXAppView : public View { std::string frame_to_hex_string(bool start); void generate_frame(BeaconParams params); void update_frame(bool updateConfig = true); + void update_bpsk_frequency(); + void update_am_transmission(); void update_mode(); void update_location(bool updateLocatorField = true); @@ -114,25 +153,33 @@ class EPIRBTXAppView : public View { // Frequency of the transmitter before starting the app (used to restore frequency when leaving) rf::Frequency original_frequency{0}; // Frequency of the AM emergency signal - rf::Frequency am_frequency{121500000}; + rf::Frequency am_frequency{AM_TEST_FREQUENCY}; // Frequency of the 406 MHz BPSK signal - rf::Frequency bpsk_frequency{406025000}; + rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM}; + // Selected am channel + uint8_t am_channel{(uint8_t)AmChannel::TEST}; + // Selected bpsk channel + uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM}; + // Manual AM frequency value + rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY}; + // Manual BPSK frequency value + rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM}; // True when using a beacon from the BEACONS.TXT file - bool mode_file{true}; + bool mode_file{false}; // True when looping on sending beacons is enabled bool loop_enabled{true}; // True if AM emergency signal transmission is enabled bool am_enabled{true}; // True if we want to send a new frame each time the user changes the current beacon - bool send_on_change{false}; + bool send_on_change{true}; // The current locator string std::string locator{"JN03RO"}; // The delay between each frame when on loop mode uint32_t delay{50}; - uint8_t beacon_type{0}; + uint8_t beacon_type{(uint8_t)BeaconType::EPIRB}; // Currently selected beacon protocol - uint8_t beacon_protocol{0}; + uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER}; // Currently selected beacon country uint32_t beacon_country{227}; // Current beacon's internal state (true for internal location system) @@ -150,6 +197,8 @@ class EPIRBTXAppView : public View { {"sbeacon"sv, &selected_beacon}, {"amfreq"sv, &am_frequency}, {"bpskfreq"sv, &bpsk_frequency}, + {"amchan"sv, &am_channel}, + {"bpskchan"sv, &bpsk_channel}, {"loop"sv, &loop_enabled}, {"delay"sv, &delay}, {"file"sv, &mode_file}, @@ -166,6 +215,8 @@ class EPIRBTXAppView : public View { uint32_t last_frame_time{0}; // True when transmission is enabled bool transmitting{false}; + // True when transmitting a BPSK frame + bool transmitting_bpsk{false}; // True when currently looping on sending beacons bool loop{false}; @@ -179,7 +230,9 @@ class EPIRBTXAppView : public View { {{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency MHz", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}}; // For file mode @@ -231,15 +284,15 @@ class EPIRBTXAppView : public View { OptionsField options_beacon_type{ {UI_POS_X(9), UI_POS_Y(1)}, 7, - {{"EPIRB", 0}, - {"ELT", 1}, - {"PLB", 2}}}; + {{"EPIRB", (uint8_t)BeaconType::EPIRB}, + {"ELT", (uint8_t)BeaconType::ELT}, + {"PLB", (uint8_t)BeaconType::PLB}}}; OptionsField options_beacon_protocol{ {UI_POS_X(9 + 7), UI_POS_Y(1)}, 30, - {{"User", 0}, - {"Standard", 1}, - {"National", 2}}}; + {{"User", (uint8_t)BeaconProtocol::USER}, + {"Standard", (uint8_t)BeaconProtocol::STANDARD}, + {"National", (uint8_t)BeaconProtocol::NATIONAL}}}; OptionsField options_beacon_country{ {UI_POS_X(9), UI_POS_Y(2)}, 7, @@ -261,8 +314,8 @@ class EPIRBTXAppView : public View { OptionsField options_mode{ {UI_POS_X(7), UI_POS_Y(0)}, 30, - {{"File (BEACONS.TXT)", 0}, - {"Manual (Editor)", 1}}}; + {{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE}, + {"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}}; // Frame content Text text_frame{ @@ -305,6 +358,25 @@ class EPIRBTXAppView : public View { "START"}; const Style& style_tx_start = *Theme::getInstance()->fg_green; const Style& style_tx_stop = *Theme::getInstance()->fg_red; + OptionsField options_am_channel{ + {UI_POS_X(11), UI_POS_Y(14)}, + 20, + {{"121.375 MHz (Test)", 0}, + {"121.500 MHz /!\\Real", 1}, + {"Manual", 2}}}; + OptionsField options_bpsk_channel{ + {UI_POS_X(11), UI_POS_Y(15)}, + 20, + {{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM}, + {"406.025 MHz (B)", (uint8_t)BpskChannel::B}, + {"406.028 MHz (C)", (uint8_t)BpskChannel::C}, + {"406.037 MHz (F)", (uint8_t)BpskChannel::F}, + {"406.040 MHz (G)", (uint8_t)BpskChannel::G}, + {"406.049 MHz (J)", (uint8_t)BpskChannel::J}, + {"406.052 MHz (K)", (uint8_t)BpskChannel::K}, + {"406.061 MHz (N)", (uint8_t)BpskChannel::N}, + {"406.064 MHz (O)", (uint8_t)BpskChannel::O}, + {"Manual", (uint8_t)BpskChannel::MANUAL}}}; // Transmitter view TransmitterView tx_view{ diff --git a/firmware/baseband/proc_epirb_tx.cpp b/firmware/baseband/proc_epirb_tx.cpp index 90a603a2b..6a288f4bc 100644 --- a/firmware/baseband/proc_epirb_tx.cpp +++ b/firmware/baseband/proc_epirb_tx.cpp @@ -46,15 +46,15 @@ void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) { if (mode_bpsk) { // BPSK Manchester beacon signal if (bpsk_pre_count < config_pre_count) { - // Pre-count state: send a negative phase carrier during pre-count + // Pre-count state: send carrier only during pre-count bpsk_pre_count++; - re = i_neg; - im = q_neg; + re = i_carrier; + im = q_carrier; } else if (bpsk_post_count > 0) { - // Post-count: send a negative phase carrier during post-count + // Post-count: send carrier only during post-count bpsk_post_count++; - re = i_neg; - im = q_neg; + re = i_carrier; + im = q_carrier; if (bpsk_post_count >= config_post_count) { // End transmission here byte_index = 0; diff --git a/firmware/baseband/proc_epirb_tx.hpp b/firmware/baseband/proc_epirb_tx.hpp index 1b624b56e..218cf2842 100644 --- a/firmware/baseband/proc_epirb_tx.hpp +++ b/firmware/baseband/proc_epirb_tx.hpp @@ -60,14 +60,17 @@ class EPIRBTXProcessor : public BasebandProcessor { // Size of the frame to send in BPSK mode uint8_t frame_data_len = 0; - // BPSK parameters: Target phase +/-63° as per COSPAS/SARSAT specifications - static constexpr float phase_rad = 63.0f * M_PI / 180.0f; + // BPSK parameters: Target phase +/-1.1 RAD as per COSPAS/SARSAT specifications + static constexpr float phase_rad = 1.1f; // I/Q values for BPSK (positive phase and negative phase) int8_t i_pos = (int8_t)(cos(phase_rad) * 127); int8_t q_pos = (int8_t)(sin(phase_rad) * 127); int8_t i_neg = i_pos; int8_t q_neg = -q_pos; + // I/Q values for carrier only + static constexpr int8_t i_carrier = 127; + static constexpr int8_t q_carrier = 0; // COSPAS/SARSAT signal is manchester (2 states per bit) encoded 400 bit/sec static const uint32_t samples_per_halfbit = TONES_SAMPLERATE / 400 / 2;