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;