From 5cc5f8faa61d48c9fe7b1191500e591f23a1c5d7 Mon Sep 17 00:00:00 2001 From: Pezsma <159525557+Pezsma@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:49:21 +0100 Subject: [PATCH] Morserx added ssb modes (#2934) * USB receive good * LSB receive good * all mode receive good, log is dead * frequency measurement good, decode dead * total killed * szutyok, gerjed * frequenty meas only. * ui ok * Add frequency measurement handling and update Morse processing logic * fix ssb speed --- firmware/application/baseband_api.cpp | 4 +- firmware/application/baseband_api.hpp | 2 +- .../external/morse_radio/ui_morse_radio.cpp | 114 +++--- .../external/morse_radio/ui_morse_radio.hpp | 40 +- firmware/baseband/proc_morse.cpp | 369 +++++++++++------- firmware/baseband/proc_morse.hpp | 69 ++-- firmware/common/message.hpp | 23 +- 7 files changed, 386 insertions(+), 235 deletions(-) diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 7ba39f452..9bfffcf6e 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -368,8 +368,8 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) { send_message(&message); } -void set_moreserx_config() { - const MorseRXConfigureMessage message{}; +void set_moreserx_config(uint8_t mode) { + const MorseRXConfigureMessage message{mode}; send_message(&message); } diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index dea7bab41..8bc5174da 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -102,7 +102,7 @@ void set_siggen_tone(const uint32_t tone); void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration); void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw); void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate); -void set_moreserx_config(); +void set_moreserx_config(uint8_t mode); void set_wefax_config(uint8_t lpm, uint8_t ioc); void set_noaaapt_config(); void set_flex_config(); diff --git a/firmware/application/external/morse_radio/ui_morse_radio.cpp b/firmware/application/external/morse_radio/ui_morse_radio.cpp index 4570d4fa2..bb3ba6d30 100644 --- a/firmware/application/external/morse_radio/ui_morse_radio.cpp +++ b/firmware/application/external/morse_radio/ui_morse_radio.cpp @@ -16,6 +16,8 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav) &field_frequency, &txt_last, &txt_speed, + &txt_freq, + &options_mode, &btn_clear, &console_text, &labels, @@ -31,28 +33,43 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav) logger->init_daily_log(logs_dir); }; - audio::set_rate(audio::Rate::Hz_24000); audio::output::start(); - receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); - receiver_model.set_hidden_offset(0); - baseband::set_moreserx_config(); - receiver_model.set_am_configuration(4); - receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); receiver_model.enable(); + options_mode.on_change = [this](size_t, int32_t value) { + current_mode = (uint8_t)value; + morse_decoder_.resetLearning(); + if (current_mode == 0) { + receiver_model.set_am_configuration(4); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + field_squelch.set_style(Theme::getInstance()->option_active); + field_squelch.set_focusable(true); + receiver_model.set_squelch_level(field_squelch.value()); + audio::set_rate(audio::Rate::Hz_24000); + } else { + receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); + receiver_model.set_am_configuration(current_mode + 7); + receiver_model.set_squelch_level(0); + field_squelch.set_style(Theme::getInstance()->fg_dark); + field_squelch.set_focusable(false); + audio::set_rate(audio::Rate::Hz_12000); + } + baseband::set_moreserx_config(current_mode); + }; + field_squelch.set_value(receiver_model.squelch_level(), false); // will be sent later, no need to send 2x + field_squelch.on_change = [this](int32_t v) { + if (current_mode == 0) + receiver_model.set_squelch_level(v); + }; + options_mode.set_selected_index(current_mode, true); + auto vol = field_volume.value(); // audio volume fix field_volume.set_value(0); field_volume.set_value(vol); - field_squelch.set_value(receiver_model.squelch_level()); - field_squelch.on_change = [this](int32_t v) { - receiver_model.set_squelch_level(v); - }; - logger = std::make_unique(); - chk_log.on_select = [this](Checkbox&, bool save) { save_log = save; if (logger && save_log) @@ -86,35 +103,29 @@ void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) { } } -void MorseLogger::radio_set_log() { +void MorseLogger::radio_set_log(uint8_t current_mode) { int64_t freq = receiver_model.target_frequency(); - std::string header = "Freq:" + to_string_dec_int(freq / 1000000) + "." + to_string_dec_int((freq % 1000000) / 1000, 3); - header += "MHz SQ:" + to_string_dec_uint(receiver_model.squelch_level()); - header += " LNA:" + to_string_dec_uint(receiver_model.lna()); - header += " VGA:" + to_string_dec_uint(receiver_model.vga()); - header += " AMP:" + std::string(receiver_model.rf_amp() ? "ON" : "OFF"); - header += "\r\nMessage:\r\n"; - + std::string header = "Freq:" + to_string_rounded_freq(freq, 4); + header += "MHz, "; + header += "RX MODE:" + std::string(current_mode == 0 ? "CW/FM" : (current_mode == 1 ? "USB" : "LSB")); + header += "\r\nMessage:"; log_file.write_raw(header); } -bool MorseLogger::on_packet(const std::string& content, bool time) { +bool MorseLogger::on_packet(const std::string& content, bool time, uint8_t current_mode) { if (!time) { log_file.write_raw_no_newline("\r\n\r\n"); auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS); log_file.write_raw("[" + timestamp + "] "); - - radio_set_log(); - + radio_set_log(current_mode); char_count = 0; time = true; } if (content.empty()) return time; - + std::string s; for (char c : content) { - std::string s(1, c); - + s = c; if ((char_count >= 35 && c == ' ') || (char_count >= 47)) { log_file.write_raw_no_newline("\r\n"); @@ -145,14 +156,11 @@ void MorseRadioView::focus() { void MorseRadioView::check_for_timeout() { uint64_t now = chTimeNow(); - if (last_activity_time == 0) { last_activity_time = now; return; } - uint64_t quiet_duration = now - last_activity_time; - if (quiet_duration >= 60000) { morse_decoder_.resetLearning(); time_stamp = false; @@ -170,13 +178,11 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) { long_pause_sent_ = false; return 0; } - if (sign == last_sign_) { accumulator_us_ += sig_time_us; if (sign < 0) { int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000; - if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) { result = accumulator_us_ / 1000; long_pause_sent_ = true; @@ -189,53 +195,55 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) { } else { result = 0; } - accumulator_us_ = sig_time_us; last_sign_ = sign; long_pause_sent_ = false; } - return result; } void MorseRadioView::on_data(const MorseRXDataMessage* message) { - uint16_t start = 0; - uint16_t stop = 0; - bool has_valid = false; int32_t r; - for (uint16_t i = 0; i <= message->maxptr; ++i) { - if (message->state_durations[i] >= 30000 || message->state_durations[i] <= -30000) { - if (!has_valid) { - start = i; - } - } else { - has_valid = true; - stop = i; - } - } - if (!has_valid) return; // no valid data arrived - - for (uint16_t i = start; i <= stop; i++) { + for (uint8_t i = 0; i <= message->state_cnt; ++i) { r = ProcessSignal(message->state_durations[i]); auto result = morse_decoder_.handleInput((r != 0) ? r : 0); if (result.isValid()) { last_activity_time = chTimeNow(); // start reset timer on valid input writeCharToConsole(result.text, result.confidence); if (logger && save_log) { - time_stamp = logger->on_packet(result.text, time_stamp); + time_stamp = logger->on_packet(result.text, time_stamp, current_mode); } float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f; - if (dah_time > 0) { uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f); - txt_speed.set(to_string_dec_uint(wpm)); } } } } +void MorseRadioView::on_freq(const MorseRXfreqMessage* message) { + std::string ret = " "; + uint32_t freq = message->measured_frequency; + if (freq < 301) { + ret = "<"; + freq = 300; + } + if (freq > 2299) { + freq = 2300; + ret = ">"; + } + if (freq < 400 || freq > 1400) + txt_freq.set_style(Theme::getInstance()->fg_red); + else if (freq <= 580 || freq >= 1220) + txt_freq.set_style(Theme::getInstance()->fg_yellow); + else + txt_freq.set_style(Theme::getInstance()->fg_green); + ret += to_string_dec_uint(freq); + txt_freq.set(ret); +} + void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) { if (ch.empty()) { return; diff --git a/firmware/application/external/morse_radio/ui_morse_radio.hpp b/firmware/application/external/morse_radio/ui_morse_radio.hpp index 379c37c1f..b78845cce 100644 --- a/firmware/application/external/morse_radio/ui_morse_radio.hpp +++ b/firmware/application/external/morse_radio/ui_morse_radio.hpp @@ -40,6 +40,7 @@ #include "file.hpp" #include "rtc_time.hpp" #include "morsedecoder.hpp" + #include namespace ui::external_app::morse_radio { @@ -53,8 +54,8 @@ class MorseLogger { } void init_daily_log(const std::filesystem::path& log_dir); - bool on_packet(const std::string& content, bool time); - void radio_set_log(); + bool on_packet(const std::string& content, bool time, uint8_t current_mode); + void radio_set_log(uint8_t current_mode); private: LogFile log_file{}; @@ -73,6 +74,7 @@ class MorseRadioView : public ui::View { private: void writeCharToConsole(const std::string& ch, double confidence); void on_data(const MorseRXDataMessage* message); + void on_freq(const MorseRXfreqMessage* message); void on_message(const Message* const p); void check_for_timeout(); int32_t ProcessSignal(int32_t sig_time_us); @@ -80,8 +82,13 @@ class MorseRadioView : public ui::View { MorseDecoder morse_decoder_{}; RxRadioState radio_state_{}; std::unique_ptr logger{}; + + uint8_t current_mode{0}; // 0=CW/FM, 1=USB, 2=LSB + app_settings::SettingsManager settings_{ - "trx_morese_radio", app_settings::Mode::RX_TX}; + "rx_morese_radio", + app_settings::Mode::RX, + {{"cwmode"sv, ¤t_mode}}}; RxFrequencyField field_frequency{ {UI_POS_X(0), UI_POS_Y(0)}, @@ -107,27 +114,35 @@ class MorseRadioView : public ui::View { }; ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"}; ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""}; + ui::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"}; ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}}; ui::Labels labels{ {{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground}, {{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground}, - }; + {{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}}; + + ui::OptionsField options_mode{ + {UI_POS_X(9), UI_POS_Y(2)}, + 5, + {{"CW/FM", 0}, {"USB", 1}, {"LSB", 2}}}; + Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false}; ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"}; - uint8_t last_color_id{255}; - uint8_t color_id{255}; std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN}; - int32_t accumulator_us_{0}; - int8_t last_sign_{0}; - uint8_t space_timer{0}; bool long_pause_sent_{false}; bool save_log{false}; bool reset_triggered_{false}; bool time_stamp{false}; + uint8_t last_color_id{255}; + uint8_t color_id{255}; + int8_t last_sign_{0}; + uint8_t space_timer{0}; + int32_t accumulator_us_{0}; uint64_t last_activity_time{0}; MessageHandlerRegistration message_handler_packet{ @@ -137,6 +152,13 @@ class MorseRadioView : public ui::View { this->on_data(message); }}; + MessageHandlerRegistration message_handler_freq{ + Message::ID::MorseRXfreq, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freq(message); + }}; + MessageHandlerRegistration message_handler_framesync{ Message::ID::DisplayFrameSync, [this](const Message* const p) { diff --git a/firmware/baseband/proc_morse.cpp b/firmware/baseband/proc_morse.cpp index 449116c05..5e08dcc41 100644 --- a/firmware/baseband/proc_morse.cpp +++ b/firmware/baseband/proc_morse.cpp @@ -25,186 +25,289 @@ #include "event_m4.hpp" #include -void MorseProcessor::configure() { +void MorseProcessor::configure(uint8_t mode) { configured = false; - baseband_fs = 3072000; - baseband_thread.set_sampling_rate(baseband_fs); - // 1. DSP filters - decim_0.configure(taps_11k0_decim_0.taps); - decim_1.configure(taps_11k0_decim_1.taps); - channel_filter.configure(taps_11k0_channel.taps, 2); - demod.configure(24000, 5000); - audio_output.configure(iir_config_passthrough, iir_config_passthrough, 0.0f); + if (mode == 0) { // CW/FM + decim_0.configure(taps_11k0_decim_0.taps); + decim_1.configure(taps_11k0_decim_1.taps); + channel_filter.configure(taps_11k0_channel.taps, 2); + demod_cw_fm.configure(24000, 5000); + } else { // USB, LSB + decim_0.configure(taps_6k0_decim_0.taps); + decim_1.configure(taps_6k0_decim_1.taps); - // 2. Resetting variables - dc_offset = 0; - lpf_sample = 0; - prev_sample = 0; + if (mode == 1) // USB + channel_filter.configure(taps_2k8_usb_channel.taps, 4); + else // LSB + channel_filter.configure(taps_2k8_lsb_channel.taps, 4); + } - // 3. Algorithm reset - zc_counter = 0; - last_zc_counter = 0; - current_freq = 700.0f; - update_goertzel_coeff(700.0f); + modulation = mode; + if (mode > 0) + audio_output.configure(audio_12k_hpf_300hz_config); + else + audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f); - goertzel_count = 0; + meas_samples_in_period = 0; + meas_last_period_len = 0; + meas_consistency_count = 0; + meas_signal_state_high = false; + meas_freq_accumulator = 0.0f; + meas_freq_count = 0; + ui_update_timer = 0; + + // Decoding reset s_prev_i = 0; s_prev2_i = 0; + goertzel_count = 0; duration_samples = 0; was_signaling = false; - noise_floor = 5000; // learning speed - + // Thresholds + noise_floor = 5000; startup_delay = 20; + squelch_is_open = true; + squelch_hold = 0; + + current_freq = 700.0f; + update_goertzel_coeff(current_freq); - squelch_is_open = false; configured = true; } -void MorseProcessor::on_message(const Message* const p) { - if (p->id == Message::ID::MorseRXConfig) { - configure(); - } else if (p->id == Message::ID::NBFMConfigure) { - auto nbfm_msg = *reinterpret_cast(p); - user_squelch_level = nbfm_msg.squelch_level; - audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f); +inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) { + if (modulation > 0) { + // SSB always keeps squelch "technically" open for the demodulator + squelch_is_open = true; + return demod_ssb.execute(channel, audio_buffer); } + return demod_cw_fm.execute(channel, audio_buffer); } void MorseProcessor::update_goertzel_coeff(float freq) { - if (freq < 400.0f) freq = 400.0f; - if (freq > 1500.0f) freq = 1500.0f; // limit to algo capacity min/max - float omega = 2.0f * M_PI * freq / 24000.0f; - coeff_int = (int32_t)(2.0f * cosf(omega) * 16384.0f); + // limit to algo capacity min/max + if (freq < 300.0f) freq = 300.0f; + if (freq > 2300.0f) freq = 2300.0f; + + float sample_rate = (modulation == 0) ? 24000.0f : 12000.0f; + float omega = 2.0f * M_PI * freq / sample_rate; + float omega_sq = omega * omega; + float cos_approx = 1.0f - (omega_sq * 0.5f); + // 16384 is the scaling factor for the Goertzel integer math + coeff_int = (int32_t)(2.0f * cos_approx * 16384.0f); +} + +void MorseProcessor::measure_frequency(int32_t sample) { + // Noise gate threshold + const int32_t gate_threshold = (modulation == 0) ? 4000 : 2000; + + if (sample > gate_threshold || sample < -gate_threshold) { + if (sample > 0 && !meas_signal_state_high) { + // Rising Edge (End of Period) + meas_signal_state_high = true; + + if (meas_samples_in_period > 0) { + bool period_is_stable = false; + if (meas_last_period_len > 0) { + int32_t diff = std::abs((int)meas_samples_in_period - (int)meas_last_period_len); + if (diff <= 1) { + meas_consistency_count++; + period_is_stable = true; + } else { + meas_consistency_count = 0; + } + } + meas_last_period_len = meas_samples_in_period; + + // Wait for AT LEAST 3 STABLE CYCLES + if (period_is_stable && meas_consistency_count > 3) { + float base_rate = (modulation == 0) ? 24000.0f : 12000.0f; + float inst_freq = base_rate / (float)meas_samples_in_period; + + // Check for overflows + if (inst_freq > 250 && inst_freq < 3000) { + meas_freq_accumulator += inst_freq; + meas_freq_count++; + } + } + } + meas_samples_in_period = 0; + + } else if (sample < 0) { + meas_signal_state_high = false; + } + } else { + // Silence detection + if (meas_samples_in_period > 200) { + meas_last_period_len = 0; + meas_consistency_count = 0; + } + } + + meas_samples_in_period++; + + ui_update_timer++; + uint32_t update_limit = (modulation == 0) ? 4800 : 2400; // ~200ms + + if (ui_update_timer > update_limit) { + if (meas_freq_count > 0) { + float avg_freq = meas_freq_accumulator / (float)meas_freq_count; + current_freq = avg_freq; + // Round to 5Hz for display + uint32_t stable_disp = (uint32_t)avg_freq; + stable_disp = (stable_disp / 5) * 5; + + freq_message.measured_frequency = stable_disp; + shared_memory.application_queue.push(freq_message); + } + + meas_freq_accumulator = 0.0f; + meas_freq_count = 0; + ui_update_timer = 0; + } +} + +void MorseProcessor::process_decoding(int32_t sample) { + update_goertzel_coeff(current_freq); + + // 1. Goertzel Algorithm + int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i; + s_prev2_i = s_prev_i; + s_prev_i = (int32_t)s; + goertzel_count++; + + // 2. Evaluation every 60 samples + if (goertzel_count >= 60) { + if (startup_delay > 0) { + startup_delay--; + // Fast learning phase + int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - + (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); + noise_floor = (noise_floor * 15 + pwr) / 16; + } else { + // Power calculation + int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - + (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); + + // Adaptive Noise Floor + if (!was_signaling) { + noise_floor = (noise_floor * 127 + power) / 128; + } + + // Threshold Calculation + int64_t sensitivity = 4 + (user_squelch_level / 10); + int64_t base_pwr_threshold = noise_floor * sensitivity; + int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold; + + // Tone Detection + bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000); + int32_t time_base = modulation == 0 ? 125 : 250; + // State Change Logic + if (is_tone != was_signaling) { + int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3); + + // Send message if significant + if (duration_us > 10000 || was_signaling) { + message.state_durations[0] = was_signaling ? duration_us : -duration_us; + message.state_cnt = 1; // 1 indicates valid state duration data + shared_memory.application_queue.push(message); + } + was_signaling = is_tone; + duration_samples = 0; + } + + // Timeout Logic + if (!was_signaling && duration_samples > 28800) { + int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3); + message.state_durations[0] = -duration_us; + message.state_cnt = 1; + shared_memory.application_queue.push(message); + duration_samples = 0; + } + } + + duration_samples += 60; + s_prev_i = 0; + s_prev2_i = 0; + goertzel_count = 0; + } } void MorseProcessor::execute(const buffer_c8_t& buffer) { if (!configured) return; - buffer_c16_t dst_buffer_c16(dst_buffer.data(), dst_buffer.size()); - const auto decim_0_out = decim_0.execute(buffer, dst_buffer_c16); - const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer_c16); - const auto channel = channel_filter.execute(decim_1_out, dst_buffer_c16); + 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_out = channel_filter.execute(decim_1_out, dst_buffer); - feed_channel_stats(channel); - - buffer_s16_t audio_buffer_s16(audio_buffer.data(), audio_buffer.size()); - auto audio_buf = demod.execute(channel, audio_buffer_s16); + auto audio_buf = demodulate(channel_out); for (size_t i = 0; i < audio_buf.count; i++) { - int32_t raw_sample = audio_buf.p[i]; + float raw_audio = audio_buf.p[i]; - // 1. DC & LPF - dc_offset += (raw_sample - dc_offset) / 32; - int32_t sample = raw_sample - dc_offset; - lpf_sample = (lpf_sample * 3 + sample) / 4; + // Squelch Logic + int32_t raw_int_abs = (int32_t)(raw_audio * 32768.0f); + if (raw_int_abs < 0) raw_int_abs = -raw_int_abs; - // 2. Squelch - int32_t abs_sample = (sample < 0) ? -sample : sample; int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3; - int32_t current_audio_threshold = squelch_is_open ? (audio_threshold / 2) : audio_threshold; - if (abs_sample > current_audio_threshold || user_squelch_level == 0) { + if (raw_int_abs > audio_threshold || user_squelch_level == 0) { squelch_is_open = true; - squelch_hold = 2400; + squelch_hold = (modulation == 0) ? 2400 : 1200; } else { if (squelch_hold > 0) squelch_hold--; - else + else if (modulation == 0) squelch_is_open = false; } - // 3. Frequency measurement (ZC) - if (squelch_is_open && !was_signaling) { - if ((lpf_sample >= 0 && prev_sample < 0) || (lpf_sample < 0 && prev_sample >= 0)) { - if (zc_counter >= 8 && zc_counter <= 32) { - int32_t diff = zc_counter - last_zc_counter; - if (diff >= -1 && diff <= 1) { - float n_freq = 24000.0f / (zc_counter * 2.0f); + measure_frequency((int32_t)(raw_audio * 32768.0f)); - // Hybrid tracking + float decode_audio = raw_audio; + if (modulation > 0) { + const float gain = 6.0f; + decode_audio *= gain; - if (zc_counter < 15) { - // This stabilizes the 1000-1400 Hz range - current_freq = (current_freq * 0.6f) + (n_freq * 0.4f); - } else { - current_freq = n_freq; - } + // Hard Limiting / Clipping + if (decode_audio > 1.0f) + decode_audio = 1.0f; + else if (decode_audio < -1.0f) + decode_audio = -1.0f; - update_goertzel_coeff(current_freq); - } - last_zc_counter = zc_counter; - } else { - last_zc_counter = 0; - } - zc_counter = 0; - } else { - if (zc_counter < 100) zc_counter++; - } - } - prev_sample = (int16_t)lpf_sample; - - // 4. Goertzel - int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i; - s_prev2_i = s_prev_i; - s_prev_i = (int32_t)s; - goertzel_count++; - - // 5. Detection - if (goertzel_count >= 60) { - if (startup_delay > 0) { - startup_delay--; - int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - - (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); - noise_floor = (noise_floor * 15 + pwr) / 16; - } else { - int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i - - (((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14); - - if (!was_signaling) { - noise_floor = (noise_floor * 127 + power) / 128; - } - - int64_t sensitivity = 4 + (user_squelch_level / 10); - int64_t base_pwr_threshold = noise_floor * sensitivity; - int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold; - - bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000); - - if (is_tone != was_signaling) { - int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3); - - if (duration_us > 10000) { - message.state_durations[0] = was_signaling ? duration_us : -duration_us; - message.measured_frequency = (uint32_t)current_freq; - message.state_cnt = 1; - shared_memory.application_queue.push(message); - } - was_signaling = is_tone; - duration_samples = 0; - } - - if (!was_signaling && duration_samples > 28800) { - int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3); - message.state_durations[0] = -duration_us; - message.state_cnt = 1; - shared_memory.application_queue.push(message); - duration_samples = 0; - } - } - - duration_samples += 60; - s_prev_i = 0; - s_prev2_i = 0; - goertzel_count = 0; + audio_buf.p[i] = decode_audio; } - audio_buf.p[i] = squelch_is_open ? (int16_t)sample : 0; + process_decoding((int32_t)(decode_audio * 32768.0f)); + + if (modulation == 0 && !squelch_is_open) { + audio_buf.p[i] = 0.0f; // mute nfm on squelch + } } + audio_output.write(audio_buf); } +void MorseProcessor::on_message(const Message* const p) { + switch (p->id) { + case Message::ID::MorseRXConfig: { + auto morse_rx_msg = *reinterpret_cast(p); + configure(morse_rx_msg.mode); + break; + } + + case Message::ID::NBFMConfigure: { + auto nbfm_msg = *reinterpret_cast(p); + user_squelch_level = nbfm_msg.squelch_level; + audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f); + break; + } + + default: + break; + } +} + int main() { audio::dma::init_audio_out(); EventDispatcher event_dispatcher{std::make_unique()}; diff --git a/firmware/baseband/proc_morse.hpp b/firmware/baseband/proc_morse.hpp index 5baa03c87..14fd2dc6e 100644 --- a/firmware/baseband/proc_morse.hpp +++ b/firmware/baseband/proc_morse.hpp @@ -37,54 +37,63 @@ class MorseProcessor : public BasebandProcessor { void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const p) override; - void update_goertzel_coeff(float freq); private: - bool configured{false}; + void configure(uint8_t mode); + buffer_f32_t demodulate(const buffer_c16_t& channel); + void update_goertzel_coeff(float freq); + void measure_frequency(int32_t sample); + void process_decoding(int32_t sample); - size_t baseband_fs = 3072000; - - BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; + BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive}; RSSIThread rssi_thread{}; - std::array dst_buffer{}; - std::array audio_buffer{}; + 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()}; dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; dsp::decimate::FIRAndDecimateComplex channel_filter{}; - dsp::demodulate::FM demod{}; + dsp::demodulate::FM demod_cw_fm{}; + dsp::demodulate::SSB demod_ssb{}; AudioOutput audio_output{}; - // Goertzel and signal detection + bool configured{false}; + + uint8_t modulation{0}; // 0=CW/FM, 1=USB, 2=LSB + int32_t user_squelch_level{0}; + bool squelch_is_open{true}; + int32_t squelch_hold{0}; + + // frequency measurement variables + uint32_t meas_samples_in_period{0}; + bool meas_signal_state_high{false}; + uint32_t meas_last_period_len{0}; + uint32_t meas_consistency_count{0}; + float meas_freq_accumulator{0.0f}; + uint32_t meas_freq_count{0}; + uint32_t ui_update_timer{0}; + + float current_freq{700.0f}; + + // --- Decoding variables (Goertzel) --- int32_t coeff_int{0}; int32_t s_prev_i{0}; int32_t s_prev2_i{0}; uint32_t goertzel_count{0}; uint32_t duration_samples{0}; bool was_signaling{false}; - - // Variables required for measurement - int16_t prev_sample{0}; - uint32_t zc_counter{0}; - float current_freq{0.0f}; - - int32_t lpf_sample{0}; - int32_t dc_offset{0}; - - // Squelch and stability - bool squelch_is_open{false}; - int32_t squelch_hold{0}; - - int32_t user_squelch_level{0}; - int64_t noise_floor{0}; - - int32_t startup_delay{0}; // Power-on delay - int32_t last_zc_counter{0}; // Previous ZC measurement for comparison + int64_t noise_floor{5000}; + int32_t startup_delay{20}; MorseRXDataMessage message{}; - - void configure(); + MorseRXfreqMessage freq_message{}; }; -#endif // __PROC_MORSE_H__ \ No newline at end of file +#endif \ No newline at end of file diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 930342111..3236a17a9 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -147,8 +147,9 @@ class Message { SSTVRXCalibration = 89, SubCarData = 90, MorseRXData = 91, - MorseRXConfig = 92, - TXDisabled = 93, + MorseRXfreq = 92, + MorseRXConfig = 93, + TXDisabled = 94, MAX }; @@ -1706,16 +1707,24 @@ class MorseRXDataMessage : public Message { public: constexpr MorseRXDataMessage() : Message{ID::MorseRXData} {} + int32_t state_durations[5] = {0}; // positive: high, negative: low + uint8_t state_cnt = 0; + const uint8_t maxptr = 4; +}; + +class MorseRXfreqMessage : public Message { + public: + constexpr MorseRXfreqMessage() + : Message{ID::MorseRXfreq} {} uint32_t measured_frequency = 0; - int32_t state_durations[2] = {0}; // positive: high, negative: low - uint16_t state_cnt = 0; - const uint16_t maxptr = 1; }; class MorseRXConfigureMessage : public Message { public: - constexpr MorseRXConfigureMessage() - : Message{ID::MorseRXConfig} {} + constexpr MorseRXConfigureMessage(uint8_t mode) + : Message{ID::MorseRXConfig}, + mode{mode} {} + uint8_t mode = 0; }; class TXDisabledMessage : public Message {