diff --git a/src/trx-client/src/audio_client.rs b/src/trx-client/src/audio_client.rs index 3150a8b..2ce6f89 100644 --- a/src/trx-client/src/audio_client.rs +++ b/src/trx-client/src/audio_client.rs @@ -29,8 +29,8 @@ use trx_core::audio::{ AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_RX_FRAME, AUDIO_MSG_RX_FRAME_CH, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED, AUDIO_MSG_VCHAN_BW, AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE, - AUDIO_MSG_VCHAN_REMOVE, AUDIO_MSG_VCHAN_SUB, AUDIO_MSG_VCHAN_UNSUB, AUDIO_MSG_VDES_DECODE, - AUDIO_MSG_WSPR_DECODE, + AUDIO_MSG_LRPT_IMAGE, AUDIO_MSG_LRPT_PROGRESS, AUDIO_MSG_VCHAN_REMOVE, AUDIO_MSG_VCHAN_SUB, + AUDIO_MSG_VCHAN_UNSUB, AUDIO_MSG_VDES_DECODE, AUDIO_MSG_WSPR_DECODE, }; use trx_core::decode::DecodedMessage; use trx_frontend::VChanAudioCmd; @@ -567,7 +567,9 @@ async fn handle_single_rig_connection( | AUDIO_MSG_FT8_DECODE | AUDIO_MSG_FT4_DECODE | AUDIO_MSG_FT2_DECODE - | AUDIO_MSG_WSPR_DECODE, + | AUDIO_MSG_WSPR_DECODE + | AUDIO_MSG_LRPT_IMAGE + | AUDIO_MSG_LRPT_PROGRESS, payload, )) => { if let Ok(mut msg) = serde_json::from_slice::(&payload) { diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 8fa34a2..03fd85f 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -523,6 +523,7 @@ async fn async_init() -> DynResult { } } DecodedMessage::LrptImage(_) => {} + DecodedMessage::LrptProgress(_) => {} } }); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 23240f0..760a25c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -9134,7 +9134,8 @@ function dispatchDecodeMessage(msg, skipStats) { if (msg.type === "ft2" && window.onServerFt2) window.onServerFt2(msg); if (msg.type === "wspr" && window.onServerWspr) window.onServerWspr(msg); if (msg.type === "lrpt_image" && window.onServerLrptImage) window.onServerLrptImage(msg); - if (!skipStats && msg.type && msg.type !== "lrpt_image") { + if (msg.type === "lrpt_progress" && window.onServerLrptProgress) window.onServerLrptProgress(msg); + if (!skipStats && msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress") { statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); scheduleStatsRender(); } @@ -9144,7 +9145,7 @@ function dispatchDecodeBatch(batch) { if (!Array.isArray(batch) || batch.length === 0) return; // Record statistics for every message in the batch regardless of dispatch path. for (const msg of batch) { - if (msg.type && msg.type !== "lrpt_image") { + if (msg.type && msg.type !== "lrpt_image" && msg.type !== "lrpt_progress") { statsRecordDecode(msg.type, msg.rig_id || msg.remote || null); } } @@ -9231,7 +9232,7 @@ function loadDecodeHistoryOnMainThread(onReady, onError) { function restoreDecodeHistoryGroup(kind, messages) { if (!Array.isArray(messages) || messages.length === 0) return; // Record statistics for restored history messages. - if (kind !== "lrpt_image") { + if (kind !== "lrpt_image" && kind !== "lrpt_progress") { for (const msg of messages) { statsRecordDecode(kind, msg.rig_id || msg.remote || null, msg.ts_ms || undefined); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js index eb53f7a..467661b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js @@ -91,6 +91,13 @@ window.updateSatLiveState = function (update) { _lastSatLrptOn = lrptOn; satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle"; satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle"); + if (satDom.status) { + if (lrptOn) { + satDom.status.textContent = "Decoder active \u2014 waiting for signal"; + } else { + satDom.status.textContent = "Decoder idle"; + } + } } }; @@ -233,6 +240,12 @@ function addSatImage(img, decoder) { } // ── Server callbacks ──────────────────────────────────────────────── +window.onServerLrptProgress = function (msg) { + if (satDom.status && msg.mcu_count > 0) { + satDom.status.textContent = "Receiving \u2014 " + msg.mcu_count + " MCU rows decoded"; + } +}; + window.onServerLrptImage = function (msg) { if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)"; addSatImage(msg, "lrpt"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index 6e438a2..181536c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -585,6 +585,7 @@ pub fn start_decode_history_collector(context: Arc) { DecodedMessage::Ft2(msg) => record_ft2(&context, msg), DecodedMessage::Wspr(msg) => record_wspr(&context, msg), DecodedMessage::LrptImage(_) => {} + DecodedMessage::LrptProgress(_) => {} }, Err(broadcast::error::RecvError::Lagged(_)) => continue, Err(broadcast::error::RecvError::Closed) => break, diff --git a/src/trx-core/src/audio.rs b/src/trx-core/src/audio.rs index 6bc0505..1eb100b 100644 --- a/src/trx-core/src/audio.rs +++ b/src/trx-core/src/audio.rs @@ -68,6 +68,8 @@ pub const AUDIO_MSG_FT4_DECODE: u8 = 0x14; pub const AUDIO_MSG_FT2_DECODE: u8 = 0x15; /// Server → client: Meteor-M LRPT image complete (JSON `DecodedMessage::LrptImage`). pub const AUDIO_MSG_LRPT_IMAGE: u8 = 0x17; +/// Server → client: LRPT decode progress update (JSON `DecodedMessage::LrptProgress`). +pub const AUDIO_MSG_LRPT_PROGRESS: u8 = 0x18; /// Maximum payload size for normal messages (1 MB). const MAX_PAYLOAD_SIZE: u32 = 1_048_576; diff --git a/src/trx-core/src/decode.rs b/src/trx-core/src/decode.rs index d6b7134..2fbc10c 100644 --- a/src/trx-core/src/decode.rs +++ b/src/trx-core/src/decode.rs @@ -30,6 +30,8 @@ pub enum DecodedMessage { Wspr(WsprMessage), #[serde(rename = "lrpt_image")] LrptImage(LrptImage), + #[serde(rename = "lrpt_progress")] + LrptProgress(LrptProgress), } 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::LrptImage(m) => m.rig_id = Some(id), + Self::LrptProgress(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::LrptImage(m) => m.rig_id.as_deref(), + Self::LrptProgress(m) => m.rig_id.as_deref(), } } } @@ -223,6 +227,15 @@ pub struct WsprMessage { pub message: String, } +/// Live LRPT decode progress update, sent periodically during active decoding. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LrptProgress { + #[serde(skip_serializing_if = "Option::is_none")] + pub rig_id: Option, + /// Number of MCU rows decoded so far in this pass. + pub mcu_count: u32, +} + /// A completed Meteor-M LRPT satellite image, saved to disk as a PNG. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LrptImage { diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 3ed3f6b..823470f 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -26,13 +26,15 @@ use trx_core::audio::{ write_vchan_uuid_msg, AudioStreamInfo, AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT2_DECODE, AUDIO_MSG_FT4_DECODE, AUDIO_MSG_FT8_DECODE, AUDIO_MSG_HF_APRS_DECODE, AUDIO_MSG_HISTORY_COMPRESSED, AUDIO_MSG_LRPT_IMAGE, + AUDIO_MSG_LRPT_PROGRESS, AUDIO_MSG_RX_FRAME, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VCHAN_ALLOCATED, AUDIO_MSG_VCHAN_BW, AUDIO_MSG_VCHAN_DESTROYED, AUDIO_MSG_VCHAN_FREQ, AUDIO_MSG_VCHAN_MODE, AUDIO_MSG_VCHAN_REMOVE, AUDIO_MSG_VCHAN_SUB, AUDIO_MSG_VCHAN_UNSUB, AUDIO_MSG_VDES_DECODE, AUDIO_MSG_WSPR_DECODE, }; use trx_core::decode::{ - AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, LrptImage, VdesMessage, + AisMessage, AprsPacket, CwEvent, DecodedMessage, Ft8Message, LrptImage, LrptProgress, + VdesMessage, WsprMessage, }; use trx_core::rig::state::{RigMode, RigState}; @@ -2445,6 +2447,12 @@ pub async fn run_lrpt_decoder( }; if new_mcus > 0 { last_mcu_at = tokio::time::Instant::now(); + let _ = decode_tx.send(DecodedMessage::LrptProgress( + LrptProgress { + rig_id: None, + mcu_count: decoder.mcu_count(), + }, + )); } } Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3301,6 +3309,7 @@ async fn handle_audio_client( DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE, DecodedMessage::LrptImage(_) => AUDIO_MSG_LRPT_IMAGE, + DecodedMessage::LrptProgress(_) => AUDIO_MSG_LRPT_PROGRESS, }; if let Ok(json) = serde_json::to_vec(&msg) { if let Err(e) = write_audio_msg(&mut writer_for_rx, msg_type, &json).await { @@ -3330,6 +3339,7 @@ async fn handle_audio_client( DecodedMessage::Wspr(_) => AUDIO_MSG_WSPR_DECODE, DecodedMessage::LrptImage(_) => AUDIO_MSG_LRPT_IMAGE, + DecodedMessage::LrptProgress(_) => AUDIO_MSG_LRPT_PROGRESS, }; if let Ok(json) = serde_json::to_vec(&msg) { if let Err(e) = write_audio_msg(&mut writer_for_rx, msg_type, &json).await {