[feat](trx-wxsat): rename trx-noaa to trx-wxsat with full NOAA APT decode

Rename the crate from trx-noaa to trx-wxsat (weather satellite) across
the entire workspace. Add full NOAA satellite decode support:

- Telemetry frame parsing: extract 16-wedge calibration data from the
  128-line telemetry frames embedded in APT lines
- Radiometric calibration: piecewise-linear LUT built from wedges 1-8
  to correct pixel values against known reference levels
- Channel identification: detect AVHRR sensor channels (VIS, NIR, MIR,
  TIR) from wedge 9 values per APT sub-channel
- Satellite identification: heuristic NOAA-15/18/19 detection from
  channel A/B sensor pairings
- Histogram equalisation: per-channel contrast enhancement for improved
  image output
- WxsatImage now carries satellite name and channel labels in decoded
  message broadcasts

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 06:37:17 +00:00
committed by Stan Grams
parent e0181c99da
commit d26ef6ca81
17 changed files with 591 additions and 99 deletions
+2 -2
View File
@@ -66,8 +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;
/// Server → client: weather satellite APT image complete (JSON `DecodedMessage::WxsatImage`).
pub const AUDIO_MSG_WXSAT_IMAGE: u8 = 0x16;
/// Maximum payload size for normal messages (1 MB).
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
+15 -6
View File
@@ -28,8 +28,8 @@ pub enum DecodedMessage {
Ft2(Ft8Message),
#[serde(rename = "wspr")]
Wspr(WsprMessage),
#[serde(rename = "noaa_image")]
NoaaImage(NoaaImage),
#[serde(rename = "wxsat_image")]
WxsatImage(WxsatImage),
}
impl DecodedMessage {
@@ -42,7 +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),
Self::WxsatImage(m) => m.rig_id = Some(id),
}
}
@@ -55,7 +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(),
Self::WxsatImage(m) => m.rig_id.as_deref(),
}
}
}
@@ -207,9 +207,9 @@ pub struct Ft8Message {
pub message: String,
}
/// A completed NOAA APT satellite image, saved to disk as a JPEG.
/// A completed weather satellite APT image, saved to disk as a JPEG.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoaaImage {
pub struct WxsatImage {
#[serde(skip_serializing_if = "Option::is_none")]
pub rig_id: Option<String>,
/// UTC timestamp (milliseconds since epoch) of pass start (first decoded line).
@@ -222,6 +222,15 @@ pub struct NoaaImage {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ts_ms: Option<i64>,
/// Identified satellite (e.g. "NOAA-15", "NOAA-18", "NOAA-19").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub satellite: Option<String>,
/// Sensor channel name for sub-channel A (e.g. "1-VIS", "2-NIR", "4-TIR").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_a: Option<String>,
/// Sensor channel name for sub-channel B.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_b: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+8 -8
View File
@@ -46,7 +46,7 @@ pub struct RigState {
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub noaa_decode_enabled: bool,
pub wxsat_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]
@@ -80,7 +80,7 @@ pub struct RigState {
#[serde(default, skip_serializing)]
pub wspr_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub noaa_decode_reset_seq: u64,
pub wxsat_decode_reset_seq: u64,
}
/// Mode supported by the rig.
@@ -163,7 +163,7 @@ impl RigState {
ft4_decode_enabled: false,
ft2_decode_enabled: false,
wspr_decode_enabled: false,
noaa_decode_enabled: false,
wxsat_decode_enabled: false,
cw_auto: true,
cw_wpm: 15,
cw_tone_hz: 700,
@@ -177,7 +177,7 @@ impl RigState {
ft4_decode_reset_seq: 0,
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
noaa_decode_reset_seq: 0,
wxsat_decode_reset_seq: 0,
}
}
@@ -237,7 +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,
wxsat_decode_enabled: snapshot.wxsat_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
@@ -248,7 +248,7 @@ impl RigState {
ft4_decode_reset_seq: 0,
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
noaa_decode_reset_seq: 0,
wxsat_decode_reset_seq: 0,
}
}
@@ -286,7 +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,
wxsat_decode_enabled: self.wxsat_decode_enabled,
filter: self.filter.clone(),
spectrum: self.spectrum.clone(),
vchan_rds: self.vchan_rds.clone(),
@@ -498,7 +498,7 @@ pub struct RigSnapshot {
#[serde(default)]
pub wspr_decode_enabled: bool,
#[serde(default)]
pub noaa_decode_enabled: bool,
pub wxsat_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]