[fix](trx-cw): emit CW transition events

Restore empty events on tone edges so the frontend signal indicator updates again.
Add synthetic-tone tests covering transitions and basic decoding.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-03 02:33:27 +01:00
parent 6f57813d36
commit 004ac19ea6
+65 -1
View File
@@ -346,6 +346,7 @@ impl CwDecoder {
}
}
self.tone_on_at = now;
self.emit_event("");
} else if !detected && self.tone_on {
// Tone just turned off
self.tone_on = false;
@@ -366,6 +367,8 @@ impl CwDecoder {
}
self.auto_detect_wpm();
}
self.emit_event("");
}
// Flush pending character after long silence
@@ -379,7 +382,7 @@ impl CwDecoder {
}
}
fn emit_text(&mut self, text: &str) {
fn emit_event(&mut self, text: &str) {
self.events.push(CwEvent {
text: text.to_string(),
wpm: self.wpm,
@@ -388,6 +391,10 @@ impl CwDecoder {
});
}
fn emit_text(&mut self, text: &str) {
self.emit_event(text);
}
pub fn process_samples(&mut self, samples: &[f32]) -> Vec<CwEvent> {
for &s in samples {
self.sample_buf[self.sample_idx] = s;
@@ -424,3 +431,60 @@ impl CwDecoder {
self.events.clear();
}
}
#[cfg(test)]
mod tests {
use super::CwDecoder;
fn tone_samples(sample_rate: u32, freq_hz: f32, ms: u32) -> Vec<f32> {
let len = (sample_rate as usize * ms as usize) / 1000;
let step = 2.0 * std::f32::consts::PI * freq_hz / sample_rate as f32;
(0..len).map(|i| (i as f32 * step).sin() * 0.8).collect()
}
fn silence_samples(sample_rate: u32, ms: u32) -> Vec<f32> {
vec![0.0; (sample_rate as usize * ms as usize) / 1000]
}
#[test]
fn emits_signal_transition_events() {
let sample_rate = 48_000;
let mut decoder = CwDecoder::new(sample_rate);
decoder.set_auto(false);
decoder.set_wpm(15);
decoder.set_tone_hz(700);
let mut input = tone_samples(sample_rate, 700.0, 100);
input.extend(silence_samples(sample_rate, 500));
let events = decoder.process_samples(&input);
assert!(events
.iter()
.any(|evt| evt.text.is_empty() && evt.signal_on));
assert!(events
.iter()
.any(|evt| evt.text.is_empty() && !evt.signal_on));
}
#[test]
fn decodes_single_e_from_synthetic_tone() {
let sample_rate = 48_000;
let mut decoder = CwDecoder::new(sample_rate);
decoder.set_auto(false);
decoder.set_wpm(15);
decoder.set_tone_hz(700);
let mut input = tone_samples(sample_rate, 700.0, 100);
input.extend(silence_samples(sample_rate, 500));
let events = decoder.process_samples(&input);
let text: String = events
.iter()
.filter(|evt| !evt.text.is_empty())
.map(|evt| evt.text.as_str())
.collect();
assert_eq!(text, "E");
}
}