diff --git a/src/decoders/trx-ais/src/lib.rs b/src/decoders/trx-ais/src/lib.rs index b9ba7a5..ce3bafe 100644 --- a/src/decoders/trx-ais/src/lib.rs +++ b/src/decoders/trx-ais/src/lib.rs @@ -50,15 +50,10 @@ struct RawFrame { #[derive(Debug, Clone)] pub struct AisDecoder { sample_rate: f32, - samples_per_symbol: f32, - sample_clock: f32, + symbol_phase: f32, dc_state: f32, - lp_fast: f32, - lp_slow: f32, + lp_state: f32, env_state: f32, - polarity: i8, - samples_since_transition: u32, - clock_locked: bool, prev_raw_bit: u8, ones: u32, in_frame: bool, @@ -68,18 +63,12 @@ pub struct AisDecoder { impl AisDecoder { pub fn new(sample_rate: u32) -> Self { - let sample_rate = sample_rate.max(1) as f32; Self { - sample_rate, - samples_per_symbol: sample_rate / AIS_BAUD, - sample_clock: 0.0, + sample_rate: sample_rate.max(1) as f32, + symbol_phase: 0.0, dc_state: 0.0, - lp_fast: 0.0, - lp_slow: 0.0, + lp_state: 0.0, env_state: 1e-3, - polarity: 1, - samples_since_transition: 0, - clock_locked: false, prev_raw_bit: 0, ones: 0, in_frame: false, @@ -89,15 +78,10 @@ impl AisDecoder { } pub fn reset(&mut self) { - self.samples_per_symbol = self.sample_rate / AIS_BAUD; - self.sample_clock = 0.0; + self.symbol_phase = 0.0; self.dc_state = 0.0; - self.lp_fast = 0.0; - self.lp_slow = 0.0; + self.lp_state = 0.0; self.env_state = 1e-3; - self.polarity = 1; - self.samples_since_transition = 0; - self.clock_locked = false; self.prev_raw_bit = 0; self.ones = 0; self.in_frame = false; @@ -125,59 +109,25 @@ impl AisDecoder { self.dc_state += 0.0025 * (sample - self.dc_state); let dc_free = sample - self.dc_state; - // A simple band-pass-ish response makes GMSK symbol transitions stand out - // without needing a full matched filter. - self.lp_fast += 0.32 * (dc_free - self.lp_fast); - self.lp_slow += 0.045 * (dc_free - self.lp_slow); - let shaped = self.lp_fast - self.lp_slow; + // Gentle low-pass smoothing to suppress narrow impulsive noise. + self.lp_state += 0.28 * (dc_free - self.lp_state); // Track envelope to keep the slicer stable on weak signals. - self.env_state += 0.015 * (shaped.abs() - self.env_state); + self.env_state += 0.02 * (self.lp_state.abs() - self.env_state); let normalized = if self.env_state > 1e-4 { - shaped / self.env_state + self.lp_state / self.env_state } else { - shaped + self.lp_state }; - let threshold = 0.12; - let next_polarity = if normalized > threshold { - 1 - } else if normalized < -threshold { - -1 - } else { - self.polarity - }; - - self.samples_since_transition = self.samples_since_transition.saturating_add(1); - if next_polarity != self.polarity { - self.observe_transition(); - self.polarity = next_polarity; - } - - if !self.clock_locked { - return; - } - - self.sample_clock += 1.0; - while self.sample_clock >= self.samples_per_symbol { - self.sample_clock -= self.samples_per_symbol; - let raw_bit = if self.polarity >= 0 { 1 } else { 0 }; + self.symbol_phase += AIS_BAUD; + while self.symbol_phase >= self.sample_rate { + self.symbol_phase -= self.sample_rate; + let raw_bit = if normalized >= 0.0 { 1 } else { 0 }; self.process_symbol(raw_bit); } } - fn observe_transition(&mut self) { - let interval = self.samples_since_transition.max(1) as f32; - self.samples_since_transition = 0; - - let nominal = (self.sample_rate / AIS_BAUD).max(1.0); - let symbols = (interval / nominal).round().clamp(1.0, 8.0); - let estimate = (interval / symbols).clamp(nominal * 0.75, nominal * 1.25); - self.samples_per_symbol += 0.18 * (estimate - self.samples_per_symbol); - self.sample_clock = self.samples_per_symbol * 0.5; - self.clock_locked = true; - } - fn process_symbol(&mut self, raw_bit: u8) { let decoded_bit = if raw_bit == self.prev_raw_bit { 1 } else { 0 }; self.prev_raw_bit = raw_bit; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 9724c0d..cfae5c7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -733,6 +733,7 @@ function showHint(msg, duration) { } let supportedModes = []; let supportedBands = []; +let lastUnsupportedFreqPopupAt = 0; let freqDirty = false; let initialized = false; let lastEventAt = Date.now(); @@ -1288,6 +1289,10 @@ async function ensureTunedBandwidthCoverage(freqHz, bandwidthHz = coverageGuardB async function setRigFrequency(freqHz) { const targetHz = Math.round(freqHz); + if (!freqAllowed(targetHz)) { + showUnsupportedFreqPopup(targetHz); + throw new Error(`Unsupported frequency: ${targetHz}`); + } await postPath(`/set_freq?hz=${targetHz}`); applyLocalTunedFrequency(targetHz); await ensureTunedBandwidthCoverage(targetHz); @@ -1680,7 +1685,7 @@ function normalizeMode(modeVal) { function updateSupportedBands(cap) { if (cap && Array.isArray(cap.supported_bands)) { supportedBands = cap.supported_bands - .filter((b) => typeof b.low_hz === "number" && typeof b.high_hz === "number" && b.tx_allowed === true) + .filter((b) => typeof b.low_hz === "number" && typeof b.high_hz === "number") .map((b) => ({ low: b.low_hz, high: b.high_hz })); } else { supportedBands = []; @@ -1693,6 +1698,32 @@ function freqAllowed(hz) { return supportedBands.some((b) => hz >= b.low && hz <= b.high); } +function unsupportedBandSummary() { + if (supportedBands.length === 0) return "No supported frequency ranges were reported by the rig."; + const ranges = supportedBands + .slice() + .sort((a, b) => a.low - b.low) + .map((b) => `${formatFreqForHumans(b.low)} to ${formatFreqForHumans(b.high)}`); + return `Supported ranges: ${ranges.join(", ")}`; +} + +function formatFreqForHumans(hz) { + if (!Number.isFinite(hz)) return "--"; + if (hz >= 1_000_000_000) return `${(hz / 1_000_000_000).toFixed(3)} GHz`; + if (hz >= 1_000_000) return `${(hz / 1_000_000).toFixed(3)} MHz`; + if (hz >= 1_000) return `${(hz / 1_000).toFixed(3)} kHz`; + return `${Math.round(hz)} Hz`; +} + +function showUnsupportedFreqPopup(hz) { + const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.\n\n${unsupportedBandSummary()}`; + showHint("Out of supported range", 1800); + const now = Date.now(); + if (now - lastUnsupportedFreqPopupAt < 1200) return; + lastUnsupportedFreqPopupAt = now; + window.alert(message); +} + // Convert dBm (wire format) to S-units (S1=-121dBm, S9=-73dBm, 6dB/S-unit). // Above S9, returns 9 + (overshoot in S-unit-equivalent, i.e. dB/10). function dbmToSUnits(dbm) { @@ -2403,7 +2434,7 @@ async function applyFreqFromInput() { return; } if (!freqAllowed(parsed)) { - showHint("Out of supported bands", 1500); + showUnsupportedFreqPopup(parsed); return; } freqDirty = false; @@ -2429,7 +2460,7 @@ async function applyCenterFreqFromInput() { return; } if (!freqAllowed(parsed)) { - showHint("Out of supported bands", 1500); + showUnsupportedFreqPopup(parsed); return; } centerFreqDirty = false; @@ -2507,7 +2538,7 @@ async function jogFreq(direction) { if (lastFreqHz === null) return; const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep); if (!freqAllowed(newHz)) { - showHint("Out of supported bands", 1500); + showUnsupportedFreqPopup(newHz); return; } jogAngle = (jogAngle + direction * 15) % 360;