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 a113247..b2fed75 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 @@ -7,11 +7,13 @@ - + + + + - - - + +
@@ -638,5 +640,7 @@ + + 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 1b30afe..13f1917 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 @@ -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)) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs index 1622628..c1d08e2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs @@ -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")) diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 84f13c2..b3d847d 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -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, state: &mut RigState) Ok(()) } +async fn refresh_after_power_on( + rig: &mut Box, + 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, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs index a3be59f..98237c8 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp.rs @@ -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 { + fn handle_read_error(&mut self, _err: &str, _streak: u32) -> Result { 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 = 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 { diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs index 3fc123d..b12945e 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs @@ -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 { + fn handle_read_error(&mut self, err: &str, streak: u32) -> Result { 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)