[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 <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 23:37:15 +01:00
parent e025be5fff
commit 28dab2d00f
8 changed files with 90 additions and 5 deletions
+67 -5
View File
@@ -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<StreamErrorState>,
}
#[derive(Default)]
struct StreamErrorState {
last_error: Option<String>,
last_logged_at: Option<Instant>,
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("<unknown>")
);
}
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<VecDeque<(Instant, AprsPacket)>> {
static HISTORY: OnceLock<Mutex<VecDeque<(Instant, AprsPacket)>>> = OnceLock::new();
@@ -208,13 +262,17 @@ fn run_capture(
let (sample_tx, sample_rx) = std::sync::mpsc::sync_channel::<Vec<f32>>(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,
)?;
+9
View File
@@ -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;