[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:
@@ -17,6 +17,7 @@ use tracing::{info, warn};
|
||||
use trx_core::audio::{
|
||||
read_audio_msg, write_audio_msg, AudioStreamInfo, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE,
|
||||
AUDIO_MSG_FT8_DECODE, AUDIO_MSG_RX_FRAME, AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME,
|
||||
AUDIO_MSG_WSPR_DECODE,
|
||||
};
|
||||
use trx_core::decode::DecodedMessage;
|
||||
|
||||
@@ -115,7 +116,10 @@ async fn handle_audio_connection(
|
||||
let _ = rx_tx.send(Bytes::from(payload));
|
||||
}
|
||||
Ok((
|
||||
AUDIO_MSG_APRS_DECODE | AUDIO_MSG_CW_DECODE | AUDIO_MSG_FT8_DECODE,
|
||||
AUDIO_MSG_APRS_DECODE
|
||||
| AUDIO_MSG_CW_DECODE
|
||||
| AUDIO_MSG_FT8_DECODE
|
||||
| AUDIO_MSG_WSPR_DECODE,
|
||||
payload,
|
||||
)) => {
|
||||
if let Ok(msg) = serde_json::from_slice::<DecodedMessage>(&payload) {
|
||||
|
||||
@@ -414,6 +414,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user