[feat](trx-noaa): add NOAA APT satellite image decoder

New trx-noaa crate: FFT-based Hilbert transform (rustfft) for 2400 Hz
AM demodulation, sync A detection via cross-correlation, line assembly
at 4160 Hz, and JPEG output via the image crate.

- trx-core: NoaaImage type, DecodedMessage::NoaaImage variant,
  noaa_decode_enabled/noaa_decode_reset_seq on RigState/RigSnapshot,
  AUDIO_MSG_NOAA_IMAGE = 0x16
- trx-server: DecoderHistories::noaa, run_noaa_decoder task (activates
  on noaa_decode_enabled, auto-finalises after 30 s silence), saves
  JPEGs to ~/.cache/trx-rs/noaa/<YYYY-MM-DD_HH-MM-SS>.jpg, forwards
  events over TCP audio channel and history replay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-28 07:00:24 +01:00
parent a8b19227d5
commit 4b40d44814
12 changed files with 852 additions and 5 deletions
+2
View File
@@ -66,6 +66,8 @@ pub const AUDIO_MSG_VCHAN_BW: u8 = 0x13;
pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
/// Server → client: FT2 decoded message (JSON `DecodedMessage::Ft2`).
pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15;
/// Server → client: NOAA APT image complete (JSON `DecodedMessage::NoaaImage`).
pub const AUDIO_MSG_NOAA_IMAGE: u8 = 0x16;
/// Maximum payload size for normal messages (1 MB).
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
+21
View File
@@ -28,6 +28,8 @@ pub enum DecodedMessage {
Ft2(Ft8Message),
#[serde(rename = "wspr")]
Wspr(WsprMessage),
#[serde(rename = "noaa_image")]
NoaaImage(NoaaImage),
}
impl DecodedMessage {
@@ -40,6 +42,7 @@ impl DecodedMessage {
Self::Cw(m) => m.rig_id = Some(id),
Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id = Some(id),
Self::Wspr(m) => m.rig_id = Some(id),
Self::NoaaImage(m) => m.rig_id = Some(id),
}
}
@@ -52,6 +55,7 @@ impl DecodedMessage {
Self::Cw(m) => m.rig_id.as_deref(),
Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id.as_deref(),
Self::Wspr(m) => m.rig_id.as_deref(),
Self::NoaaImage(m) => m.rig_id.as_deref(),
}
}
}
@@ -203,6 +207,23 @@ pub struct Ft8Message {
pub message: String,
}
/// A completed NOAA APT satellite image, saved to disk as a JPEG.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoaaImage {
#[serde(skip_serializing_if = "Option::is_none")]
pub rig_id: Option<String>,
/// UTC timestamp (milliseconds since epoch) of pass start (first decoded line).
pub pass_start_ms: i64,
/// UTC timestamp (milliseconds since epoch) when the image was finalised.
pub pass_end_ms: i64,
/// Number of decoded image lines.
pub line_count: u32,
/// Absolute filesystem path to the saved JPEG file.
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ts_ms: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WsprMessage {
#[serde(skip_serializing_if = "Option::is_none")]
+11
View File
@@ -46,6 +46,8 @@ pub struct RigState {
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub noaa_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]
pub cw_wpm: u32,
@@ -77,6 +79,8 @@ pub struct RigState {
pub ft2_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub wspr_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub noaa_decode_reset_seq: u64,
}
/// Mode supported by the rig.
@@ -159,6 +163,7 @@ impl RigState {
ft4_decode_enabled: false,
ft2_decode_enabled: false,
wspr_decode_enabled: false,
noaa_decode_enabled: false,
cw_auto: true,
cw_wpm: 15,
cw_tone_hz: 700,
@@ -172,6 +177,7 @@ impl RigState {
ft4_decode_reset_seq: 0,
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
noaa_decode_reset_seq: 0,
}
}
@@ -231,6 +237,7 @@ impl RigState {
ft4_decode_enabled: snapshot.ft4_decode_enabled,
ft2_decode_enabled: snapshot.ft2_decode_enabled,
wspr_decode_enabled: snapshot.wspr_decode_enabled,
noaa_decode_enabled: snapshot.noaa_decode_enabled,
filter: snapshot.filter,
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
vchan_rds: None, // vchan RDS flows through /api/spectrum, not persistent state
@@ -241,6 +248,7 @@ impl RigState {
ft4_decode_reset_seq: 0,
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
noaa_decode_reset_seq: 0,
}
}
@@ -278,6 +286,7 @@ impl RigState {
ft4_decode_enabled: self.ft4_decode_enabled,
ft2_decode_enabled: self.ft2_decode_enabled,
wspr_decode_enabled: self.wspr_decode_enabled,
noaa_decode_enabled: self.noaa_decode_enabled,
filter: self.filter.clone(),
spectrum: self.spectrum.clone(),
vchan_rds: self.vchan_rds.clone(),
@@ -489,6 +498,8 @@ pub struct RigSnapshot {
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub noaa_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]
pub cw_wpm: u32,