[refactor](workspace): complete remaining architecture phases
Bundle all pending repository updates, including plugin context de-globalization, runtime hardening, config validation, boundary tests, and supporting docs/scripts. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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::<RigRequest>(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::<RigRequest>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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<Mutex<RegistrationContext>> {
|
||||
static BOOTSTRAP_CONTEXT: OnceLock<Arc<Mutex<RegistrationContext>>> = 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<Box<dyn RigCat>> {
|
||||
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<String> {
|
||||
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<Box<dyn RigCat>> {
|
||||
let ctx = bootstrap_context().lock().expect("backend context mutex poisoned");
|
||||
ctx.build_rig(name, access)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ft817")]
|
||||
fn ft817_factory(access: RigAccess) -> DynResult<Box<dyn RigCat>> {
|
||||
match access {
|
||||
|
||||
@@ -325,7 +325,9 @@ impl Ft450d {
|
||||
|
||||
async fn read_freq(&mut self) -> DynResult<u64> {
|
||||
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<RigMode> {
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user