[fix](trx): speed up web load and harden FT817/Soapy recovery
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user