[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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr>
|
||||
<tr><td>Supported modes</td><td id="about-modes">--</td></tr>
|
||||
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
|
||||
<tr><td>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
|
||||
<tr><td>Client</td><td>{pkg} v{ver}</td></tr>
|
||||
<tr><td>Connected clients</td><td id="about-clients">--</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -588,6 +588,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
||||
server_version: state.server_version,
|
||||
server_latitude: state.server_latitude,
|
||||
server_longitude: state.server_longitude,
|
||||
pskreporter_status: state.pskreporter_status,
|
||||
aprs_decode_enabled: state.aprs_decode_enabled,
|
||||
cw_decode_enabled: state.cw_decode_enabled,
|
||||
cw_auto: state.cw_auto,
|
||||
|
||||
@@ -24,6 +24,8 @@ pub struct RigState {
|
||||
pub server_latitude: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server_longitude: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pskreporter_status: Option<String>,
|
||||
#[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<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server_longitude: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pskreporter_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub aprs_decode_enabled: bool,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user