[feat](trx-rs): add AIS decoder mode and frontend

Add dual-channel AIS decode support across the SoapySDR backend, server decode pipeline, and frontend plugins, including the new AIS tab, live bar, and map filtering.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-02 22:42:12 +01:00
parent b6692b759e
commit c778d4b9a8
28 changed files with 1200 additions and 86 deletions
+1
View File
@@ -15,6 +15,7 @@ pub const AUDIO_MSG_APRS_DECODE: u8 = 0x03;
pub const AUDIO_MSG_CW_DECODE: u8 = 0x04;
pub const AUDIO_MSG_FT8_DECODE: u8 = 0x05;
pub const AUDIO_MSG_WSPR_DECODE: u8 = 0x06;
pub const AUDIO_MSG_AIS_DECODE: u8 = 0x07;
/// Maximum payload size (1 MB) to reject bogus frames early.
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
+34 -1
View File
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: BSD-2-Clause
//! Shared types for server-side decoded messages (APRS, CW).
//! Shared types for server-side decoded messages (APRS, AIS, CW).
use serde::{Deserialize, Serialize};
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DecodedMessage {
#[serde(rename = "ais")]
Ais(AisMessage),
#[serde(rename = "aprs")]
Aprs(AprsPacket),
#[serde(rename = "cw")]
@@ -20,6 +22,37 @@ pub enum DecodedMessage {
Wspr(WsprMessage),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AisMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub ts_ms: Option<i64>,
pub channel: String,
pub message_type: u8,
pub repeat: u8,
pub mmsi: u32,
pub crc_ok: bool,
pub bit_len: usize,
pub raw_bytes: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lat: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lon: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sog_knots: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cog_deg: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub heading_deg: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nav_status: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vessel_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callsign: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AprsPacket {
#[serde(skip_serializing_if = "Option::is_none")]
+12
View File
@@ -72,6 +72,18 @@ pub trait AudioSource: Send + Sync {
/// Subscribe to demodulated PCM audio from the primary channel.
/// Returns a broadcast receiver that yields 20ms frames of mono f32 PCM.
fn subscribe_pcm(&self) -> tokio::sync::broadcast::Receiver<Vec<f32>>;
/// Subscribe to PCM from a specific backend channel when available.
/// Channel `0` is always the primary channel.
fn subscribe_pcm_channel(&self, channel_idx: usize) -> tokio::sync::broadcast::Receiver<Vec<f32>> {
if channel_idx == 0 {
self.subscribe_pcm()
} else {
let (tx, rx) = tokio::sync::broadcast::channel(1);
drop(tx);
rx
}
}
}
/// Common interface for rig backends.
+1
View File
@@ -70,6 +70,7 @@ pub enum RigMode {
AM,
WFM,
FM,
AIS,
DIG,
PKT,
Other(String),