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;