[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:
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -70,6 +70,7 @@ pub enum RigMode {
|
||||
AM,
|
||||
WFM,
|
||||
FM,
|
||||
AIS,
|
||||
DIG,
|
||||
PKT,
|
||||
Other(String),
|
||||
|
||||
Reference in New Issue
Block a user