[refactor](trx-rs): resolve all P1/P2 improvement areas
P1 (High Priority): - Fix LIFO command batching in rig_task.rs (batch.pop→batch.remove(0)) - Add ±25% jitter to ExponentialBackoff to prevent thundering herd - Add 10,000-entry capacity bounds to decoder history queues - Add rig task crash detection with Error state broadcast - Decompose FrontendRuntimeContext 50-field god-struct into 9 sub-structs (AudioContext, DecodeHistoryContext, HttpAuthConfig, HttpUiConfig, RigRoutingContext, OwnerInfo, VChanContext, SpectrumContext, PerRigAudioContext) - Migrate std::sync::RwLock to tokio::sync::RwLock in background_decode.rs - Extract find_input_device/find_output_device helpers from audio pipeline P2 (Medium Priority): - Introduce SoapySdrConfig builder struct (replaces 20+ positional params) - Add define_command_mappings! macro for ClientCommand↔RigCommand mapping - Replace silent lock poison recovery with lock_or_recover() warning logger - Make timeouts configurable via RigTaskConfig/ListenerConfig and TOML - Extract shared config types to trx-app/src/shared_config.rs Documentation updated in CLAUDE.md, Architecture.md, Improvement-Areas.md. https://claude.ai/code/session_01P9G7QCWfiYbPVJ7cgiXznf Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use trx_app::{ConfigError, ConfigFile};
|
||||
use trx_app::{validate_log_level, validate_tokens, ConfigError, ConfigFile};
|
||||
|
||||
/// Top-level client configuration structure.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
@@ -677,28 +677,6 @@ impl ClientConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_log_level(level: Option<&str>) -> Result<(), String> {
|
||||
if let Some(level) = level {
|
||||
match level {
|
||||
"trace" | "debug" | "info" | "warn" | "error" => {}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"[general].log_level '{}' is invalid (expected one of: trace, debug, info, warn, error)",
|
||||
level
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_tokens(path: &str, tokens: &[String]) -> Result<(), String> {
|
||||
if tokens.iter().any(|t| t.trim().is_empty()) {
|
||||
return Err(format!("{path} must not contain empty tokens"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_http_auth(auth: &HttpAuthConfig) -> Result<(), String> {
|
||||
if !auth.enabled {
|
||||
return Ok(());
|
||||
|
||||
+53
-50
@@ -150,7 +150,7 @@ async fn async_init() -> DynResult<AppState> {
|
||||
info!("Loaded configuration from {}", path.display());
|
||||
}
|
||||
|
||||
frontend_runtime.auth_tokens = cfg
|
||||
frontend_runtime.http_auth.tokens = cfg
|
||||
.frontends
|
||||
.http_json
|
||||
.auth
|
||||
@@ -161,28 +161,28 @@ async fn async_init() -> DynResult<AppState> {
|
||||
.collect();
|
||||
|
||||
// Set HTTP frontend authentication config
|
||||
frontend_runtime.http_auth_enabled = cfg.frontends.http.auth.enabled;
|
||||
frontend_runtime.http_auth_rx_passphrase = cfg.frontends.http.auth.rx_passphrase.clone();
|
||||
frontend_runtime.http_auth_control_passphrase =
|
||||
frontend_runtime.http_auth.enabled = cfg.frontends.http.auth.enabled;
|
||||
frontend_runtime.http_auth.rx_passphrase = cfg.frontends.http.auth.rx_passphrase.clone();
|
||||
frontend_runtime.http_auth.control_passphrase =
|
||||
cfg.frontends.http.auth.control_passphrase.clone();
|
||||
frontend_runtime.http_auth_tx_access_control_enabled =
|
||||
frontend_runtime.http_auth.tx_access_control_enabled =
|
||||
cfg.frontends.http.auth.tx_access_control_enabled;
|
||||
frontend_runtime.http_auth_session_ttl_secs = cfg.frontends.http.auth.session_ttl_min * 60;
|
||||
frontend_runtime.http_auth_cookie_secure = cfg.frontends.http.auth.cookie_secure;
|
||||
frontend_runtime.http_auth_cookie_same_site = match cfg.frontends.http.auth.cookie_same_site {
|
||||
frontend_runtime.http_auth.session_ttl_secs = cfg.frontends.http.auth.session_ttl_min * 60;
|
||||
frontend_runtime.http_auth.cookie_secure = cfg.frontends.http.auth.cookie_secure;
|
||||
frontend_runtime.http_auth.cookie_same_site = match cfg.frontends.http.auth.cookie_same_site {
|
||||
config::CookieSameSite::Strict => "Strict".to_string(),
|
||||
config::CookieSameSite::Lax => "Lax".to_string(),
|
||||
config::CookieSameSite::None => "None".to_string(),
|
||||
};
|
||||
frontend_runtime.http_show_sdr_gain_control = cfg.frontends.http.show_sdr_gain_control;
|
||||
frontend_runtime.http_initial_map_zoom = cfg.frontends.http.initial_map_zoom;
|
||||
frontend_runtime.http_spectrum_coverage_margin_hz =
|
||||
frontend_runtime.http_ui.show_sdr_gain_control = cfg.frontends.http.show_sdr_gain_control;
|
||||
frontend_runtime.http_ui.initial_map_zoom = cfg.frontends.http.initial_map_zoom;
|
||||
frontend_runtime.http_ui.spectrum_coverage_margin_hz =
|
||||
cfg.frontends.http.spectrum_coverage_margin_hz;
|
||||
frontend_runtime.http_spectrum_usable_span_ratio =
|
||||
frontend_runtime.http_ui.spectrum_usable_span_ratio =
|
||||
cfg.frontends.http.spectrum_usable_span_ratio;
|
||||
frontend_runtime.http_decode_history_retention_min =
|
||||
frontend_runtime.http_ui.decode_history_retention_min =
|
||||
cfg.frontends.http.decode_history_retention_min;
|
||||
frontend_runtime.http_decode_history_retention_min_by_rig = cfg
|
||||
frontend_runtime.http_ui.decode_history_retention_min_by_rig = cfg
|
||||
.frontends
|
||||
.http
|
||||
.decode_history_retention_min_by_rig
|
||||
@@ -219,7 +219,7 @@ async fn async_init() -> DynResult<AppState> {
|
||||
.clone()
|
||||
.or_else(|| cfg.frontends.http.default_rig_name.clone())
|
||||
.or_else(|| resolved_remotes.first().map(|e| e.name.clone()));
|
||||
if let Ok(mut guard) = frontend_runtime.remote_active_rig_id.lock() {
|
||||
if let Ok(mut guard) = frontend_runtime.routing.active_rig_id.lock() {
|
||||
*guard = default_rig.clone();
|
||||
}
|
||||
|
||||
@@ -264,10 +264,10 @@ async fn async_init() -> DynResult<AppState> {
|
||||
.callsign
|
||||
.clone()
|
||||
.or_else(|| cfg.general.callsign.clone());
|
||||
frontend_runtime.owner_callsign = callsign.clone();
|
||||
frontend_runtime.owner_website_url = cfg.general.website_url.clone();
|
||||
frontend_runtime.owner_website_name = cfg.general.website_name.clone();
|
||||
frontend_runtime.ais_vessel_url_base = cfg.general.ais_vessel_url_base.clone();
|
||||
frontend_runtime.owner.callsign = callsign.clone();
|
||||
frontend_runtime.owner.website_url = cfg.general.website_url.clone();
|
||||
frontend_runtime.owner.website_name = cfg.general.website_name.clone();
|
||||
frontend_runtime.owner.ais_vessel_url_base = cfg.general.ais_vessel_url_base.clone();
|
||||
|
||||
let remote_names: Vec<&str> = resolved_remotes.iter().map(|e| e.name.as_str()).collect();
|
||||
info!(
|
||||
@@ -373,17 +373,17 @@ async fn async_init() -> DynResult<AppState> {
|
||||
let remote_cfg = RemoteClientConfig {
|
||||
addr: addr.clone(),
|
||||
token: token.clone(),
|
||||
selected_rig_id: frontend_runtime.remote_active_rig_id.clone(),
|
||||
known_rigs: frontend_runtime.remote_rigs.clone(),
|
||||
rig_states: frontend_runtime.rig_states.clone(),
|
||||
selected_rig_id: frontend_runtime.routing.active_rig_id.clone(),
|
||||
known_rigs: frontend_runtime.routing.remote_rigs.clone(),
|
||||
rig_states: frontend_runtime.routing.rig_states.clone(),
|
||||
poll_interval: Duration::from_millis(poll_interval),
|
||||
spectrum: frontend_runtime.spectrum.clone(),
|
||||
rig_spectrums: frontend_runtime.rig_spectrums.clone(),
|
||||
server_connected: frontend_runtime.server_connected.clone(),
|
||||
rig_server_connected: frontend_runtime.rig_server_connected.clone(),
|
||||
spectrum: frontend_runtime.spectrum.sender.clone(),
|
||||
rig_spectrums: frontend_runtime.spectrum.per_rig.clone(),
|
||||
server_connected: frontend_runtime.routing.server_connected.clone(),
|
||||
rig_server_connected: frontend_runtime.routing.rig_server_connected.clone(),
|
||||
rig_id_to_short_name,
|
||||
short_name_to_rig_id: Arc::new(RwLock::new(HashMap::new())),
|
||||
sat_passes: frontend_runtime.sat_passes.clone(),
|
||||
sat_passes: frontend_runtime.routing.sat_passes.clone(),
|
||||
};
|
||||
let state_tx = state_tx.clone();
|
||||
let remote_shutdown_rx = shutdown_rx.clone();
|
||||
@@ -405,7 +405,7 @@ async fn async_init() -> DynResult<AppState> {
|
||||
// channel and dispatches to the per-server channel based on rig_id_override
|
||||
// (short name).
|
||||
let route_map = Arc::new(route_map);
|
||||
let default_rig_for_router = frontend_runtime.remote_active_rig_id.clone();
|
||||
let default_rig_for_router = frontend_runtime.routing.active_rig_id.clone();
|
||||
{
|
||||
let route_map = route_map.clone();
|
||||
let mut frontend_rx = rx;
|
||||
@@ -446,24 +446,24 @@ async fn async_init() -> DynResult<AppState> {
|
||||
let (stream_info_tx, stream_info_rx) = watch::channel::<Option<AudioStreamInfo>>(None);
|
||||
let (decode_tx, _) = broadcast::channel::<DecodedMessage>(256);
|
||||
|
||||
frontend_runtime.audio_rx = Some(rx_audio_tx.clone());
|
||||
frontend_runtime.audio_tx = Some(tx_audio_tx);
|
||||
frontend_runtime.audio_info = Some(stream_info_rx);
|
||||
frontend_runtime.decode_rx = Some(decode_tx.clone());
|
||||
frontend_runtime.audio.rx = Some(rx_audio_tx.clone());
|
||||
frontend_runtime.audio.tx = Some(tx_audio_tx);
|
||||
frontend_runtime.audio.info = Some(stream_info_rx);
|
||||
frontend_runtime.audio.decode_rx = Some(decode_tx.clone());
|
||||
|
||||
// Virtual-channel audio: shared broadcaster map + command channel.
|
||||
let (vchan_cmd_tx, vchan_cmd_rx) = mpsc::channel::<trx_frontend::VChanAudioCmd>(256);
|
||||
*frontend_runtime.vchan_audio_cmd.lock().unwrap() = Some(vchan_cmd_tx);
|
||||
*frontend_runtime.vchan.audio_cmd.lock().unwrap() = Some(vchan_cmd_tx);
|
||||
|
||||
let (vchan_destroyed_tx, _) = broadcast::channel::<uuid::Uuid>(64);
|
||||
frontend_runtime.vchan_destroyed = Some(vchan_destroyed_tx.clone());
|
||||
let ais_history = frontend_runtime.ais_history.clone();
|
||||
let vdes_history = frontend_runtime.vdes_history.clone();
|
||||
let aprs_history = frontend_runtime.aprs_history.clone();
|
||||
let hf_aprs_history = frontend_runtime.hf_aprs_history.clone();
|
||||
let cw_history = frontend_runtime.cw_history.clone();
|
||||
let ft8_history = frontend_runtime.ft8_history.clone();
|
||||
let wspr_history = frontend_runtime.wspr_history.clone();
|
||||
frontend_runtime.vchan.destroyed = Some(vchan_destroyed_tx.clone());
|
||||
let ais_history = frontend_runtime.decode_history.ais.clone();
|
||||
let vdes_history = frontend_runtime.decode_history.vdes.clone();
|
||||
let aprs_history = frontend_runtime.decode_history.aprs.clone();
|
||||
let hf_aprs_history = frontend_runtime.decode_history.hf_aprs.clone();
|
||||
let cw_history = frontend_runtime.decode_history.cw.clone();
|
||||
let ft8_history = frontend_runtime.decode_history.ft8.clone();
|
||||
let wspr_history = frontend_runtime.decode_history.wspr.clone();
|
||||
let replay_history_sink: Arc<dyn Fn(DecodedMessage) + Send + Sync> = Arc::new(move |msg| {
|
||||
let now = std::time::Instant::now();
|
||||
match msg {
|
||||
@@ -527,10 +527,10 @@ async fn async_init() -> DynResult<AppState> {
|
||||
info!("Audio enabled: decode channel set");
|
||||
|
||||
let audio_shutdown_rx = shutdown_rx.clone();
|
||||
let vchan_audio_map = frontend_runtime.vchan_audio.clone();
|
||||
let rig_audio_rx_map = frontend_runtime.rig_audio_rx.clone();
|
||||
let rig_audio_info_map = frontend_runtime.rig_audio_info.clone();
|
||||
let rig_vchan_cmd_map = frontend_runtime.rig_vchan_audio_cmd.clone();
|
||||
let vchan_audio_map = frontend_runtime.vchan.audio.clone();
|
||||
let rig_audio_rx_map = frontend_runtime.rig_audio.rx.clone();
|
||||
let rig_audio_info_map = frontend_runtime.rig_audio.info.clone();
|
||||
let rig_vchan_cmd_map = frontend_runtime.vchan.rig_audio_cmd.clone();
|
||||
let default_audio_connect = if let Some(addr) = global_audio_addr {
|
||||
AudioConnectConfig::fixed(addr)
|
||||
} else {
|
||||
@@ -539,8 +539,8 @@ async fn async_init() -> DynResult<AppState> {
|
||||
pending_audio_client = Some(tokio::spawn(audio_client::run_multi_rig_audio_manager(
|
||||
default_audio_connect,
|
||||
audio_connect,
|
||||
frontend_runtime.remote_active_rig_id.clone(),
|
||||
frontend_runtime.remote_rigs.clone(),
|
||||
frontend_runtime.routing.active_rig_id.clone(),
|
||||
frontend_runtime.routing.remote_rigs.clone(),
|
||||
rx_audio_tx,
|
||||
tx_audio_rx,
|
||||
stream_info_tx,
|
||||
@@ -642,17 +642,20 @@ async fn async_init() -> DynResult<AppState> {
|
||||
task_handles.push(audio_bridge::spawn_audio_bridge(
|
||||
bridge_cfg,
|
||||
frontend_runtime_ctx
|
||||
.audio_rx
|
||||
.audio
|
||||
.rx
|
||||
.as_ref()
|
||||
.expect("audio rx must be set")
|
||||
.clone(),
|
||||
frontend_runtime_ctx
|
||||
.audio_tx
|
||||
.audio
|
||||
.tx
|
||||
.as_ref()
|
||||
.expect("audio tx must be set")
|
||||
.clone(),
|
||||
frontend_runtime_ctx
|
||||
.audio_info
|
||||
.audio
|
||||
.info
|
||||
.as_ref()
|
||||
.expect("audio info must be set")
|
||||
.clone(),
|
||||
|
||||
@@ -189,130 +189,253 @@ impl Default for FrontendRegistrationContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime context for frontend operation, containing audio channels and decode state.
|
||||
pub struct FrontendRuntimeContext {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-structs for FrontendRuntimeContext decomposition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Audio streaming channels (server ↔ browser).
|
||||
pub struct AudioContext {
|
||||
/// Audio RX broadcast channel (server → browser)
|
||||
pub audio_rx: Option<broadcast::Sender<Bytes>>,
|
||||
pub rx: Option<broadcast::Sender<Bytes>>,
|
||||
/// Audio TX channel (browser → server)
|
||||
pub audio_tx: Option<mpsc::Sender<Bytes>>,
|
||||
pub tx: Option<mpsc::Sender<Bytes>>,
|
||||
/// Audio stream info watch channel
|
||||
pub audio_info: Option<watch::Receiver<Option<AudioStreamInfo>>>,
|
||||
pub info: Option<watch::Receiver<Option<AudioStreamInfo>>>,
|
||||
/// Decode message broadcast channel
|
||||
pub decode_rx: Option<broadcast::Sender<DecodedMessage>>,
|
||||
/// Decode history entry: (record_time, rig_id, message).
|
||||
/// AIS decode history
|
||||
pub ais_history: DecodeHistory<AisMessage>,
|
||||
/// VDES decode history
|
||||
pub vdes_history: DecodeHistory<VdesMessage>,
|
||||
/// APRS decode history
|
||||
pub aprs_history: DecodeHistory<AprsPacket>,
|
||||
/// HF APRS decode history
|
||||
pub hf_aprs_history: DecodeHistory<AprsPacket>,
|
||||
/// CW decode history
|
||||
pub cw_history: DecodeHistory<CwEvent>,
|
||||
/// FT8 decode history
|
||||
pub ft8_history: DecodeHistory<Ft8Message>,
|
||||
/// FT4 decode history
|
||||
pub ft4_history: DecodeHistory<Ft8Message>,
|
||||
/// FT2 decode history
|
||||
pub ft2_history: DecodeHistory<Ft8Message>,
|
||||
/// WSPR decode history
|
||||
pub wspr_history: DecodeHistory<WsprMessage>,
|
||||
/// Authentication tokens for HTTP-JSON frontend
|
||||
pub auth_tokens: HashSet<String>,
|
||||
/// Active HTTP SSE clients (incremented on /events connect, decremented on disconnect).
|
||||
pub sse_clients: Arc<AtomicUsize>,
|
||||
/// Active rigctl TCP clients.
|
||||
pub rigctl_clients: Arc<AtomicUsize>,
|
||||
/// Active audio WebSocket streams.
|
||||
pub audio_clients: Arc<AtomicUsize>,
|
||||
/// rigctl listen endpoint, if enabled.
|
||||
pub rigctl_listen_addr: Arc<Mutex<Option<SocketAddr>>>,
|
||||
/// Guard to avoid spawning duplicate decode collectors.
|
||||
pub decode_collector_started: AtomicBool,
|
||||
/// HTTP frontend authentication configuration (enabled, passphrases, TTL, etc.)
|
||||
pub http_auth_enabled: bool,
|
||||
/// HTTP frontend auth rx passphrase
|
||||
pub http_auth_rx_passphrase: Option<String>,
|
||||
/// HTTP frontend auth control passphrase
|
||||
pub http_auth_control_passphrase: Option<String>,
|
||||
/// HTTP frontend auth tx access control enabled
|
||||
pub http_auth_tx_access_control_enabled: bool,
|
||||
/// HTTP frontend auth session TTL in seconds
|
||||
pub http_auth_session_ttl_secs: u64,
|
||||
/// HTTP frontend auth cookie secure flag
|
||||
pub http_auth_cookie_secure: bool,
|
||||
/// HTTP frontend auth cookie same-site policy
|
||||
pub http_auth_cookie_same_site: String,
|
||||
/// Whether the HTTP UI should expose the RF Gain control.
|
||||
pub http_show_sdr_gain_control: bool,
|
||||
/// Initial APRS map zoom level when receiver coordinates are available.
|
||||
pub http_initial_map_zoom: u8,
|
||||
/// Spectrum center-retune guard margin on each side of the tuned passband.
|
||||
pub http_spectrum_coverage_margin_hz: u32,
|
||||
/// Fraction of the sampled spectrum span treated as usable by the web UI.
|
||||
pub http_spectrum_usable_span_ratio: f32,
|
||||
/// Default decode history retention in minutes.
|
||||
pub http_decode_history_retention_min: u64,
|
||||
/// Per-rig decode history retention overrides in minutes.
|
||||
pub http_decode_history_retention_min_by_rig: HashMap<String, u64>,
|
||||
/// Currently selected remote rig id (used by remote client routing).
|
||||
pub remote_active_rig_id: Arc<Mutex<Option<String>>>,
|
||||
pub clients: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Default for AudioContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rx: None,
|
||||
tx: None,
|
||||
info: None,
|
||||
decode_rx: None,
|
||||
clients: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode history entries for all decoder types.
|
||||
pub struct DecodeHistoryContext {
|
||||
pub ais: DecodeHistory<AisMessage>,
|
||||
pub vdes: DecodeHistory<VdesMessage>,
|
||||
pub aprs: DecodeHistory<AprsPacket>,
|
||||
pub hf_aprs: DecodeHistory<AprsPacket>,
|
||||
pub cw: DecodeHistory<CwEvent>,
|
||||
pub ft8: DecodeHistory<Ft8Message>,
|
||||
pub ft4: DecodeHistory<Ft8Message>,
|
||||
pub ft2: DecodeHistory<Ft8Message>,
|
||||
pub wspr: DecodeHistory<WsprMessage>,
|
||||
}
|
||||
|
||||
impl Default for DecodeHistoryContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ais: Arc::new(Mutex::new(VecDeque::new())),
|
||||
vdes: Arc::new(Mutex::new(VecDeque::new())),
|
||||
aprs: Arc::new(Mutex::new(VecDeque::new())),
|
||||
hf_aprs: Arc::new(Mutex::new(VecDeque::new())),
|
||||
cw: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft8: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft4: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft2: Arc::new(Mutex::new(VecDeque::new())),
|
||||
wspr: Arc::new(Mutex::new(VecDeque::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP authentication configuration.
|
||||
pub struct HttpAuthConfig {
|
||||
pub enabled: bool,
|
||||
pub rx_passphrase: Option<String>,
|
||||
pub control_passphrase: Option<String>,
|
||||
pub tx_access_control_enabled: bool,
|
||||
pub session_ttl_secs: u64,
|
||||
pub cookie_secure: bool,
|
||||
pub cookie_same_site: String,
|
||||
/// Authentication tokens for HTTP-JSON frontend.
|
||||
pub tokens: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for HttpAuthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
rx_passphrase: None,
|
||||
control_passphrase: None,
|
||||
tx_access_control_enabled: true,
|
||||
session_ttl_secs: 480 * 60,
|
||||
cookie_secure: false,
|
||||
cookie_same_site: "Lax".to_string(),
|
||||
tokens: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP UI display configuration.
|
||||
pub struct HttpUiConfig {
|
||||
pub show_sdr_gain_control: bool,
|
||||
pub initial_map_zoom: u8,
|
||||
pub spectrum_coverage_margin_hz: u32,
|
||||
pub spectrum_usable_span_ratio: f32,
|
||||
pub decode_history_retention_min: u64,
|
||||
pub decode_history_retention_min_by_rig: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
impl Default for HttpUiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_sdr_gain_control: true,
|
||||
initial_map_zoom: 10,
|
||||
spectrum_coverage_margin_hz: 50_000,
|
||||
spectrum_usable_span_ratio: 0.92,
|
||||
decode_history_retention_min: 24 * 60,
|
||||
decode_history_retention_min_by_rig: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remote rig routing and state management.
|
||||
pub struct RigRoutingContext {
|
||||
/// Currently selected remote rig id.
|
||||
pub active_rig_id: Arc<Mutex<Option<String>>>,
|
||||
/// Cached remote rig list from GetRigs polling.
|
||||
pub remote_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
|
||||
/// Cached satellite pass predictions from the server (GetSatPasses).
|
||||
pub sat_passes: Arc<RwLock<Option<trx_core::geo::PassPredictionResult>>>,
|
||||
/// Per-rig state watch channels, keyed by rig_id.
|
||||
/// Populated by the remote client poll loop so each SSE session can
|
||||
/// subscribe to a specific rig's state independently.
|
||||
pub rig_states: Arc<RwLock<HashMap<String, watch::Sender<RigState>>>>,
|
||||
/// Owner callsign from trx-client config/CLI for frontend display.
|
||||
pub owner_callsign: Option<String>,
|
||||
/// Optional website URL for the web UI header title link.
|
||||
pub owner_website_url: Option<String>,
|
||||
/// Optional website name for the web UI header title label.
|
||||
pub owner_website_name: Option<String>,
|
||||
/// Optional base URL used to link AIS vessel names as `<base><mmsi>`.
|
||||
pub ais_vessel_url_base: Option<String>,
|
||||
/// Spectrum sender; SSE clients subscribe via `spectrum.subscribe()`.
|
||||
pub spectrum: Arc<watch::Sender<SharedSpectrum>>,
|
||||
/// Per-rig spectrum watch channels, keyed by rig_id.
|
||||
/// Populated by the remote client spectrum polling task so each SSE
|
||||
/// session can subscribe to a specific rig's spectrum independently.
|
||||
pub rig_spectrums: Arc<RwLock<HashMap<String, watch::Sender<SharedSpectrum>>>>,
|
||||
/// Per-rig RX audio broadcast senders, keyed by rig_id.
|
||||
/// Each rig's audio client task publishes Opus frames here.
|
||||
pub rig_audio_rx: Arc<RwLock<HashMap<String, broadcast::Sender<Bytes>>>>,
|
||||
/// Per-rig audio stream info watch channels, keyed by rig_id.
|
||||
pub rig_audio_info: Arc<RwLock<HashMap<String, watch::Sender<Option<AudioStreamInfo>>>>>,
|
||||
/// Per-rig virtual-channel command senders, keyed by rig_id.
|
||||
pub rig_vchan_audio_cmd: Arc<RwLock<HashMap<String, mpsc::Sender<VChanAudioCmd>>>>,
|
||||
/// Per-virtual-channel Opus audio senders.
|
||||
/// Key: server-side virtual channel UUID.
|
||||
/// Value: `broadcast::Sender<Bytes>` that receives per-channel Opus packets
|
||||
/// forwarded by the audio-client task from `AUDIO_MSG_RX_FRAME_CH` frames.
|
||||
pub vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>,
|
||||
/// Channel to send `VChanAudioCmd` to the audio-client task, which in turn
|
||||
/// forwards `VCHAN_SUB` / `VCHAN_UNSUB` frames over the audio TCP connection.
|
||||
/// `None` when no audio connection is active.
|
||||
pub vchan_audio_cmd: Arc<Mutex<Option<mpsc::Sender<VChanAudioCmd>>>>,
|
||||
/// Broadcast sender that fires whenever the server destroys a virtual
|
||||
/// channel (e.g. out-of-bandwidth after center-frequency retune).
|
||||
/// The HTTP frontend subscribes to clean up `ClientChannelManager`.
|
||||
pub vchan_destroyed: Option<broadcast::Sender<Uuid>>,
|
||||
/// Whether the remote client currently has an active TCP connection to
|
||||
/// trx-server. Set to `true` on successful connect, `false` on drop.
|
||||
/// Whether the remote client currently has an active TCP connection.
|
||||
pub server_connected: Arc<AtomicBool>,
|
||||
/// Per-rig server connection state, keyed by short name (or rig_id in legacy mode).
|
||||
/// `true` while the rig's trx-server connection is active.
|
||||
/// Allows the UI to freeze only the rig that lost its connection.
|
||||
/// Per-rig server connection state.
|
||||
pub rig_server_connected: Arc<RwLock<HashMap<String, bool>>>,
|
||||
}
|
||||
|
||||
impl Default for RigRoutingContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active_rig_id: Arc::new(Mutex::new(None)),
|
||||
remote_rigs: Arc::new(Mutex::new(Vec::new())),
|
||||
sat_passes: Arc::new(RwLock::new(None)),
|
||||
rig_states: Arc::new(RwLock::new(HashMap::new())),
|
||||
server_connected: Arc::new(AtomicBool::new(false)),
|
||||
rig_server_connected: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owner/station metadata for frontend display.
|
||||
#[derive(Default)]
|
||||
pub struct OwnerInfo {
|
||||
pub callsign: Option<String>,
|
||||
pub website_url: Option<String>,
|
||||
pub website_name: Option<String>,
|
||||
pub ais_vessel_url_base: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
/// Virtual channel audio management.
|
||||
pub struct VChanContext {
|
||||
/// Per-virtual-channel Opus audio senders.
|
||||
pub audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>,
|
||||
/// Channel to send `VChanAudioCmd` to the audio-client task.
|
||||
pub audio_cmd: Arc<Mutex<Option<mpsc::Sender<VChanAudioCmd>>>>,
|
||||
/// Broadcast sender that fires when the server destroys a virtual channel.
|
||||
pub destroyed: Option<broadcast::Sender<Uuid>>,
|
||||
/// Per-rig virtual-channel command senders.
|
||||
pub rig_audio_cmd: Arc<RwLock<HashMap<String, mpsc::Sender<VChanAudioCmd>>>>,
|
||||
}
|
||||
|
||||
impl Default for VChanContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
audio: Arc::new(RwLock::new(HashMap::new())),
|
||||
audio_cmd: Arc::new(Mutex::new(None)),
|
||||
destroyed: None,
|
||||
rig_audio_cmd: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spectrum data management.
|
||||
pub struct SpectrumContext {
|
||||
/// Spectrum sender; SSE clients subscribe via `sender.subscribe()`.
|
||||
pub sender: Arc<watch::Sender<SharedSpectrum>>,
|
||||
/// Per-rig spectrum watch channels, keyed by rig_id.
|
||||
pub per_rig: Arc<RwLock<HashMap<String, watch::Sender<SharedSpectrum>>>>,
|
||||
}
|
||||
|
||||
impl Default for SpectrumContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sender: {
|
||||
let (tx, _rx) = watch::channel(SharedSpectrum::default());
|
||||
Arc::new(tx)
|
||||
},
|
||||
per_rig: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-rig audio channels for multi-rig setups.
|
||||
pub struct PerRigAudioContext {
|
||||
/// Per-rig RX audio broadcast senders.
|
||||
pub rx: Arc<RwLock<HashMap<String, broadcast::Sender<Bytes>>>>,
|
||||
/// Per-rig audio stream info watch channels.
|
||||
pub info: Arc<RwLock<HashMap<String, watch::Sender<Option<AudioStreamInfo>>>>>,
|
||||
}
|
||||
|
||||
impl Default for PerRigAudioContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rx: Arc::new(RwLock::new(HashMap::new())),
|
||||
info: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime context for frontend operation.
|
||||
///
|
||||
/// Decomposed into coherent sub-structs to improve readability and allow
|
||||
/// frontends to access only the context groups they need.
|
||||
pub struct FrontendRuntimeContext {
|
||||
/// Audio streaming channels.
|
||||
pub audio: AudioContext,
|
||||
/// Decode history for all decoder types.
|
||||
pub decode_history: DecodeHistoryContext,
|
||||
/// HTTP authentication configuration.
|
||||
pub http_auth: HttpAuthConfig,
|
||||
/// HTTP UI display configuration.
|
||||
pub http_ui: HttpUiConfig,
|
||||
/// Remote rig routing and state.
|
||||
pub routing: RigRoutingContext,
|
||||
/// Owner/station metadata.
|
||||
pub owner: OwnerInfo,
|
||||
/// Virtual channel management.
|
||||
pub vchan: VChanContext,
|
||||
/// Spectrum data.
|
||||
pub spectrum: SpectrumContext,
|
||||
/// Per-rig audio channels.
|
||||
pub rig_audio: PerRigAudioContext,
|
||||
/// Active HTTP SSE clients.
|
||||
pub sse_clients: Arc<AtomicUsize>,
|
||||
/// Active rigctl TCP clients.
|
||||
pub rigctl_clients: Arc<AtomicUsize>,
|
||||
/// rigctl listen endpoint, if enabled.
|
||||
pub rigctl_listen_addr: Arc<Mutex<Option<SocketAddr>>>,
|
||||
/// Guard to avoid spawning duplicate decode collectors.
|
||||
pub decode_collector_started: AtomicBool,
|
||||
}
|
||||
|
||||
impl FrontendRuntimeContext {
|
||||
/// Get a watch receiver for a specific rig's state.
|
||||
pub fn rig_state_rx(&self, rig_id: &str) -> Option<watch::Receiver<RigState>> {
|
||||
self.rig_states
|
||||
self.routing
|
||||
.rig_states
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|map| map.get(rig_id).map(|tx| tx.subscribe()))
|
||||
@@ -321,13 +444,13 @@ impl FrontendRuntimeContext {
|
||||
/// Get a watch receiver for a specific rig's spectrum.
|
||||
/// Lazily inserts a new channel if the rig_id is not yet present.
|
||||
pub fn rig_spectrum_rx(&self, rig_id: &str) -> watch::Receiver<SharedSpectrum> {
|
||||
if let Ok(map) = self.rig_spectrums.read() {
|
||||
if let Ok(map) = self.spectrum.per_rig.read() {
|
||||
if let Some(tx) = map.get(rig_id) {
|
||||
return tx.subscribe();
|
||||
}
|
||||
}
|
||||
// Insert on miss.
|
||||
if let Ok(mut map) = self.rig_spectrums.write() {
|
||||
if let Ok(mut map) = self.spectrum.per_rig.write() {
|
||||
map.entry(rig_id.to_string())
|
||||
.or_insert_with(|| watch::channel(SharedSpectrum::default()).0)
|
||||
.subscribe()
|
||||
@@ -339,7 +462,8 @@ impl FrontendRuntimeContext {
|
||||
|
||||
/// Subscribe to a specific rig's RX audio broadcast.
|
||||
pub fn rig_audio_subscribe(&self, rig_id: &str) -> Option<broadcast::Receiver<Bytes>> {
|
||||
self.rig_audio_rx
|
||||
self.rig_audio
|
||||
.rx
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|map| map.get(rig_id).map(|tx| tx.subscribe()))
|
||||
@@ -350,7 +474,8 @@ impl FrontendRuntimeContext {
|
||||
&self,
|
||||
rig_id: &str,
|
||||
) -> Option<watch::Receiver<Option<AudioStreamInfo>>> {
|
||||
self.rig_audio_info
|
||||
self.rig_audio
|
||||
.info
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|map| map.get(rig_id).map(|tx| tx.subscribe()))
|
||||
@@ -359,59 +484,19 @@ impl FrontendRuntimeContext {
|
||||
/// Create a new empty runtime context.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
audio_rx: None,
|
||||
audio_tx: None,
|
||||
audio_info: None,
|
||||
decode_rx: None,
|
||||
ais_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
vdes_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
aprs_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
hf_aprs_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
cw_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft4_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
ft2_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||
auth_tokens: HashSet::new(),
|
||||
audio: AudioContext::default(),
|
||||
decode_history: DecodeHistoryContext::default(),
|
||||
http_auth: HttpAuthConfig::default(),
|
||||
http_ui: HttpUiConfig::default(),
|
||||
routing: RigRoutingContext::default(),
|
||||
owner: OwnerInfo::default(),
|
||||
vchan: VChanContext::default(),
|
||||
spectrum: SpectrumContext::default(),
|
||||
rig_audio: PerRigAudioContext::default(),
|
||||
sse_clients: Arc::new(AtomicUsize::new(0)),
|
||||
rigctl_clients: Arc::new(AtomicUsize::new(0)),
|
||||
audio_clients: Arc::new(AtomicUsize::new(0)),
|
||||
rigctl_listen_addr: Arc::new(Mutex::new(None)),
|
||||
decode_collector_started: AtomicBool::new(false),
|
||||
http_auth_enabled: false,
|
||||
http_auth_rx_passphrase: None,
|
||||
http_auth_control_passphrase: None,
|
||||
http_auth_tx_access_control_enabled: true,
|
||||
http_auth_session_ttl_secs: 480 * 60,
|
||||
http_auth_cookie_secure: false,
|
||||
http_auth_cookie_same_site: "Lax".to_string(),
|
||||
http_show_sdr_gain_control: true,
|
||||
http_initial_map_zoom: 10,
|
||||
http_spectrum_coverage_margin_hz: 50_000,
|
||||
http_spectrum_usable_span_ratio: 0.92,
|
||||
http_decode_history_retention_min: 24 * 60,
|
||||
http_decode_history_retention_min_by_rig: HashMap::new(),
|
||||
remote_active_rig_id: Arc::new(Mutex::new(None)),
|
||||
remote_rigs: Arc::new(Mutex::new(Vec::new())),
|
||||
sat_passes: Arc::new(RwLock::new(None)),
|
||||
rig_states: Arc::new(RwLock::new(HashMap::new())),
|
||||
owner_callsign: None,
|
||||
owner_website_url: None,
|
||||
owner_website_name: None,
|
||||
ais_vessel_url_base: None,
|
||||
spectrum: {
|
||||
let (tx, _rx) = watch::channel(SharedSpectrum::default());
|
||||
Arc::new(tx)
|
||||
},
|
||||
rig_spectrums: Arc::new(RwLock::new(HashMap::new())),
|
||||
rig_audio_rx: Arc::new(RwLock::new(HashMap::new())),
|
||||
rig_audio_info: Arc::new(RwLock::new(HashMap::new())),
|
||||
rig_vchan_audio_cmd: Arc::new(RwLock::new(HashMap::new())),
|
||||
vchan_audio: Arc::new(RwLock::new(HashMap::new())),
|
||||
vchan_audio_cmd: Arc::new(Mutex::new(None)),
|
||||
vchan_destroyed: None,
|
||||
server_connected: Arc::new(AtomicBool::new(false)),
|
||||
rig_server_connected: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ async fn handle_client(
|
||||
}
|
||||
|
||||
if let Some(rig_id) = envelope.rig_id.as_ref() {
|
||||
if let Ok(mut active) = context.remote_active_rig_id.lock() {
|
||||
if let Ok(mut active) = context.routing.active_rig_id.lock() {
|
||||
*active = Some(rig_id.clone());
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,8 @@ async fn handle_client(
|
||||
}
|
||||
|
||||
let active_rig_id = context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|v| v.clone());
|
||||
@@ -245,6 +246,7 @@ async fn handle_client(
|
||||
|
||||
fn snapshot_remote_rigs(context: &FrontendRuntimeContext) -> Vec<RigEntry> {
|
||||
context
|
||||
.routing
|
||||
.remote_rigs
|
||||
.lock()
|
||||
.ok()
|
||||
@@ -333,7 +335,7 @@ async fn send_response(
|
||||
}
|
||||
|
||||
fn authorize(token: &Option<String>, context: &FrontendRuntimeContext) -> Result<(), String> {
|
||||
let validator = SimpleTokenValidator::new(context.auth_tokens.clone());
|
||||
let validator = SimpleTokenValidator::new(context.http_auth.tokens.clone());
|
||||
validator.validate(token)
|
||||
}
|
||||
|
||||
@@ -436,7 +438,7 @@ mod tests {
|
||||
let addr = loopback_addr();
|
||||
let (rig_tx, _rig_rx) = mpsc::channel::<RigRequest>(8);
|
||||
let mut runtime = FrontendRuntimeContext::new();
|
||||
runtime.auth_tokens = HashSet::from(["secret".to_string()]);
|
||||
runtime.http_auth.tokens = HashSet::from(["secret".to_string()]);
|
||||
let ctx = Arc::new(runtime);
|
||||
|
||||
let handle = tokio::spawn(serve(addr, rig_tx, ctx));
|
||||
|
||||
@@ -211,16 +211,17 @@ fn frontend_meta_from_context(
|
||||
let server_connected = rig_id
|
||||
.and_then(|rid| {
|
||||
context
|
||||
.routing
|
||||
.rig_server_connected
|
||||
.read()
|
||||
.ok()
|
||||
.and_then(|m| m.get(rid).copied())
|
||||
})
|
||||
.unwrap_or_else(|| context.server_connected.load(Ordering::Relaxed));
|
||||
.unwrap_or_else(|| context.routing.server_connected.load(Ordering::Relaxed));
|
||||
FrontendMeta {
|
||||
http_clients,
|
||||
rigctl_clients: context.rigctl_clients.load(Ordering::Relaxed),
|
||||
audio_clients: context.audio_clients.load(Ordering::Relaxed),
|
||||
audio_clients: context.audio.clients.load(Ordering::Relaxed),
|
||||
rigctl_addr: rigctl_addr_from_context(context),
|
||||
active_remote: active_rig_id_from_context(context),
|
||||
remotes: rig_ids_from_context(context),
|
||||
@@ -248,7 +249,8 @@ fn rigctl_addr_from_context(context: &FrontendRuntimeContext) -> Option<String>
|
||||
|
||||
fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|v| v.clone())
|
||||
@@ -256,6 +258,7 @@ fn active_rig_id_from_context(context: &FrontendRuntimeContext) -> Option<String
|
||||
|
||||
fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec<String> {
|
||||
context
|
||||
.routing
|
||||
.remote_rigs
|
||||
.lock()
|
||||
.ok()
|
||||
@@ -264,41 +267,42 @@ fn rig_ids_from_context(context: &FrontendRuntimeContext) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn owner_callsign_from_context(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context.owner_callsign.clone()
|
||||
context.owner.callsign.clone()
|
||||
}
|
||||
|
||||
fn owner_website_url_from_context(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context.owner_website_url.clone()
|
||||
context.owner.website_url.clone()
|
||||
}
|
||||
|
||||
fn owner_website_name_from_context(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context.owner_website_name.clone()
|
||||
context.owner.website_name.clone()
|
||||
}
|
||||
|
||||
fn ais_vessel_url_base_from_context(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context.ais_vessel_url_base.clone()
|
||||
context.owner.ais_vessel_url_base.clone()
|
||||
}
|
||||
|
||||
fn show_sdr_gain_control_from_context(context: &FrontendRuntimeContext) -> bool {
|
||||
context.http_show_sdr_gain_control
|
||||
context.http_ui.show_sdr_gain_control
|
||||
}
|
||||
|
||||
fn initial_map_zoom_from_context(context: &FrontendRuntimeContext) -> u8 {
|
||||
context.http_initial_map_zoom
|
||||
context.http_ui.initial_map_zoom
|
||||
}
|
||||
|
||||
fn spectrum_coverage_margin_hz_from_context(context: &FrontendRuntimeContext) -> u32 {
|
||||
context.http_spectrum_coverage_margin_hz
|
||||
context.http_ui.spectrum_coverage_margin_hz
|
||||
}
|
||||
|
||||
fn spectrum_usable_span_ratio_from_context(context: &FrontendRuntimeContext) -> f32 {
|
||||
context.http_spectrum_usable_span_ratio
|
||||
context.http_ui.spectrum_usable_span_ratio
|
||||
}
|
||||
|
||||
fn decode_history_retention_min_from_context(context: &FrontendRuntimeContext) -> u64 {
|
||||
let default_minutes = context.http_decode_history_retention_min.max(1);
|
||||
let default_minutes = context.http_ui.decode_history_retention_min.max(1);
|
||||
let Some(active_rig_id) = context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|v| v.clone())
|
||||
@@ -306,7 +310,8 @@ fn decode_history_retention_min_from_context(context: &FrontendRuntimeContext) -
|
||||
return default_minutes;
|
||||
};
|
||||
context
|
||||
.http_decode_history_retention_min_by_rig
|
||||
.http_ui
|
||||
.decode_history_retention_min_by_rig
|
||||
.get(&active_rig_id)
|
||||
.copied()
|
||||
.filter(|minutes| *minutes > 0)
|
||||
@@ -343,7 +348,8 @@ pub async fn events(
|
||||
// rig it has selected without mutating global state.
|
||||
let active_rig_id = query.remote.clone().filter(|s| !s.is_empty()).or_else(|| {
|
||||
context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|g| g.clone())
|
||||
@@ -419,7 +425,8 @@ pub async fn events(
|
||||
state.snapshot().and_then(|v| {
|
||||
let rig_id_opt = session_rig_mgr.get_rig(session_id).or_else(|| {
|
||||
context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|g| g.clone())
|
||||
@@ -687,7 +694,7 @@ pub async fn decode_history(
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
query: web::Query<RemoteQuery>,
|
||||
) -> impl Responder {
|
||||
if context.decode_rx.is_none() {
|
||||
if context.audio.decode_rx.is_none() {
|
||||
return HttpResponse::NotFound().body("decode not enabled");
|
||||
}
|
||||
let rig_filter = query.remote.as_deref().filter(|s| !s.is_empty());
|
||||
@@ -807,7 +814,7 @@ pub async fn spectrum(
|
||||
let rx = if let Some(ref remote) = query.remote {
|
||||
context.rig_spectrum_rx(remote)
|
||||
} else {
|
||||
context.spectrum.subscribe()
|
||||
context.spectrum.sender.subscribe()
|
||||
};
|
||||
let mut last_rds_json: Option<String> = None;
|
||||
let mut last_vchan_rds_json: Option<String> = None;
|
||||
@@ -1351,7 +1358,7 @@ struct SatPassesResponse {
|
||||
/// are not yet available.
|
||||
#[get("/sat_passes")]
|
||||
pub async fn sat_passes(context: web::Data<Arc<FrontendRuntimeContext>>) -> impl Responder {
|
||||
let cached = context.sat_passes.read().ok().and_then(|g| g.clone());
|
||||
let cached = context.routing.sat_passes.read().ok().and_then(|g| g.clone());
|
||||
match cached {
|
||||
Some(result) => {
|
||||
let error = match result.tle_source {
|
||||
@@ -1901,6 +1908,7 @@ struct RigListResponse {
|
||||
fn build_rig_list_payload(context: &FrontendRuntimeContext) -> RigListResponse {
|
||||
let active_remote = active_rig_id_from_context(context);
|
||||
let rigs = context
|
||||
.routing
|
||||
.remote_rigs
|
||||
.lock()
|
||||
.ok()
|
||||
@@ -1952,6 +1960,7 @@ pub async fn select_rig(
|
||||
}
|
||||
|
||||
let known = context
|
||||
.routing
|
||||
.remote_rigs
|
||||
.lock()
|
||||
.ok()
|
||||
|
||||
@@ -36,15 +36,17 @@ fn current_timestamp_ms() -> i64 {
|
||||
}
|
||||
|
||||
fn decode_history_retention(context: &FrontendRuntimeContext) -> Duration {
|
||||
let default_minutes = context.http_decode_history_retention_min.max(1);
|
||||
let default_minutes = context.http_ui.decode_history_retention_min.max(1);
|
||||
let minutes = context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|v| v.clone())
|
||||
.and_then(|rig_id| {
|
||||
context
|
||||
.http_decode_history_retention_min_by_rig
|
||||
.http_ui
|
||||
.decode_history_retention_min_by_rig
|
||||
.get(&rig_id)
|
||||
.copied()
|
||||
})
|
||||
@@ -111,7 +113,8 @@ fn prune_vdes_history(
|
||||
|
||||
fn active_rig_id(context: &FrontendRuntimeContext) -> Option<String> {
|
||||
context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|g| g.clone())
|
||||
@@ -123,7 +126,7 @@ fn record_ais(context: &FrontendRuntimeContext, mut msg: AisMessage) {
|
||||
}
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.ais_history
|
||||
.decode_history.ais
|
||||
.lock()
|
||||
.expect("ais history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -136,7 +139,7 @@ fn record_vdes(context: &FrontendRuntimeContext, mut msg: VdesMessage) {
|
||||
}
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.vdes_history
|
||||
.decode_history.vdes
|
||||
.lock()
|
||||
.expect("vdes history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -214,7 +217,7 @@ fn record_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
|
||||
}
|
||||
let rig_id = pkt.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.aprs_history
|
||||
.decode_history.aprs
|
||||
.lock()
|
||||
.expect("aprs history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, pkt));
|
||||
@@ -227,7 +230,7 @@ fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
|
||||
}
|
||||
let rig_id = pkt.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.hf_aprs_history
|
||||
.decode_history.hf_aprs
|
||||
.lock()
|
||||
.expect("hf_aprs history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, pkt));
|
||||
@@ -237,7 +240,7 @@ fn record_hf_aprs(context: &FrontendRuntimeContext, mut pkt: AprsPacket) {
|
||||
fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
|
||||
let rig_id = event.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.cw_history
|
||||
.decode_history.cw
|
||||
.lock()
|
||||
.expect("cw history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, event));
|
||||
@@ -247,7 +250,7 @@ fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) {
|
||||
fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.ft8_history
|
||||
.decode_history.ft8
|
||||
.lock()
|
||||
.expect("ft8 history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -257,7 +260,7 @@ fn record_ft8(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.decode_history.ft4
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -267,7 +270,7 @@ fn record_ft4(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
fn record_ft2(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.ft2_history
|
||||
.decode_history.ft2
|
||||
.lock()
|
||||
.expect("ft2 history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -277,7 +280,7 @@ fn record_ft2(context: &FrontendRuntimeContext, msg: Ft8Message) {
|
||||
fn record_wspr(context: &FrontendRuntimeContext, msg: WsprMessage) {
|
||||
let rig_id = msg.rig_id.clone().or_else(|| active_rig_id(context));
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
.decode_history.wspr
|
||||
.lock()
|
||||
.expect("wspr history mutex poisoned");
|
||||
history.push_back((Instant::now(), rig_id, msg));
|
||||
@@ -298,7 +301,7 @@ pub fn snapshot_aprs_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<AprsPacket> {
|
||||
let mut history = context
|
||||
.aprs_history
|
||||
.decode_history.aprs
|
||||
.lock()
|
||||
.expect("aprs history mutex poisoned");
|
||||
prune_aprs_history(context, &mut history);
|
||||
@@ -314,7 +317,7 @@ pub fn snapshot_hf_aprs_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<AprsPacket> {
|
||||
let mut history = context
|
||||
.hf_aprs_history
|
||||
.decode_history.hf_aprs
|
||||
.lock()
|
||||
.expect("hf_aprs history mutex poisoned");
|
||||
prune_hf_aprs_history(context, &mut history);
|
||||
@@ -337,7 +340,7 @@ pub fn snapshot_ais_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<AisMessage> {
|
||||
let mut history = context
|
||||
.ais_history
|
||||
.decode_history.ais
|
||||
.lock()
|
||||
.expect("ais history mutex poisoned");
|
||||
prune_ais_history(context, &mut history);
|
||||
@@ -359,7 +362,7 @@ pub fn snapshot_vdes_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<VdesMessage> {
|
||||
let mut history = context
|
||||
.vdes_history
|
||||
.decode_history.vdes
|
||||
.lock()
|
||||
.expect("vdes history mutex poisoned");
|
||||
prune_vdes_history(context, &mut history);
|
||||
@@ -375,7 +378,7 @@ pub fn snapshot_cw_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<CwEvent> {
|
||||
let mut history = context
|
||||
.cw_history
|
||||
.decode_history.cw
|
||||
.lock()
|
||||
.expect("cw history mutex poisoned");
|
||||
prune_cw_history(context, &mut history);
|
||||
@@ -391,7 +394,7 @@ pub fn snapshot_ft8_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<Ft8Message> {
|
||||
let mut history = context
|
||||
.ft8_history
|
||||
.decode_history.ft8
|
||||
.lock()
|
||||
.expect("ft8 history mutex poisoned");
|
||||
prune_ft8_history(context, &mut history);
|
||||
@@ -407,7 +410,7 @@ pub fn snapshot_ft4_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<Ft8Message> {
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.decode_history.ft4
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
prune_ft4_history(context, &mut history);
|
||||
@@ -423,7 +426,7 @@ pub fn snapshot_ft2_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<Ft8Message> {
|
||||
let mut history = context
|
||||
.ft2_history
|
||||
.decode_history.ft2
|
||||
.lock()
|
||||
.expect("ft2 history mutex poisoned");
|
||||
prune_ft2_history(context, &mut history);
|
||||
@@ -439,7 +442,7 @@ pub fn snapshot_wspr_history(
|
||||
rig_filter: Option<&str>,
|
||||
) -> Vec<WsprMessage> {
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
.decode_history.wspr
|
||||
.lock()
|
||||
.expect("wspr history mutex poisoned");
|
||||
prune_wspr_history(context, &mut history);
|
||||
@@ -452,7 +455,7 @@ pub fn snapshot_wspr_history(
|
||||
|
||||
pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.aprs_history
|
||||
.decode_history.aprs
|
||||
.lock()
|
||||
.expect("aprs history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -460,7 +463,7 @@ pub fn clear_aprs_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_hf_aprs_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.hf_aprs_history
|
||||
.decode_history.hf_aprs
|
||||
.lock()
|
||||
.expect("hf_aprs history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -468,7 +471,7 @@ pub fn clear_hf_aprs_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_ais_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.ais_history
|
||||
.decode_history.ais
|
||||
.lock()
|
||||
.expect("ais history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -476,7 +479,7 @@ pub fn clear_ais_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_vdes_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.vdes_history
|
||||
.decode_history.vdes
|
||||
.lock()
|
||||
.expect("vdes history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -484,7 +487,7 @@ pub fn clear_vdes_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_cw_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.cw_history
|
||||
.decode_history.cw
|
||||
.lock()
|
||||
.expect("cw history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -492,7 +495,7 @@ pub fn clear_cw_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.ft8_history
|
||||
.decode_history.ft8
|
||||
.lock()
|
||||
.expect("ft8 history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -500,7 +503,7 @@ pub fn clear_ft8_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.ft4_history
|
||||
.decode_history.ft4
|
||||
.lock()
|
||||
.expect("ft4 history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -508,7 +511,7 @@ pub fn clear_ft4_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_ft2_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.ft2_history
|
||||
.decode_history.ft2
|
||||
.lock()
|
||||
.expect("ft2 history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -516,7 +519,7 @@ pub fn clear_ft2_history(context: &FrontendRuntimeContext) {
|
||||
|
||||
pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
|
||||
let mut history = context
|
||||
.wspr_history
|
||||
.decode_history.wspr
|
||||
.lock()
|
||||
.expect("wspr history mutex poisoned");
|
||||
history.clear();
|
||||
@@ -525,7 +528,7 @@ pub fn clear_wspr_history(context: &FrontendRuntimeContext) {
|
||||
pub fn subscribe_decode(
|
||||
context: &FrontendRuntimeContext,
|
||||
) -> Option<broadcast::Receiver<DecodedMessage>> {
|
||||
context.decode_rx.as_ref().map(|tx| tx.subscribe())
|
||||
context.audio.decode_rx.as_ref().map(|tx| tx.subscribe())
|
||||
}
|
||||
|
||||
pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
|
||||
@@ -536,7 +539,7 @@ pub fn start_decode_history_collector(context: Arc<FrontendRuntimeContext>) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(tx) = context.decode_rx.as_ref().cloned() else {
|
||||
let Some(tx) = context.audio.decode_rx.as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -576,7 +579,7 @@ pub async fn audio_ws(
|
||||
query: web::Query<AudioQuery>,
|
||||
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let Some(tx_sender) = context.audio_tx.as_ref().cloned() else {
|
||||
let Some(tx_sender) = context.audio.tx.as_ref().cloned() else {
|
||||
return Ok(HttpResponse::NotFound().body("audio not enabled"));
|
||||
};
|
||||
|
||||
@@ -596,14 +599,14 @@ pub async fn audio_ws(
|
||||
let info_rx = if let Some(ref remote) = query.remote {
|
||||
context.rig_audio_info_rx(remote)
|
||||
} else {
|
||||
context.audio_info.as_ref().cloned()
|
||||
context.audio.info.as_ref().cloned()
|
||||
};
|
||||
let Some(info_rx) = info_rx else {
|
||||
return Ok(HttpResponse::NotFound().body("audio not enabled"));
|
||||
};
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let rx_sub = loop {
|
||||
match context.vchan_audio.read() {
|
||||
match context.vchan.audio.read() {
|
||||
Ok(map) => {
|
||||
if let Some(tx) = map.get(&ch_id) {
|
||||
break tx.subscribe();
|
||||
@@ -639,10 +642,10 @@ pub async fn audio_ws(
|
||||
};
|
||||
(rx_sub, info_rx)
|
||||
} else {
|
||||
let Some(info_rx) = context.audio_info.as_ref().cloned() else {
|
||||
let Some(info_rx) = context.audio.info.as_ref().cloned() else {
|
||||
return Ok(HttpResponse::NotFound().body("audio not enabled"));
|
||||
};
|
||||
let Some(rx) = context.audio_rx.as_ref() else {
|
||||
let Some(rx) = context.audio.rx.as_ref() else {
|
||||
return Ok(HttpResponse::NotFound().body("audio not enabled"));
|
||||
};
|
||||
(rx.subscribe(), info_rx)
|
||||
@@ -651,7 +654,7 @@ pub async fn audio_ws(
|
||||
|
||||
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?;
|
||||
|
||||
let audio_clients = context.audio_clients.clone();
|
||||
let audio_clients = context.audio.clients.clone();
|
||||
audio_clients.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::{delete, get, put, web, HttpResponse, Responder};
|
||||
@@ -115,18 +116,18 @@ impl BackgroundDecodeStore {
|
||||
.unwrap_or_else(|| PathBuf::from("background_decode.db"))
|
||||
}
|
||||
|
||||
pub fn get(&self, rig_id: &str) -> Option<BackgroundDecodeConfig> {
|
||||
let db = self.db.read().unwrap_or_else(|e| e.into_inner());
|
||||
pub async fn get(&self, rig_id: &str) -> Option<BackgroundDecodeConfig> {
|
||||
let db = self.db.read().await;
|
||||
db.get::<BackgroundDecodeConfig>(&format!("bgd:{rig_id}"))
|
||||
}
|
||||
|
||||
pub fn upsert(&self, config: &BackgroundDecodeConfig) -> bool {
|
||||
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
|
||||
pub async fn upsert(&self, config: &BackgroundDecodeConfig) -> bool {
|
||||
let mut db = self.db.write().await;
|
||||
db.set(&format!("bgd:{}", config.rig_id), config).is_ok()
|
||||
}
|
||||
|
||||
pub fn remove(&self, rig_id: &str) -> bool {
|
||||
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
|
||||
pub async fn remove(&self, rig_id: &str) -> bool {
|
||||
let mut db = self.db.write().await;
|
||||
db.rem(&format!("bgd:{rig_id}")).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
@@ -171,9 +172,10 @@ impl BackgroundDecodeManager {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_config(&self, rig_id: &str) -> BackgroundDecodeConfig {
|
||||
pub async fn get_config(&self, rig_id: &str) -> BackgroundDecodeConfig {
|
||||
self.store
|
||||
.get(rig_id)
|
||||
.await
|
||||
.unwrap_or_else(|| BackgroundDecodeConfig {
|
||||
rig_id: rig_id.to_string(),
|
||||
enabled: false,
|
||||
@@ -181,9 +183,9 @@ impl BackgroundDecodeManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn put_config(&self, mut config: BackgroundDecodeConfig) -> Option<BackgroundDecodeConfig> {
|
||||
pub async fn put_config(&self, mut config: BackgroundDecodeConfig) -> Option<BackgroundDecodeConfig> {
|
||||
config.bookmark_ids = dedup_ids(&config.bookmark_ids);
|
||||
if self.store.upsert(&config) {
|
||||
if self.store.upsert(&config).await {
|
||||
self.trigger();
|
||||
Some(config)
|
||||
} else {
|
||||
@@ -191,19 +193,20 @@ impl BackgroundDecodeManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_config(&self, rig_id: &str) -> bool {
|
||||
let removed = self.store.remove(rig_id);
|
||||
pub async fn reset_config(&self, rig_id: &str) -> bool {
|
||||
let removed = self.store.remove(rig_id).await;
|
||||
self.trigger();
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn status(&self, rig_id: &str) -> BackgroundDecodeStatus {
|
||||
if let Ok(status) = self.status.read() {
|
||||
pub async fn status(&self, rig_id: &str) -> BackgroundDecodeStatus {
|
||||
{
|
||||
let status = self.status.read().await;
|
||||
if let Some(entry) = status.get(rig_id) {
|
||||
return entry.clone();
|
||||
}
|
||||
}
|
||||
let cfg = self.get_config(rig_id);
|
||||
let cfg = self.get_config(rig_id).await;
|
||||
let bookmarks: HashMap<String, Bookmark> = self
|
||||
.bookmarks
|
||||
.list_for_rig(rig_id)
|
||||
@@ -243,7 +246,8 @@ impl BackgroundDecodeManager {
|
||||
|
||||
fn active_rig_id(&self) -> Option<String> {
|
||||
self.context
|
||||
.remote_active_rig_id
|
||||
.routing
|
||||
.active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
@@ -252,7 +256,7 @@ impl BackgroundDecodeManager {
|
||||
fn send_audio_cmd(&self, cmd: VChanAudioCmd) {
|
||||
// Route through per-rig sender when available.
|
||||
if let Some(rig_id) = self.active_rig_id() {
|
||||
if let Ok(map) = self.context.rig_vchan_audio_cmd.read() {
|
||||
if let Ok(map) = self.context.vchan.rig_audio_cmd.read() {
|
||||
if let Some(tx) = map.get(&rig_id) {
|
||||
let _ = tx.try_send(cmd);
|
||||
return;
|
||||
@@ -260,7 +264,7 @@ impl BackgroundDecodeManager {
|
||||
}
|
||||
}
|
||||
// Fall back to global sender.
|
||||
if let Ok(guard) = self.context.vchan_audio_cmd.lock() {
|
||||
if let Ok(guard) = self.context.vchan.audio_cmd.lock() {
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
let _ = tx.try_send(cmd);
|
||||
}
|
||||
@@ -316,15 +320,14 @@ impl BackgroundDecodeManager {
|
||||
.any(|channel| channel_matches_bookmark(&channel, bookmark))
|
||||
}
|
||||
|
||||
fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
|
||||
async fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
|
||||
let active_rig_id = self.active_rig_id();
|
||||
|
||||
if runtime.current_rig_id != active_rig_id {
|
||||
if let Some(prev_rig_id) = runtime.current_rig_id.clone() {
|
||||
if let Ok(mut guard) = self.status.write() {
|
||||
if let Some(prev_status) = guard.get_mut(&prev_rig_id) {
|
||||
prev_status.active_rig = false;
|
||||
}
|
||||
let mut guard = self.status.write().await;
|
||||
if let Some(prev_status) = guard.get_mut(&prev_rig_id) {
|
||||
prev_status.active_rig = false;
|
||||
}
|
||||
}
|
||||
self.clear_runtime_channels(runtime);
|
||||
@@ -335,7 +338,7 @@ impl BackgroundDecodeManager {
|
||||
};
|
||||
runtime.current_rig_id = Some(rig_id.clone());
|
||||
|
||||
let config = self.get_config(&rig_id);
|
||||
let config = self.get_config(&rig_id).await;
|
||||
let selected = dedup_ids(&config.bookmark_ids);
|
||||
let users_connected = self.context.sse_clients.load(Ordering::Relaxed) > 0;
|
||||
let scheduler_has_control = self.scheduler_control.scheduler_allowed() && users_connected;
|
||||
@@ -467,19 +470,18 @@ impl BackgroundDecodeManager {
|
||||
runtime.active_channels.insert(bookmark_id, desired);
|
||||
}
|
||||
|
||||
if let Ok(mut guard) = self.status.write() {
|
||||
guard.insert(
|
||||
rig_id.clone(),
|
||||
BackgroundDecodeStatus {
|
||||
rig_id,
|
||||
enabled: config.enabled,
|
||||
active_rig: true,
|
||||
center_hz,
|
||||
sample_rate,
|
||||
entries: statuses,
|
||||
},
|
||||
);
|
||||
}
|
||||
let mut guard = self.status.write().await;
|
||||
guard.insert(
|
||||
rig_id.clone(),
|
||||
BackgroundDecodeStatus {
|
||||
rig_id,
|
||||
enabled: config.enabled,
|
||||
active_rig: true,
|
||||
center_hz,
|
||||
sample_rate,
|
||||
entries: statuses,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn scheduler_bookmark_ids(&self, rig_id: &str) -> Vec<String> {
|
||||
@@ -513,7 +515,7 @@ impl BackgroundDecodeManager {
|
||||
loop {
|
||||
let users_connected = self.context.sse_clients.load(Ordering::Relaxed) > 0;
|
||||
if users_connected && spectrum_rx.is_none() {
|
||||
spectrum_rx = Some(self.context.spectrum.subscribe());
|
||||
spectrum_rx = Some(self.context.spectrum.sender.subscribe());
|
||||
} else if !users_connected {
|
||||
spectrum_rx = None;
|
||||
}
|
||||
@@ -522,7 +524,7 @@ impl BackgroundDecodeManager {
|
||||
.as_ref()
|
||||
.map(|rx| rx.borrow().clone())
|
||||
.unwrap_or_default();
|
||||
self.reconcile(&mut runtime, &spectrum);
|
||||
self.reconcile(&mut runtime, &spectrum).await;
|
||||
tokio::select! {
|
||||
changed = async {
|
||||
match spectrum_rx.as_mut() {
|
||||
@@ -599,7 +601,7 @@ pub async fn get_background_decode(
|
||||
path: web::Path<String>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().json(manager.get_config(&path.into_inner()))
|
||||
HttpResponse::Ok().json(manager.get_config(&path.into_inner()).await)
|
||||
}
|
||||
|
||||
#[put("/background-decode/{rig_id}")]
|
||||
@@ -611,7 +613,7 @@ pub async fn put_background_decode(
|
||||
let rig_id = path.into_inner();
|
||||
let mut config = body.into_inner();
|
||||
config.rig_id = rig_id;
|
||||
match manager.put_config(config) {
|
||||
match manager.put_config(config).await {
|
||||
Some(saved) => HttpResponse::Ok().json(saved),
|
||||
None => HttpResponse::InternalServerError().body("failed to save background decode config"),
|
||||
}
|
||||
@@ -623,7 +625,7 @@ pub async fn delete_background_decode(
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
let rig_id = path.into_inner();
|
||||
manager.reset_config(&rig_id);
|
||||
manager.reset_config(&rig_id).await;
|
||||
HttpResponse::Ok().json(BackgroundDecodeConfig {
|
||||
rig_id,
|
||||
enabled: false,
|
||||
@@ -636,5 +638,5 @@ pub async fn get_background_decode_status(
|
||||
path: web::Path<String>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().json(manager.status(&path.into_inner()))
|
||||
HttpResponse::Ok().json(manager.status(&path.into_inner()).await)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ async fn serve(
|
||||
|
||||
// Collect rig IDs for per-rig store initialisation / migration.
|
||||
let rig_ids: Vec<String> = context
|
||||
.routing
|
||||
.remote_rigs
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
@@ -98,7 +99,7 @@ async fn serve(
|
||||
let background_decode_store = Arc::new(BackgroundDecodeStore::open(&background_decode_path));
|
||||
let vchan_mgr = Arc::new(ClientChannelManager::new(
|
||||
4,
|
||||
context.rig_vchan_audio_cmd.clone(),
|
||||
context.vchan.rig_audio_cmd.clone(),
|
||||
));
|
||||
let session_rig_mgr = Arc::new(api::SessionRigManager::default());
|
||||
let background_decode_mgr = BackgroundDecodeManager::new(
|
||||
@@ -113,7 +114,7 @@ async fn serve(
|
||||
|
||||
// Wire the audio-command sender so allocate/delete/freq/mode operations on
|
||||
// virtual channels are forwarded to the audio-client task.
|
||||
if let Ok(guard) = context.vchan_audio_cmd.lock() {
|
||||
if let Ok(guard) = context.vchan.audio_cmd.lock() {
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
vchan_mgr.set_audio_cmd(tx.clone());
|
||||
}
|
||||
@@ -121,7 +122,7 @@ async fn serve(
|
||||
|
||||
// Spawn a task that removes channels destroyed server-side (OOB) from the
|
||||
// client-side registry so the SSE channel list stays in sync.
|
||||
if let Some(ref destroyed_tx) = context.vchan_destroyed {
|
||||
if let Some(ref destroyed_tx) = context.vchan.destroyed {
|
||||
let mut destroyed_rx = destroyed_tx.subscribe();
|
||||
let mgr_for_destroyed = vchan_mgr.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -193,18 +194,18 @@ fn build_server(
|
||||
let background_decode_mgr = web::Data::new(background_decode_mgr);
|
||||
|
||||
// Extract auth config values before moving context
|
||||
let same_site = match context.http_auth_cookie_same_site.as_str() {
|
||||
let same_site = match context.http_auth.cookie_same_site.as_str() {
|
||||
"Strict" => SameSite::Strict,
|
||||
"None" => SameSite::None,
|
||||
_ => SameSite::Lax, // default
|
||||
};
|
||||
let auth_config = AuthConfig::new(
|
||||
context.http_auth_enabled,
|
||||
context.http_auth_rx_passphrase.clone(),
|
||||
context.http_auth_control_passphrase.clone(),
|
||||
context.http_auth_tx_access_control_enabled,
|
||||
Duration::from_secs(context.http_auth_session_ttl_secs),
|
||||
context.http_auth_cookie_secure,
|
||||
context.http_auth.enabled,
|
||||
context.http_auth.rx_passphrase.clone(),
|
||||
context.http_auth.control_passphrase.clone(),
|
||||
context.http_auth.tx_access_control_enabled,
|
||||
Duration::from_secs(context.http_auth.session_ttl_secs),
|
||||
context.http_auth.cookie_secure,
|
||||
same_site,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user