[feat](trx-core): add WSPR decode plumbing across stack

Introduce WSPR command/state/protocol/transport wiring and integrate lifecycle, history, and frontend API paths mirroring the FT8 architecture.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 22:38:05 +01:00
parent 8e59e205a8
commit 63ba6882cd
16 changed files with 310 additions and 5 deletions
+4 -1
View File
@@ -13,7 +13,7 @@ use tokio::sync::{broadcast, mpsc, watch};
use tokio::task::JoinHandle;
use trx_core::audio::AudioStreamInfo;
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message};
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message, WsprMessage};
use trx_core::{DynResult, RigRequest, RigState};
/// Trait implemented by concrete frontends to expose a runner entrypoint.
@@ -116,6 +116,8 @@ pub struct FrontendRuntimeContext {
pub cw_history: Arc<Mutex<VecDeque<(Instant, CwEvent)>>>,
/// FT8 decode history (timestamp, message)
pub ft8_history: Arc<Mutex<VecDeque<(Instant, Ft8Message)>>>,
/// WSPR decode history (timestamp, message)
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
/// Authentication tokens for HTTP-JSON frontend
pub auth_tokens: HashSet<String>,
/// Guard to avoid spawning duplicate decode collectors.
@@ -133,6 +135,7 @@ impl FrontendRuntimeContext {
aprs_history: Arc::new(Mutex::new(VecDeque::new())),
cw_history: Arc::new(Mutex::new(VecDeque::new())),
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
auth_tokens: HashSet::new(),
decode_collector_started: AtomicBool::new(false),
}
@@ -337,6 +337,7 @@ mod tests {
aprs_decode_enabled: false,
cw_decode_enabled: false,
ft8_decode_enabled: false,
wspr_decode_enabled: false,
cw_auto: true,
cw_wpm: 15,
cw_tone_hz: 700,
@@ -124,6 +124,11 @@ pub async fn decode_events(
.into_iter()
.map(trx_core::decode::DecodedMessage::Ft8),
);
out.extend(
crate::server::audio::snapshot_wspr_history(context.get_ref())
.into_iter()
.map(trx_core::decode::DecodedMessage::Wspr),
);
out
};
@@ -360,6 +365,15 @@ pub async fn toggle_ft8_decode(
send_command(&rig_tx, RigCommand::SetFt8DecodeEnabled(!enabled)).await
}
#[post("/toggle_wspr_decode")]
pub async fn toggle_wspr_decode(
state: web::Data<watch::Receiver<RigState>>,
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
let enabled = state.get_ref().borrow().wspr_decode_enabled;
send_command(&rig_tx, RigCommand::SetWsprDecodeEnabled(!enabled)).await
}
#[post("/clear_ft8_decode")]
pub async fn clear_ft8_decode(
context: web::Data<Arc<FrontendRuntimeContext>>,
@@ -369,6 +383,15 @@ pub async fn clear_ft8_decode(
send_command(&rig_tx, RigCommand::ResetFt8Decoder).await
}
#[post("/clear_wspr_decode")]
pub async fn clear_wspr_decode(
context: web::Data<Arc<FrontendRuntimeContext>>,
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
) -> Result<HttpResponse, Error> {
crate::server::audio::clear_wspr_history(context.get_ref());
send_command(&rig_tx, RigCommand::ResetWsprDecoder).await
}
#[post("/clear_aprs_decode")]
pub async fn clear_aprs_decode(
context: web::Data<Arc<FrontendRuntimeContext>>,
@@ -406,9 +429,11 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(set_cw_wpm)
.service(set_cw_tone)
.service(toggle_ft8_decode)
.service(toggle_wspr_decode)
.service(clear_aprs_decode)
.service(clear_cw_decode)
.service(clear_ft8_decode)
.service(clear_wspr_decode)
.service(crate::server::audio::audio_ws)
.service(favicon)
.service(logo)
@@ -558,6 +583,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
cw_wpm: state.cw_wpm,
cw_tone_hz: state.cw_tone_hz,
ft8_decode_enabled: state.ft8_decode_enabled,
wspr_decode_enabled: state.wspr_decode_enabled,
})
}
@@ -20,7 +20,7 @@ use bytes::Bytes;
use tokio::sync::broadcast;
use tracing::warn;
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message};
use trx_core::decode::{AprsPacket, CwEvent, DecodedMessage, Ft8Message, WsprMessage};
use trx_frontend::FrontendRuntimeContext;
const HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60);
@@ -52,6 +52,15 @@ fn prune_ft8_history(history: &mut VecDeque<(Instant, Ft8Message)>) {
}
}
fn prune_wspr_history(history: &mut VecDeque<(Instant, WsprMessage)>) {
while let Some((ts, _)) = history.front() {
if ts.elapsed() <= HISTORY_RETENTION {
break;
}
history.pop_front();
}
}
fn record_aprs(context: &FrontendRuntimeContext, pkt: AprsPacket) {
let mut history = context
.aprs_history
@@ -79,6 +88,15 @@ fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
prune_ft8_history(&mut history);
}
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
let mut history = context
.wspr_history
.lock()
.expect("wspr history mutex poisoned");
history.push_back((Instant::now(), msg));
prune_wspr_history(&mut history);
}
pub fn snapshot_aprs_history(context: &FrontendRuntimeContext) -> Vec<AprsPacket> {
let mut history = context
.aprs_history
@@ -106,6 +124,15 @@ pub fn snapshot_ft8_history(context: &FrontendRuntimeContext) -> Vec<Ft8Message>
history.iter().map(|(_, msg)| msg.clone()).collect()
}
pub fn snapshot_wspr_history(context: &FrontendRuntimeContext) -> Vec<WsprMessage> {
let mut history = context
.wspr_history
.lock()
.expect("wspr history mutex poisoned");
prune_wspr_history(&mut history);
history.iter().map(|(_, msg)| msg.clone()).collect()
}
pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
let mut history = context
.aprs_history
@@ -130,6 +157,14 @@ pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
history.clear();
}
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
let mut history = context
.wspr_history
.lock()
.expect("wspr history mutex poisoned");
history.clear();
}
pub fn subscribe_decode(
context: &FrontendRuntimeContext,
) -> Option<broadcast::Receiver<DecodedMessage>> {
@@ -156,6 +191,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
DecodedMessage::Aprs(pkt) => record_aprs(&context, pkt),
DecodedMessage::Cw(evt) => record_cw(&context, evt),
DecodedMessage::Ft8(msg) => record_ft8(&context, msg),
DecodedMessage::Wspr(msg) => record_wspr(&context, msg),
},
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,