1use serde::Deserialize;
8use std::path::{Path, PathBuf};
9
10pub const CONFIG_FILE: &str = "banc-rig.toml";
11pub const ENV_VAR: &str = "BANC_RIG";
12
13#[derive(Debug, Clone, Deserialize, Default)]
14#[serde(deny_unknown_fields)]
15pub struct RigConfig {
16 #[serde(default)]
17 pub rig: RigMeta,
18 pub target: Option<TargetConfig>,
19 #[serde(default, rename = "assistant")]
20 pub assistants: Vec<AssistantConfig>,
21 #[serde(default, rename = "instrument")]
22 pub instruments: Vec<InstrumentConfig>,
23}
24
25#[derive(Debug, Clone, Deserialize, Default)]
26#[serde(deny_unknown_fields)]
27pub struct RigMeta {
28 pub name: Option<String>,
29 pub lock_file: Option<PathBuf>,
33}
34
35#[derive(Debug, Clone, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct TargetConfig {
39 pub chip: String,
41 pub probe: Option<String>,
43 pub probe_host: Option<String>,
46}
47
48#[derive(Debug, Clone, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct AssistantConfig {
52 pub name: String,
54 pub serial: Option<String>,
56 pub product: Option<String>,
58}
59
60#[derive(Debug, Clone, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct InstrumentConfig {
64 pub name: String,
65 pub kind: String,
67 pub address: Option<String>,
69 #[serde(default)]
71 pub params: toml::Table,
72}
73
74impl RigConfig {
75 pub fn locate() -> anyhow::Result<Option<PathBuf>> {
79 if let Ok(path) = std::env::var(ENV_VAR) {
80 let path = PathBuf::from(path);
81 anyhow::ensure!(
82 path.is_file(),
83 "{ENV_VAR} points at {} which does not exist",
84 path.display()
85 );
86 return Ok(Some(path));
87 }
88 let mut dir = std::env::current_dir()?;
89 loop {
90 let candidate = dir.join(CONFIG_FILE);
91 if candidate.is_file() {
92 return Ok(Some(candidate));
93 }
94 if !dir.pop() {
95 return Ok(None);
96 }
97 }
98 }
99
100 pub fn load(path: &Path) -> anyhow::Result<Self> {
101 let text = std::fs::read_to_string(path)?;
102 let config: RigConfig = toml::from_str(&text)
103 .map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
104 Ok(config)
105 }
106
107 pub fn assistant(&self, name: &str) -> Option<&AssistantConfig> {
108 self.assistants.iter().find(|a| a.name == name)
109 }
110
111 pub fn instrument(&self, name: &str) -> Option<&InstrumentConfig> {
112 self.instruments.iter().find(|i| i.name == name)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn parses_full_config() {
122 let cfg: RigConfig = toml::from_str(
123 r#"
124 [rig]
125 name = "bench-1"
126
127 [target]
128 chip = "RP2350"
129 probe = "2e8a:000c"
130
131 [[assistant]]
132 name = "a0"
133 serial = "0123456789ABCDEF"
134
135 [[instrument]]
136 name = "att0"
137 kind = "rcdat"
138 address = "192.168.1.50:23"
139 params = { max_db = 90.0 }
140 "#,
141 )
142 .unwrap();
143 assert_eq!(cfg.rig.name.as_deref(), Some("bench-1"));
144 assert_eq!(cfg.target.as_ref().unwrap().chip, "RP2350");
145 assert_eq!(cfg.assistant("a0").unwrap().serial.as_deref(), Some("0123456789ABCDEF"));
146 assert_eq!(cfg.instrument("att0").unwrap().kind, "rcdat");
147 assert!(cfg.assistant("nope").is_none());
148 }
149
150 #[test]
151 fn empty_config_is_valid() {
152 let cfg: RigConfig = toml::from_str("").unwrap();
153 assert!(cfg.target.is_none());
154 assert!(cfg.assistants.is_empty());
155 }
156}