[fix](trx-ais): restore adaptive AIS symbol timing

Bring back the transition-locked AIS sampler with adaptive symbol timing and shaped discriminator filtering while keeping the shorter-frame acceptance path.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-02 23:52:58 +01:00
parent 8bb0497066
commit e7b38c52f7
+66 -16
View File
@@ -50,10 +50,15 @@ struct RawFrame {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AisDecoder { pub struct AisDecoder {
sample_rate: f32, sample_rate: f32,
symbol_phase: f32, samples_per_symbol: f32,
sample_clock: f32,
dc_state: f32, dc_state: f32,
lp_state: f32, lp_fast: 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,
@@ -63,12 +68,18 @@ 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.max(1) as f32, sample_rate,
symbol_phase: 0.0, samples_per_symbol: sample_rate / AIS_BAUD,
sample_clock: 0.0,
dc_state: 0.0, dc_state: 0.0,
lp_state: 0.0, lp_fast: 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,
@@ -78,10 +89,15 @@ impl AisDecoder {
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.symbol_phase = 0.0; self.samples_per_symbol = self.sample_rate / AIS_BAUD;
self.sample_clock = 0.0;
self.dc_state = 0.0; self.dc_state = 0.0;
self.lp_state = 0.0; self.lp_fast = 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;
@@ -109,25 +125,59 @@ 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;
// Gentle low-pass smoothing to suppress narrow impulsive noise. // A simple band-pass-ish response makes GMSK symbol transitions stand out
self.lp_state += 0.28 * (dc_free - self.lp_state); // 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;
// Track envelope to keep the slicer stable on weak signals. // Track envelope to keep the slicer stable on weak signals.
self.env_state += 0.02 * (self.lp_state.abs() - self.env_state); self.env_state += 0.015 * (shaped.abs() - self.env_state);
let normalized = if self.env_state > 1e-4 { let normalized = if self.env_state > 1e-4 {
self.lp_state / self.env_state shaped / self.env_state
} else { } else {
self.lp_state shaped
}; };
self.symbol_phase += AIS_BAUD; let threshold = 0.12;
while self.symbol_phase >= self.sample_rate { let next_polarity = if normalized > threshold {
self.symbol_phase -= self.sample_rate; 1
let raw_bit = if normalized >= 0.0 { 1 } else { 0 }; } 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.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;