Skip to main content

banc_host/
config.rs

1//! Rig topology configuration.
2//!
3//! A rig is described by a `banc-rig.toml`, located via the `BANC_RIG` env
4//! var or by searching from the current directory upward. Its absence is the
5//! signal that this machine has no hardware attached: suites self-skip.
6
7use 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    /// Advisory lock file serializing rig access across processes (nextest
30    /// runs one process per test). Default: target/banc.lock next to the
31    /// config file.
32    pub lock_file: Option<PathBuf>,
33}
34
35/// The device under test, driven via probe-rs.
36#[derive(Debug, Clone, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct TargetConfig {
39    /// probe-rs target-database chip name, e.g. "STM32WL55JCIx".
40    pub chip: String,
41    /// Probe selector "VID:PID[:SERIAL]". None: the only probe attached.
42    pub probe: Option<String>,
43    /// probe-rs remote server (`probe-rs serve`), e.g. "https://pi:3000".
44    /// None: local USB probe via the probe-rs library.
45    pub probe_host: Option<String>,
46}
47
48/// An assistant node speaking banc-icd over postcard-rpc USB.
49#[derive(Debug, Clone, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct AssistantConfig {
52    /// Name tests use to look the node up, e.g. "a0".
53    pub name: String,
54    /// USB serial string (assistants surface their unique ID here).
55    pub serial: Option<String>,
56    /// USB product string to match when serial is not given.
57    pub product: Option<String>,
58}
59
60/// A bench instrument. Drivers are matched on `kind` by the suite.
61#[derive(Debug, Clone, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct InstrumentConfig {
64    pub name: String,
65    /// Driver key, e.g. "rcdat", "scpi".
66    pub kind: String,
67    /// Free-form address: host:port, VISA resource, hidraw path...
68    pub address: Option<String>,
69    /// Driver-specific settings, passed through untouched.
70    #[serde(default)]
71    pub params: toml::Table,
72}
73
74impl RigConfig {
75    /// Find the rig config for this machine/checkout. `Ok(None)` means "no
76    /// rig here" (the self-skip signal); `Err` means a config exists but is
77    /// unusable, which is a real failure, not a skip.
78    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}