morse: fix stop restart loop and lower SNR threshold for decoding

Guard checkStatus() against in-flight stop to prevent status poller
from overriding stopping state and reconnecting SSE. Lower SNR floor
from 1.3 to 1.15 to accommodate weaker CW signals. Add SNR/noise_ref
to scope events and metrics for real-time threshold debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-26 21:57:08 +00:00
parent 97b10b3ac9
commit c0fb22124b
2 changed files with 31 additions and 3 deletions
+16 -2
View File
@@ -415,6 +415,7 @@ var MorseMode = (function () {
function checkStatus() { function checkStatus() {
if (!state.initialized) return; if (!state.initialized) return;
if (state.stopPromise) return; // Don't poll during in-flight stop
fetch('/morse/status') fetch('/morse/status')
.then(function (r) { return parseJsonSafe(r); }) .then(function (r) { return parseJsonSafe(r); })
@@ -640,10 +641,23 @@ var MorseMode = (function () {
state.lastMetrics.noise_floor = Number(metrics.noise_floor) || 0; state.lastMetrics.noise_floor = Number(metrics.noise_floor) || 0;
} }
if (metrics.snr !== undefined) {
state.lastMetrics.snr = Number(metrics.snr) || 0;
}
if (metrics.noise_ref !== undefined) {
state.lastMetrics.noise_ref = Number(metrics.noise_ref) || 0;
}
if (metrics.snr_on !== undefined) {
state.lastMetrics.snr_on = Number(metrics.snr_on) || 0;
}
if (metrics.snr_off !== undefined) {
state.lastMetrics.snr_off = Number(metrics.snr_off) || 0;
}
updateMetricLabel('morseMetricTone', 'TONE ' + Math.round(state.lastMetrics.tone_freq || 700) + ' Hz'); updateMetricLabel('morseMetricTone', 'TONE ' + Math.round(state.lastMetrics.tone_freq || 700) + ' Hz');
updateMetricLabel('morseMetricLevel', 'LEVEL ' + (state.lastMetrics.level || 0).toFixed(2)); updateMetricLabel('morseMetricLevel', 'SNR ' + (state.lastMetrics.snr || 0).toFixed(2) + ' (on>' + (state.lastMetrics.snr_on || 0).toFixed(2) + ' off>' + (state.lastMetrics.snr_off || 0).toFixed(2) + ')');
updateMetricLabel('morseMetricThreshold', 'THRESH ' + (state.lastMetrics.threshold || 0).toFixed(2)); updateMetricLabel('morseMetricThreshold', 'THRESH ' + (state.lastMetrics.threshold || 0).toFixed(2));
updateMetricLabel('morseMetricNoise', 'NOISE ' + (state.lastMetrics.noise_floor || 0).toFixed(2)); updateMetricLabel('morseMetricNoise', 'NOISE_REF ' + (state.lastMetrics.noise_ref || 0).toFixed(4));
var toneScope = el('morseScopeToneLabel'); var toneScope = el('morseScopeToneLabel');
if (toneScope) { if (toneScope) {
+15 -1
View File
@@ -236,6 +236,9 @@ class MorseDecoder:
def get_metrics(self) -> dict[str, float | bool]: def get_metrics(self) -> dict[str, float | bool]:
"""Return latest decoder metrics for UI/status messages.""" """Return latest decoder metrics for UI/status messages."""
snr_mult = max(1.15, self.threshold_multiplier * 0.5)
snr_on = snr_mult * (1.0 + self._hysteresis)
snr_off = snr_mult * (1.0 - self._hysteresis)
return { return {
'wpm': float(self._estimated_wpm), 'wpm': float(self._estimated_wpm),
'tone_freq': float(self._active_tone_freq), 'tone_freq': float(self._active_tone_freq),
@@ -244,6 +247,10 @@ class MorseDecoder:
'threshold': float(self._threshold), 'threshold': float(self._threshold),
'tone_on': bool(self._tone_on), 'tone_on': bool(self._tone_on),
'dit_ms': float((self._effective_dit_blocks() * self._block_duration) * 1000.0), 'dit_ms': float((self._effective_dit_blocks() * self._block_duration) * 1000.0),
'snr': float(self._last_level / max(self._noise_floor, 1e-6)),
'noise_ref': float(self._noise_floor),
'snr_on': float(snr_on),
'snr_off': float(snr_off),
} }
def _rebuild_detectors(self) -> None: def _rebuild_detectors(self) -> None:
@@ -457,7 +464,7 @@ class MorseDecoder:
# gain-invariant — fixes stuck-ON tone when AGC amplifies # gain-invariant — fixes stuck-ON tone when AGC amplifies
# inter-element silence above the raw magnitude threshold. # inter-element silence above the raw magnitude threshold.
snr = level / max(noise_ref, 1e-6) snr = level / max(noise_ref, 1e-6)
snr_mult = max(1.3, self.threshold_multiplier * 0.55) snr_mult = max(1.15, self.threshold_multiplier * 0.5)
snr_on = snr_mult * (1.0 + self._hysteresis) snr_on = snr_mult * (1.0 + self._hysteresis)
snr_off = snr_mult * (1.0 - self._hysteresis) snr_off = snr_mult * (1.0 - self._hysteresis)
@@ -541,6 +548,9 @@ class MorseDecoder:
self._silence_blocks += 1.0 self._silence_blocks += 1.0
if amplitudes: if amplitudes:
snr_mult = max(1.15, self.threshold_multiplier * 0.5)
snr_on = snr_mult * (1.0 + self._hysteresis)
snr_off = snr_mult * (1.0 - self._hysteresis)
events.append({ events.append({
'type': 'scope', 'type': 'scope',
'amplitudes': amplitudes, 'amplitudes': amplitudes,
@@ -551,6 +561,10 @@ class MorseDecoder:
'noise_floor': self._noise_floor, 'noise_floor': self._noise_floor,
'wpm': round(self._estimated_wpm, 1), 'wpm': round(self._estimated_wpm, 1),
'dit_ms': round(self._effective_dit_blocks() * self._block_duration * 1000.0, 1), 'dit_ms': round(self._effective_dit_blocks() * self._block_duration * 1000.0, 1),
'snr': round(self._last_level / max(self._noise_floor, 1e-6), 2),
'noise_ref': round(self._noise_floor, 4),
'snr_on': round(snr_on, 2),
'snr_off': round(snr_off, 2),
}) })
return events return events