[feat](trx-server): add per-rig enable switch in multi-rig config
Add [[rigs]].enable (default true) and skip disabled rigs during\nresolution/uniqueness checks.\n\nCo-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -27,9 +27,12 @@ use trx_core::rig::state::RigMode;
|
|||||||
/// `[behavior]` / `[decode_logs]` fields are still supported via
|
/// `[behavior]` / `[decode_logs]` fields are still supported via
|
||||||
/// `ServerConfig::resolved_rigs()` which synthesises a single-element list
|
/// `ServerConfig::resolved_rigs()` which synthesises a single-element list
|
||||||
/// with `id = "default"` when `rigs` is empty.
|
/// with `id = "default"` when `rigs` is empty.
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct RigInstanceConfig {
|
pub struct RigInstanceConfig {
|
||||||
|
/// Whether this rig instance should be started.
|
||||||
|
/// Defaults to true so existing configs remain unchanged.
|
||||||
|
pub enable: bool,
|
||||||
/// Stable rig identifier used in protocol routing.
|
/// Stable rig identifier used in protocol routing.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Display name for the rig (e.g., "HF Transceiver", "VHF/UHF SDR").
|
/// Display name for the rig (e.g., "HF Transceiver", "VHF/UHF SDR").
|
||||||
@@ -51,6 +54,23 @@ pub struct RigInstanceConfig {
|
|||||||
pub decode_logs: DecodeLogsConfig,
|
pub decode_logs: DecodeLogsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for RigInstanceConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enable: true,
|
||||||
|
id: String::new(),
|
||||||
|
name: None,
|
||||||
|
rig: RigConfig::default(),
|
||||||
|
behavior: BehaviorConfig::default(),
|
||||||
|
audio: AudioConfig::default(),
|
||||||
|
sdr: SdrConfig::default(),
|
||||||
|
pskreporter: PskReporterConfig::default(),
|
||||||
|
aprsfi: AprsFiConfig::default(),
|
||||||
|
decode_logs: DecodeLogsConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RigInstanceConfig {
|
impl RigInstanceConfig {
|
||||||
/// Get the display name for this rig.
|
/// Get the display name for this rig.
|
||||||
/// Returns the configured name if set, otherwise the id.
|
/// Returns the configured name if set, otherwise the id.
|
||||||
@@ -502,7 +522,12 @@ impl ServerConfig {
|
|||||||
if !self.rigs.is_empty() {
|
if !self.rigs.is_empty() {
|
||||||
let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
let mut seen_ports: std::collections::HashSet<u16> = std::collections::HashSet::new();
|
let mut seen_ports: std::collections::HashSet<u16> = std::collections::HashSet::new();
|
||||||
|
let mut enabled_count = 0usize;
|
||||||
for rig in &self.rigs {
|
for rig in &self.rigs {
|
||||||
|
if !rig.enable {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
enabled_count += 1;
|
||||||
// Check for explicit duplicate IDs (empty IDs are auto-generated later).
|
// Check for explicit duplicate IDs (empty IDs are auto-generated later).
|
||||||
if !rig.id.trim().is_empty() && !seen_ids.insert(rig.id.clone()) {
|
if !rig.id.trim().is_empty() && !seen_ids.insert(rig.id.clone()) {
|
||||||
return Err(format!("[[rigs]] duplicate rig id: \"{}\"", rig.id));
|
return Err(format!("[[rigs]] duplicate rig id: \"{}\"", rig.id));
|
||||||
@@ -528,6 +553,12 @@ impl ServerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if enabled_count == 0 {
|
||||||
|
return Err(
|
||||||
|
"[[rigs]] has no enabled entries; set at least one [[rigs]].enable = true"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.decode_logs.enabled {
|
if self.decode_logs.enabled {
|
||||||
@@ -631,7 +662,7 @@ impl ServerConfig {
|
|||||||
|
|
||||||
/// Return the effective list of rig instances to spawn.
|
/// Return the effective list of rig instances to spawn.
|
||||||
///
|
///
|
||||||
/// When `[[rigs]]` entries are present they are returned as-is.
|
/// When `[[rigs]]` entries are present, only enabled entries are returned.
|
||||||
/// Otherwise the legacy flat `[rig]` / `[audio]` / … fields are synthesised
|
/// Otherwise the legacy flat `[rig]` / `[audio]` / … fields are synthesised
|
||||||
/// into a single `RigInstanceConfig` with `id = "default"`.
|
/// into a single `RigInstanceConfig` with `id = "default"`.
|
||||||
pub fn resolved_rigs(&self) -> Vec<RigInstanceConfig> {
|
pub fn resolved_rigs(&self) -> Vec<RigInstanceConfig> {
|
||||||
@@ -641,6 +672,7 @@ impl ServerConfig {
|
|||||||
.rigs
|
.rigs
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.filter(|(_, rig)| rig.enable)
|
||||||
.map(|(idx, rig)| {
|
.map(|(idx, rig)| {
|
||||||
let id = if rig.id.trim().is_empty() {
|
let id = if rig.id.trim().is_empty() {
|
||||||
// Generate ID from model name with counter.
|
// Generate ID from model name with counter.
|
||||||
@@ -655,6 +687,7 @@ impl ServerConfig {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
vec![RigInstanceConfig {
|
vec![RigInstanceConfig {
|
||||||
|
enable: true,
|
||||||
id: "default".to_string(),
|
id: "default".to_string(),
|
||||||
name: None,
|
name: None,
|
||||||
rig: self.rig.clone(),
|
rig: self.rig.clone(),
|
||||||
@@ -1205,6 +1238,38 @@ port = 4532
|
|||||||
assert_eq!(rigs[1].audio.port, 4532);
|
assert_eq!(rigs[1].audio.port, 4532);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolved_rigs_skips_disabled_entries() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[[rigs]]
|
||||||
|
id = "disabled"
|
||||||
|
enable = false
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft817"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4531
|
||||||
|
|
||||||
|
[[rigs]]
|
||||||
|
id = "enabled"
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft450d"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB1"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4532
|
||||||
|
"#;
|
||||||
|
let cfg: ServerConfig = toml::from_str(toml_str).unwrap();
|
||||||
|
let rigs = cfg.resolved_rigs();
|
||||||
|
assert_eq!(rigs.len(), 1);
|
||||||
|
assert_eq!(rigs[0].id, "enabled");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_rejects_duplicate_rig_ids() {
|
fn test_validate_rejects_duplicate_rig_ids() {
|
||||||
let toml_str = r#"
|
let toml_str = r#"
|
||||||
@@ -1273,6 +1338,63 @@ port = 4531
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_allows_disabled_duplicate_rig_ids() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[[rigs]]
|
||||||
|
id = "rig1"
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft817"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4531
|
||||||
|
|
||||||
|
[[rigs]]
|
||||||
|
id = "rig1"
|
||||||
|
enable = false
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft450d"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB1"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4532
|
||||||
|
"#;
|
||||||
|
let cfg: ServerConfig = toml::from_str(toml_str).unwrap();
|
||||||
|
assert!(
|
||||||
|
cfg.validate().is_ok(),
|
||||||
|
"expected Ok because disabled rigs are excluded from uniqueness checks"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_rejects_when_all_multi_rigs_disabled() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[[rigs]]
|
||||||
|
id = "rig1"
|
||||||
|
enable = false
|
||||||
|
[rigs.rig]
|
||||||
|
model = "ft817"
|
||||||
|
[rigs.rig.access]
|
||||||
|
type = "serial"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 9600
|
||||||
|
[rigs.audio]
|
||||||
|
port = 4531
|
||||||
|
"#;
|
||||||
|
let cfg: ServerConfig = toml::from_str(toml_str).unwrap();
|
||||||
|
let result = cfg.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
result.unwrap_err().contains("no enabled entries"),
|
||||||
|
"expected error about enabled rig entries"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_accepts_multi_rig_unique_ids_and_ports() {
|
fn test_validate_accepts_multi_rig_unique_ids_and_ports() {
|
||||||
let toml_str = r#"
|
let toml_str = r#"
|
||||||
|
|||||||
@@ -171,7 +171,10 @@ pub async fn run_rig_task(
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
state.control.enabled = Some(true);
|
state.control.enabled = Some(true);
|
||||||
if let Err(e) = refresh_after_power_on(&mut rig, &mut state, retry).await {
|
if let Err(e) = refresh_after_power_on(&mut rig, &mut state, retry).await {
|
||||||
warn!("Initial PowerOn refresh failed after retries (continuing): {}", e);
|
warn!(
|
||||||
|
"Initial PowerOn refresh failed after retries (continuing): {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
initial_status_read = true;
|
initial_status_read = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user