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 pub lease: Option<LeaseConfig>,
37}
38
39#[derive(Debug, Clone, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct LeaseConfig {
43 pub addr: String,
45 pub token_file: PathBuf,
48}
49
50#[derive(Debug, Clone, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct TargetConfig {
54 pub chip: String,
56 pub probe: Option<String>,
58 pub probe_host: Option<String>,
61}
62
63#[derive(Debug, Clone, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct AssistantConfig {
68 pub name: String,
70 pub serial: Option<String>,
72 pub product: Option<String>,
74 pub addr: Option<String>,
77 pub token_file: Option<PathBuf>,
80}
81
82#[derive(Debug, Clone, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub struct InstrumentConfig {
86 pub name: String,
87 pub kind: String,
89 pub address: Option<String>,
91 #[serde(default)]
93 pub params: toml::Table,
94}
95
96impl RigConfig {
97 pub fn locate() -> anyhow::Result<Option<PathBuf>> {
101 if let Ok(path) = std::env::var(ENV_VAR) {
102 let path = PathBuf::from(path);
103 anyhow::ensure!(
104 path.is_file(),
105 "{ENV_VAR} points at {} which does not exist",
106 path.display()
107 );
108 return Ok(Some(path));
109 }
110 let mut dir = std::env::current_dir()?;
111 loop {
112 let candidate = dir.join(CONFIG_FILE);
113 if candidate.is_file() {
114 return Ok(Some(candidate));
115 }
116 if !dir.pop() {
117 return Ok(None);
118 }
119 }
120 }
121
122 pub fn load(path: &Path) -> anyhow::Result<Self> {
123 let text = std::fs::read_to_string(path)?;
124 let config: RigConfig = toml::from_str(&text)
125 .map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
126 Ok(config)
127 }
128
129 pub fn assistant(&self, name: &str) -> Option<&AssistantConfig> {
130 self.assistants.iter().find(|a| a.name == name)
131 }
132
133 pub fn instrument(&self, name: &str) -> Option<&InstrumentConfig> {
134 self.instruments.iter().find(|i| i.name == name)
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn parses_full_config() {
144 let cfg: RigConfig = toml::from_str(
145 r#"
146 [rig]
147 name = "bench-1"
148
149 [target]
150 chip = "RP2350"
151 probe = "2e8a:000c"
152
153 [[assistant]]
154 name = "a0"
155 serial = "0123456789ABCDEF"
156
157 [[instrument]]
158 name = "att0"
159 kind = "rcdat"
160 address = "192.168.1.50:23"
161 params = { max_db = 90.0 }
162 "#,
163 )
164 .unwrap();
165 assert_eq!(cfg.rig.name.as_deref(), Some("bench-1"));
166 assert_eq!(cfg.target.as_ref().unwrap().chip, "RP2350");
167 assert_eq!(cfg.assistant("a0").unwrap().serial.as_deref(), Some("0123456789ABCDEF"));
168 assert_eq!(cfg.instrument("att0").unwrap().kind, "rcdat");
169 assert!(cfg.assistant("nope").is_none());
170 }
171
172 #[test]
173 fn empty_config_is_valid() {
174 let cfg: RigConfig = toml::from_str("").unwrap();
175 assert!(cfg.target.is_none());
176 assert!(cfg.assistants.is_empty());
177 }
178}