Skip to main content

instrument/
catalog.rs

1use crate::classes::{
2    Counter, DcPowerSupply, Dmm, FunctionGenerator, Oscilloscope, PowerMeter, SpectrumAnalyzer,
3    Switch,
4};
5use crate::device::DeviceRef;
6use crate::mock_backend::MockSessionOpener;
7use instrument_core::address::ResourceAddress;
8use instrument_core::classifier::{classify_from_address, merge_classifications};
9use instrument_core::connect::ConnectOptions;
10use instrument_core::diagnostics::{CommsObserver, DeviceHealth};
11use instrument_core::error::Result;
12use instrument_core::identity::DeviceId;
13use instrument_core::kind::InstrumentKind;
14use instrument_core::mock::{mock_address, ScriptedFixture};
15use instrument_core::registry::ModelRegistry;
16use instrument_core::session::SessionOpener;
17use instrument_core::DiscoveredDevice;
18use std::collections::HashMap;
19use std::sync::{Arc, Mutex};
20
21/// Catalog of discovered or injected devices.
22pub struct DeviceCatalog {
23    opener: Arc<dyn SessionOpener>,
24    devices: Vec<DiscoveredDevice>,
25    by_address: HashMap<String, usize>,
26    by_device_id: HashMap<String, usize>,
27    connect_options: ConnectOptions,
28    health_registry: HashMap<String, Arc<Mutex<DeviceHealth>>>,
29    observer: Option<Arc<dyn CommsObserver>>,
30}
31
32impl DeviceCatalog {
33    pub fn from_devices(opener: Arc<dyn SessionOpener>, devices: Vec<DiscoveredDevice>) -> Self {
34        Self::from_devices_with_observer(opener, devices, None)
35    }
36
37    pub fn from_devices_with_observer(
38        opener: Arc<dyn SessionOpener>,
39        devices: Vec<DiscoveredDevice>,
40        observer: Option<Arc<dyn CommsObserver>>,
41    ) -> Self {
42        let mut by_address = HashMap::new();
43        let mut by_device_id = HashMap::new();
44        let mut health_registry = HashMap::new();
45
46        for (idx, dev) in devices.iter().enumerate() {
47            by_address.insert(dev.address.raw.clone(), idx);
48            by_device_id.insert(dev.device_id().0.clone(), idx);
49            health_registry.insert(
50                dev.address.raw.clone(),
51                Arc::new(Mutex::new(DeviceHealth::default())),
52            );
53        }
54
55        Self {
56            opener,
57            devices,
58            by_address,
59            by_device_id,
60            connect_options: ConnectOptions::default(),
61            health_registry,
62            observer,
63        }
64    }
65
66    /// Sets the default connect options used when opening sessions from this catalog.
67    pub fn with_connect_options(mut self, opts: ConnectOptions) -> Self {
68        self.connect_options = opts;
69        self
70    }
71
72    pub fn connect_options(&self) -> &ConnectOptions {
73        &self.connect_options
74    }
75
76    pub fn from_fixture(address: &str, fixture: ScriptedFixture) -> Result<Self> {
77        let addr = if address.starts_with("mock://") {
78            ResourceAddress::parse(address)?
79        } else {
80            mock_address(address)?
81        };
82
83        let idn = fixture.idn().clone();
84        let kinds: Vec<_> = fixture.kinds().to_vec();
85        let transport = fixture.into_transport();
86        let opener = MockSessionOpener::new();
87        opener.register(&addr.raw, transport);
88
89        let (identity, layer1) = classify_from_address(&addr, &ModelRegistry::embedded());
90        let mut id = identity;
91        id.manufacturer = Some(idn.manufacturer.clone());
92        id.model = Some(idn.model.clone());
93        id.serial = Some(idn.serial.clone());
94        id.firmware = Some(idn.firmware.clone());
95
96        let (supported_kinds, classification) = merge_classifications(
97            vec![layer1],
98            if kinds.is_empty() {
99                None
100            } else {
101                Some(kinds.as_slice())
102            },
103        );
104
105        let device = DiscoveredDevice {
106            address: addr.clone(),
107            identity: id,
108            supported_kinds,
109            classification,
110            reachable: true,
111            error: None,
112        };
113
114        Ok(Self::from_devices(Arc::new(opener), vec![device]))
115    }
116
117    pub fn devices(&self) -> &[DiscoveredDevice] {
118        &self.devices
119    }
120
121    pub fn devices_by_kind(&self, kind: InstrumentKind) -> Vec<&DiscoveredDevice> {
122        self.devices
123            .iter()
124            .filter(|d| d.supported_kinds.contains(&kind))
125            .collect()
126    }
127
128    pub fn device(&self, address: &str) -> Result<DeviceRef> {
129        let idx = self.by_address.get(address).copied().ok_or_else(|| {
130            instrument_core::Error::DeviceNotFound {
131                address: address.to_string(),
132            }
133        })?;
134        Ok(self.device_at(idx))
135    }
136
137    pub fn device_by_id(&self, id: &DeviceId) -> Result<DeviceRef> {
138        let idx = self.by_device_id.get(&id.0).copied().ok_or_else(|| {
139            instrument_core::Error::DeviceNotFound {
140                address: id.0.clone(),
141            }
142        })?;
143        Ok(self.device_at(idx))
144    }
145
146    /// Reconnects to a device by stable identity after replacement or address change.
147    pub fn reconnect_by_identity(&self, id: &DeviceId) -> Result<DeviceRef> {
148        self.device_by_id(id)
149    }
150
151    pub fn health(&self, address: &str) -> Result<DeviceHealth> {
152        let health = self.health_registry.get(address).ok_or_else(|| {
153            instrument_core::Error::DeviceNotFound {
154                address: address.to_string(),
155            }
156        })?;
157        Ok(health.lock().unwrap().clone())
158    }
159
160    fn device_at(&self, idx: usize) -> DeviceRef {
161        let dev = &self.devices[idx];
162        let health = self
163            .health_registry
164            .get(&dev.address.raw)
165            .cloned()
166            .unwrap_or_else(|| Arc::new(Mutex::new(DeviceHealth::default())));
167        DeviceRef {
168            device: dev.clone(),
169            opener: self.opener.clone(),
170            connect_options: self.connect_options.clone(),
171            health,
172            observer: self.observer.clone(),
173        }
174    }
175
176    pub fn open_dmm(&self, address: &str) -> Result<Dmm> {
177        self.device(address)?.open_dmm()
178    }
179
180    pub fn open_dc_power_supply(&self, address: &str) -> Result<DcPowerSupply> {
181        self.device(address)?.open_dc_power_supply()
182    }
183
184    pub fn open_function_generator(&self, address: &str) -> Result<FunctionGenerator> {
185        self.device(address)?.open_function_generator()
186    }
187
188    pub fn open_oscilloscope(&self, address: &str) -> Result<Oscilloscope> {
189        self.device(address)?.open_oscilloscope()
190    }
191
192    pub fn open_switch(&self, address: &str) -> Result<Switch> {
193        self.device(address)?.open_switch()
194    }
195
196    pub fn open_counter(&self, address: &str) -> Result<Counter> {
197        self.device(address)?.open_counter()
198    }
199
200    pub fn open_power_meter(&self, address: &str) -> Result<PowerMeter> {
201        self.device(address)?.open_power_meter()
202    }
203
204    pub fn open_spectrum_analyzer(&self, address: &str) -> Result<SpectrumAnalyzer> {
205        self.device(address)?.open_spectrum_analyzer()
206    }
207
208    pub fn print_summary(&self) {
209        for dev in &self.devices {
210            println!(
211                "{} ({}) @ {} — kinds: {:?} reachable: {}",
212                dev.identity.model.as_deref().unwrap_or("?"),
213                dev.device_id(),
214                dev.address.raw,
215                dev.supported_kinds,
216                dev.reachable
217            );
218        }
219    }
220}