diff --git a/ENHANCEMENT.md b/ENHANCEMENT.md new file mode 100644 index 0000000..0c21482 --- /dev/null +++ b/ENHANCEMENT.md @@ -0,0 +1,70 @@ +# Top 5 Real Architecture Issues + +## 1) Global plugin compatibility registries still exist +### Files +- `src/trx-server/trx-backend/src/lib.rs` +- `src/trx-client/trx-frontend/src/lib.rs` + +### Why this matters +`OnceLock>` registry shims still hold mutable global state. This keeps plugin registration behavior implicit and harder to test. + +### Fix steps +1. Introduce explicit plugin registration API that takes a mutable context. +2. Make plugin loader return registration data instead of relying on global side effects. +3. Remove global `register_*`/`snapshot_bootstrap_context` wrappers after migration. + +## 2) No supervised shutdown/lifecycle model +### Files +- `src/trx-server/src/main.rs` +- `src/trx-client/src/main.rs` + +### Why this matters +Many tasks are detached via `tokio::spawn` and process shutdown mostly waits on Ctrl+C. Task failures and cancellation order are not centrally managed. + +### Fix steps +1. Add shared cancellation token. +2. Track tasks in `JoinSet`. +3. On shutdown: stop listeners, cancel workers, await joins with timeout, then exit. + +## 3) Protocol/network hardening gaps +### Files +- `src/trx-client/src/remote_client.rs` +- `src/trx-server/src/listener.rs` +- `src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs` + +### Why this matters +`parse_remote_url` is ad-hoc and line-based listeners accept unbounded lines. This risks parsing edge cases and memory pressure. + +### Fix steps +1. Replace string URL parsing with typed address parsing (support IPv4/IPv6/hostnames explicitly). +2. Enforce maximum line/frame size for JSON-over-TCP. +3. Add read/write/request timeouts and explicit error messages. + +## 4) Config has parse defaults but weak semantic validation +### Files +- `src/trx-server/src/config.rs` +- `src/trx-client/src/config.rs` + +### Why this matters +Config loads successfully even when values are semantically bad (timings, ports, audio params), leading to runtime failures. + +### Fix steps +1. Add `validate()` to server/client config models. +2. Validate ranges and required field combinations. +3. Call `validate()` in startup before spawning tasks; fail fast with clear path-based errors. + +## 5) Integration coverage is still thin at boundaries +### Files +- `src/trx-server/src/listener.rs` +- `src/trx-client/src/remote_client.rs` +- `src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs` +- `src/trx-app/src/plugins.rs` + +### Why this matters +Most coverage is unit-level. Critical network/plugin/runtime flows can regress without tests. + +### Fix steps +1. Add integration tests for JSON TCP auth/command flow. +2. Add reconnect tests for remote client. +3. Add plugin load/failure isolation tests. +4. Add shutdown behavior tests once lifecycle supervision is added. diff --git a/examples/trx-plugin-example/src/lib.rs b/examples/trx-plugin-example/src/lib.rs index b7e793d..0d4b217 100644 --- a/examples/trx-plugin-example/src/lib.rs +++ b/examples/trx-plugin-example/src/lib.rs @@ -8,18 +8,25 @@ use tokio::sync::{mpsc, watch}; use tokio::task::JoinHandle; use tracing::info; -use trx_backend::{register_backend, RigAccess}; +use trx_backend::{RegistrationContext, RigAccess}; use trx_core::{DynResult, RigRequest, RigState}; -use trx_frontend::{register_frontend, FrontendSpawner}; +use trx_frontend::{FrontendRuntimeContext, FrontendSpawner, FrontendRegistrationContext}; const BACKEND_NAME: &str = "example"; const FRONTEND_NAME: &str = "example-frontend"; -/// Entry point called by trx-server/trx-client when the plugin is loaded. +/// Entry point called by trx-server when the plugin is loaded. #[no_mangle] -pub extern "C" fn trx_register() { - register_backend(BACKEND_NAME, example_backend_factory); - register_frontend(FRONTEND_NAME, ExampleFrontend::spawn_frontend); +pub extern "C" fn trx_register_backend(context: *mut std::ffi::c_void) { + let context = unsafe { &mut *(context as *mut RegistrationContext) }; + context.register_backend(BACKEND_NAME, example_backend_factory); +} + +/// Entry point called by trx-client when the plugin is loaded. +#[no_mangle] +pub extern "C" fn trx_register_frontend(context: *mut std::ffi::c_void) { + let context = unsafe { &mut *(context as *mut FrontendRegistrationContext) }; + context.register_frontend(FRONTEND_NAME, ExampleFrontend::spawn_frontend); } fn example_backend_factory(_access: RigAccess) -> DynResult> { @@ -34,6 +41,7 @@ impl FrontendSpawner for ExampleFrontend { _rig_tx: mpsc::Sender, _callsign: Option, listen_addr: SocketAddr, + _context: std::sync::Arc, ) -> JoinHandle<()> { tokio::spawn(async move { info!("example frontend loaded at {} (no-op)", listen_addr); diff --git a/script/dummy-server.sh b/script/dummy-server.sh new file mode 100755 index 0000000..a38bb3e --- /dev/null +++ b/script/dummy-server.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Run trx-server with the dummy backend for development and testing. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +exec cargo run --manifest-path "$PROJECT_ROOT/Cargo.toml" \ + -p trx-server -- \ + --rig dummy \ + --access serial \ + "/dev/null 9600" \ + "$@" diff --git a/src/trx-app/src/lib.rs b/src/trx-app/src/lib.rs index 16c81ec..931e981 100644 --- a/src/trx-app/src/lib.rs +++ b/src/trx-app/src/lib.rs @@ -9,5 +9,5 @@ pub mod util; pub use config::{ConfigError, ConfigFile}; pub use logging::init_logging; -pub use plugins::load_plugins; +pub use plugins::{load_backend_plugins, load_frontend_plugins}; pub use util::normalize_name; diff --git a/src/trx-app/src/plugins.rs b/src/trx-app/src/plugins.rs index 4456f17..4f0e1da 100644 --- a/src/trx-app/src/plugins.rs +++ b/src/trx-app/src/plugins.rs @@ -4,12 +4,14 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; +use std::ptr::NonNull; use libloading::{Library, Symbol}; use tracing::{info, warn}; const PLUGIN_ENV: &str = "TRX_PLUGIN_DIRS"; -const PLUGIN_ENTRYPOINT: &str = "trx_register"; +const BACKEND_ENTRYPOINT: &str = "trx_register_backend"; +const FRONTEND_ENTRYPOINT: &str = "trx_register_frontend"; #[cfg(windows)] const PATH_SEPARATOR: char = ';'; @@ -23,7 +25,18 @@ const PLUGIN_EXTENSIONS: &[&str] = &["dylib"]; #[cfg(all(unix, not(target_os = "macos")))] const PLUGIN_EXTENSIONS: &[&str] = &["so"]; -pub fn load_plugins() -> Vec { +pub fn load_backend_plugins(context: NonNull) -> Vec { + load_plugins_for_entrypoint(BACKEND_ENTRYPOINT, context) +} + +pub fn load_frontend_plugins(context: NonNull) -> Vec { + load_plugins_for_entrypoint(FRONTEND_ENTRYPOINT, context) +} + +fn load_plugins_for_entrypoint( + entrypoint: &str, + context: NonNull, +) -> Vec { let mut libraries = Vec::new(); let search_paths = plugin_search_paths(); @@ -34,7 +47,7 @@ pub fn load_plugins() -> Vec { info!("Plugin search paths: {:?}", search_paths); for path in search_paths { - if let Err(err) = load_plugins_from_dir(&path, &mut libraries) { + if let Err(err) = load_plugins_from_dir(&path, entrypoint, context, &mut libraries) { warn!("Plugin scan failed for {:?}: {}", path, err); } } @@ -42,7 +55,12 @@ pub fn load_plugins() -> Vec { libraries } -fn load_plugins_from_dir(path: &Path, libraries: &mut Vec) -> std::io::Result<()> { +fn load_plugins_from_dir( + path: &Path, + entrypoint: &str, + context: NonNull, + libraries: &mut Vec, +) -> std::io::Result<()> { if !path.exists() { return Ok(()); } @@ -60,7 +78,7 @@ fn load_plugins_from_dir(path: &Path, libraries: &mut Vec) -> std::io:: unsafe { match Library::new(&path) { Ok(lib) => { - if let Err(err) = register_library(&lib, &path) { + if let Err(err) = register_library(&lib, &path, entrypoint, context) { warn!("Plugin {:?} failed to register: {}", path, err); continue; } @@ -77,11 +95,16 @@ fn load_plugins_from_dir(path: &Path, libraries: &mut Vec) -> std::io:: Ok(()) } -unsafe fn register_library(lib: &Library, path: &Path) -> Result<(), String> { - let entry: Symbol = lib - .get(PLUGIN_ENTRYPOINT.as_bytes()) - .map_err(|e| format!("missing entrypoint {}: {}", PLUGIN_ENTRYPOINT, e))?; - entry(); +unsafe fn register_library( + lib: &Library, + path: &Path, + entrypoint: &str, + context: NonNull, +) -> Result<(), String> { + let entry: Symbol = lib + .get(entrypoint.as_bytes()) + .map_err(|e| format!("missing entrypoint {}: {}", entrypoint, e))?; + entry(context.as_ptr()); info!("Registered plugin {:?}", path); Ok(()) } @@ -110,6 +133,10 @@ fn plugin_search_paths() -> Vec { fn is_plugin_file(path: &Path) -> bool { path.extension() .and_then(OsStr::to_str) - .map(|ext| PLUGIN_EXTENSIONS.iter().any(|e| ext.eq_ignore_ascii_case(e))) + .map(|ext| { + PLUGIN_EXTENSIONS + .iter() + .any(|e| ext.eq_ignore_ascii_case(e)) + }) .unwrap_or(false) } diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index e02b586..2a388dd 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -8,6 +8,7 @@ mod remote_client; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; +use std::ptr::NonNull; use std::time::Duration; use bytes::Bytes; @@ -17,16 +18,14 @@ use tokio::sync::{broadcast, mpsc, watch}; use tokio::task::JoinHandle; use tracing::{error, info}; -use trx_app::{init_logging, load_plugins, normalize_name}; +use trx_app::{init_logging, load_frontend_plugins, normalize_name}; use trx_core::audio::AudioStreamInfo; use trx_core::decode::DecodedMessage; use trx_core::rig::request::RigRequest; use trx_core::rig::state::RigState; use trx_core::DynResult; -use trx_frontend::{ - snapshot_bootstrap_context, FrontendRegistrationContext, FrontendRuntimeContext, -}; +use trx_frontend::{FrontendRegistrationContext, FrontendRuntimeContext}; use trx_frontend_http::register_frontend_on as register_http_frontend; use trx_frontend_http_json::register_frontend_on as register_http_json_frontend; use trx_frontend_rigctl::register_frontend_on as register_rigctl_frontend; @@ -142,8 +141,8 @@ async fn async_init() -> DynResult { init_logging(cfg.general.log_level.as_deref()); - let _plugin_libs = load_plugins(); - frontend_reg_ctx.extend_from(&snapshot_bootstrap_context()); + let frontend_ctx_ptr = NonNull::from(&mut frontend_reg_ctx).cast(); + let _plugin_libs = load_frontend_plugins(frontend_ctx_ptr); if let Some(ref path) = config_path { info!("Loaded configuration from {}", path.display()); diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 63778c9..30ef3ed 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -309,7 +309,18 @@ fn parse_port(port_str: &str) -> Result { #[cfg(test)] mod tests { - use super::{parse_remote_url, RemoteEndpoint}; + use super::{parse_remote_url, RemoteClientConfig, RemoteEndpoint}; + use std::time::Duration; + + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + use tokio::net::TcpListener; + use tokio::sync::{mpsc, watch}; + + use trx_core::radio::freq::{Band, Freq}; + use trx_core::rig::state::RigSnapshot; + use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo, RigStatus, RigTxStatus}; + use trx_core::{RigMode, RigState}; + use trx_protocol::ClientResponse; #[test] fn parse_host_default_port() { @@ -352,4 +363,131 @@ mod tests { let err = parse_remote_url("::1:7000").expect_err("must fail"); assert!(err.contains("must be bracketed")); } + + fn sample_snapshot() -> RigSnapshot { + RigSnapshot { + info: RigInfo { + manufacturer: "Test".to_string(), + model: "Dummy".to_string(), + revision: "1".to_string(), + capabilities: RigCapabilities { + supported_bands: vec![Band { + low_hz: 7_000_000, + high_hz: 7_200_000, + tx_allowed: true, + }], + supported_modes: vec![RigMode::USB], + num_vfos: 1, + lock: false, + lockable: true, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + }, + access: RigAccessMethod::Tcp { + addr: "127.0.0.1:1234".to_string(), + }, + }, + status: RigStatus { + freq: Freq { hz: 7_100_000 }, + mode: RigMode::USB, + tx_en: false, + vfo: None, + tx: Some(RigTxStatus { + power: None, + limit: None, + swr: None, + alc: None, + }), + rx: None, + lock: Some(false), + }, + band: None, + enabled: Some(true), + initialized: true, + server_callsign: Some("N0CALL".to_string()), + server_version: Some("test".to_string()), + server_latitude: None, + server_longitude: None, + aprs_decode_enabled: false, + cw_decode_enabled: false, + ft8_decode_enabled: false, + cw_auto: true, + cw_wpm: 15, + cw_tone_hz: 700, + } + } + + #[tokio::test] + #[ignore = "requires TCP bind permissions"] + async fn reconnects_and_updates_state_after_drop() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let addr = listener.local_addr().expect("local addr"); + let response = serde_json::to_string(&ClientResponse { + success: true, + state: Some(sample_snapshot()), + error: None, + }) + .expect("serialize response") + + "\n"; + + let server = tokio::spawn(async move { + let (first, _) = listener.accept().await.expect("accept first"); + let (first_reader, _) = first.into_split(); + let mut first_reader = BufReader::new(first_reader); + let mut buf = String::new(); + let _ = first_reader.read_line(&mut buf).await.expect("read first"); + + let (second, _) = listener.accept().await.expect("accept second"); + let (second_reader, mut second_writer) = second.into_split(); + let mut second_reader = BufReader::new(second_reader); + buf.clear(); + let _ = second_reader + .read_line(&mut buf) + .await + .expect("read second"); + second_writer + .write_all(response.as_bytes()) + .await + .expect("write response"); + second_writer.flush().await.expect("flush"); + }); + + let (_req_tx, req_rx) = mpsc::channel(8); + let (state_tx, mut state_rx) = watch::channel(RigState::new_uninitialized()); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let client = tokio::spawn(super::run_remote_client( + RemoteClientConfig { + addr: addr.to_string(), + token: None, + poll_interval: Duration::from_millis(100), + }, + req_rx, + state_tx, + shutdown_rx, + )); + + tokio::time::timeout(Duration::from_secs(5), async { + loop { + if state_rx.borrow().initialized { + break; + } + state_rx.changed().await.expect("state channel"); + } + }) + .await + .expect("state update timeout"); + assert_eq!(state_rx.borrow().status.freq.hz, 7_100_000); + + let _ = shutdown_tx.send(true); + tokio::time::timeout(Duration::from_secs(2), async { + let _ = client.await; + }) + .await + .expect("client shutdown timeout"); + let _ = server.await; + } } diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index a7370fe..d6178aa 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: BSD-2-Clause -use std::collections::{HashMap, VecDeque, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::net::SocketAddr; use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::{Arc, Mutex}; use std::time::Instant; use bytes::Bytes; @@ -151,52 +151,3 @@ fn normalize_name(name: &str) -> String { .filter(|c| c.is_ascii_alphanumeric()) .collect() } - -/// Phase 3D: Plugin compatibility adapter - delegates to bootstrap context. -fn bootstrap_context() -> &'static Arc> { - static BOOTSTRAP_CONTEXT: OnceLock>> = OnceLock::new(); - BOOTSTRAP_CONTEXT.get_or_init(|| Arc::new(Mutex::new(FrontendRegistrationContext::new()))) -} - -/// Snapshot current plugin/bootstrap registrations into an owned context. -pub fn snapshot_bootstrap_context() -> FrontendRegistrationContext { - let ctx = bootstrap_context() - .lock() - .expect("frontend context mutex poisoned"); - ctx.clone() -} - -/// Register a frontend spawner under a stable name (e.g. "http"). -/// Plugin compatibility: delegates to bootstrap context. -pub fn register_frontend(name: &str, spawner: FrontendSpawnFn) { - let mut ctx = bootstrap_context().lock().expect("frontend context mutex poisoned"); - ctx.register_frontend(name, spawner); -} - -/// Check whether a frontend name is registered. -/// Plugin compatibility: reads from bootstrap context. -pub fn is_frontend_registered(name: &str) -> bool { - let ctx = bootstrap_context().lock().expect("frontend context mutex poisoned"); - ctx.is_frontend_registered(name) -} - -/// List registered frontend names. -/// Plugin compatibility: reads from bootstrap context. -pub fn registered_frontends() -> Vec { - let ctx = bootstrap_context().lock().expect("frontend context mutex poisoned"); - ctx.registered_frontends() -} - -/// Spawn a registered frontend by name with runtime context. -/// Plugin compatibility: reads from bootstrap context. -pub fn spawn_frontend( - name: &str, - state_rx: watch::Receiver, - rig_tx: mpsc::Sender, - callsign: Option, - listen_addr: SocketAddr, - context: Arc, -) -> DynResult> { - let ctx = bootstrap_context().lock().expect("frontend context mutex poisoned"); - ctx.spawn_frontend(name, state_rx, rig_tx, callsign, listen_addr, context) -} diff --git a/src/trx-client/trx-frontend/trx-frontend-http-json/src/lib.rs b/src/trx-client/trx-frontend/trx-frontend-http-json/src/lib.rs index df798e4..b18fe1b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http-json/src/lib.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http-json/src/lib.rs @@ -8,8 +8,3 @@ pub fn register_frontend_on(context: &mut trx_frontend::FrontendRegistrationCont use trx_frontend::FrontendSpawner; context.register_frontend("http-json", server::HttpJsonFrontend::spawn_frontend); } - -pub fn register_frontend() { - use trx_frontend::FrontendSpawner; - trx_frontend::register_frontend("http-json", server::HttpJsonFrontend::spawn_frontend); -} diff --git a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs index e6fc4a6..234a824 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs @@ -266,3 +266,148 @@ fn authorize(token: &Option, context: &FrontendRuntimeContext) -> Result let validator = SimpleTokenValidator::new(context.auth_tokens.clone()); validator.validate(token) } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::Ipv4Addr; + + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + + use trx_core::radio::freq::{Band, Freq}; + use trx_core::rig::state::RigSnapshot; + use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo, RigStatus, RigTxStatus}; + use trx_core::RigMode; + + fn loopback_addr() -> SocketAddr { + let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind"); + let addr = listener.local_addr().expect("local_addr"); + drop(listener); + addr + } + + fn sample_snapshot() -> RigSnapshot { + RigSnapshot { + info: RigInfo { + manufacturer: "Test".to_string(), + model: "Dummy".to_string(), + revision: "1".to_string(), + capabilities: RigCapabilities { + supported_bands: vec![Band { + low_hz: 14_000_000, + high_hz: 14_350_000, + tx_allowed: true, + }], + supported_modes: vec![RigMode::USB], + num_vfos: 1, + lock: false, + lockable: true, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + }, + access: RigAccessMethod::Tcp { + addr: "127.0.0.1:1234".to_string(), + }, + }, + status: RigStatus { + freq: Freq { hz: 14_074_000 }, + mode: RigMode::USB, + tx_en: false, + vfo: None, + tx: Some(RigTxStatus { + power: None, + limit: None, + swr: None, + alc: None, + }), + rx: None, + lock: Some(false), + }, + band: None, + enabled: Some(true), + initialized: true, + server_callsign: Some("N0CALL".to_string()), + server_version: Some("test".to_string()), + server_latitude: None, + server_longitude: None, + aprs_decode_enabled: false, + cw_decode_enabled: false, + ft8_decode_enabled: false, + cw_auto: true, + cw_wpm: 15, + cw_tone_hz: 700, + } + } + + #[tokio::test] + #[ignore = "requires TCP bind permissions"] + async fn rejects_missing_token() { + let addr = loopback_addr(); + let (rig_tx, _rig_rx) = mpsc::channel::(8); + let mut runtime = FrontendRuntimeContext::new(); + runtime.auth_tokens = HashSet::from(["secret".to_string()]); + let ctx = Arc::new(runtime); + + let handle = tokio::spawn(serve(addr, rig_tx, ctx)); + + let stream = TcpStream::connect(addr).await.expect("connect"); + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + + writer + .write_all(br#"{"cmd":"get_state"}"#) + .await + .expect("write"); + writer.write_all(b"\n").await.expect("newline"); + writer.flush().await.expect("flush"); + + let mut line = String::new(); + reader.read_line(&mut line).await.expect("read"); + let resp: ClientResponse = serde_json::from_str(line.trim_end()).expect("response json"); + assert!(!resp.success); + assert_eq!(resp.error.as_deref(), Some("missing authorization token")); + + handle.abort(); + let _ = handle.await; + } + + #[tokio::test] + #[ignore = "requires TCP bind permissions"] + async fn forwards_command_and_returns_snapshot() { + let addr = loopback_addr(); + let (rig_tx, mut rig_rx) = mpsc::channel::(8); + let ctx = Arc::new(FrontendRuntimeContext::new()); + + let rig_worker = tokio::spawn(async move { + if let Some(req) = rig_rx.recv().await { + let _ = req.respond_to.send(Ok(sample_snapshot())); + } + }); + let handle = tokio::spawn(serve(addr, rig_tx, ctx)); + + let stream = TcpStream::connect(addr).await.expect("connect"); + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + + writer + .write_all(br#"{"cmd":"get_state"}"#) + .await + .expect("write"); + writer.write_all(b"\n").await.expect("newline"); + writer.flush().await.expect("flush"); + + let mut line = String::new(); + reader.read_line(&mut line).await.expect("read"); + let resp: ClientResponse = serde_json::from_str(line.trim_end()).expect("response json"); + assert!(resp.success); + assert_eq!(resp.state.expect("snapshot").status.freq.hz, 14_074_000); + + let _ = rig_worker.await; + handle.abort(); + let _ = handle.await; + } +} 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 6d251a0..56e1ab7 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 @@ -13,11 +13,11 @@ use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio::time::{self, Duration}; use tokio_stream::wrappers::{IntervalStream, WatchStream}; -use trx_frontend::FrontendRuntimeContext; -use trx_protocol::{ClientResponse, parse_mode}; use trx_core::radio::freq::Freq; use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; use trx_core::{RigCommand, RigRequest, RigSnapshot, RigState}; +use trx_frontend::FrontendRuntimeContext; +use trx_protocol::{parse_mode, ClientResponse}; use crate::server::status; @@ -450,28 +450,40 @@ async fn style_css() -> impl Responder { #[get("/app.js")] async fn app_js() -> impl Responder { HttpResponse::Ok() - .insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8")) + .insert_header(( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + )) .body(status::APP_JS) } #[get("/aprs.js")] async fn aprs_js() -> impl Responder { HttpResponse::Ok() - .insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8")) + .insert_header(( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + )) .body(status::APRS_JS) } #[get("/ft8.js")] async fn ft8_js() -> impl Responder { HttpResponse::Ok() - .insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8")) + .insert_header(( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + )) .body(status::FT8_JS) } #[get("/cw.js")] async fn cw_js() -> impl Responder { HttpResponse::Ok() - .insert_header((header::CONTENT_TYPE, "application/javascript; charset=utf-8")) + .insert_header(( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + )) .body(status::CW_JS) } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs index 2987fbe..1b6ebd9 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/audio.rs @@ -10,11 +10,11 @@ //! - Browser sends binary messages: raw Opus packets (TX) use std::collections::VecDeque; -use std::sync::Arc; use std::sync::atomic::Ordering; +use std::sync::Arc; use std::time::{Duration, Instant}; -use actix_web::{Error, HttpRequest, HttpResponse, get, web}; +use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use actix_ws::Message; use bytes::Bytes; use tokio::sync::broadcast; @@ -62,7 +62,10 @@ fn record_aprs(context: &FrontendRuntimeContext, pkt: AprsPacket) { } fn record_cw(context: &FrontendRuntimeContext, event: CwEvent) { - let mut history = context.cw_history.lock().expect("cw history mutex poisoned"); + let mut history = context + .cw_history + .lock() + .expect("cw history mutex poisoned"); history.push_back((Instant::now(), event)); prune_cw_history(&mut history); } @@ -86,7 +89,10 @@ pub fn snapshot_aprs_history(context: &FrontendRuntimeContext) -> Vec Vec { - let mut history = context.cw_history.lock().expect("cw history mutex poisoned"); + let mut history = context + .cw_history + .lock() + .expect("cw history mutex poisoned"); prune_cw_history(&mut history); history.iter().map(|(_, evt)| evt.clone()).collect() } @@ -109,7 +115,10 @@ pub fn clear_aprs_history(context: &FrontendRuntimeContext) { } pub fn clear_cw_history(context: &FrontendRuntimeContext) { - let mut history = context.cw_history.lock().expect("cw history mutex poisoned"); + let mut history = context + .cw_history + .lock() + .expect("cw history mutex poisoned"); history.clear(); } @@ -128,7 +137,10 @@ pub fn subscribe_decode( } pub fn start_decode_history_collector(context: Arc) { - if context.decode_collector_started.swap(true, Ordering::AcqRel) { + if context + .decode_collector_started + .swap(true, Ordering::AcqRel) + { return; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/lib.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/lib.rs index 77cd9fc..ee98e27 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/lib.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/lib.rs @@ -8,8 +8,3 @@ pub fn register_frontend_on(context: &mut trx_frontend::FrontendRegistrationCont use trx_frontend::FrontendSpawner; context.register_frontend("http", server::HttpFrontend::spawn_frontend); } - -pub fn register_frontend() { - use trx_frontend::FrontendSpawner; - trx_frontend::register_frontend("http", server::HttpFrontend::spawn_frontend); -} 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 10e299d..6f81ee2 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 @@ -22,7 +22,7 @@ use tracing::{error, info}; use trx_core::RigRequest; use trx_core::RigState; -use trx_frontend::{FrontendSpawner, FrontendRuntimeContext}; +use trx_frontend::{FrontendRuntimeContext, FrontendSpawner}; /// HTTP frontend implementation. pub struct HttpFrontend; diff --git a/src/trx-client/trx-frontend/trx-frontend-rigctl/src/lib.rs b/src/trx-client/trx-frontend/trx-frontend-rigctl/src/lib.rs index 62744c8..3bb0c5f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-rigctl/src/lib.rs +++ b/src/trx-client/trx-frontend/trx-frontend-rigctl/src/lib.rs @@ -8,8 +8,3 @@ pub fn register_frontend_on(context: &mut trx_frontend::FrontendRegistrationCont use trx_frontend::FrontendSpawner; context.register_frontend("rigctl", server::RigctlFrontend::spawn_frontend); } - -pub fn register_frontend() { - use trx_frontend::FrontendSpawner; - trx_frontend::register_frontend("rigctl", server::RigctlFrontend::spawn_frontend); -} diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs index a4bb039..361bdd0 100644 --- a/src/trx-core/src/rig/state.rs +++ b/src/trx-core/src/rig/state.rs @@ -62,7 +62,7 @@ pub enum RigMode { impl Default for RigStatus { fn default() -> Self { Self { - freq: Freq { hz: 144_300_000 }, // 2m calling frequency + freq: Freq { hz: 144_300_000 }, // 2m calling frequency mode: RigMode::USB, tx_en: false, vfo: None, @@ -136,7 +136,9 @@ impl RigState { state.server_version = version; state.server_latitude = latitude; state.server_longitude = longitude; - state.status.freq = Freq { hz: initial_freq_hz }; + state.status.freq = Freq { + hz: initial_freq_hz, + }; state.status.mode = initial_mode; state } diff --git a/src/trx-ft8/src/lib.rs b/src/trx-ft8/src/lib.rs index 59ba313..cacbb53 100644 --- a/src/trx-ft8/src/lib.rs +++ b/src/trx-ft8/src/lib.rs @@ -43,7 +43,11 @@ extern "C" { fn ft8_decoder_reset(dec: *mut c_void); fn ft8_decoder_process(dec: *mut c_void, frame: *const c_float); fn ft8_decoder_is_ready(dec: *const c_void) -> c_int; - fn ft8_decoder_decode(dec: *mut c_void, out: *mut Ft8DecodeResultRaw, max_results: c_int) -> c_int; + fn ft8_decoder_decode( + dec: *mut c_void, + out: *mut Ft8DecodeResultRaw, + max_results: c_int, + ) -> c_int; } pub struct Ft8Decoder { @@ -108,13 +112,17 @@ impl Ft8Decoder { if ft8_decoder_is_ready(self.inner.as_ptr()) == 0 { return Vec::new(); } - let mut raw = vec![Ft8DecodeResultRaw { - text: [0; FTX_MAX_MESSAGE_LENGTH], - snr_db: 0.0, - dt_s: 0.0, - freq_hz: 0.0, - }; max_results]; - let count = ft8_decoder_decode(self.inner.as_ptr(), raw.as_mut_ptr(), max_results as c_int); + let mut raw = vec![ + Ft8DecodeResultRaw { + text: [0; FTX_MAX_MESSAGE_LENGTH], + snr_db: 0.0, + dt_s: 0.0, + freq_hz: 0.0, + }; + max_results + ]; + let count = + ft8_decoder_decode(self.inner.as_ptr(), raw.as_mut_ptr(), max_results as c_int); let count = count.max(0) as usize; let mut out = Vec::with_capacity(count); for item in raw.into_iter().take(count) { diff --git a/src/trx-protocol/src/auth.rs b/src/trx-protocol/src/auth.rs index 3e611fb..7dd9724 100644 --- a/src/trx-protocol/src/auth.rs +++ b/src/trx-protocol/src/auth.rs @@ -187,9 +187,7 @@ mod tests { assert!(validator.validate(&Some("token1".to_string())).is_ok()); assert!(validator.validate(&Some("token2".to_string())).is_ok()); - assert!(validator - .validate(&Some("token3".to_string())) - .is_err()); + assert!(validator.validate(&Some("token3".to_string())).is_err()); } #[test] diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs index c610e5d..71d86cb 100644 --- a/src/trx-protocol/src/codec.rs +++ b/src/trx-protocol/src/codec.rs @@ -6,8 +6,8 @@ use serde_json; -use trx_core::rig::state::RigMode; use crate::types::{ClientCommand, ClientEnvelope}; +use trx_core::rig::state::RigMode; /// Parse a mode string into a RigMode. /// diff --git a/src/trx-protocol/src/mapping.rs b/src/trx-protocol/src/mapping.rs index 65a8dad..615f34b 100644 --- a/src/trx-protocol/src/mapping.rs +++ b/src/trx-protocol/src/mapping.rs @@ -27,7 +27,9 @@ pub fn client_command_to_rig(cmd: ClientCommand) -> RigCommand { ClientCommand::Unlock => RigCommand::Unlock, ClientCommand::GetTxLimit => RigCommand::GetTxLimit, ClientCommand::SetTxLimit { limit } => RigCommand::SetTxLimit(limit), - ClientCommand::SetAprsDecodeEnabled { enabled } => RigCommand::SetAprsDecodeEnabled(enabled), + ClientCommand::SetAprsDecodeEnabled { enabled } => { + RigCommand::SetAprsDecodeEnabled(enabled) + } ClientCommand::SetCwDecodeEnabled { enabled } => RigCommand::SetCwDecodeEnabled(enabled), ClientCommand::SetCwAuto { enabled } => RigCommand::SetCwAuto(enabled), ClientCommand::SetCwWpm { wpm } => RigCommand::SetCwWpm(wpm), @@ -58,7 +60,9 @@ pub fn rig_command_to_client(cmd: RigCommand) -> ClientCommand { RigCommand::Unlock => ClientCommand::Unlock, RigCommand::GetTxLimit => ClientCommand::GetTxLimit, RigCommand::SetTxLimit(limit) => ClientCommand::SetTxLimit { limit }, - RigCommand::SetAprsDecodeEnabled(enabled) => ClientCommand::SetAprsDecodeEnabled { enabled }, + RigCommand::SetAprsDecodeEnabled(enabled) => { + ClientCommand::SetAprsDecodeEnabled { enabled } + } RigCommand::SetCwDecodeEnabled(enabled) => ClientCommand::SetCwDecodeEnabled { enabled }, RigCommand::SetCwAuto(enabled) => ClientCommand::SetCwAuto { enabled }, RigCommand::SetCwWpm(wpm) => ClientCommand::SetCwWpm { wpm }, diff --git a/src/trx-server/src/decode/aprs.rs b/src/trx-server/src/decode/aprs.rs index 70c4646..bed29ca 100644 --- a/src/trx-server/src/decode/aprs.rs +++ b/src/trx-server/src/decode/aprs.rs @@ -200,8 +200,7 @@ impl Demodulator { self.corr_idx = (idx + 1) % self.corr_len; // Compare mark vs space energy - let mark_energy = - self.mark_i_sum * self.mark_i_sum + self.mark_q_sum * self.mark_q_sum; + let mark_energy = self.mark_i_sum * self.mark_i_sum + self.mark_q_sum * self.mark_q_sum; let space_energy = self.space_i_sum * self.space_i_sum + self.space_q_sum * self.space_q_sum; let bit: u8 = if mark_energy > space_energy { 1 } else { 0 }; diff --git a/src/trx-server/src/decode/cw.rs b/src/trx-server/src/decode/cw.rs index b6a6899..38ea230 100644 --- a/src/trx-server/src/decode/cw.rs +++ b/src/trx-server/src/decode/cw.rs @@ -153,8 +153,7 @@ impl CwDecoder { let mut tone_scan_bins = Vec::new(); let mut f = TONE_SCAN_LOW; while f <= TONE_SCAN_HIGH { - let bk = - (f as f32 * window_size as f32 / sample_rate as f32).round(); + let bk = (f as f32 * window_size as f32 / sample_rate as f32).round(); let b_omega = (2.0 * std::f32::consts::PI * bk) / window_size as f32; tone_scan_bins.push(ToneScanBin { freq: f, @@ -202,8 +201,7 @@ impl CwDecoder { fn recompute_goertzel(&mut self, new_freq: u32) { self.tone_freq = new_freq; - let k = (new_freq as f32 * self.window_size as f32 / self.sample_rate as f32) - .round(); + let k = (new_freq as f32 * self.window_size as f32 / self.sample_rate as f32).round(); let omega = (2.0 * std::f32::consts::PI * k) / self.window_size as f32; self.coeff = 2.0 * omega.cos(); } @@ -256,9 +254,7 @@ impl CwDecoder { return; } - if self.tone_stable_bin >= 0 - && (best_idx - self.tone_stable_bin).unsigned_abs() <= 1 - { + if self.tone_stable_bin >= 0 && (best_idx - self.tone_stable_bin).unsigned_abs() <= 1 { self.tone_stable_count += 1; } else { self.tone_stable_bin = best_idx; @@ -267,9 +263,7 @@ impl CwDecoder { if self.tone_stable_count >= TONE_STABLE_NEEDED { let detected_freq = self.tone_scan_bins[self.tone_stable_bin as usize].freq; - if (detected_freq as i32 - self.tone_freq as i32).unsigned_abs() - > TONE_SCAN_STEP - { + if (detected_freq as i32 - self.tone_freq as i32).unsigned_abs() > TONE_SCAN_STEP { self.recompute_goertzel(detected_freq); } } @@ -337,8 +331,7 @@ impl CwDecoder { if off_duration > u * 5.0 { // Word gap if !self.current_symbol.is_empty() { - let ch = morse_lookup(&self.current_symbol) - .unwrap_or('?'); + let ch = morse_lookup(&self.current_symbol).unwrap_or('?'); self.emit_text(&ch.to_string()); self.current_symbol.clear(); } @@ -346,8 +339,7 @@ impl CwDecoder { } else if off_duration > u * 2.0 { // Character gap if !self.current_symbol.is_empty() { - let ch = morse_lookup(&self.current_symbol) - .unwrap_or('?'); + let ch = morse_lookup(&self.current_symbol).unwrap_or('?'); self.emit_text(&ch.to_string()); self.current_symbol.clear(); } diff --git a/src/trx-server/src/listener.rs b/src/trx-server/src/listener.rs index 5d35992..8432d6f 100644 --- a/src/trx-server/src/listener.rs +++ b/src/trx-server/src/listener.rs @@ -316,3 +316,129 @@ async fn handle_client( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + use std::net::{Ipv4Addr, SocketAddr}; + + use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; + use tokio::net::TcpStream; + + use trx_core::radio::freq::Band; + use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo}; + + fn loopback_addr() -> SocketAddr { + let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind"); + let addr = listener.local_addr().expect("local_addr"); + drop(listener); + addr + } + + fn sample_state() -> RigState { + let mut state = RigState::new_uninitialized(); + state.initialized = true; + state.rig_info = Some(RigInfo { + manufacturer: "Test".to_string(), + model: "Dummy".to_string(), + revision: "1".to_string(), + capabilities: RigCapabilities { + supported_bands: vec![Band { + low_hz: 7_000_000, + high_hz: 7_200_000, + tx_allowed: true, + }], + supported_modes: vec![trx_core::RigMode::USB], + num_vfos: 1, + lock: false, + lockable: true, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + }, + access: RigAccessMethod::Tcp { + addr: "127.0.0.1:1234".to_string(), + }, + }); + state + } + + #[tokio::test] + #[ignore = "requires TCP bind permissions"] + async fn listener_rejects_missing_token() { + let addr = loopback_addr(); + let (rig_tx, _rig_rx) = mpsc::channel::(8); + let (state_tx, state_rx) = watch::channel(sample_state()); + let _state_tx = state_tx; + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let mut auth = HashSet::new(); + auth.insert("secret".to_string()); + let handle = tokio::spawn(run_listener(addr, rig_tx, auth, state_rx, shutdown_rx)); + + let stream = TcpStream::connect(addr).await.expect("connect"); + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + + writer + .write_all(br#"{"cmd":"get_state"}"#) + .await + .expect("write"); + writer.write_all(b"\n").await.expect("newline"); + writer.flush().await.expect("flush"); + + let mut line = String::new(); + reader.read_line(&mut line).await.expect("read"); + let resp: ClientResponse = serde_json::from_str(line.trim_end()).expect("response json"); + assert!(!resp.success); + assert_eq!(resp.error.as_deref(), Some("missing authorization token")); + + let _ = shutdown_tx.send(true); + handle.abort(); + let _ = handle.await; + } + + #[tokio::test] + #[ignore = "requires TCP bind permissions"] + async fn listener_serves_get_state_snapshot() { + let addr = loopback_addr(); + let (rig_tx, _rig_rx) = mpsc::channel::(8); + let (state_tx, state_rx) = watch::channel(sample_state()); + let _state_tx = state_tx; + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let handle = tokio::spawn(run_listener( + addr, + rig_tx, + HashSet::new(), + state_rx, + shutdown_rx, + )); + + let stream = TcpStream::connect(addr).await.expect("connect"); + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + + writer + .write_all(br#"{"cmd":"get_state"}"#) + .await + .expect("write"); + writer.write_all(b"\n").await.expect("newline"); + writer.flush().await.expect("flush"); + + let mut line = String::new(); + reader.read_line(&mut line).await.expect("read"); + let resp: ClientResponse = serde_json::from_str(line.trim_end()).expect("response json"); + assert!(resp.success); + let snapshot = resp.state.expect("snapshot"); + assert_eq!(snapshot.info.model, "Dummy"); + assert_eq!(snapshot.status.freq.hz, 144_300_000); + + let _ = shutdown_tx.send(true); + handle.abort(); + let _ = handle.await; + } +} diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 1ea933f..912b1c6 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -12,6 +12,7 @@ mod rig_task; use std::collections::HashSet; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; +use std::ptr::NonNull; use std::time::Duration; use bytes::Bytes; @@ -23,10 +24,8 @@ use tracing::{error, info}; use trx_core::audio::AudioStreamInfo; -use trx_app::{init_logging, load_plugins, normalize_name}; -use trx_backend::{ - register_builtin_backends_on, snapshot_bootstrap_context, RegistrationContext, RigAccess, -}; +use trx_app::{init_logging, load_backend_plugins, normalize_name}; +use trx_backend::{register_builtin_backends_on, RegistrationContext, RigAccess}; use trx_core::rig::controller::{AdaptivePolling, ExponentialBackoff}; use trx_core::rig::request::RigRequest; use trx_core::rig::state::RigState; @@ -247,8 +246,8 @@ async fn main() -> DynResult<()> { init_logging(cfg.general.log_level.as_deref()); - let _plugin_libs = load_plugins(); - bootstrap_ctx.extend_from(&snapshot_bootstrap_context()); + let bootstrap_ctx_ptr = NonNull::from(&mut bootstrap_ctx).cast(); + let _plugin_libs = load_backend_plugins(bootstrap_ctx_ptr); if let Some(ref path) = config_path { info!("Loaded configuration from {}", path.display()); diff --git a/src/trx-server/trx-backend/src/dummy.rs b/src/trx-server/trx-backend/src/dummy.rs index f62ef21..ca4194d 100644 --- a/src/trx-server/trx-backend/src/dummy.rs +++ b/src/trx-server/trx-backend/src/dummy.rs @@ -38,15 +38,51 @@ impl DummyRig { revision: "1.0".to_string(), capabilities: RigCapabilities { supported_bands: vec![ - Band { low_hz: 1_800_000, high_hz: 2_000_000, tx_allowed: true }, - Band { low_hz: 3_500_000, high_hz: 4_000_000, tx_allowed: true }, - Band { low_hz: 7_000_000, high_hz: 7_300_000, tx_allowed: true }, - Band { low_hz: 14_000_000, high_hz: 14_350_000, tx_allowed: true }, - Band { low_hz: 21_000_000, high_hz: 21_450_000, tx_allowed: true }, - Band { low_hz: 28_000_000, high_hz: 29_700_000, tx_allowed: true }, - Band { low_hz: 50_000_000, high_hz: 54_000_000, tx_allowed: true }, - Band { low_hz: 144_000_000, high_hz: 148_000_000, tx_allowed: true }, - Band { low_hz: 430_000_000, high_hz: 440_000_000, tx_allowed: true }, + Band { + low_hz: 1_800_000, + high_hz: 2_000_000, + tx_allowed: true, + }, + Band { + low_hz: 3_500_000, + high_hz: 4_000_000, + tx_allowed: true, + }, + Band { + low_hz: 7_000_000, + high_hz: 7_300_000, + tx_allowed: true, + }, + Band { + low_hz: 14_000_000, + high_hz: 14_350_000, + tx_allowed: true, + }, + Band { + low_hz: 21_000_000, + high_hz: 21_450_000, + tx_allowed: true, + }, + Band { + low_hz: 28_000_000, + high_hz: 29_700_000, + tx_allowed: true, + }, + Band { + low_hz: 50_000_000, + high_hz: 54_000_000, + tx_allowed: true, + }, + Band { + low_hz: 144_000_000, + high_hz: 148_000_000, + tx_allowed: true, + }, + Band { + low_hz: 430_000_000, + high_hz: 440_000_000, + tx_allowed: true, + }, ], supported_modes: vec![ RigMode::LSB, @@ -112,9 +148,7 @@ impl Rig for DummyRig { impl RigCat for DummyRig { fn get_status<'a>(&'a mut self) -> RigStatusFuture<'a> { - Box::pin(async move { - Ok((self.freq, self.mode.clone(), Some(self.build_vfo()))) - }) + Box::pin(async move { Ok((self.freq, self.mode.clone(), Some(self.build_vfo()))) }) } fn set_freq<'a>( diff --git a/src/trx-server/trx-backend/src/lib.rs b/src/trx-server/trx-backend/src/lib.rs index b21df51..1d87fbb 100644 --- a/src/trx-server/trx-backend/src/lib.rs +++ b/src/trx-server/trx-backend/src/lib.rs @@ -3,17 +3,16 @@ // SPDX-License-Identifier: BSD-2-Clause use std::collections::HashMap; -use std::sync::{Arc, Mutex, OnceLock}; use trx_core::rig::RigCat; use trx_core::DynResult; mod dummy; -#[cfg(feature = "ft817")] -use trx_backend_ft817::Ft817; #[cfg(feature = "ft450d")] use trx_backend_ft450d::Ft450d; +#[cfg(feature = "ft817")] +use trx_backend_ft817::Ft817; /// Connection details for instantiating a rig backend. #[derive(Debug, Clone)] @@ -88,27 +87,6 @@ fn normalize_name(name: &str) -> String { .collect() } -/// Phase 3D: Plugin compatibility adapter - delegates to bootstrap context. -fn bootstrap_context() -> &'static Arc> { - static BOOTSTRAP_CONTEXT: OnceLock>> = OnceLock::new(); - BOOTSTRAP_CONTEXT.get_or_init(|| Arc::new(Mutex::new(RegistrationContext::new()))) -} - -/// Snapshot current plugin/bootstrap registrations into an owned context. -pub fn snapshot_bootstrap_context() -> RegistrationContext { - let ctx = bootstrap_context() - .lock() - .expect("backend context mutex poisoned"); - ctx.clone() -} - -/// Register a backend factory under a stable name (e.g. "ft817"). -/// Plugin compatibility: delegates to bootstrap context. -pub fn register_backend(name: &str, factory: BackendFactory) { - let mut ctx = bootstrap_context().lock().expect("backend context mutex poisoned"); - ctx.register_backend(name, factory); -} - /// Register all built-in backends enabled by features on a context. pub fn register_builtin_backends_on(context: &mut RegistrationContext) { context.register_backend("dummy", dummy_factory); @@ -118,40 +96,10 @@ pub fn register_builtin_backends_on(context: &mut RegistrationContext) { context.register_backend("ft450d", ft450d_factory); } -/// Register all built-in backends enabled by features (global, for plugin compatibility). -pub fn register_builtin_backends() { - register_backend("dummy", dummy_factory); - #[cfg(feature = "ft817")] - register_backend("ft817", ft817_factory); - #[cfg(feature = "ft450d")] - register_backend("ft450d", ft450d_factory); -} - fn dummy_factory(_access: RigAccess) -> DynResult> { Ok(Box::new(dummy::DummyRig::new())) } -/// Check whether a backend name is registered. -/// Plugin compatibility: reads from bootstrap context. -pub fn is_backend_registered(name: &str) -> bool { - let ctx = bootstrap_context().lock().expect("backend context mutex poisoned"); - ctx.is_backend_registered(name) -} - -/// List registered backend names. -/// Plugin compatibility: reads from bootstrap context. -pub fn registered_backends() -> Vec { - let ctx = bootstrap_context().lock().expect("backend context mutex poisoned"); - ctx.registered_backends() -} - -/// Instantiate a rig backend based on the selected name and access method. -/// Plugin compatibility: reads from bootstrap context. -pub fn build_rig(name: &str, access: RigAccess) -> DynResult> { - let ctx = bootstrap_context().lock().expect("backend context mutex poisoned"); - ctx.build_rig(name, access) -} - #[cfg(feature = "ft817")] fn ft817_factory(access: RigAccess) -> DynResult> { match access { diff --git a/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs b/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs index bc0145d..547883d 100644 --- a/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs @@ -325,7 +325,9 @@ impl Ft450d { async fn read_freq(&mut self) -> DynResult { let resp = self.query("FA;").await?; - let data = resp.strip_prefix("FA").ok_or("CAT freq response missing FA")?; + let data = resp + .strip_prefix("FA") + .ok_or("CAT freq response missing FA")?; let digits: String = data.chars().filter(|c| c.is_ascii_digit()).collect(); let freq: u64 = digits.parse().map_err(|_| "CAT freq parse failed")?; Ok(freq) @@ -333,7 +335,9 @@ impl Ft450d { async fn read_mode(&mut self) -> DynResult { let resp = self.query("MD0;").await?; - let data = resp.strip_prefix("MD").ok_or("CAT mode response missing MD")?; + let data = resp + .strip_prefix("MD") + .ok_or("CAT mode response missing MD")?; let code = data.chars().last().ok_or("CAT mode parse failed")?; Ok(decode_mode(code)) }