[feat](trx-wefax): show decoder state transitions in frontend

Emit WefaxProgress events with a state label on each decoder state
transition (APT Start, Phasing, Receiving) so the frontend can display
the current decoder phase instead of just "listening for packets".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-04-03 22:48:10 +02:00
parent a01609212e
commit 2035e0cd6f
3 changed files with 40 additions and 8 deletions
+28 -8
View File
@@ -114,13 +114,13 @@ impl WefaxDecoder {
match tone {
AptTone::Start576 => {
self.idle_phasing = None;
self.transition_to_start_detected(576);
events.push(self.transition_to_start_detected(576));
got_start = true;
break;
}
AptTone::Start288 => {
self.idle_phasing = None;
self.transition_to_start_detected(288);
events.push(self.transition_to_start_detected(288));
got_start = true;
break;
}
@@ -143,7 +143,7 @@ impl WefaxDecoder {
.as_millis() as i64,
);
self.idle_phasing = None;
self.transition_to_receiving(ioc, lpm, offset);
events.push(self.transition_to_receiving(ioc, lpm, offset));
}
}
}
@@ -157,7 +157,7 @@ impl WefaxDecoder {
.any(|r| matches!(r.tone, Some(AptTone::Start576 | AptTone::Start288)));
if !still_start {
self.transition_to_phasing(ioc);
events.push(self.transition_to_phasing(ioc));
}
}
@@ -173,7 +173,7 @@ impl WefaxDecoder {
if let Some(ref mut phasing) = self.phasing {
if let Some(offset) = phasing.process(&luminance) {
self.transition_to_receiving(ioc, lpm, offset);
events.push(self.transition_to_receiving(ioc, lpm, offset));
}
}
}
@@ -214,6 +214,7 @@ impl WefaxDecoder {
ioc,
pixels_per_line: WefaxConfig::pixels_per_line(ioc),
line_data: Some(b64),
state: None,
},
line_data,
));
@@ -255,7 +256,22 @@ impl WefaxDecoder {
)
}
fn transition_to_start_detected(&mut self, ioc: u16) {
fn state_event(&self, label: &str, ioc: u16, lpm: u16) -> WefaxEvent {
WefaxEvent::Progress(
WefaxProgress {
rig_id: None,
line_count: 0,
lpm,
ioc,
pixels_per_line: WefaxConfig::pixels_per_line(ioc),
line_data: None,
state: Some(label.to_string()),
},
Vec::new(),
)
}
fn transition_to_start_detected(&mut self, ioc: u16) -> WefaxEvent {
let ioc = self.config.ioc.unwrap_or(ioc);
self.state = State::StartDetected { ioc };
self.reception_start_ms = Some(
@@ -264,22 +280,26 @@ impl WefaxDecoder {
.unwrap_or_default()
.as_millis() as i64,
);
let lpm = self.config.lpm.unwrap_or(120);
self.state_event(&format!("APT Start {}", ioc), ioc, lpm)
}
fn transition_to_phasing(&mut self, ioc: u16) {
fn transition_to_phasing(&mut self, ioc: u16) -> WefaxEvent {
let lpm = self.config.lpm.unwrap_or(120); // Default 120 LPM.
self.tone_detector.reset();
self.phasing = Some(PhasingDetector::new(lpm, INTERNAL_RATE));
self.demodulator.reset();
self.state = State::Phasing { ioc, lpm };
self.state_event("Phasing", ioc, lpm)
}
fn transition_to_receiving(&mut self, ioc: u16, lpm: u16, phase_offset: usize) {
fn transition_to_receiving(&mut self, ioc: u16, lpm: u16, phase_offset: usize) -> WefaxEvent {
let ppl = WefaxConfig::pixels_per_line(ioc) as usize;
self.slicer = Some(LineSlicer::new(lpm, ioc, INTERNAL_RATE, phase_offset));
self.image = Some(ImageAssembler::new(ppl));
self.tone_detector.reset();
self.state = State::Receiving { ioc, lpm };
self.state_event("Receiving", ioc, lpm)
}
fn transition_to_idle(&mut self) {
@@ -249,6 +249,15 @@ function addWefaxImage(msg) {
// ── SSE event handlers (public API) ─────────────────────────────────
window.onServerWefaxProgress = function (msg) {
// State-only update (no image data): show decoder state in status.
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
wefaxDom.status.textContent = msg.state;
wefaxDom.status.style.color = 'var(--text-accent)';
}
return;
}
if (msg.line_count <= 1 || !wefaxLiveCtx) {
resetLiveCanvas(msg.pixels_per_line || 1809);
}
+3
View File
@@ -311,4 +311,7 @@ pub struct WefaxProgress {
/// Base64-encoded greyscale line data (one row of pixels).
#[serde(skip_serializing_if = "Option::is_none")]
pub line_data: Option<String>,
/// Decoder state label (e.g. "APT Start 576", "Phasing", "Receiving").
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
}