Files
trx-rs/src/decoders/trx-ais/src/lib.rs
T
sjg 977f7b709e
CI / lint (pull_request) Failing after 2m28s
CI / test (pull_request) Successful in 3m23s
CI / reuse (pull_request) Successful in 7s
[style](trx-ais): drop redundant u32 cast
get_uint returns Option<u32>, so `? as u32` is an unnecessary same-type
cast flagged by clippy under -D warnings (rust 1.97).

Assisted-By: Claude Code (claude-opus-4)
Claude-Session: https://claude.ai/code/session_01NFpGtGTWUEYXLwZeZs2RAV
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-07-18 10:28:55 +02:00

473 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
//! Basic AIS GMSK/HDLC decoder.
//!
//! This decoder operates on narrowband FM-demodulated audio. It uses a simple
//! sign slicer at the symbol rate, HDLC flag detection with NRZI decoding and
//! bit de-stuffing, then parses common AIS position/static messages.
use trx_core::decode::AisMessage;
const AIS_BAUD: f32 = 9_600.0;
const CRC_CCITT_TABLE: [u16; 256] = {
let mut table = [0u16; 256];
let mut i = 0usize;
while i < 256 {
let mut crc = i as u16;
let mut j = 0;
while j < 8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0x8408;
} else {
crc >>= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
};
fn crc16ccitt(bytes: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &b in bytes {
crc = (crc >> 8) ^ CRC_CCITT_TABLE[((crc ^ b as u16) & 0xFF) as usize];
}
crc ^ 0xFFFF
}
#[derive(Debug, Clone)]
struct RawFrame {
payload: Vec<u8>,
bits: Vec<u8>,
crc_ok: bool,
}
/// AIS (Automatic Identification System) GMSK/HDLC decoder.
///
/// Operates on narrowband FM-demodulated audio at any sample rate (internally
/// resampled to the 9,600 baud AIS symbol rate). The decoder performs sign
/// slicing, NRZI decoding, HDLC flag detection with bit de-stuffing, CRC-16
/// validation, and parsing of common AIS message types (13, 5, 18, 19).
///
/// # Usage
///
/// ```ignore
/// let mut decoder = AisDecoder::new(48_000);
/// let messages = decoder.process_samples(&pcm_samples, "A");
/// ```
///
/// Call [`reset()`](Self::reset) when switching frequency or restarting
/// reception to clear internal symbol-tracking state.
#[derive(Debug, Clone)]
pub struct AisDecoder {
sample_rate: f32,
symbol_phase: f32,
dc_state: f32,
lp_state: f32,
env_state: f32,
prev_raw_bit: u8,
ones: u32,
in_frame: bool,
frame_bits: Vec<u8>,
frames: Vec<RawFrame>,
}
impl AisDecoder {
pub fn new(sample_rate: u32) -> Self {
Self {
sample_rate: sample_rate.max(1) as f32,
symbol_phase: 0.0,
dc_state: 0.0,
lp_state: 0.0,
env_state: 1e-3,
prev_raw_bit: 0,
ones: 0,
in_frame: false,
frame_bits: Vec::new(),
frames: Vec::new(),
}
}
pub fn reset(&mut self) {
self.symbol_phase = 0.0;
self.dc_state = 0.0;
self.lp_state = 0.0;
self.env_state = 1e-3;
self.prev_raw_bit = 0;
self.ones = 0;
self.in_frame = false;
self.frame_bits.clear();
self.frames.clear();
}
pub fn process_samples(&mut self, samples: &[f32], channel: &str) -> Vec<AisMessage> {
for &sample in samples {
self.process_sample(sample);
}
let frames = std::mem::take(&mut self.frames);
let mut out = Vec::new();
for frame in frames {
if let Some(msg) = parse_frame(frame, channel) {
out.push(msg);
}
}
out
}
fn process_sample(&mut self, sample: f32) {
// Remove slow DC drift from the FM discriminator output.
self.dc_state += 0.0025 * (sample - self.dc_state);
let dc_free = sample - self.dc_state;
// 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.02 * (self.lp_state.abs() - self.env_state);
let normalized = if self.env_state > 1e-4 {
self.lp_state / self.env_state
} else {
self.lp_state
};
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 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;
if decoded_bit == 1 {
self.ones += 1;
return;
}
// A zero terminates the current run of ones.
if self.ones >= 7 {
self.in_frame = false;
self.frame_bits.clear();
self.ones = 0;
return;
}
if self.ones == 6 {
if self.in_frame {
if let Some(frame) = self.bits_to_frame() {
self.frames.push(frame);
}
}
self.frame_bits.clear();
self.in_frame = true;
self.ones = 0;
return;
}
if self.ones == 5 {
if self.in_frame {
for _ in 0..5 {
self.frame_bits.push(1);
}
}
self.ones = 0;
return;
}
if self.in_frame {
for _ in 0..self.ones {
self.frame_bits.push(1);
}
self.frame_bits.push(0);
}
self.ones = 0;
}
fn bits_to_frame(&self) -> Option<RawFrame> {
if self.frame_bits.len() < 24 {
return None;
}
let usable_bits = self.frame_bits.len() - (self.frame_bits.len() % 8);
if usable_bits < 24 {
return None;
}
let bits = self.frame_bits[..usable_bits].to_vec();
let mut bytes = Vec::with_capacity(usable_bits / 8);
for chunk in bits.chunks(8) {
let mut byte = 0u8;
for (idx, &bit) in chunk.iter().enumerate() {
if bit != 0 {
byte |= 1 << idx;
}
}
bytes.push(byte);
}
if bytes.len() < 3 {
return None;
}
let payload_len = bytes.len() - 2;
let payload = bytes[..payload_len].to_vec();
let received_fcs = u16::from_le_bytes([bytes[payload_len], bytes[payload_len + 1]]);
let crc_ok = crc16ccitt(&payload) == received_fcs;
Some(RawFrame {
payload,
bits,
crc_ok,
})
}
}
fn parse_frame(frame: RawFrame, channel: &str) -> Option<AisMessage> {
if !frame.crc_ok {
return None;
}
let bits = bytes_to_msb_bits(&frame.payload);
if bits.len() < 40 {
return None;
}
let message_type = get_uint(&bits, 0, 6)? as u8;
let repeat = get_uint(&bits, 6, 2)? as u8;
let mmsi = get_uint(&bits, 8, 30)?;
let mut msg = AisMessage {
rig_id: None,
ts_ms: None,
channel: channel.to_string(),
message_type,
repeat,
mmsi,
crc_ok: frame.crc_ok,
bit_len: frame.bits.len(),
raw_bytes: frame.payload,
lat: None,
lon: None,
sog_knots: None,
cog_deg: None,
heading_deg: None,
nav_status: None,
vessel_name: None,
callsign: None,
destination: None,
};
match message_type {
1..=3 => {
msg.nav_status = get_uint(&bits, 38, 4).map(|v| v as u8);
msg.sog_knots = decode_tenths(get_uint(&bits, 50, 10)?, 1023);
msg.lon = decode_coord(get_int(&bits, 61, 28)?, 181.0);
msg.lat = decode_coord(get_int(&bits, 89, 27)?, 91.0);
msg.cog_deg = decode_tenths(get_uint(&bits, 116, 12)?, 3600);
msg.heading_deg = decode_heading(get_uint(&bits, 128, 9)?);
}
18 => {
msg.sog_knots = decode_tenths(get_uint(&bits, 46, 10)?, 1023);
msg.lon = decode_coord(get_int(&bits, 57, 28)?, 181.0);
msg.lat = decode_coord(get_int(&bits, 85, 27)?, 91.0);
msg.cog_deg = decode_tenths(get_uint(&bits, 112, 12)?, 3600);
msg.heading_deg = decode_heading(get_uint(&bits, 124, 9)?);
}
19 => {
msg.sog_knots = decode_tenths(get_uint(&bits, 46, 10)?, 1023);
msg.lon = decode_coord(get_int(&bits, 57, 28)?, 181.0);
msg.lat = decode_coord(get_int(&bits, 85, 27)?, 91.0);
msg.cog_deg = decode_tenths(get_uint(&bits, 112, 12)?, 3600);
msg.heading_deg = decode_heading(get_uint(&bits, 124, 9)?);
msg.vessel_name = decode_sixbit_text(&bits, 143, 120);
}
5 => {
msg.callsign = decode_sixbit_text(&bits, 70, 42);
msg.vessel_name = decode_sixbit_text(&bits, 112, 120);
msg.destination = decode_sixbit_text(&bits, 302, 120);
}
_ => {}
}
Some(msg)
}
fn bytes_to_msb_bits(bytes: &[u8]) -> Vec<u8> {
let mut bits = Vec::with_capacity(bytes.len() * 8);
for &byte in bytes {
for shift in (0..8).rev() {
bits.push((byte >> shift) & 1);
}
}
bits
}
fn get_uint(bits: &[u8], start: usize, len: usize) -> Option<u32> {
if len == 0 || start.checked_add(len)? > bits.len() || len > 32 {
return None;
}
let mut out = 0u32;
for &bit in &bits[start..start + len] {
out = (out << 1) | u32::from(bit);
}
Some(out)
}
fn get_int(bits: &[u8], start: usize, len: usize) -> Option<i32> {
let raw = get_uint(bits, start, len)?;
if len == 0 || len > 31 {
return None;
}
let sign_mask = 1u32 << (len - 1);
if raw & sign_mask == 0 {
Some(raw as i32)
} else {
Some((raw as i32) - ((1u32 << len) as i32))
}
}
fn decode_tenths(raw: u32, invalid: u32) -> Option<f32> {
if raw == invalid {
None
} else {
Some(raw as f32 / 10.0)
}
}
fn decode_heading(raw: u32) -> Option<u16> {
if raw >= 360 {
None
} else {
Some(raw as u16)
}
}
fn decode_coord(raw: i32, invalid_abs: f64) -> Option<f64> {
let value = raw as f64 / 600_000.0;
if value.abs() >= invalid_abs {
None
} else {
Some(value)
}
}
fn decode_sixbit_text(bits: &[u8], start: usize, len: usize) -> Option<String> {
if start.checked_add(len)? > bits.len() || !len.is_multiple_of(6) {
return None;
}
let mut out = String::new();
for offset in (0..len).step_by(6) {
let value = get_uint(bits, start + offset, 6)? as u8;
let ch = if value < 32 {
char::from(value + 64)
} else {
char::from(value)
};
if ch != '@' {
out.push(ch);
}
}
let trimmed = out.trim().trim_matches('@').trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn payload_with_crc(payload: &[u8]) -> Vec<u8> {
let mut out = payload.to_vec();
out.extend_from_slice(&crc16ccitt(payload).to_le_bytes());
out
}
fn bytes_to_lsb_bits(bytes: &[u8]) -> Vec<u8> {
let mut bits = Vec::with_capacity(bytes.len() * 8);
for &byte in bytes {
for shift in 0..8 {
bits.push((byte >> shift) & 1);
}
}
bits
}
fn bitstuff(bits: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(bits.len() + bits.len() / 5);
let mut ones = 0u32;
for &bit in bits {
out.push(bit);
if bit == 1 {
ones += 1;
if ones == 5 {
out.push(0);
ones = 0;
}
} else {
ones = 0;
}
}
out
}
fn nrzi_encode(bits: &[u8]) -> Vec<u8> {
let mut state = 0u8;
let mut out = Vec::with_capacity(bits.len());
for &bit in bits {
if bit == 0 {
state ^= 1;
}
out.push(state);
}
out
}
#[test]
fn decodes_signed_coordinates() {
assert_eq!(decode_coord(60_000, 181.0), Some(0.1));
assert_eq!(decode_coord(-60_000, 181.0), Some(-0.1));
}
#[test]
fn decodes_sixbit_name() {
let bytes = [0x10_u8, 0x41_u8, 0x11_u8, 0x92_u8, 0x08_u8, 0x00_u8];
let bits = bytes_to_msb_bits(&bytes);
let text = decode_sixbit_text(&bits, 0, 36);
assert!(text.is_some());
}
#[test]
fn recovers_hdlc_frame_from_raw_nrzi_bits() {
let payload = [0x11_u8, 0x22_u8, 0x7E_u8, 0x00_u8, 0xF0_u8];
let frame_bytes = payload_with_crc(&payload);
let mut hdlc_bits = bytes_to_lsb_bits(&[0x7E]);
hdlc_bits.extend(bitstuff(&bytes_to_lsb_bits(&frame_bytes)));
hdlc_bits.extend(bytes_to_lsb_bits(&[0x7E]));
let raw_bits = nrzi_encode(&hdlc_bits);
let mut decoder = AisDecoder::new(48_000);
for raw_bit in raw_bits {
decoder.process_symbol(raw_bit);
}
assert_eq!(decoder.frames.len(), 1);
let frame = &decoder.frames[0];
assert!(frame.crc_ok);
assert_eq!(frame.payload, payload);
}
}