diff --git a/bins/bluestation-bs/src/main.rs b/bins/bluestation-bs/src/main.rs index b25dc63..93c4e53 100644 --- a/bins/bluestation-bs/src/main.rs +++ b/bins/bluestation-bs/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use tetra_config::{PhyBackend, SharedConfig, StackMode, toml_config}; +use tetra_config::bluestation::{PhyBackend, SharedConfig, StackMode, parsing}; use tetra_core::{TdmaTime, debug}; use tetra_entities::MessageRouter; use tetra_entities::brew::entity::BrewEntity; @@ -20,7 +20,7 @@ use tetra_entities::{ /// Load configuration file fn load_config_from_toml(cfg_path: &str) -> SharedConfig { - match toml_config::from_file(cfg_path) { + match parsing::from_file(cfg_path) { Ok(c) => c, Err(e) => { println!("Failed to load configuration from {}: {}", cfg_path, e); @@ -98,6 +98,7 @@ fn main() { let args = Args::parse(); let mut cfg = load_config_from_toml(&args.config); + eprintln!("Loaded configuration from {:?}", cfg.config()); let _log_guard = debug::setup_logging_default(cfg.config().debug_log.clone()); let mut router = match cfg.config().stack_mode { diff --git a/crates/tetra-config/src/bluestation/config.rs b/crates/tetra-config/src/bluestation/config.rs new file mode 100644 index 0000000..7da7aa3 --- /dev/null +++ b/crates/tetra-config/src/bluestation/config.rs @@ -0,0 +1,138 @@ +use serde::Deserialize; +use std::sync::{Arc, RwLock}; +use tetra_core::freqs::FreqInfo; + +use crate::bluestation::{CfgCellInfo, CfgNetInfo, CfgPhyIo, PhyBackend, StackState}; + +use super::sec_brew::CfgBrew; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub enum StackMode { + Bs, + Ms, + Mon, +} + +#[derive(Debug, Clone)] +pub struct StackConfig { + pub stack_mode: StackMode, + pub debug_log: Option, + + pub phy_io: CfgPhyIo, + pub net: CfgNetInfo, + pub cell: CfgCellInfo, + + /// Brew protocol (TetraPack/BrandMeister) configuration + pub brew: Option, +} + +impl StackConfig { + /// Validate that all required configuration fields are properly set. + pub fn validate(&self) -> Result<(), &str> { + // Check input device settings + match self.phy_io.backend { + PhyBackend::SoapySdr => { + let Some(ref soapy_cfg) = self.phy_io.soapysdr else { + return Err("soapysdr configuration must be provided for Soapysdr backend"); + }; + + // Validate that exactly one hardware configuration is present + let config_count = [ + soapy_cfg.io_cfg.iocfg_usrpb2xx.is_some(), + soapy_cfg.io_cfg.iocfg_limesdr.is_some(), + soapy_cfg.io_cfg.iocfg_sxceiver.is_some(), + ] + .iter() + .filter(|&&x| x) + .count(); + if config_count != 1 { + return Err( + "soapysdr backend requires exactly one hardware configuration (iocfg_usrpb2xx, iocfg_limesdr, or iocfg_sxceiver)", + ); + } + } + PhyBackend::None => {} // For testing + PhyBackend::Undefined => { + return Err("phy_io backend must be defined"); + } + }; + + // Sanity check on main carrier property fields in SYSINFO + if self.phy_io.backend == PhyBackend::SoapySdr { + let soapy_cfg = self + .phy_io + .soapysdr + .as_ref() + .expect("SoapySdr config must be set for SoapySdr PhyIo"); + + let Ok(freq_info) = FreqInfo::from_components( + self.cell.freq_band, + self.cell.main_carrier, + self.cell.freq_offset_hz, + self.cell.reverse_operation, + self.cell.duplex_spacing_id, + self.cell.custom_duplex_spacing, + ) else { + return Err("Invalid cell info frequency settings"); + }; + + let (dlfreq, ulfreq) = freq_info.get_freqs(); + + println!(" {:?}", freq_info); + println!(" Derived DL freq: {} Hz, UL freq: {} Hz\n", dlfreq, ulfreq); + + if soapy_cfg.dl_freq as u32 != dlfreq { + return Err("PhyIo DlFrequency does not match computed FreqInfo"); + }; + if soapy_cfg.ul_freq as u32 != ulfreq { + return Err("PhyIo UlFrequency does not match computed FreqInfo"); + }; + } + + Ok(()) + } +} + +/// Global shared configuration: immutable config + mutable state. +#[derive(Clone)] +pub struct SharedConfig { + /// Read-only configuration (immutable after construction). + cfg: Arc, + /// Mutable state guarded with RwLock (write by the stack, read by others). + state: Arc>, +} + +impl SharedConfig { + pub fn from_config(cfg: StackConfig) -> Self { + Self::from_parts(cfg, StackState::default()) + } + + pub fn from_parts(cfg: StackConfig, state: StackState) -> Self { + // Check config for validity before returning the SharedConfig object + match cfg.validate() { + Ok(_) => {} + Err(e) => panic!("Invalid stack configuration: {}", e), + } + + Self { + cfg: Arc::new(cfg), + state: Arc::new(RwLock::new(state)), + } + } + + /// Access immutable config. + pub fn config(&self) -> Arc { + Arc::clone(&self.cfg) + } + + /// Read guard for mutable state. + pub fn state_read(&self) -> std::sync::RwLockReadGuard<'_, StackState> { + self.state.read().expect("StackState RwLock blocked") + } + + /// Write guard for mutable state. + pub fn state_write(&self) -> std::sync::RwLockWriteGuard<'_, StackState> { + self.state.write().expect("StackState RwLock blocked") + } +} diff --git a/crates/tetra-config/src/bluestation/mod.rs b/crates/tetra-config/src/bluestation/mod.rs new file mode 100644 index 0000000..582dc3f --- /dev/null +++ b/crates/tetra-config/src/bluestation/mod.rs @@ -0,0 +1,23 @@ +pub mod parsing; +pub use parsing::*; + +pub mod config; +pub use config::*; + +pub mod sec_phy; +pub use sec_phy::*; + +pub mod sec_net; +pub use sec_net::*; + +pub mod sec_cell; +pub use sec_cell::*; + +pub mod sec_phy_soapy; +pub use sec_phy_soapy::*; + +pub mod sec_brew; +pub use sec_brew::*; + +pub mod state; +pub use state::*; diff --git a/crates/tetra-config/src/bluestation/parsing.rs b/crates/tetra-config/src/bluestation/parsing.rs new file mode 100644 index 0000000..1cdf1b1 --- /dev/null +++ b/crates/tetra-config/src/bluestation/parsing.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; + +use serde::Deserialize; +use toml::Value; + +use crate::bluestation::{CellInfoDto, NetInfoDto, cell_dto_to_cfg, net_dto_to_cfg}; + +use super::config::{SharedConfig, StackConfig, StackMode}; +use super::sec_brew::{CfgBrewDto, apply_brew_patch}; +use super::{PhyIoDto, StackState, phy_dto_to_cfg}; + +/// Build `SharedConfig` from a TOML configuration file +pub fn from_toml_str(toml_str: &str) -> Result> { + let root: TomlConfigRoot = toml::from_str(toml_str)?; + + // Various sanity checks + let expected_config_version = "0.5"; + if !root.config_version.eq(expected_config_version) { + return Err(format!( + "Unrecognized config_version: {}, expect {}", + root.config_version, expected_config_version + ) + .into()); + } + if !root.extra.is_empty() { + return Err(format!("Unrecognized top-level fields: {:?}", sorted_keys(&root.extra)).into()); + } + + if !root.phy_io.extra.is_empty() { + return Err(format!("Unrecognized fields: phy_io::{:?}", sorted_keys(&root.phy_io.extra)).into()); + } + if let Some(ref soapy) = root.phy_io.soapysdr { + if !soapy.extra.is_empty() { + return Err(format!("Unrecognized fields: phy_io.soapysdr::{:?}", sorted_keys(&soapy.extra)).into()); + } + } + if !root.net_info.extra.is_empty() { + return Err(format!("Unrecognized fields in net_info: {:?}", sorted_keys(&root.net_info.extra)).into()); + } + if !root.cell_info.extra.is_empty() { + return Err(format!("Unrecognized fields in cell_info: {:?}", sorted_keys(&root.cell_info.extra)).into()); + } + + // Optional brew section + if let Some(ref brew) = root.brew { + if !brew.extra.is_empty() { + return Err(format!("Unrecognized fields in brew config: {:?}", sorted_keys(&brew.extra)).into()); + } + } + + // Build config from required and optional values + let mut cfg = StackConfig { + stack_mode: root.stack_mode, + debug_log: root.debug_log, + phy_io: phy_dto_to_cfg(root.phy_io), + net: net_dto_to_cfg(root.net_info), + cell: cell_dto_to_cfg(root.cell_info), + brew: None, + }; + + if let Some(brew) = root.brew { + cfg.brew = Some(apply_brew_patch(brew)); + } + + // Mutable runtime state + let state = StackState::default(); + + Ok(SharedConfig::from_parts(cfg, state)) +} + +/// Build `SharedConfig` from any reader. +pub fn from_reader(reader: R) -> Result> { + let mut contents = String::new(); + let mut reader = BufReader::new(reader); + reader.read_to_string(&mut contents)?; + from_toml_str(&contents) +} + +/// Build `SharedConfig` from a file path. +pub fn from_file>(path: P) -> Result> { + let f = File::open(path)?; + let r = BufReader::new(f); + let cfg = from_reader(r)?; + Ok(cfg) +} + +fn sorted_keys(map: &HashMap) -> Vec<&str> { + let mut v: Vec<&str> = map.keys().map(|s| s.as_str()).collect(); + v.sort_unstable(); + v +} + +/// ----------------------- DTOs for input shape ----------------------- + +#[derive(Deserialize)] +struct TomlConfigRoot { + config_version: String, + stack_mode: StackMode, + debug_log: Option, + + phy_io: PhyIoDto, + net_info: NetInfoDto, + cell_info: CellInfoDto, + + brew: Option, + + #[serde(flatten)] + extra: HashMap, +} diff --git a/crates/tetra-config/src/stack_config_brew.rs b/crates/tetra-config/src/bluestation/sec_brew.rs similarity index 100% rename from crates/tetra-config/src/stack_config_brew.rs rename to crates/tetra-config/src/bluestation/sec_brew.rs diff --git a/crates/tetra-config/src/bluestation/sec_cell.rs b/crates/tetra-config/src/bluestation/sec_cell.rs new file mode 100644 index 0000000..47269dd --- /dev/null +++ b/crates/tetra-config/src/bluestation/sec_cell.rs @@ -0,0 +1,131 @@ +use serde::Deserialize; +use std::collections::HashMap; + +use tetra_core::ranges::SortedDisjointSsiRanges; +use toml::Value; + +#[derive(Debug, Clone)] +pub struct CfgCellInfo { + // 2 bits, from 18.4.2.1 D-MLE-SYNC + pub neighbor_cell_broadcast: u8, + // 2 bits, from 18.4.2.1 D-MLE-SYNC + pub late_entry_supported: bool, + + /// 12 bits, from MAC SYSINFO + pub main_carrier: u16, + /// 4 bits, from MAC SYSINFO + pub freq_band: u8, + /// Offset in Hz from 25kHz aligned carrier. Options: 0, 6250, -6250, 12500 Hz + /// Represented as 0-3 in SYSINFO + pub freq_offset_hz: i16, + /// Index in duplex setting table. Sent in SYSINFO. Maps to a specific duplex spacing in Hz. + /// Custom spacing can be provided optionally by setting + pub duplex_spacing_id: u8, + /// Custom duplex spacing in Hz, for users that use a modified, non-standard duplex spacing table. + pub custom_duplex_spacing: Option, + /// 1 bits, from MAC SYSINFO + pub reverse_operation: bool, + + // 14 bits, from 18.4.2.2 D-MLE-SYSINFO + pub location_area: u16, + // 16 bits, from 18.4.2.2 D-MLE-SYSINFO + pub subscriber_class: u16, + + // 1-bit service flags + pub registration: bool, + pub deregistration: bool, + pub priority_cell: bool, + pub no_minimum_mode: bool, + pub migration: bool, + pub system_wide_services: bool, + pub voice_service: bool, + pub circuit_mode_data_service: bool, + pub sndcp_service: bool, + pub aie_service: bool, + pub advanced_link: bool, + + // From SYNC + pub system_code: u8, + pub colour_code: u8, + pub sharing_mode: u8, + pub ts_reserved_frames: u8, + pub u_plane_dtx: bool, + pub frame_18_ext: bool, + + pub local_ssi_ranges: SortedDisjointSsiRanges, +} + +#[derive(Default, Deserialize)] +pub struct CellInfoDto { + pub main_carrier: u16, + pub freq_band: u8, + pub freq_offset: i16, + pub duplex_spacing: u8, + pub reverse_operation: bool, + pub custom_duplex_spacing: Option, + + pub location_area: u16, + + pub neighbor_cell_broadcast: Option, + pub late_entry_supported: Option, + pub subscriber_class: Option, + pub registration: Option, + pub deregistration: Option, + pub priority_cell: Option, + pub no_minimum_mode: Option, + pub migration: Option, + pub system_wide_services: Option, + pub voice_service: Option, + pub circuit_mode_data_service: Option, + pub sndcp_service: Option, + pub aie_service: Option, + pub advanced_link: Option, + + pub system_code: Option, + pub colour_code: Option, + pub sharing_mode: Option, + pub ts_reserved_frames: Option, + pub u_plane_dtx: Option, + pub frame_18_ext: Option, + + pub local_ssi_ranges: Option>, + + #[serde(flatten)] + pub extra: HashMap, +} + +pub fn cell_dto_to_cfg(ci: CellInfoDto) -> CfgCellInfo { + CfgCellInfo { + main_carrier: ci.main_carrier, + freq_band: ci.freq_band, + freq_offset_hz: ci.freq_offset, + duplex_spacing_id: ci.duplex_spacing, + reverse_operation: ci.reverse_operation, + custom_duplex_spacing: ci.custom_duplex_spacing, + location_area: ci.location_area, + neighbor_cell_broadcast: ci.neighbor_cell_broadcast.unwrap_or(0), + late_entry_supported: ci.late_entry_supported.unwrap_or(false), + subscriber_class: ci.subscriber_class.unwrap_or(65535), // All subscriber classes allowed + registration: ci.registration.unwrap_or(true), + deregistration: ci.deregistration.unwrap_or(true), + priority_cell: ci.priority_cell.unwrap_or(false), + no_minimum_mode: ci.no_minimum_mode.unwrap_or(false), + migration: ci.migration.unwrap_or(false), + system_wide_services: ci.system_wide_services.unwrap_or(false), + voice_service: ci.voice_service.unwrap_or(true), + circuit_mode_data_service: ci.circuit_mode_data_service.unwrap_or(false), + sndcp_service: ci.sndcp_service.unwrap_or(false), + aie_service: ci.aie_service.unwrap_or(false), + advanced_link: ci.advanced_link.unwrap_or(false), + system_code: ci.system_code.unwrap_or(3), // 3 = ETSI EN 300 392-2 V3.1.1 + colour_code: ci.colour_code.unwrap_or(0), + sharing_mode: ci.sharing_mode.unwrap_or(0), + ts_reserved_frames: ci.ts_reserved_frames.unwrap_or(0), + u_plane_dtx: ci.u_plane_dtx.unwrap_or(false), + frame_18_ext: ci.frame_18_ext.unwrap_or(false), + local_ssi_ranges: ci + .local_ssi_ranges + .map(SortedDisjointSsiRanges::from_vec_tuple) + .unwrap_or(SortedDisjointSsiRanges::from_vec_ssirange(vec![])), + } +} diff --git a/crates/tetra-config/src/bluestation/sec_net.rs b/crates/tetra-config/src/bluestation/sec_net.rs new file mode 100644 index 0000000..326cb2b --- /dev/null +++ b/crates/tetra-config/src/bluestation/sec_net.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; +use std::collections::HashMap; +use toml::Value; + +#[derive(Debug, Clone)] +pub struct CfgNetInfo { + /// 10 bits, from 18.4.2.1 D-MLE-SYNC + pub mcc: u16, + /// 14 bits, from 18.4.2.1 D-MLE-SYNC + pub mnc: u16, +} + +#[derive(Default, Deserialize)] +pub struct NetInfoDto { + pub mcc: u16, + pub mnc: u16, + + #[serde(flatten)] + pub extra: HashMap, +} + +pub fn net_dto_to_cfg(ni: NetInfoDto) -> CfgNetInfo { + CfgNetInfo { mcc: ni.mcc, mnc: ni.mnc } +} diff --git a/crates/tetra-config/src/bluestation/sec_phy.rs b/crates/tetra-config/src/bluestation/sec_phy.rs new file mode 100644 index 0000000..79b7028 --- /dev/null +++ b/crates/tetra-config/src/bluestation/sec_phy.rs @@ -0,0 +1,97 @@ +use std::collections::HashMap; + +use serde::Deserialize; +use toml::Value; + +use crate::bluestation::{CfgLimeSdr, CfgSoapySdr, CfgSxCeiver, CfgUsrpB2xx, SoapySdrDto, SoapySdrIoCfg}; + +/// The PHY layer backend type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub enum PhyBackend { + Undefined, + None, + SoapySdr, +} + +/// PHY layer I/O configuration +#[derive(Debug, Clone)] +pub struct CfgPhyIo { + /// Backend type: Soapysdr, File, or None + pub backend: PhyBackend, + + pub dl_tx_file: Option, + pub ul_rx_file: Option, + pub ul_input_file: Option, + pub dl_input_file: Option, + + /// For Soapysdr backend: SoapySDR configuration + pub soapysdr: Option, +} + +#[derive(Deserialize)] +pub struct PhyIoDto { + pub backend: PhyBackend, + + pub dl_tx_file: Option, + pub ul_rx_file: Option, + pub ul_input_file: Option, + pub dl_input_file: Option, + + pub soapysdr: Option, + + #[serde(flatten)] + pub extra: HashMap, +} + +pub fn phy_dto_to_cfg(src: PhyIoDto) -> CfgPhyIo { + let soapysdr = src.soapysdr.map(|soapy_dto| { + let mut soapy_cfg = CfgSoapySdr { + ul_freq: soapy_dto.rx_freq, + dl_freq: soapy_dto.tx_freq, + ppm_err: soapy_dto.ppm_err, + io_cfg: SoapySdrIoCfg::default(), + }; + + if let Some(usrp_dto) = soapy_dto.iocfg_usrpb2xx { + soapy_cfg.io_cfg.iocfg_usrpb2xx = Some(CfgUsrpB2xx { + rx_ant: usrp_dto.rx_ant, + tx_ant: usrp_dto.tx_ant, + rx_gain_pga: usrp_dto.rx_gain_pga, + tx_gain_pga: usrp_dto.tx_gain_pga, + }); + } + if let Some(lime_dto) = soapy_dto.iocfg_limesdr { + soapy_cfg.io_cfg.iocfg_limesdr = Some(CfgLimeSdr { + rx_ant: lime_dto.rx_ant, + tx_ant: lime_dto.tx_ant, + rx_gain_lna: lime_dto.rx_gain_lna, + rx_gain_tia: lime_dto.rx_gain_tia, + rx_gain_pga: lime_dto.rx_gain_pga, + tx_gain_pad: lime_dto.tx_gain_pad, + tx_gain_iamp: lime_dto.tx_gain_iamp, + }); + } + if let Some(sx_dto) = soapy_dto.iocfg_sxceiver { + soapy_cfg.io_cfg.iocfg_sxceiver = Some(CfgSxCeiver { + rx_ant: sx_dto.rx_ant, + tx_ant: sx_dto.tx_ant, + rx_gain_lna: sx_dto.rx_gain_lna, + rx_gain_pga: sx_dto.rx_gain_pga, + tx_gain_dac: sx_dto.tx_gain_dac, + tx_gain_mixer: sx_dto.tx_gain_mixer, + }); + } + + soapy_cfg + }); + + CfgPhyIo { + backend: src.backend, + dl_tx_file: src.dl_tx_file, + ul_rx_file: src.ul_rx_file, + ul_input_file: src.ul_input_file, + dl_input_file: src.dl_input_file, + soapysdr, + } +} diff --git a/crates/tetra-config/src/stack_config_soapy.rs b/crates/tetra-config/src/bluestation/sec_phy_soapy.rs similarity index 59% rename from crates/tetra-config/src/stack_config_soapy.rs rename to crates/tetra-config/src/bluestation/sec_phy_soapy.rs index bf14ddd..f913605 100644 --- a/crates/tetra-config/src/stack_config_soapy.rs +++ b/crates/tetra-config/src/bluestation/sec_phy_soapy.rs @@ -1,19 +1,18 @@ use serde::Deserialize; +use std::collections::HashMap; +use toml::Value; /// Configuration for different SDR hardware devices -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct SoapySdrIoCfg { /// USRP B2xx series configuration (B200, B210) - #[serde(default)] - pub iocfg_usrpb2xx: Option, + pub iocfg_usrpb2xx: Option, /// LimeSDR configuration - #[serde(default)] - pub iocfg_limesdr: Option, + pub iocfg_limesdr: Option, /// SXceiver configuration - #[serde(default)] - pub iocfg_sxceiver: Option, + pub iocfg_sxceiver: Option, } impl SoapySdrIoCfg { @@ -42,7 +41,7 @@ impl Default for SoapySdrIoCfg { /// Configuration for Ettus USRP B2xx series #[derive(Debug, Clone, Deserialize)] -pub struct UsrpB2xxCfg { +pub struct CfgUsrpB2xx { pub rx_ant: Option, pub tx_ant: Option, pub rx_gain_pga: Option, @@ -51,7 +50,7 @@ pub struct UsrpB2xxCfg { /// Configuration for LimeSDR #[derive(Debug, Clone, Deserialize)] -pub struct LimeSdrCfg { +pub struct CfgLimeSdr { pub rx_ant: Option, pub tx_ant: Option, pub rx_gain_lna: Option, @@ -63,7 +62,7 @@ pub struct LimeSdrCfg { /// Configuration for SXceiver #[derive(Debug, Clone, Deserialize)] -pub struct SXceiverCfg { +pub struct CfgSxCeiver { pub rx_ant: Option, pub tx_ant: Option, pub rx_gain_lna: Option, @@ -73,7 +72,7 @@ pub struct SXceiverCfg { } /// SoapySDR configuration -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone)] pub struct CfgSoapySdr { /// Uplink frequency in Hz pub ul_freq: f64, @@ -101,13 +100,56 @@ impl CfgSoapySdr { } } -impl Default for CfgSoapySdr { - fn default() -> Self { - Self { - ul_freq: 0.0, - dl_freq: 0.0, - ppm_err: None, - io_cfg: SoapySdrIoCfg::default(), - } - } +// impl Default for CfgSoapySdr { +// fn default() -> Self { +// Self { +// ul_freq: 0.0, +// dl_freq: 0.0, +// ppm_err: None, +// io_cfg: SoapySdrIoCfg::default(), +// } +// } +// } + +#[derive(Deserialize)] +pub struct SoapySdrDto { + pub rx_freq: f64, + pub tx_freq: f64, + pub ppm_err: Option, + + pub iocfg_usrpb2xx: Option, + pub iocfg_limesdr: Option, + pub iocfg_sxceiver: Option, + + #[serde(flatten)] + pub extra: HashMap, +} + +#[derive(Deserialize)] +pub struct UsrpB2xxDto { + pub rx_ant: Option, + pub tx_ant: Option, + pub rx_gain_pga: Option, + pub tx_gain_pga: Option, +} + +#[derive(Deserialize)] +pub struct LimeSdrDto { + pub rx_ant: Option, + pub tx_ant: Option, + pub rx_gain_lna: Option, + pub rx_gain_tia: Option, + pub rx_gain_pga: Option, + pub tx_gain_pad: Option, + pub tx_gain_iamp: Option, +} + +#[derive(Deserialize)] +pub struct SXceiverDto { + pub rx_ant: Option, + pub tx_ant: Option, + pub rx_gain_lna: Option, + pub rx_gain_pga: Option, + pub tx_gain_dac: Option, + pub tx_gain_mixer: Option, } diff --git a/crates/tetra-config/src/bluestation/state.rs b/crates/tetra-config/src/bluestation/state.rs new file mode 100644 index 0000000..171ef10 --- /dev/null +++ b/crates/tetra-config/src/bluestation/state.rs @@ -0,0 +1,18 @@ +use tetra_core::TimeslotAllocator; + +/// Mutable, stack-editable state (mutex-protected). +#[derive(Debug, Clone)] +pub struct StackState { + pub timeslot_alloc: TimeslotAllocator, + /// Backhaul/network connection to SwMI (e.g., Brew/TetraPack). False -> fallback mode. + pub network_connected: bool, +} + +impl Default for StackState { + fn default() -> Self { + Self { + timeslot_alloc: TimeslotAllocator::default(), + network_connected: false, + } + } +} diff --git a/crates/tetra-config/src/lib.rs b/crates/tetra-config/src/lib.rs index 00d96a2..8003c11 100644 --- a/crates/tetra-config/src/lib.rs +++ b/crates/tetra-config/src/lib.rs @@ -1,15 +1 @@ -//! TETRA configuration management -//! -//! This crate provides configuration loading and parsing for TETRA BlueStation: -//! - TOML configuration file parsing -//! - Stack configuration structures -//! - SoapySDR-specific configuration - -pub mod stack_config; -pub mod stack_config_brew; -pub mod stack_config_soapy; - -pub mod toml_config; - -pub use stack_config::*; -pub use toml_config::*; +pub mod bluestation; diff --git a/crates/tetra-config/src/stack_config.rs b/crates/tetra-config/src/stack_config.rs deleted file mode 100644 index f9328ef..0000000 --- a/crates/tetra-config/src/stack_config.rs +++ /dev/null @@ -1,328 +0,0 @@ -use serde::Deserialize; -use std::sync::{Arc, RwLock}; -use tetra_core::TimeslotAllocator; -use tetra_core::freqs::FreqInfo; -use tetra_core::ranges::SortedDisjointSsiRanges; - -use crate::stack_config_brew::CfgBrew; - -use super::stack_config_soapy::CfgSoapySdr; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub enum StackMode { - Bs, - Ms, - Mon, -} - -/// The PHY layer backend type -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub enum PhyBackend { - Undefined, - None, - SoapySdr, -} - -/// PHY layer I/O configuration -#[derive(Debug, Clone)] -pub struct CfgPhyIo { - /// Backend type: Soapysdr, File, or None - pub backend: PhyBackend, - - pub dl_tx_file: Option, - pub ul_rx_file: Option, - pub ul_input_file: Option, - pub dl_input_file: Option, - - /// For Soapysdr backend: SoapySDR configuration - pub soapysdr: Option, -} - -impl Default for CfgPhyIo { - fn default() -> Self { - Self { - backend: PhyBackend::Undefined, - dl_tx_file: None, - ul_rx_file: None, - ul_input_file: None, - dl_input_file: None, - soapysdr: None, - } - } -} - -#[derive(Debug, Clone)] -pub struct CfgNetInfo { - /// 10 bits, from 18.4.2.1 D-MLE-SYNC - pub mcc: u16, - /// 14 bits, from 18.4.2.1 D-MLE-SYNC - pub mnc: u16, -} - -#[derive(Debug, Clone)] -pub struct CfgCellInfo { - // 2 bits, from 18.4.2.1 D-MLE-SYNC - pub neighbor_cell_broadcast: u8, - // 2 bits, from 18.4.2.1 D-MLE-SYNC - pub cell_load_ca: u8, - // 1 bit, from 18.4.2.1 D-MLE-SYNC - pub late_entry_supported: bool, - - /// 12 bits, from MAC SYSINFO - pub main_carrier: u16, - /// 4 bits, from MAC SYSINFO - pub freq_band: u8, - /// Offset in Hz from 25kHz aligned carrier. Options: 0, 6250, -6250, 12500 Hz - /// Represented as 0-3 in SYSINFO - pub freq_offset_hz: i16, - /// Index in duplex setting table. Sent in SYSINFO. Maps to a specific duplex spacing in Hz. - /// Custom spacing can be provided optionally by setting - pub duplex_spacing_id: u8, - /// Custom duplex spacing in Hz, for users that use a modified, non-standard duplex spacing table. - pub custom_duplex_spacing: Option, - /// 1 bits, from MAC SYSINFO - pub reverse_operation: bool, - - // 14 bits, from 18.4.2.2 D-MLE-SYSINFO - pub location_area: u16, - // 16 bits, from 18.4.2.2 D-MLE-SYSINFO - pub subscriber_class: u16, - - // 1-bit service flags - pub registration: bool, - pub deregistration: bool, - pub priority_cell: bool, - pub no_minimum_mode: bool, - pub migration: bool, - pub system_wide_services: bool, - pub voice_service: bool, - pub circuit_mode_data_service: bool, - pub sndcp_service: bool, - pub aie_service: bool, - pub advanced_link: bool, - - // From SYNC - pub system_code: u8, - pub colour_code: u8, - pub sharing_mode: u8, - pub ts_reserved_frames: u8, - pub u_plane_dtx: bool, - pub frame_18_ext: bool, - - pub local_ssi_ranges: SortedDisjointSsiRanges, -} - -impl Default for CfgCellInfo { - fn default() -> Self { - Self { - freq_band: default_freq_band(), - main_carrier: default_main_carrier(), - freq_offset_hz: 0, - duplex_spacing_id: 0, - custom_duplex_spacing: None, - reverse_operation: false, - - neighbor_cell_broadcast: 0, - cell_load_ca: 0, - late_entry_supported: false, - location_area: 0, - subscriber_class: 0, - registration: true, - deregistration: true, - priority_cell: false, - no_minimum_mode: false, - migration: false, - system_wide_services: false, - voice_service: false, - circuit_mode_data_service: false, - sndcp_service: false, - aie_service: false, - advanced_link: false, - - system_code: 0, - colour_code: 0, - sharing_mode: 0, - ts_reserved_frames: 0, - u_plane_dtx: false, - frame_18_ext: false, - - local_ssi_ranges: SortedDisjointSsiRanges::from_vec_ssirange(vec![]), - } - } -} - -#[inline] -fn default_freq_band() -> u8 { - 4 -} - -#[inline] -fn default_main_carrier() -> u16 { - 1521 -} - -#[derive(Debug, Clone)] -pub struct StackConfig { - pub stack_mode: StackMode, - pub debug_log: Option, - - pub phy_io: CfgPhyIo, - - /// Network info is REQUIRED - no default provided - pub net: CfgNetInfo, - - pub cell: CfgCellInfo, - - /// Brew protocol (TetraPack/BrandMeister) configuration - pub brew: Option, -} - -impl StackConfig { - pub fn new(mode: StackMode, mcc: u16, mnc: u16) -> Self { - StackConfig { - stack_mode: mode, - debug_log: None, - phy_io: CfgPhyIo::default(), - net: CfgNetInfo { mcc, mnc }, - cell: CfgCellInfo::default(), - - brew: None, - } - } - - /// Validate that all required configuration fields are properly set. - pub fn validate(&self) -> Result<(), &str> { - // Check input device settings - match self.phy_io.backend { - PhyBackend::SoapySdr => { - let Some(ref soapy_cfg) = self.phy_io.soapysdr else { - return Err("soapysdr configuration must be provided for Soapysdr backend"); - }; - - // Validate that exactly one hardware configuration is present - let config_count = [ - soapy_cfg.io_cfg.iocfg_usrpb2xx.is_some(), - soapy_cfg.io_cfg.iocfg_limesdr.is_some(), - soapy_cfg.io_cfg.iocfg_sxceiver.is_some(), - ] - .iter() - .filter(|&&x| x) - .count(); - if config_count != 1 { - return Err( - "soapysdr backend requires exactly one hardware configuration (iocfg_usrpb2xx, iocfg_limesdr, or iocfg_sxceiver)", - ); - } - } - PhyBackend::None => {} // For testing - PhyBackend::Undefined => { - return Err("phy_io backend must be defined"); - } - }; - - // Sanity check on main carrier property fields in SYSINFO - if self.phy_io.backend == PhyBackend::SoapySdr { - let soapy_cfg = self - .phy_io - .soapysdr - .as_ref() - .expect("SoapySdr config must be set for SoapySdr PhyIo"); - - // let Ok(freqinfo) = FreqInfo::from_dlul_freqs(soapy_cfg.dl_freq as u32, soapy_cfg.ul_freq as u32) else { - // return Err("Invalid PhyIo DL/UL frequencies (can't map to TETRA SYSINFO settings)"); - // }; - let Ok(freq_info) = FreqInfo::from_components( - self.cell.freq_band, - self.cell.main_carrier, - self.cell.freq_offset_hz, - self.cell.reverse_operation, - self.cell.duplex_spacing_id, - self.cell.custom_duplex_spacing, - ) else { - return Err("Invalid cell info frequency settings"); - }; - - let (dlfreq, ulfreq) = freq_info.get_freqs(); - - println!(" {:?}", freq_info); - println!(" Derived DL freq: {} Hz, UL freq: {} Hz\n", dlfreq, ulfreq); - - if soapy_cfg.dl_freq as u32 != dlfreq { - return Err("PhyIo DlFrequency does not match computed FreqInfo"); - }; - if soapy_cfg.ul_freq as u32 != ulfreq { - return Err("PhyIo UlFrequency does not match computed FreqInfo"); - }; - } - - Ok(()) - } -} - -/// Mutable, stack-editable state (mutex-protected). -#[derive(Debug, Clone)] -pub struct StackState { - pub cell_load_ca: u8, - pub timeslot_alloc: TimeslotAllocator, - /// Backhaul/network connection to SwMI (e.g., Brew/TetraPack). False -> fallback mode. - pub network_connected: bool, -} - -impl Default for StackState { - fn default() -> Self { - Self { - cell_load_ca: 0, - timeslot_alloc: TimeslotAllocator::default(), - network_connected: false, - } - } -} - -/// Global shared configuration: immutable config + mutable state. -#[derive(Clone)] -pub struct SharedConfig { - /// Read-only configuration (immutable after construction). - cfg: Arc, - /// Mutable state guarded with RwLock (write by the stack, read by others). - state: Arc>, -} - -impl SharedConfig { - pub fn new(mode: StackMode, mcc: u16, mnc: u16) -> Self { - Self::from_config(StackConfig::new(mode, mcc, mnc)) - } - - pub fn from_config(cfg: StackConfig) -> Self { - Self::from_parts(cfg, StackState::default()) - } - - pub fn from_parts(cfg: StackConfig, state: StackState) -> Self { - // Check config for validity before returning the SharedConfig object - match cfg.validate() { - Ok(_) => {} - Err(e) => panic!("Invalid stack configuration: {}", e), - } - - Self { - cfg: Arc::new(cfg), - state: Arc::new(RwLock::new(state)), - } - } - - /// Access immutable config. - pub fn config(&self) -> Arc { - Arc::clone(&self.cfg) - } - - /// Read guard for mutable state. - pub fn state_read(&self) -> std::sync::RwLockReadGuard<'_, StackState> { - self.state.read().expect("StackState RwLock blocked") - } - - /// Write guard for mutable state. - pub fn state_write(&self) -> std::sync::RwLockWriteGuard<'_, StackState> { - self.state.write().expect("StackState RwLock blocked") - } -} diff --git a/crates/tetra-config/src/toml_config.rs b/crates/tetra-config/src/toml_config.rs deleted file mode 100644 index adb830c..0000000 --- a/crates/tetra-config/src/toml_config.rs +++ /dev/null @@ -1,402 +0,0 @@ -use std::collections::HashMap; -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::Path; - -use serde::Deserialize; -use tetra_core::ranges::SortedDisjointSsiRanges; -use toml::Value; - -use super::stack_config_brew::{CfgBrewDto, apply_brew_patch}; - -use super::stack_config::{CfgCellInfo, CfgNetInfo, CfgPhyIo, PhyBackend, SharedConfig, StackConfig, StackMode, StackState}; -use super::stack_config_soapy::{CfgSoapySdr, LimeSdrCfg, SXceiverCfg, UsrpB2xxCfg}; - -/// Build `SharedConfig` from a TOML configuration file -pub fn from_toml_str(toml_str: &str) -> Result> { - let root: TomlConfigRoot = toml::from_str(toml_str)?; - - // Various sanity checks - let expected_config_version = "0.5"; - if !root.config_version.eq(expected_config_version) { - return Err(format!( - "Unrecognized config_version: {}, expect {}", - root.config_version, expected_config_version - ) - .into()); - } - if !root.extra.is_empty() { - return Err(format!("Unrecognized top-level fields: {:?}", sorted_keys(&root.extra)).into()); - } - if let Some(ref phy) = root.phy_io { - if !phy.extra.is_empty() { - return Err(format!("Unrecognized fields: phy_io::{:?}", sorted_keys(&phy.extra)).into()); - } - if let Some(ref soapy) = phy.soapysdr { - if !soapy.extra.is_empty() { - return Err(format!("Unrecognized fields: phy_io.soapysdr::{:?}", sorted_keys(&soapy.extra)).into()); - } - } - } - - if let Some(ref brew) = root.brew { - if !brew.extra.is_empty() { - return Err(format!("Unrecognized fields in brew config: {:?}", sorted_keys(&brew.extra)).into()); - } - } - - if !root.net_info.extra.is_empty() { - return Err(format!("Unrecognized fields in net_info: {:?}", sorted_keys(&root.net_info.extra)).into()); - } - if let Some(ref ci) = root.cell_info { - if !ci.extra.is_empty() { - return Err(format!("Unrecognized fields in cell_info: {:?}", sorted_keys(&ci.extra)).into()); - } - } - if let Some(ref ss) = root.stack_state { - if !ss.extra.is_empty() { - return Err(format!("Unrecognized fields in stack_state: {:?}", sorted_keys(&ss.extra)).into()); - } - } - - // Build config from required and optional values - let mut cfg = StackConfig { - stack_mode: root.stack_mode, - debug_log: root.debug_log, - phy_io: CfgPhyIo::default(), - net: CfgNetInfo { - mcc: root.net_info.mcc, - mnc: root.net_info.mnc, - }, - cell: CfgCellInfo::default(), - brew: None, - }; - - // Handle new phy_io structure - if let Some(phy) = root.phy_io { - apply_phy_io_patch(&mut cfg.phy_io, phy); - } - - if let Some(ci) = root.cell_info { - apply_cell_info_patch(&mut cfg.cell, ci); - } - - if let Some(brew) = root.brew { - cfg.brew = Some(apply_brew_patch(brew)); - } - - // Mutable runtime state. Currently just a placeholder and not yet actually used - let mut state = StackState::default(); - if let Some(ss) = root.stack_state { - if let Some(v) = ss.cell_load_ca { - state.cell_load_ca = v; - } - } - - Ok(SharedConfig::from_parts(cfg, state)) -} - -/// Build `SharedConfig` from any reader. -pub fn from_reader(reader: R) -> Result> { - let mut contents = String::new(); - let mut reader = BufReader::new(reader); - reader.read_to_string(&mut contents)?; - from_toml_str(&contents) -} - -/// Build `SharedConfig` from a file path. -pub fn from_file>(path: P) -> Result> { - let f = File::open(path)?; - let r = BufReader::new(f); - let cfg = from_reader(r)?; - Ok(cfg) -} - -fn apply_phy_io_patch(dst: &mut CfgPhyIo, src: PhyIoDto) { - dst.backend = src.backend; - - dst.dl_tx_file = src.dl_tx_file; - dst.ul_rx_file = src.ul_rx_file; - dst.ul_input_file = src.ul_input_file; - dst.dl_input_file = src.dl_input_file; - - if let Some(soapy_dto) = src.soapysdr { - let mut soapy_cfg = CfgSoapySdr::default(); - soapy_cfg.ul_freq = soapy_dto.rx_freq; - soapy_cfg.dl_freq = soapy_dto.tx_freq; - soapy_cfg.ppm_err = soapy_dto.ppm_err; - - // Apply hardware-specific configurations - if let Some(usrp_dto) = soapy_dto.iocfg_usrpb2xx { - soapy_cfg.io_cfg.iocfg_usrpb2xx = Some(UsrpB2xxCfg { - rx_ant: usrp_dto.rx_ant, - tx_ant: usrp_dto.tx_ant, - rx_gain_pga: usrp_dto.rx_gain_pga, - tx_gain_pga: usrp_dto.tx_gain_pga, - }); - } - - if let Some(lime_dto) = soapy_dto.iocfg_limesdr { - soapy_cfg.io_cfg.iocfg_limesdr = Some(LimeSdrCfg { - rx_ant: lime_dto.rx_ant, - tx_ant: lime_dto.tx_ant, - rx_gain_lna: lime_dto.rx_gain_lna, - rx_gain_tia: lime_dto.rx_gain_tia, - rx_gain_pga: lime_dto.rx_gain_pga, - tx_gain_pad: lime_dto.tx_gain_pad, - tx_gain_iamp: lime_dto.tx_gain_iamp, - }); - } - - if let Some(sx_dto) = soapy_dto.iocfg_sxceiver { - soapy_cfg.io_cfg.iocfg_sxceiver = Some(SXceiverCfg { - rx_ant: sx_dto.rx_ant, - tx_ant: sx_dto.tx_ant, - rx_gain_lna: sx_dto.rx_gain_lna, - rx_gain_pga: sx_dto.rx_gain_pga, - tx_gain_dac: sx_dto.tx_gain_dac, - tx_gain_mixer: sx_dto.tx_gain_mixer, - }); - } - - dst.soapysdr = Some(soapy_cfg); - } -} - -fn apply_cell_info_patch(dst: &mut CfgCellInfo, ci: CellInfoDto) { - dst.main_carrier = ci.main_carrier; - dst.freq_band = ci.freq_band; - dst.freq_offset_hz = ci.freq_offset; - dst.duplex_spacing_id = ci.duplex_spacing; - dst.reverse_operation = ci.reverse_operation; - - // Option - dst.custom_duplex_spacing = ci.custom_duplex_spacing; - - dst.location_area = ci.location_area; - - if let Some(v) = ci.neighbor_cell_broadcast { - dst.neighbor_cell_broadcast = v; - } - if let Some(v) = ci.cell_load_ca { - dst.cell_load_ca = v; - } - if let Some(v) = ci.late_entry_supported { - dst.late_entry_supported = v; - } - if let Some(v) = ci.subscriber_class { - dst.subscriber_class = v; - } - if let Some(v) = ci.registration { - dst.registration = v; - } - if let Some(v) = ci.deregistration { - dst.deregistration = v; - } - if let Some(v) = ci.priority_cell { - dst.priority_cell = v; - } - if let Some(v) = ci.no_minimum_mode { - dst.no_minimum_mode = v; - } - if let Some(v) = ci.migration { - dst.migration = v; - } - if let Some(v) = ci.system_wide_services { - dst.system_wide_services = v; - } - if let Some(v) = ci.voice_service { - dst.voice_service = v; - } - if let Some(v) = ci.circuit_mode_data_service { - dst.circuit_mode_data_service = v; - } - if let Some(v) = ci.sndcp_service { - dst.sndcp_service = v; - } - if let Some(v) = ci.aie_service { - dst.aie_service = v; - } - if let Some(v) = ci.advanced_link { - dst.advanced_link = v; - } - if let Some(v) = ci.system_code { - dst.system_code = v; - } - if let Some(v) = ci.colour_code { - dst.colour_code = v; - } - if let Some(v) = ci.sharing_mode { - dst.sharing_mode = v; - } - if let Some(v) = ci.ts_reserved_frames { - dst.ts_reserved_frames = v; - } - if let Some(v) = ci.u_plane_dtx { - dst.u_plane_dtx = v; - } - if let Some(v) = ci.frame_18_ext { - dst.frame_18_ext = v; - } - - if let Some(ranges) = ci.local_ssi_ranges { - dst.local_ssi_ranges = SortedDisjointSsiRanges::from_vec_tuple(ranges) - } -} - -fn sorted_keys(map: &HashMap) -> Vec<&str> { - let mut v: Vec<&str> = map.keys().map(|s| s.as_str()).collect(); - v.sort_unstable(); - v -} - -/// ----------------------- DTOs for input shape ----------------------- - -#[derive(Deserialize)] -struct TomlConfigRoot { - config_version: String, - stack_mode: StackMode, - debug_log: Option, - - // New phy_io structure - #[serde(default)] - phy_io: Option, - - #[serde(default)] - net_info: NetInfoDto, - - #[serde(default)] - cell_info: Option, - - #[serde(default)] - stack_state: Option, - - #[serde(default)] - brew: Option, - - #[serde(flatten)] - extra: HashMap, -} - -#[derive(Deserialize)] -struct PhyIoDto { - pub backend: PhyBackend, - - dl_tx_file: Option, - ul_rx_file: Option, - ul_input_file: Option, - dl_input_file: Option, - - #[serde(default)] - pub soapysdr: Option, - - #[serde(flatten)] - extra: HashMap, -} - -#[derive(Deserialize)] -struct SoapySdrDto { - pub rx_freq: f64, - pub tx_freq: f64, - pub ppm_err: Option, - - #[serde(default)] - pub iocfg_usrpb2xx: Option, - - #[serde(default)] - pub iocfg_limesdr: Option, - - #[serde(default)] - pub iocfg_sxceiver: Option, - - #[serde(flatten)] - extra: HashMap, -} - -#[derive(Deserialize)] -struct UsrpB2xxDto { - pub rx_ant: Option, - pub tx_ant: Option, - pub rx_gain_pga: Option, - pub tx_gain_pga: Option, -} - -#[derive(Deserialize)] -struct LimeSdrDto { - pub rx_ant: Option, - pub tx_ant: Option, - pub rx_gain_lna: Option, - pub rx_gain_tia: Option, - pub rx_gain_pga: Option, - pub tx_gain_pad: Option, - pub tx_gain_iamp: Option, -} - -#[derive(Deserialize)] -struct SXceiverDto { - pub rx_ant: Option, - pub tx_ant: Option, - pub rx_gain_lna: Option, - pub rx_gain_pga: Option, - pub tx_gain_dac: Option, - pub tx_gain_mixer: Option, -} - -#[derive(Default, Deserialize)] -struct NetInfoDto { - pub mcc: u16, - pub mnc: u16, - - #[serde(flatten)] - extra: HashMap, -} - -#[derive(Default, Deserialize)] -struct CellInfoDto { - pub main_carrier: u16, - pub freq_band: u8, - pub freq_offset: i16, - pub duplex_spacing: u8, - pub reverse_operation: bool, - pub custom_duplex_spacing: Option, - - pub location_area: u16, - - pub neighbor_cell_broadcast: Option, - pub cell_load_ca: Option, - pub late_entry_supported: Option, - - pub subscriber_class: Option, - - pub registration: Option, - pub deregistration: Option, - pub priority_cell: Option, - pub no_minimum_mode: Option, - pub migration: Option, - pub system_wide_services: Option, - pub voice_service: Option, - pub circuit_mode_data_service: Option, - pub sndcp_service: Option, - pub aie_service: Option, - pub advanced_link: Option, - - pub system_code: Option, - pub colour_code: Option, - pub sharing_mode: Option, - pub ts_reserved_frames: Option, - pub u_plane_dtx: Option, - pub frame_18_ext: Option, - - pub local_ssi_ranges: Option>, - - #[serde(flatten)] - extra: HashMap, -} - -#[derive(Default, Deserialize)] -struct StackStatePatch { - pub cell_load_ca: Option, - - #[serde(flatten)] - extra: HashMap, -} diff --git a/crates/tetra-entities/src/brew/components/brew_routable.rs b/crates/tetra-entities/src/brew/components/brew_routable.rs index 73e2686..037d036 100644 --- a/crates/tetra-entities/src/brew/components/brew_routable.rs +++ b/crates/tetra-entities/src/brew/components/brew_routable.rs @@ -1,4 +1,4 @@ -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; /// Returns true if the Brew component is active #[inline] diff --git a/crates/tetra-entities/src/brew/entity.rs b/crates/tetra-entities/src/brew/entity.rs index 2107e48..d8596dc 100644 --- a/crates/tetra-entities/src/brew/entity.rs +++ b/crates/tetra-entities/src/brew/entity.rs @@ -5,11 +5,10 @@ use std::thread; use std::time::{Duration, Instant}; use crossbeam_channel::{Receiver, Sender, unbounded}; -use tetra_config::stack_config_brew::CfgBrew; use uuid::Uuid; use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::{CfgBrew, SharedConfig}; use tetra_core::{Sap, TdmaTime, tetra_entities::TetraEntity}; use tetra_saps::control::brew::{BrewSubscriberAction, MmSubscriberUpdate}; use tetra_saps::{SapMsg, SapMsgInner, control::call_control::CallControl, tmd::TmdCircuitDataReq}; diff --git a/crates/tetra-entities/src/brew/worker.rs b/crates/tetra-entities/src/brew/worker.rs index 88d5c8d..2a4a8bc 100644 --- a/crates/tetra-entities/src/brew/worker.rs +++ b/crates/tetra-entities/src/brew/worker.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use crossbeam_channel::{Receiver, Sender}; -use tetra_config::SharedConfig; -use tetra_config::stack_config_brew::CfgBrew; +use tetra_config::bluestation::CfgBrew; +use tetra_config::bluestation::SharedConfig; use tungstenite::{Message, WebSocket, stream::MaybeTlsStream}; use uuid::Uuid; diff --git a/crates/tetra-entities/src/cmce/cmce_bs.rs b/crates/tetra-entities/src/cmce/cmce_bs.rs index 2f91335..8dbfe53 100644 --- a/crates/tetra-entities/src/cmce/cmce_bs.rs +++ b/crates/tetra-entities/src/cmce/cmce_bs.rs @@ -1,5 +1,5 @@ use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{Sap, TdmaTime, unimplemented_log}; use tetra_saps::{SapMsg, SapMsgInner}; diff --git a/crates/tetra-entities/src/cmce/cmce_ms.rs b/crates/tetra-entities/src/cmce/cmce_ms.rs index 6ddd1df..68b8398 100644 --- a/crates/tetra-entities/src/cmce/cmce_ms.rs +++ b/crates/tetra-entities/src/cmce/cmce_ms.rs @@ -1,5 +1,5 @@ use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::Sap; use tetra_core::tetra_entities::TetraEntity; use tetra_saps::{SapMsg, SapMsgInner}; diff --git a/crates/tetra-entities/src/cmce/subentities/cc_bs.rs b/crates/tetra-entities/src/cmce/subentities/cc_bs.rs index b9b677a..d2f2ad2 100644 --- a/crates/tetra-entities/src/cmce/subentities/cc_bs.rs +++ b/crates/tetra-entities/src/cmce/subentities/cc_bs.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::{BitBuffer, Direction, Sap, SsiType, TdmaTime, TetraAddress, tetra_entities::TetraEntity, unimplemented_log}; use tetra_core::{TimeslotOwner, TxReceipt, TxReporter}; use tetra_pdus::cmce::enums::disconnect_cause::DisconnectCause; diff --git a/crates/tetra-entities/src/entity_trait.rs b/crates/tetra-entities/src/entity_trait.rs index b56a6f9..9714e83 100644 --- a/crates/tetra-entities/src/entity_trait.rs +++ b/crates/tetra-entities/src/entity_trait.rs @@ -1,6 +1,6 @@ use crate::MessageQueue; use as_any::AsAny; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::{TdmaTime, tetra_entities::TetraEntity}; use tetra_saps::SapMsg; diff --git a/crates/tetra-entities/src/llc/llc_bs_ms.rs b/crates/tetra-entities/src/llc/llc_bs_ms.rs index b7396d1..10341ae 100644 --- a/crates/tetra-entities/src/llc/llc_bs_ms.rs +++ b/crates/tetra-entities/src/llc/llc_bs_ms.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::panic; use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, TxReporter, unimplemented_log}; use tetra_saps::lcmc::enums::alloc_type::ChanAllocType; @@ -29,6 +29,8 @@ pub struct ExpectedInAck { pub ts: u8, /// Optional TxReporter, used to acknowledge reception by target MS pub tx_reporter: Option, + // Optional retransmission buffer, to allow for automatic retransmission of the PDU if no acknowledgement is received + pub retransmission_buf: Option, } /// Struct that maintains state for an ACK we still need to send back. @@ -102,6 +104,7 @@ impl Llc { addr, ts: t.t, tx_reporter, + retransmission_buf: None, }); } diff --git a/crates/tetra-entities/src/lmac/lmac_bs.rs b/crates/tetra-entities/src/lmac/lmac_bs.rs index 30c56d5..6035f18 100644 --- a/crates/tetra-entities/src/lmac/lmac_bs.rs +++ b/crates/tetra-entities/src/lmac/lmac_bs.rs @@ -1,4 +1,4 @@ -use tetra_config::{SharedConfig, StackMode}; +use tetra_config::bluestation::{SharedConfig, StackMode}; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BurstType, PhyBlockNum, PhysicalChannel, Sap, TdmaTime, TrainingSequence}; use tetra_saps::tmv::TmvUnitdataInd; diff --git a/crates/tetra-entities/src/lmac/lmac_ms.rs b/crates/tetra-entities/src/lmac/lmac_ms.rs index c52afec..605ae86 100644 --- a/crates/tetra-entities/src/lmac/lmac_ms.rs +++ b/crates/tetra-entities/src/lmac/lmac_ms.rs @@ -1,5 +1,5 @@ use crate::{MessagePrio, MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{PhyBlockNum, PhyBlockType, Sap, TdmaTime, unimplemented_log}; use tetra_saps::tmv::TmvUnitdataInd; diff --git a/crates/tetra-entities/src/messagerouter.rs b/crates/tetra-entities/src/messagerouter.rs index 48bd352..08a13f0 100644 --- a/crates/tetra-entities/src/messagerouter.rs +++ b/crates/tetra-entities/src/messagerouter.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::{TdmaTime, tetra_entities::TetraEntity}; use tetra_saps::SapMsg; diff --git a/crates/tetra-entities/src/mle/mle_bs_ms.rs b/crates/tetra-entities/src/mle/mle_bs_ms.rs index 07995e2..0bb6ba8 100644 --- a/crates/tetra-entities/src/mle/mle_bs_ms.rs +++ b/crates/tetra-entities/src/mle/mle_bs_ms.rs @@ -1,6 +1,6 @@ use crate::mle::components::mle_router::MleRouter; use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Sap, unimplemented_log}; use tetra_saps::lcmc::LcmcMleUnitdataInd; diff --git a/crates/tetra-entities/src/mm/mm_bs.rs b/crates/tetra-entities/src/mm/mm_bs.rs index 24a7ca5..5ab2744 100644 --- a/crates/tetra-entities/src/mm/mm_bs.rs +++ b/crates/tetra-entities/src/mm/mm_bs.rs @@ -1,5 +1,5 @@ use crate::{MessageQueue, TetraEntityTrait, brew}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, assert_warn, unimplemented_log}; use tetra_saps::control::brew::{BrewSubscriberAction, MmSubscriberUpdate}; diff --git a/crates/tetra-entities/src/mm/mm_ms.rs b/crates/tetra-entities/src/mm/mm_ms.rs index def0e64..2b54e45 100644 --- a/crates/tetra-entities/src/mm/mm_ms.rs +++ b/crates/tetra-entities/src/mm/mm_ms.rs @@ -1,5 +1,5 @@ use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{Sap, unimplemented_log}; use tetra_saps::{SapMsg, SapMsgInner}; diff --git a/crates/tetra-entities/src/network/netentity.rs b/crates/tetra-entities/src/network/netentity.rs index 3f57296..229226e 100644 --- a/crates/tetra-entities/src/network/netentity.rs +++ b/crates/tetra-entities/src/network/netentity.rs @@ -1,7 +1,7 @@ use crossbeam_channel::{Receiver, Sender, unbounded}; use std::{marker::PhantomData, thread}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::{TdmaTime, tetra_common::Sap, tetra_entities::TetraEntity}; use tetra_saps::SapMsg; diff --git a/crates/tetra-entities/src/phy/components/soapy_dev.rs b/crates/tetra-entities/src/phy/components/soapy_dev.rs index 62a602e..e4cefc0 100644 --- a/crates/tetra-entities/src/phy/components/soapy_dev.rs +++ b/crates/tetra-entities/src/phy/components/soapy_dev.rs @@ -2,8 +2,8 @@ //! between SDR device and modulator/demodulator code. use rustfft; -use tetra_config::SharedConfig; -use tetra_config::StackMode; +use tetra_config::bluestation::SharedConfig; +use tetra_config::bluestation::StackMode; use tetra_pdus::phy::traits::rxtx_dev::RxSlotBits; use tetra_pdus::phy::traits::rxtx_dev::RxTxDev; diff --git a/crates/tetra-entities/src/phy/components/soapyio.rs b/crates/tetra-entities/src/phy/components/soapyio.rs index 84ae307..39e4d70 100644 --- a/crates/tetra-entities/src/phy/components/soapyio.rs +++ b/crates/tetra-entities/src/phy/components/soapyio.rs @@ -1,5 +1,5 @@ use soapysdr; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_pdus::phy::traits::rxtx_dev::RxTxDevError; diff --git a/crates/tetra-entities/src/phy/phy_bs.rs b/crates/tetra-entities/src/phy/phy_bs.rs index 55f6eec..a0e0701 100644 --- a/crates/tetra-entities/src/phy/phy_bs.rs +++ b/crates/tetra-entities/src/phy/phy_bs.rs @@ -1,7 +1,7 @@ use crossbeam_channel::Sender; use std::panic; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, BurstType, PhyBlockNum, PhyBlockType, Sap, TdmaTime, TrainingSequence}; use tetra_pdus::phy::traits::rxtx_dev::RxBurstBits; diff --git a/crates/tetra-entities/src/sndcp/sndcp_bs.rs b/crates/tetra-entities/src/sndcp/sndcp_bs.rs index b422aea..59a2957 100644 --- a/crates/tetra-entities/src/sndcp/sndcp_bs.rs +++ b/crates/tetra-entities/src/sndcp/sndcp_bs.rs @@ -1,5 +1,5 @@ use crate::{MessageQueue, TetraEntityTrait}; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{Sap, unimplemented_log}; use tetra_saps::SapMsg; diff --git a/crates/tetra-entities/src/umac/umac_bs.rs b/crates/tetra-entities/src/umac/umac_bs.rs index e6dc15b..c0da565 100644 --- a/crates/tetra-entities/src/umac/umac_bs.rs +++ b/crates/tetra-entities/src/umac/umac_bs.rs @@ -1,6 +1,6 @@ use std::panic; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::freqs::FreqInfo; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Direction, PhyBlockNum, Sap, SsiType, TdmaTime, TetraAddress, Todo, assert_warn, unimplemented_log}; @@ -113,13 +113,13 @@ impl UmacBs { freq_offset_index: FreqInfo::freq_offset_hz_to_id(c.cell.freq_offset_hz).unwrap(), duplex_spacing: c.cell.duplex_spacing_id, reverse_operation: c.cell.reverse_operation, - num_of_csch: 0, + num_of_csch: 0, // Common secondary control channels ms_txpwr_max_cell: 5, rxlev_access_min: 3, access_parameter: 7, radio_dl_timeout: 3, cck_id: None, - hyperframe_number: Some(0), + hyperframe_number: Some(0), // Updated dynamically in scheduler option_field: SysinfoOptFieldFlag::DefaultDefForAccCodeA, ts_common_frames: None, default_access_code: Some(def_access), @@ -148,7 +148,7 @@ impl UmacBs { let system_wide_services = Self::get_system_wide_services_state(config); let mle_sysinfo_pdu = DMleSysinfo { location_area: c.cell.location_area, - subscriber_class: 65535, // All subscriber classes allowed + subscriber_class: c.cell.subscriber_class, bs_service_details: BsServiceDetails { registration: c.cell.registration, deregistration: c.cell.deregistration, @@ -156,29 +156,29 @@ impl UmacBs { no_minimum_mode: c.cell.no_minimum_mode, migration: c.cell.migration, system_wide_services, - voice_service: true, - circuit_mode_data_service: false, - sndcp_service: false, - aie_service: false, - advanced_link: false, + voice_service: c.cell.voice_service, + circuit_mode_data_service: c.cell.circuit_mode_data_service, + sndcp_service: c.cell.sndcp_service, + aie_service: c.cell.aie_service, + advanced_link: c.cell.advanced_link, }, }; let mac_sync_pdu = MacSync { - system_code: 1, + system_code: c.cell.system_code, colour_code: c.cell.colour_code, - time: TdmaTime::default(), - sharing_mode: 0, // Continuous transmission - ts_reserved_frames: 0, - u_plane_dtx: false, - frame_18_ext: false, + time: TdmaTime::default(), // replaced dynamically in scheduler + sharing_mode: c.cell.sharing_mode, + ts_reserved_frames: c.cell.ts_reserved_frames, + u_plane_dtx: c.cell.u_plane_dtx, + frame_18_ext: c.cell.frame_18_ext, }; let mle_sync_pdu = DMleSync { mcc: c.net.mcc, mnc: c.net.mnc, neighbor_cell_broadcast: 2, // Broadcast supported, but enquiry not supported - cell_load_ca: 0, + cell_load_ca: 0, // TODO implement dynamic setting. 0 = info unavailable late_entry_supported: c.cell.late_entry_supported, }; diff --git a/crates/tetra-entities/src/umac/umac_ms.rs b/crates/tetra-entities/src/umac/umac_ms.rs index b2f67f9..faf4548 100644 --- a/crates/tetra-entities/src/umac/umac_ms.rs +++ b/crates/tetra-entities/src/umac/umac_ms.rs @@ -1,6 +1,6 @@ use std::panic; -use tetra_config::SharedConfig; +use tetra_config::bluestation::SharedConfig; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, PhyBlockNum, Sap, TdmaTime, Todo, unimplemented_log}; use tetra_saps::tlmb::TlmbSysinfoInd; diff --git a/crates/tetra-entities/tests/test_llc_bs.rs b/crates/tetra-entities/tests/test_llc_bs.rs index 9417063..be9be4e 100644 --- a/crates/tetra-entities/tests/test_llc_bs.rs +++ b/crates/tetra-entities/tests/test_llc_bs.rs @@ -1,7 +1,7 @@ mod common; use common::{ComponentTest, default_test_config}; -use tetra_config::StackMode; +use tetra_config::bluestation::StackMode; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, debug}; use tetra_saps::sapmsg::{SapMsg, SapMsgInner}; diff --git a/crates/tetra-entities/tests/test_mm_bs.rs b/crates/tetra-entities/tests/test_mm_bs.rs index 883a0f9..06fa9ea 100644 --- a/crates/tetra-entities/tests/test_mm_bs.rs +++ b/crates/tetra-entities/tests/test_mm_bs.rs @@ -1,7 +1,7 @@ mod common; use common::{ComponentTest, default_test_config}; -use tetra_config::StackMode; +use tetra_config::bluestation::StackMode; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, Sap, SsiType, TdmaTime, TetraAddress, debug}; use tetra_saps::lmm::LmmMleUnitdataInd; diff --git a/crates/tetra-entities/tests/test_tnmm_bs_net.rs b/crates/tetra-entities/tests/test_tnmm_bs_net.rs index a9dc88e..0960f6b 100644 --- a/crates/tetra-entities/tests/test_tnmm_bs_net.rs +++ b/crates/tetra-entities/tests/test_tnmm_bs_net.rs @@ -2,7 +2,7 @@ mod common; use common::{ComponentTest, default_test_config}; use std::time::Duration; -use tetra_config::StackMode; +use tetra_config::bluestation::StackMode; use tetra_core::debug::setup_logging_verbose; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{Sap, TdmaTime}; diff --git a/crates/tetra-entities/tests/test_umac_bs.rs b/crates/tetra-entities/tests/test_umac_bs.rs index dd46e42..2ef6238 100644 --- a/crates/tetra-entities/tests/test_umac_bs.rs +++ b/crates/tetra-entities/tests/test_umac_bs.rs @@ -1,7 +1,7 @@ mod common; use common::{ComponentTest, default_test_config}; -use tetra_config::StackMode; +use tetra_config::bluestation::StackMode; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, PhyBlockNum, Sap, SsiType, TdmaTime, TetraAddress, debug}; use tetra_saps::lmm::LmmMleUnitdataReq; diff --git a/crates/tetra-entities/tests/test_umac_ms.rs b/crates/tetra-entities/tests/test_umac_ms.rs index 43bbd95..7f1281e 100644 --- a/crates/tetra-entities/tests/test_umac_ms.rs +++ b/crates/tetra-entities/tests/test_umac_ms.rs @@ -1,7 +1,7 @@ mod common; use common::{ComponentTest, default_test_config}; -use tetra_config::StackMode; +use tetra_config::bluestation::StackMode; use tetra_core::tetra_entities::TetraEntity; use tetra_core::{BitBuffer, PhyBlockNum, Sap, TdmaTime, debug}; use tetra_saps::sapmsg::{SapMsg, SapMsgInner};