[feat](trx-wxsat): add Meteor-M LRPT decoder and Weather Satellites frontend panel

Restructure trx-wxsat into noaa/ (APT) and lrpt/ (Meteor-M LRPT) submodules
with shared crate base. Add QPSK demodulator, CCSDS CADU framer, MCU channel
assembler for LRPT. Wire LRPT through full stack (core types, protocol, server
decoder task, client). Add Weather Satellites sub-tab in Digital Modes with
toggle buttons for NOAA APT and Meteor LRPT, descriptions, and image history.

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 07:13:05 +00:00
committed by Stan Grams
parent d26ef6ca81
commit 1a3b815ed8
29 changed files with 1526 additions and 158 deletions
+2
View File
@@ -68,6 +68,8 @@ pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14;
pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15;
/// Server → client: weather satellite APT image complete (JSON `DecodedMessage::WxsatImage`).
pub const AUDIO_MSG_WXSAT_IMAGE: u8 = 0x16;
/// Server → client: Meteor-M LRPT image complete (JSON `DecodedMessage::LrptImage`).
pub const AUDIO_MSG_LRPT_IMAGE: u8 = 0x17;
/// Maximum payload size for normal messages (1 MB).
const MAX_PAYLOAD_SIZE: u32 = 1_048_576;
+27
View File
@@ -30,6 +30,8 @@ pub enum DecodedMessage {
Wspr(WsprMessage),
#[serde(rename = "wxsat_image")]
WxsatImage(WxsatImage),
#[serde(rename = "lrpt_image")]
LrptImage(LrptImage),
}
impl DecodedMessage {
@@ -43,6 +45,7 @@ impl DecodedMessage {
Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id = Some(id),
Self::Wspr(m) => m.rig_id = Some(id),
Self::WxsatImage(m) => m.rig_id = Some(id),
Self::LrptImage(m) => m.rig_id = Some(id),
}
}
@@ -56,6 +59,7 @@ impl DecodedMessage {
Self::Ft8(m) | Self::Ft4(m) | Self::Ft2(m) => m.rig_id.as_deref(),
Self::Wspr(m) => m.rig_id.as_deref(),
Self::WxsatImage(m) => m.rig_id.as_deref(),
Self::LrptImage(m) => m.rig_id.as_deref(),
}
}
}
@@ -248,3 +252,26 @@ pub struct WsprMessage {
/// Decoded message text
pub message: String,
}
/// A completed Meteor-M LRPT satellite image, saved to disk as a PNG.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LrptImage {
#[serde(skip_serializing_if = "Option::is_none")]
pub rig_id: Option<String>,
/// UTC timestamp (milliseconds since epoch) of pass start.
pub pass_start_ms: i64,
/// UTC timestamp (milliseconds since epoch) when the image was finalised.
pub pass_end_ms: i64,
/// Number of decoded MCU rows.
pub mcu_count: u32,
/// Absolute filesystem path to the saved image file.
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ts_ms: Option<i64>,
/// Identified satellite (e.g. "Meteor-M N2-3", "Meteor-M N2-4").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub satellite: Option<String>,
/// APID channels decoded (e.g. "64,65,66" for RGB).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channels: Option<String>,
}
+4
View File
@@ -31,6 +31,8 @@ pub enum RigCommand {
SetFt4DecodeEnabled(bool),
SetFt2DecodeEnabled(bool),
SetWsprDecodeEnabled(bool),
SetWxsatDecodeEnabled(bool),
SetLrptDecodeEnabled(bool),
ResetAprsDecoder,
ResetHfAprsDecoder,
ResetCwDecoder,
@@ -38,6 +40,8 @@ pub enum RigCommand {
ResetFt4Decoder,
ResetFt2Decoder,
ResetWsprDecoder,
ResetWxsatDecoder,
ResetLrptDecoder,
SetBandwidth(u32),
SetSdrGain(f64),
SetSdrLnaGain(f64),
@@ -520,6 +520,10 @@ pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
| RigCommand::ResetFt4Decoder
| RigCommand::ResetFt2Decoder
| RigCommand::ResetWsprDecoder
| RigCommand::SetWxsatDecodeEnabled(_)
| RigCommand::SetLrptDecodeEnabled(_)
| RigCommand::ResetWxsatDecoder
| RigCommand::ResetLrptDecoder
| RigCommand::SetBandwidth(_)
| RigCommand::SetSdrGain(_)
| RigCommand::SetSdrLnaGain(_)
+11
View File
@@ -48,6 +48,8 @@ pub struct RigState {
#[serde(default)]
pub wxsat_decode_enabled: bool,
#[serde(default)]
pub lrpt_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]
pub cw_wpm: u32,
@@ -81,6 +83,8 @@ pub struct RigState {
pub wspr_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub wxsat_decode_reset_seq: u64,
#[serde(default, skip_serializing)]
pub lrpt_decode_reset_seq: u64,
}
/// Mode supported by the rig.
@@ -164,6 +168,7 @@ impl RigState {
ft2_decode_enabled: false,
wspr_decode_enabled: false,
wxsat_decode_enabled: false,
lrpt_decode_enabled: false,
cw_auto: true,
cw_wpm: 15,
cw_tone_hz: 700,
@@ -178,6 +183,7 @@ impl RigState {
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
wxsat_decode_reset_seq: 0,
lrpt_decode_reset_seq: 0,
}
}
@@ -238,6 +244,7 @@ impl RigState {
ft2_decode_enabled: snapshot.ft2_decode_enabled,
wspr_decode_enabled: snapshot.wspr_decode_enabled,
wxsat_decode_enabled: snapshot.wxsat_decode_enabled,
lrpt_decode_enabled: snapshot.lrpt_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
@@ -249,6 +256,7 @@ impl RigState {
ft2_decode_reset_seq: 0,
wspr_decode_reset_seq: 0,
wxsat_decode_reset_seq: 0,
lrpt_decode_reset_seq: 0,
}
}
@@ -287,6 +295,7 @@ impl RigState {
ft2_decode_enabled: self.ft2_decode_enabled,
wspr_decode_enabled: self.wspr_decode_enabled,
wxsat_decode_enabled: self.wxsat_decode_enabled,
lrpt_decode_enabled: self.lrpt_decode_enabled,
filter: self.filter.clone(),
spectrum: self.spectrum.clone(),
vchan_rds: self.vchan_rds.clone(),
@@ -500,6 +509,8 @@ pub struct RigSnapshot {
#[serde(default)]
pub wxsat_decode_enabled: bool,
#[serde(default)]
pub lrpt_decode_enabled: bool,
#[serde(default)]
pub cw_auto: bool,
#[serde(default)]
pub cw_wpm: u32,