[fix](trx): speed up web load and harden FT817/Soapy recovery

This commit is contained in:
2026-03-05 20:36:46 +01:00
parent bc0d9a6273
commit ccef359034
6 changed files with 108 additions and 46 deletions
@@ -7,11 +7,13 @@
<link rel="icon" type="image/png" sizes="any" href="/favicon.ico?v=5" />
<link rel="shortcut icon" href="/favicon.ico?v=5" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.png?v=5" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/400.css" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://unpkg.com" crossorigin />
<link rel="preload" as="style" href="https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/400.css" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/dseg14-classic/400.css" /></noscript>
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/leaflet-ais-tracksymbol.js"></script>
<link rel="preload" as="style" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /></noscript>
</head>
<body>
<div class="card" id="card">
@@ -638,5 +640,7 @@
<script src="/wspr.js"></script>
<script src="/cw.js"></script>
<script src="/bookmarks.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="/leaflet-ais-tracksymbol.js"></script>
</body>
</html>
@@ -250,6 +250,7 @@ pub async fn events(
Ok(HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/event-stream"))
.insert_header((header::CONTENT_ENCODING, "identity"))
.insert_header((header::CACHE_CONTROL, "no-cache"))
.insert_header((header::CONNECTION, "keep-alive"))
.streaming(stream))
@@ -330,6 +331,7 @@ pub async fn decode_events(
Ok(HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/event-stream"))
.insert_header((header::CONTENT_ENCODING, "identity"))
.insert_header((header::CACHE_CONTROL, "no-cache"))
.insert_header((header::CONNECTION, "keep-alive"))
.streaming(stream))
@@ -416,6 +418,7 @@ pub async fn spectrum(
Ok(HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "text/event-stream"))
.insert_header((header::CONTENT_ENCODING, "identity"))
.insert_header((header::CACHE_CONTROL, "no-cache"))
.insert_header((header::CONNECTION, "keep-alive"))
.streaming(stream))
@@ -20,7 +20,7 @@ use std::time::Duration;
use actix_web::dev::Server;
use actix_web::{
middleware::{DefaultHeaders, Logger},
middleware::{Compress, DefaultHeaders, Logger},
web, App, HttpServer,
};
use tokio::signal;
@@ -126,6 +126,7 @@ fn build_server(
.app_data(context_data.clone())
.app_data(auth_state.clone())
.app_data(bookmark_store.clone())
.wrap(Compress::default())
.wrap(
DefaultHeaders::new()
.add(("Referrer-Policy", "same-origin"))
+44 -27
View File
@@ -170,19 +170,8 @@ pub async fn run_rig_task(
match rig.power_on().await {
Ok(()) => {
state.control.enabled = Some(true);
time::sleep(Duration::from_secs(3)).await;
if let Err(e) = refresh_state_with_retry(&mut rig, &mut state, retry).await {
warn!(
"Initial PowerOn refresh failed: {:?}; retrying once after short delay",
e
);
time::sleep(Duration::from_millis(500)).await;
if let Err(e2) = refresh_state_with_retry(&mut rig, &mut state, retry).await {
warn!(
"Initial PowerOn second refresh failed (continuing): {:?}",
e2
);
}
if let Err(e) = refresh_after_power_on(&mut rig, &mut state, retry).await {
warn!("Initial PowerOn refresh failed after retries (continuing): {}", e);
} else {
initial_status_read = true;
}
@@ -562,23 +551,14 @@ async fn process_command(
CommandResult::PowerUpdated(on) => {
ctx.state.control.enabled = Some(on);
if on {
time::sleep(Duration::from_secs(3)).await;
if let Err(e) = refresh_after_power_on(ctx.rig, ctx.state, ctx.retry).await
{
error!("Failed to refresh after PowerOn: {}", e);
return Err(RigError::communication(format!("CAT error: {}", e)));
}
let now = Instant::now();
*ctx.poll_pause_until = Some(now + Duration::from_secs(3));
*ctx.last_power_on = Some(now);
// Refresh state after power on
if let Err(e) =
refresh_state_with_retry(ctx.rig, ctx.state, ctx.retry).await
{
if is_invalid_bcd_error(e.as_ref()) {
warn!("Transient CAT decode after PowerOn (ignored): {:?}", e);
*ctx.poll_pause_until =
Some(Instant::now() + Duration::from_millis(1500));
} else {
error!("Failed to refresh after PowerOn: {:?}", e);
return Err(RigError::communication(format!("CAT error: {}", e)));
}
}
} else {
ctx.state.status.tx_en = false;
}
@@ -716,6 +696,43 @@ async fn refresh_state_from_cat(rig: &mut Box<dyn RigCat>, state: &mut RigState)
Ok(())
}
async fn refresh_after_power_on(
rig: &mut Box<dyn RigCat>,
state: &mut RigState,
retry: &ExponentialBackoff,
) -> DynResult<()> {
let mut last_err = String::new();
for attempt in 1..=3 {
if attempt == 1 {
time::sleep(Duration::from_secs(3)).await;
} else {
warn!(
"PowerOn refresh attempt {} failed; issuing additional PowerOn pulse",
attempt - 1
);
if let Err(e) = rig.power_on().await {
warn!("PowerOn retry {} failed: {:?}", attempt, e);
}
time::sleep(Duration::from_millis(1300)).await;
}
match refresh_state_with_retry(rig, state, retry).await {
Ok(()) => return Ok(()),
Err(e) => {
last_err = e.to_string();
if is_invalid_bcd_error(e.as_ref()) {
warn!(
"Transient CAT decode after PowerOn attempt {}: {:?}",
attempt, e
);
} else {
warn!("PowerOn refresh attempt {} failed: {:?}", attempt, e);
}
}
}
}
Err(format!("refresh after PowerOn failed after retries: {}", last_err).into())
}
/// Apply initial mode/frequency after a successful CAT status read.
async fn apply_initial_tune(
rig: &mut Box<dyn RigCat>,
@@ -16,6 +16,7 @@ mod filter;
mod spectrum;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use num_complex::Complex;
use tokio::sync::broadcast;
@@ -57,7 +58,7 @@ pub trait IqSource: Send + 'static {
/// Gives a source-specific implementation a chance to recover from a
/// read error (for example, by rearming a hardware stream after overflow).
/// Returns `true` when an active recovery action was attempted.
fn handle_read_error(&mut self, _err: &str) -> Result<bool, String> {
fn handle_read_error(&mut self, _err: &str, _streak: u32) -> Result<bool, String> {
Ok(false)
}
}
@@ -199,6 +200,8 @@ fn iq_read_loop(
let mut spectrum = SpectrumSnapshotter::new();
let mut read_error_streak: u32 = 0;
let mut overflow_log_window_start: Option<Instant> = None;
let mut overflow_log_suppressed: u32 = 0;
loop {
// Apply any pending hardware retune before the next read.
@@ -228,7 +231,9 @@ fn iq_read_loop(
}
Err(e) => {
read_error_streak = read_error_streak.saturating_add(1);
let recovered = match source.handle_read_error(&e) {
let err_lc = e.to_ascii_lowercase();
let is_overflow = err_lc.contains("overflow") || err_lc.contains("overrun");
let recovered = match source.handle_read_error(&e, read_error_streak) {
Ok(result) => result,
Err(recovery_err) => {
tracing::warn!(
@@ -238,12 +243,39 @@ fn iq_read_loop(
false
}
};
tracing::warn!(
"IQ source read error: {}; retrying (streak={}, recovered={})",
e,
read_error_streak,
recovered
);
if is_overflow {
let now = Instant::now();
if overflow_log_window_start
.map(|ts| now.duration_since(ts) >= Duration::from_secs(3))
.unwrap_or(true)
{
let suppressed = overflow_log_suppressed;
overflow_log_window_start = Some(now);
overflow_log_suppressed = 0;
tracing::warn!(
"IQ source overflow: {}; retrying (streak={}, recovered={}, suppressed={})",
e,
read_error_streak,
recovered,
suppressed
);
} else {
overflow_log_suppressed = overflow_log_suppressed.saturating_add(1);
tracing::debug!(
"IQ source overflow suppressed: {} (streak={}, recovered={})",
e,
read_error_streak,
recovered
);
}
} else {
tracing::warn!(
"IQ source read error: {}; retrying (streak={}, recovered={})",
e,
read_error_streak,
recovered
);
}
let base_sleep_ms = if recovered {
block_duration_ms.max(20)
} else {
@@ -179,17 +179,22 @@ impl IqSource for RealIqSource {
.map_err(|e| format!("Failed to set SDR gain: {}", e))
}
fn handle_read_error(&mut self, err: &str) -> Result<bool, String> {
fn handle_read_error(&mut self, err: &str, streak: u32) -> Result<bool, String> {
let err_lc = err.to_ascii_lowercase();
let is_overrun = err_lc.contains("overflow") || err_lc.contains("overrun");
if !is_overrun {
return Ok(false);
}
tracing::warn!("SoapySDR RX overflow detected; restarting RX stream");
self.stream
.deactivate(None)
.map_err(|e| format!("Failed to deactivate RX stream after overflow: {}", e))?;
// Overflow is often transient; avoid immediate stream restart churn.
// Only restart after several consecutive read failures.
if streak < 3 {
return Ok(true);
}
tracing::warn!(
"SoapySDR RX overflow persists (streak={}); restarting RX stream",
streak
);
let _ = self.stream.deactivate(None);
std::thread::sleep(std::time::Duration::from_millis(25));
self.stream
.activate(None)