From 28dab2d00ffd468ba4608c2fda63eb6ade733b76 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Thu, 12 Feb 2026 23:37:15 +0100 Subject: [PATCH] [feat](trx-server): expose PSK Reporter status in About Add pskreporter_status to shared rig snapshots and display it in the HTTP frontend About tab. Also include audio stream error log throttling to avoid repetitive ALSA error flooding in backend logs. Co-authored-by: Codex Signed-off-by: Stanislaw Grams --- src/trx-client/src/remote_client.rs | 1 + .../trx-frontend-http-json/src/server.rs | 1 + .../trx-frontend-http/assets/web/app.js | 3 + .../trx-frontend-http/assets/web/index.html | 1 + .../trx-frontend/trx-frontend-http/src/api.rs | 1 + src/trx-core/src/rig/state.rs | 7 ++ src/trx-server/src/audio.rs | 72 +++++++++++++++++-- src/trx-server/src/main.rs | 9 +++ 8 files changed, 90 insertions(+), 5 deletions(-) diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 584c057..df9f534 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -411,6 +411,7 @@ mod tests { server_version: Some("test".to_string()), server_latitude: None, server_longitude: None, + pskreporter_status: Some("Disabled".to_string()), aprs_decode_enabled: false, cw_decode_enabled: false, ft8_decode_enabled: false, diff --git a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs index d2cb0d5..308a86f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs @@ -334,6 +334,7 @@ mod tests { server_version: Some("test".to_string()), server_latitude: None, server_longitude: None, + pskreporter_status: Some("Disabled".to_string()), aprs_decode_enabled: false, cw_decode_enabled: false, ft8_decode_enabled: false, 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 fcb6262..94e0233 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 @@ -391,6 +391,9 @@ function render(update) { if (update.server_callsign) { document.getElementById("about-server-call").textContent = update.server_callsign; } + if (update.pskreporter_status) { + document.getElementById("about-pskreporter").textContent = update.pskreporter_status; + } if (update.info) { const parts = [update.info.manufacturer, update.info.model, update.info.revision].filter(Boolean).join(" "); if (parts) document.getElementById("about-rig-info").textContent = parts; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 9223f62..e569ec2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -224,6 +224,7 @@ Rig connection-- Supported modes-- VFOs-- + PSK Reporter-- Client{pkg} v{ver} Connected clients-- diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 1542a6a..05df604 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -588,6 +588,7 @@ async fn wait_for_view(mut rx: watch::Receiver) -> Result, #[serde(default, skip_serializing_if = "Option::is_none")] pub server_longitude: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pskreporter_status: Option, #[serde(default)] pub aprs_decode_enabled: bool, #[serde(default)] @@ -114,6 +116,7 @@ impl RigState { server_version: None, server_latitude: None, server_longitude: None, + pskreporter_status: None, aprs_decode_enabled: false, cw_decode_enabled: false, ft8_decode_enabled: false, @@ -169,6 +172,7 @@ impl RigState { server_version: snapshot.server_version, server_latitude: snapshot.server_latitude, server_longitude: snapshot.server_longitude, + pskreporter_status: snapshot.pskreporter_status, aprs_decode_enabled: snapshot.aprs_decode_enabled, cw_decode_enabled: snapshot.cw_decode_enabled, cw_auto: snapshot.cw_auto, @@ -204,6 +208,7 @@ impl RigState { server_version: self.server_version.clone(), server_latitude: self.server_latitude, server_longitude: self.server_longitude, + pskreporter_status: self.pskreporter_status.clone(), aprs_decode_enabled: self.aprs_decode_enabled, cw_decode_enabled: self.cw_decode_enabled, cw_auto: self.cw_auto, @@ -253,6 +258,8 @@ pub struct RigSnapshot { pub server_latitude: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub server_longitude: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pskreporter_status: Option, #[serde(default)] pub aprs_decode_enabled: bool, #[serde(default)] diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 1964988..beda264 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -5,7 +5,7 @@ //! Audio capture, playback, and TCP streaming for trx-server. use std::net::SocketAddr; -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; use std::{collections::VecDeque, sync::Mutex}; @@ -31,6 +31,60 @@ const APRS_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const WSPR_HISTORY_RETENTION: Duration = Duration::from_secs(24 * 60 * 60); const FT8_SAMPLE_RATE: u32 = 12_000; +const AUDIO_STREAM_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(10); + +struct StreamErrorLogger { + label: &'static str, + state: Mutex, +} + +#[derive(Default)] +struct StreamErrorState { + last_error: Option, + last_logged_at: Option, + suppressed: u64, +} + +impl StreamErrorLogger { + fn new(label: &'static str) -> Self { + Self { + label, + state: Mutex::new(StreamErrorState::default()), + } + } + + fn log(&self, err: &str) { + let now = Instant::now(); + let mut state = self + .state + .lock() + .expect("stream error logger mutex poisoned"); + let should_log_now = match (&state.last_error, state.last_logged_at) { + (None, _) => true, + (Some(prev), Some(ts)) => { + prev != err || now.duration_since(ts) >= AUDIO_STREAM_ERROR_LOG_INTERVAL + } + (Some(_), None) => true, + }; + + if should_log_now { + if state.suppressed > 0 { + warn!( + "{} repeated {} times: {}", + self.label, + state.suppressed, + state.last_error.as_deref().unwrap_or("") + ); + } + error!("{}: {}", self.label, err); + state.last_error = Some(err.to_string()); + state.last_logged_at = Some(now); + state.suppressed = 0; + } else { + state.suppressed += 1; + } + } +} fn aprs_history() -> &'static Mutex> { static HISTORY: OnceLock>> = OnceLock::new(); @@ -208,13 +262,17 @@ fn run_capture( let (sample_tx, sample_rx) = std::sync::mpsc::sync_channel::>(64); + let input_err_logger = Arc::new(StreamErrorLogger::new("Audio input stream error")); let stream = device.build_input_stream( &config, move |data: &[f32], _: &cpal::InputCallbackInfo| { let _ = sample_tx.try_send(data.to_vec()); }, - move |err| { - error!("Audio input stream error: {}", err); + { + let input_err_logger = input_err_logger.clone(); + move |err| { + input_err_logger.log(&err.to_string()); + } }, None, )?; @@ -342,6 +400,7 @@ fn run_playback( )); let ring_writer = ring.clone(); + let output_err_logger = Arc::new(StreamErrorLogger::new("Audio output stream error")); let stream = device.build_output_stream( &config, move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { @@ -350,8 +409,11 @@ fn run_playback( *sample = ring.pop_front().unwrap_or(0.0); } }, - move |err| { - error!("Audio output stream error: {}", err); + { + let output_err_logger = output_err_logger.clone(); + move |err| { + output_err_logger.log(&err.to_string()); + } }, None, )?; diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 66adc5f..8737033 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -286,6 +286,15 @@ async fn main() -> DynResult<()> { cfg.rig.initial_freq_hz, cfg.rig.initial_mode.clone(), ); + let mut initial_state = initial_state; + initial_state.pskreporter_status = if cfg.pskreporter.enabled { + Some(format!( + "Enabled ({}:{})", + cfg.pskreporter.host, cfg.pskreporter.port + )) + } else { + Some("Disabled".to_string()) + }; let (state_tx, state_rx) = watch::channel(initial_state); // Keep receivers alive so channels don't close prematurely let _state_rx = state_rx;