[fix](trx-rs): restore AIS decoder and enforce SDR limits
Revert the AIS decoder to the simpler sampling path while keeping the valid frame-length fix, and correct frontend frequency-range validation so SDR uses all reported bands and shows an explicit popup when tuning is unsupported. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -50,15 +50,10 @@ struct RawFrame {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AisDecoder {
|
pub struct AisDecoder {
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
samples_per_symbol: f32,
|
symbol_phase: f32,
|
||||||
sample_clock: f32,
|
|
||||||
dc_state: f32,
|
dc_state: f32,
|
||||||
lp_fast: f32,
|
lp_state: f32,
|
||||||
lp_slow: f32,
|
|
||||||
env_state: f32,
|
env_state: f32,
|
||||||
polarity: i8,
|
|
||||||
samples_since_transition: u32,
|
|
||||||
clock_locked: bool,
|
|
||||||
prev_raw_bit: u8,
|
prev_raw_bit: u8,
|
||||||
ones: u32,
|
ones: u32,
|
||||||
in_frame: bool,
|
in_frame: bool,
|
||||||
@@ -68,18 +63,12 @@ pub struct AisDecoder {
|
|||||||
|
|
||||||
impl AisDecoder {
|
impl AisDecoder {
|
||||||
pub fn new(sample_rate: u32) -> Self {
|
pub fn new(sample_rate: u32) -> Self {
|
||||||
let sample_rate = sample_rate.max(1) as f32;
|
|
||||||
Self {
|
Self {
|
||||||
sample_rate,
|
sample_rate: sample_rate.max(1) as f32,
|
||||||
samples_per_symbol: sample_rate / AIS_BAUD,
|
symbol_phase: 0.0,
|
||||||
sample_clock: 0.0,
|
|
||||||
dc_state: 0.0,
|
dc_state: 0.0,
|
||||||
lp_fast: 0.0,
|
lp_state: 0.0,
|
||||||
lp_slow: 0.0,
|
|
||||||
env_state: 1e-3,
|
env_state: 1e-3,
|
||||||
polarity: 1,
|
|
||||||
samples_since_transition: 0,
|
|
||||||
clock_locked: false,
|
|
||||||
prev_raw_bit: 0,
|
prev_raw_bit: 0,
|
||||||
ones: 0,
|
ones: 0,
|
||||||
in_frame: false,
|
in_frame: false,
|
||||||
@@ -89,15 +78,10 @@ impl AisDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.samples_per_symbol = self.sample_rate / AIS_BAUD;
|
self.symbol_phase = 0.0;
|
||||||
self.sample_clock = 0.0;
|
|
||||||
self.dc_state = 0.0;
|
self.dc_state = 0.0;
|
||||||
self.lp_fast = 0.0;
|
self.lp_state = 0.0;
|
||||||
self.lp_slow = 0.0;
|
|
||||||
self.env_state = 1e-3;
|
self.env_state = 1e-3;
|
||||||
self.polarity = 1;
|
|
||||||
self.samples_since_transition = 0;
|
|
||||||
self.clock_locked = false;
|
|
||||||
self.prev_raw_bit = 0;
|
self.prev_raw_bit = 0;
|
||||||
self.ones = 0;
|
self.ones = 0;
|
||||||
self.in_frame = false;
|
self.in_frame = false;
|
||||||
@@ -125,59 +109,25 @@ impl AisDecoder {
|
|||||||
self.dc_state += 0.0025 * (sample - self.dc_state);
|
self.dc_state += 0.0025 * (sample - self.dc_state);
|
||||||
let dc_free = sample - self.dc_state;
|
let dc_free = sample - self.dc_state;
|
||||||
|
|
||||||
// A simple band-pass-ish response makes GMSK symbol transitions stand out
|
// Gentle low-pass smoothing to suppress narrow impulsive noise.
|
||||||
// without needing a full matched filter.
|
self.lp_state += 0.28 * (dc_free - self.lp_state);
|
||||||
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;
|
|
||||||
|
|
||||||
// Track envelope to keep the slicer stable on weak signals.
|
// 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 {
|
let normalized = if self.env_state > 1e-4 {
|
||||||
shaped / self.env_state
|
self.lp_state / self.env_state
|
||||||
} else {
|
} else {
|
||||||
shaped
|
self.lp_state
|
||||||
};
|
};
|
||||||
|
|
||||||
let threshold = 0.12;
|
self.symbol_phase += AIS_BAUD;
|
||||||
let next_polarity = if normalized > threshold {
|
while self.symbol_phase >= self.sample_rate {
|
||||||
1
|
self.symbol_phase -= self.sample_rate;
|
||||||
} else if normalized < -threshold {
|
let raw_bit = if normalized >= 0.0 { 1 } else { 0 };
|
||||||
-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.process_symbol(raw_bit);
|
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) {
|
fn process_symbol(&mut self, raw_bit: u8) {
|
||||||
let decoded_bit = if raw_bit == self.prev_raw_bit { 1 } else { 0 };
|
let decoded_bit = if raw_bit == self.prev_raw_bit { 1 } else { 0 };
|
||||||
self.prev_raw_bit = raw_bit;
|
self.prev_raw_bit = raw_bit;
|
||||||
|
|||||||
@@ -733,6 +733,7 @@ function showHint(msg, duration) {
|
|||||||
}
|
}
|
||||||
let supportedModes = [];
|
let supportedModes = [];
|
||||||
let supportedBands = [];
|
let supportedBands = [];
|
||||||
|
let lastUnsupportedFreqPopupAt = 0;
|
||||||
let freqDirty = false;
|
let freqDirty = false;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let lastEventAt = Date.now();
|
let lastEventAt = Date.now();
|
||||||
@@ -1288,6 +1289,10 @@ async function ensureTunedBandwidthCoverage(freqHz, bandwidthHz = coverageGuardB
|
|||||||
|
|
||||||
async function setRigFrequency(freqHz) {
|
async function setRigFrequency(freqHz) {
|
||||||
const targetHz = Math.round(freqHz);
|
const targetHz = Math.round(freqHz);
|
||||||
|
if (!freqAllowed(targetHz)) {
|
||||||
|
showUnsupportedFreqPopup(targetHz);
|
||||||
|
throw new Error(`Unsupported frequency: ${targetHz}`);
|
||||||
|
}
|
||||||
await postPath(`/set_freq?hz=${targetHz}`);
|
await postPath(`/set_freq?hz=${targetHz}`);
|
||||||
applyLocalTunedFrequency(targetHz);
|
applyLocalTunedFrequency(targetHz);
|
||||||
await ensureTunedBandwidthCoverage(targetHz);
|
await ensureTunedBandwidthCoverage(targetHz);
|
||||||
@@ -1680,7 +1685,7 @@ function normalizeMode(modeVal) {
|
|||||||
function updateSupportedBands(cap) {
|
function updateSupportedBands(cap) {
|
||||||
if (cap && Array.isArray(cap.supported_bands)) {
|
if (cap && Array.isArray(cap.supported_bands)) {
|
||||||
supportedBands = 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 }));
|
.map((b) => ({ low: b.low_hz, high: b.high_hz }));
|
||||||
} else {
|
} else {
|
||||||
supportedBands = [];
|
supportedBands = [];
|
||||||
@@ -1693,6 +1698,32 @@ function freqAllowed(hz) {
|
|||||||
return supportedBands.some((b) => hz >= b.low && hz <= b.high);
|
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).
|
// 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).
|
// Above S9, returns 9 + (overshoot in S-unit-equivalent, i.e. dB/10).
|
||||||
function dbmToSUnits(dbm) {
|
function dbmToSUnits(dbm) {
|
||||||
@@ -2403,7 +2434,7 @@ async function applyFreqFromInput() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!freqAllowed(parsed)) {
|
if (!freqAllowed(parsed)) {
|
||||||
showHint("Out of supported bands", 1500);
|
showUnsupportedFreqPopup(parsed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
freqDirty = false;
|
freqDirty = false;
|
||||||
@@ -2429,7 +2460,7 @@ async function applyCenterFreqFromInput() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!freqAllowed(parsed)) {
|
if (!freqAllowed(parsed)) {
|
||||||
showHint("Out of supported bands", 1500);
|
showUnsupportedFreqPopup(parsed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
centerFreqDirty = false;
|
centerFreqDirty = false;
|
||||||
@@ -2507,7 +2538,7 @@ async function jogFreq(direction) {
|
|||||||
if (lastFreqHz === null) return;
|
if (lastFreqHz === null) return;
|
||||||
const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep);
|
const newHz = alignFreqToRigStep(lastFreqHz + direction * jogStep);
|
||||||
if (!freqAllowed(newHz)) {
|
if (!freqAllowed(newHz)) {
|
||||||
showHint("Out of supported bands", 1500);
|
showUnsupportedFreqPopup(newHz);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
jogAngle = (jogAngle + direction * 15) % 360;
|
jogAngle = (jogAngle + direction * 15) % 360;
|
||||||
|
|||||||
Reference in New Issue
Block a user