Morse rx further development (#2959)

* filters OK, radio configurations set

* all modes are working, noise still needs work, especially in FM. A baseband enum is also needed

* audio and decoding fixed, clipping and the baseband enum are still missing
This commit is contained in:
Pezsma
2026-02-03 22:25:52 +01:00
committed by GitHub
parent 6739b43914
commit a44a00acf0
6 changed files with 483 additions and 48 deletions
@@ -17,6 +17,7 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
&txt_last,
&txt_speed,
&txt_freq,
&txt_clip,
&options_mode,
&btn_clear,
&console_text,
@@ -39,9 +40,9 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
receiver_model.enable();
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
current_mode = static_cast<ModulationMode>((uint8_t)value);
morse_decoder_.resetLearning();
if (current_mode == 0) {
if (current_mode == ModulationMode::FM) {
receiver_model.set_am_configuration(4);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
field_squelch.set_style(Theme::getInstance()->option_active);
@@ -49,21 +50,26 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
receiver_model.set_squelch_level(field_squelch.value());
audio::set_rate(audio::Rate::Hz_24000);
} else {
audio::set_rate(audio::Rate::Hz_12000);
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_am_configuration(current_mode + 7);
if (current_mode == ModulationMode::AM || current_mode == ModulationMode::DSB)
receiver_model.set_am_configuration(7);
else if (current_mode == ModulationMode::USB)
receiver_model.set_am_configuration(9);
else // LSB
receiver_model.set_am_configuration(10);
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);
baseband::set_moreserx_config(static_cast<uint8_t>(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)
if (current_mode == ModulationMode::FM)
receiver_model.set_squelch_level(v);
};
options_mode.set_selected_index(current_mode, true);
options_mode.set_selected_index(static_cast<int32_t>(current_mode), true);
auto vol = field_volume.value(); // audio volume fix
field_volume.set_value(0);
@@ -105,9 +111,30 @@ void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
void MorseLogger::radio_set_log(uint8_t current_mode) {
int64_t freq = receiver_model.target_frequency();
std::string mode_str;
switch (static_cast<MorseRadioView::ModulationMode>(current_mode)) {
case MorseRadioView::ModulationMode::AM:
mode_str = "AM";
break;
case MorseRadioView::ModulationMode::FM:
mode_str = "FM";
break;
case MorseRadioView::ModulationMode::DSB:
mode_str = "DSB";
break;
case MorseRadioView::ModulationMode::USB:
mode_str = "USB";
break;
case MorseRadioView::ModulationMode::LSB:
mode_str = "LSB";
break;
default:
mode_str = "???";
break;
}
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 += "RX MODE:" + mode_str;
header += "\r\nMessage:";
log_file.write_raw(header);
}
@@ -152,6 +179,8 @@ MorseRadioView::~MorseRadioView() {
void MorseRadioView::focus() {
field_frequency.focus();
txt_clip.set_style(Theme::getInstance()->fg_red);
txt_clip.hidden(true);
}
void MorseRadioView::check_for_timeout() {
@@ -205,6 +234,8 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
int32_t r;
txt_clip.hidden(!message->clipped);
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);
@@ -212,7 +243,7 @@ void MorseRadioView::on_data(const MorseRXDataMessage* message) {
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, current_mode);
time_stamp = logger->on_packet(result.text, time_stamp, static_cast<uint8_t>(current_mode));
}
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
if (dah_time > 0) {
@@ -64,6 +64,14 @@ class MorseLogger {
class MorseRadioView : public ui::View {
public:
enum class ModulationMode : uint8_t {
AM = 0,
FM = 1,
DSB = 2,
USB = 3,
LSB = 4
};
MorseRadioView(ui::NavigationView& nav);
~MorseRadioView();
std::string title() const override {
@@ -83,12 +91,12 @@ class MorseRadioView : public ui::View {
RxRadioState radio_state_{};
std::unique_ptr<MorseLogger> logger{};
uint8_t current_mode{0}; // 0=CW/FM, 1=USB, 2=LSB
ModulationMode current_mode = ModulationMode::AM;
app_settings::SettingsManager settings_{
"rx_morese_radio",
app_settings::Mode::RX,
{{"cwmode"sv, &current_mode}}};
{{"cwmode"sv, reinterpret_cast<uint8_t*>(&current_mode)}}};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -115,6 +123,7 @@ 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::Text txt_clip{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "clipping"};
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},
@@ -127,7 +136,7 @@ class MorseRadioView : public ui::View {
ui::OptionsField options_mode{
{UI_POS_X(9), UI_POS_Y(2)},
5,
{{"CW/FM", 0}, {"USB", 1}, {"LSB", 2}}};
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
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"};
+59 -34
View File
@@ -28,26 +28,32 @@
void MorseProcessor::configure(uint8_t mode) {
configured = false;
if (mode == 0) { // CW/FM
modulation = static_cast<ModulationMode>((uint8_t)mode);
if (modulation == ModulationMode::FM) { // 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);
if (mode == 1) // USB
channel_filter.configure(taps_2k8_usb_channel.taps, 4);
else // LSB
channel_filter.configure(taps_2k8_lsb_channel.taps, 4);
} else {
decim_0.configure(taps_4k25_decim_0.taps);
decim_1.configure(taps_4k25_decim_1.taps);
if (modulation == ModulationMode::AM) { // AM
channel_filter.configure(taps_2k0_am_lpf_channel.taps, 4);
} else { // SSB, DSB
if (modulation == ModulationMode::DSB) // DSB
channel_filter.configure(taps_1k5_dsb_lpf.taps, 4);
else if (modulation == ModulationMode::USB) // USB
channel_filter.configure(taps_1k5_USB_channel.taps, 4);
else // LSB
channel_filter.configure(taps_1k5_LSB_channel.taps, 4);
}
}
modulation = mode;
if (mode > 0)
audio_output.configure(audio_12k_hpf_300hz_config);
else
if (modulation == ModulationMode::FM)
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
else
audio_output.configure(audio_12k_hpf_300hz_config);
meas_samples_in_period = 0;
meas_last_period_len = 0;
@@ -70,6 +76,8 @@ void MorseProcessor::configure(uint8_t mode) {
squelch_is_open = true;
squelch_hold = 0;
dc_average = 0.0f;
current_freq = 700.0f;
update_goertzel_coeff(current_freq);
@@ -77,10 +85,15 @@ void MorseProcessor::configure(uint8_t mode) {
}
inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) {
if (modulation > 0) {
// SSB always keeps squelch "technically" open for the demodulator
// AM,SSB,DSB always keeps squelch "technically" open for the demodulator
if (modulation == ModulationMode::AM || modulation == ModulationMode::DSB) {
squelch_is_open = true;
return demod_ssb.execute(channel, audio_buffer);
return demod_AM.execute(channel, audio_buffer);
} else {
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB) {
squelch_is_open = true;
return demod_ssb.execute(channel, audio_buffer);
}
}
return demod_cw_fm.execute(channel, audio_buffer);
}
@@ -90,7 +103,7 @@ void MorseProcessor::update_goertzel_coeff(float freq) {
if (freq < 300.0f) freq = 300.0f;
if (freq > 2300.0f) freq = 2300.0f;
float sample_rate = (modulation == 0) ? 24000.0f : 12000.0f;
float sample_rate = (modulation == ModulationMode::FM) ? 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);
@@ -100,7 +113,7 @@ void MorseProcessor::update_goertzel_coeff(float freq) {
void MorseProcessor::measure_frequency(int32_t sample) {
// Noise gate threshold
const int32_t gate_threshold = (modulation == 0) ? 4000 : 2000;
const int32_t gate_threshold = (modulation == ModulationMode::FM) ? 4000 : 2000;
if (sample > gate_threshold || sample < -gate_threshold) {
if (sample > 0 && !meas_signal_state_high) {
@@ -111,7 +124,7 @@ void MorseProcessor::measure_frequency(int32_t sample) {
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) {
if (diff <= 2) {
meas_consistency_count++;
period_is_stable = true;
} else {
@@ -121,10 +134,12 @@ void MorseProcessor::measure_frequency(int32_t sample) {
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;
if (period_is_stable && meas_consistency_count > 5) {
float base_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f;
float inst_freq = base_rate / (float)meas_samples_in_period;
if (modulation == ModulationMode::DSB) {
inst_freq /= 2.0f;
}
// Check for overflows
if (inst_freq > 250 && inst_freq < 3000) {
meas_freq_accumulator += inst_freq;
@@ -148,7 +163,7 @@ void MorseProcessor::measure_frequency(int32_t sample) {
meas_samples_in_period++;
ui_update_timer++;
uint32_t update_limit = (modulation == 0) ? 4800 : 2400; // ~200ms
uint32_t update_limit = (modulation == ModulationMode::FM) ? 4800 : 2400; // ~200ms
if (ui_update_timer > update_limit) {
if (meas_freq_count > 0) {
@@ -202,7 +217,7 @@ void MorseProcessor::process_decoding(int32_t sample) {
// Tone Detection
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
int32_t time_base = modulation == 0 ? 125 : 250;
int32_t time_base = 250;
// State Change Logic
if (is_tone != was_signaling) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3);
@@ -254,33 +269,43 @@ void MorseProcessor::execute(const buffer_c8_t& buffer) {
if (raw_int_abs > audio_threshold || user_squelch_level == 0) {
squelch_is_open = true;
squelch_hold = (modulation == 0) ? 2400 : 1200;
squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200;
} else {
if (squelch_hold > 0)
squelch_hold--;
else if (modulation == 0)
else if (modulation == ModulationMode::FM)
squelch_is_open = false;
}
measure_frequency((int32_t)(raw_audio * 32768.0f));
float decode_audio = raw_audio;
if (modulation > 0) {
const float gain = 6.0f;
if (modulation != ModulationMode::FM) {
float gain = 16.0f;
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB)
gain = 5.0f;
decode_audio *= gain;
// Hard Limiting / Clipping
if (decode_audio > 1.0f)
message.clipped = false;
if (decode_audio > 1.0f) {
decode_audio = 1.0f;
else if (decode_audio < -1.0f)
message.clipped = true;
} else if (decode_audio < -1.0f) {
decode_audio = -1.0f;
}
audio_buf.p[i] = decode_audio;
}
// DC BLOCKING
if (modulation != ModulationMode::FM) {
dc_average = (dc_average * 0.95f) + (decode_audio * 0.05f);
measure_frequency((int32_t)((decode_audio - dc_average) * 32768.0f));
} else {
measure_frequency((int32_t)(raw_audio * 32768.0f));
}
process_decoding((int32_t)(decode_audio * 32768.0f));
if (modulation == 0 && !squelch_is_open) {
if (modulation == ModulationMode::FM && !squelch_is_open) {
audio_buf.p[i] = 0.0f; // mute nfm on squelch
}
}
+13 -2
View File
@@ -60,13 +60,22 @@ class MorseProcessor : public BasebandProcessor {
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::AM demod_AM{};
dsp::demodulate::FM demod_cw_fm{};
dsp::demodulate::SSB demod_ssb{};
AudioOutput audio_output{};
bool configured{false};
uint8_t modulation{0}; // 0=CW/FM, 1=USB, 2=LSB
enum class ModulationMode : uint8_t {
AM = 0,
FM = 1,
DSB = 2,
USB = 3,
LSB = 4
};
ModulationMode modulation = ModulationMode::AM;
int32_t user_squelch_level{0};
bool squelch_is_open{true};
int32_t squelch_hold{0};
@@ -79,7 +88,6 @@ class MorseProcessor : public BasebandProcessor {
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) ---
@@ -92,6 +100,9 @@ class MorseProcessor : public BasebandProcessor {
int64_t noise_floor{5000};
int32_t startup_delay{20};
float dc_average = 0.0f; // DC level tracking
int32_t dc_average_int = 0;
MorseRXDataMessage message{};
MorseRXfreqMessage freq_message{};
};
+358
View File
@@ -1681,4 +1681,362 @@ static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = {
3}},
};
static constexpr fir_taps_real<63> taps_2k0_am_lpf_channel = {
.low_frequency_normalized = -2000.0f / 48000.0f,
.high_frequency_normalized = 2000.0f / 48000.0f,
.transition_normalized = 1500.0f / 48000.0f,
.taps = {{
26,
29,
31,
33,
32,
28,
18,
0,
-26,
-61,
-104,
-152,
-202,
-246,
-279,
-292,
-277,
-227,
-136,
0,
182,
409,
673,
968,
1280,
1595,
1899,
2174,
2407,
2583,
2693,
2731,
2693,
2583,
2407,
2174,
1899,
1595,
1280,
968,
673,
409,
182,
0,
-136,
-227,
-277,
-292,
-279,
-246,
-202,
-152,
-104,
-61,
-26,
0,
18,
28,
32,
33,
31,
29,
26,
}},
};
static constexpr fir_taps_real<64> taps_1K5_FM_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
24,
22,
20,
15,
9,
-2,
-18,
-39,
-66,
-98,
-132,
-167,
-198,
-220,
-228,
-217,
-181,
-117,
-19,
112,
277,
474,
697,
941,
1197,
1454,
1702,
1930,
2128,
2285,
2394,
2451,
2451,
2394,
2285,
2128,
1930,
1702,
1454,
1197,
941,
697,
474,
277,
112,
-19,
-117,
-181,
-217,
-228,
-220,
-198,
-167,
-132,
-98,
-66,
-39,
-18,
-2,
9,
15,
20,
22,
24,
}},
};
static constexpr fir_taps_real<63> taps_1k5_dsb_lpf = {
.low_frequency_normalized = -1500.0f / 48000.0f,
.high_frequency_normalized = 1500.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
0,
0,
-1,
-5,
-10,
-19,
-31,
-46,
-64,
-82,
-99,
-111,
-113,
-100,
-64,
0,
99,
239,
424,
655,
932,
1251,
1605,
1983,
2372,
2757,
3120,
3447,
3719,
3925,
4053,
4096,
4053,
3925,
3719,
3447,
3120,
2757,
2372,
1983,
1605,
1251,
932,
655,
424,
239,
99,
0,
-64,
-100,
-113,
-111,
-99,
-82,
-64,
-46,
-31,
-19,
-10,
-5,
-1,
0,
0,
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_USB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, 2},
{10, 6},
{17, 14},
{27, 29},
{37, 53},
{45, 89},
{49, 140},
{41, 207},
{17, 290},
{-31, 389},
{-109, 499},
{-227, 614},
{-387, 725},
{-595, 819},
{-850, 884},
{-1147, 904},
{-1477, 866},
{-1827, 757},
{-2179, 569},
{-2512, 297},
{-2804, -55},
{-3031, -480},
{-3173, -962},
{-3213, -1481},
{-3142, -2011},
{-2955, -2524},
{-2658, -2991},
{-2262, -3386},
{-1788, -3685},
{-1258, -3873},
{-703, -3939},
{-153, -3884},
{366, -3713},
{826, -3440},
{1208, -3087},
{1499, -2677},
{1693, -2236},
{1789, -1789},
{1796, -1359},
{1726, -966},
{1594, -624},
{1420, -341},
{1220, -120},
{1012, 40},
{809, 144},
{622, 202},
{460, 223},
{324, 217},
{217, 193},
{137, 160},
{80, 125},
{42, 91},
{19, 62},
{6, 39},
{0, 22},
{-1, 11},
{-1, 5},
{-1, 1},
{0, 0},
{0, 0},
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, -2},
{10, -6},
{17, -14},
{27, -29},
{37, -53},
{45, -89},
{49, -140},
{41, -207},
{17, -290},
{-31, -389},
{-109, -499},
{-227, -614},
{-387, -725},
{-595, -819},
{-850, -884},
{-1147, -904},
{-1477, -866},
{-1827, -757},
{-2179, -569},
{-2512, -297},
{-2804, 55},
{-3031, 480},
{-3173, 962},
{-3213, 1481},
{-3142, 2011},
{-2955, 2524},
{-2658, 2991},
{-2262, 3386},
{-1788, 3685},
{-1258, 3873},
{-703, 3939},
{-153, 3884},
{366, 3713},
{826, 3440},
{1208, 3087},
{1499, 2677},
{1693, 2236},
{1789, 1789},
{1796, 1359},
{1726, 966},
{1594, 624},
{1420, 341},
{1220, 120},
{1012, -40},
{809, -144},
{622, -202},
{460, -223},
{324, -217},
{217, -193},
{137, -160},
{80, -125},
{42, -91},
{19, -62},
{6, -39},
{0, -22},
{-1, -11},
{-1, -5},
{-1, -1},
{0, 0},
{0, 0},
}},
};
#endif /*__DSP_FIR_TAPS_H__*/
+1
View File
@@ -1711,6 +1711,7 @@ class MorseRXDataMessage : public Message {
: Message{ID::MorseRXData} {}
int32_t state_durations[5] = {0}; // positive: high, negative: low
uint8_t state_cnt = 0;
bool clipped = false;
const uint8_t maxptr = 4;
};