Skip to main content

instrument/
discovery.rs

1use crate::catalog::DeviceCatalog;
2
3use instrument_core::address::ResourceAddress;
4use instrument_core::classifier::{
5    classify_from_address, classify_from_identity, classify_from_transport_hint,
6    classify_with_policy, merge_classifications,
7};
8use instrument_core::connect::ConnectOptions;
9use instrument_core::diagnostics::CommsObserver;
10use instrument_core::enumerator::{RawResource, ResourceEnumerator};
11use instrument_core::error::Result;
12use instrument_core::identity::DeviceIdentity;
13use instrument_core::ieee4882::Ieee4882;
14use instrument_core::kind::InstrumentKind;
15use instrument_core::probe_policy::ProbePolicy;
16use instrument_core::registry::ModelRegistry;
17use instrument_core::session::{InstrumentSession, SessionOpener};
18use instrument_core::DiscoveredDevice;
19use std::collections::HashMap;
20use std::panic::{catch_unwind, AssertUnwindSafe};
21use std::sync::Arc;
22use std::thread;
23
24const DEFAULT_PATTERNS: &[&str] = &["?*INSTR", "USB?*::INSTR", "GPIB?*INSTR", "ASRL?*INSTR"];
25
26/// Discovery builder for scanning and classifying instruments.
27pub struct Discovery {
28    enumerator: Arc<dyn ResourceEnumerator>,
29    opener: Arc<dyn SessionOpener>,
30    registry: ModelRegistry,
31    manual_addresses: Vec<String>,
32    kind_overrides: HashMap<String, Vec<InstrumentKind>>,
33    connect_options: ConnectOptions,
34    probe_policy: ProbePolicy,
35    probe_concurrency: usize,
36    observer: Option<Arc<dyn CommsObserver>>,
37}
38
39impl Discovery {
40    #[cfg(feature = "visa")]
41    pub fn visa() -> Result<Self> {
42        let rm = instrument_visa::SharedRm::new()?;
43        Ok(Self::new(
44            Arc::new(instrument_visa::VisaEnumerator::new(rm.clone())),
45            Arc::new(instrument_visa::VisaSessionOpener::new(rm)),
46            ModelRegistry::embedded(),
47        ))
48    }
49
50    pub fn new(
51        enumerator: Arc<dyn ResourceEnumerator>,
52        opener: Arc<dyn SessionOpener>,
53        registry: ModelRegistry,
54    ) -> Self {
55        Self {
56            enumerator,
57            opener,
58            registry,
59            manual_addresses: Vec::new(),
60            kind_overrides: HashMap::new(),
61            connect_options: ConnectOptions::default(),
62            probe_policy: ProbePolicy::default(),
63            probe_concurrency: 4,
64            observer: None,
65        }
66    }
67
68    pub fn manual_address(mut self, address: impl Into<String>) -> Self {
69        self.manual_addresses.push(address.into());
70        self
71    }
72
73    pub fn override_kinds(
74        mut self,
75        address: impl Into<String>,
76        kinds: Vec<InstrumentKind>,
77    ) -> Self {
78        self.kind_overrides.insert(address.into(), kinds);
79        self
80    }
81
82    pub fn with_registry(mut self, registry: ModelRegistry) -> Self {
83        self.registry = registry;
84        self
85    }
86
87    pub fn probe_policy(mut self, policy: ProbePolicy) -> Self {
88        self.probe_policy = policy;
89        self
90    }
91
92    pub fn connect_options(mut self, opts: ConnectOptions) -> Self {
93        self.connect_options = opts;
94        self
95    }
96
97    pub fn observer(mut self, observer: Arc<dyn CommsObserver>) -> Self {
98        self.observer = Some(observer);
99        self
100    }
101
102    pub fn scan(self) -> Result<DeviceCatalog> {
103        let mut raw_map: HashMap<u64, RawResource> = HashMap::new();
104        for pattern in DEFAULT_PATTERNS {
105            if let Ok(list) = self.enumerator.list(pattern) {
106                for res in list {
107                    raw_map.insert(res.address.dedup_key(), res);
108                }
109            }
110        }
111
112        for manual in &self.manual_addresses {
113            let address = ResourceAddress::parse(manual)?;
114            raw_map.entry(address.dedup_key()).or_insert(RawResource {
115                address,
116                identity_hint: instrument_core::transport::TransportIdentity::default(),
117            });
118        }
119
120        let candidates: Vec<RawResource> = raw_map.into_values().collect();
121        let registry = self.registry.clone();
122        let overrides = self.kind_overrides.clone();
123        let opener = self.opener.clone();
124        let opts = self.connect_options.clone();
125        let policy = self.probe_policy;
126        let concurrency = self.probe_concurrency;
127
128        let devices = probe_devices_parallel(
129            candidates,
130            registry,
131            overrides,
132            opener,
133            opts,
134            policy,
135            concurrency,
136        )?;
137
138        Ok(
139            DeviceCatalog::from_devices_with_observer(self.opener.clone(), devices, self.observer)
140                .with_connect_options(self.connect_options),
141        )
142    }
143}
144
145fn probe_devices_parallel(
146    candidates: Vec<RawResource>,
147    registry: ModelRegistry,
148    overrides: HashMap<String, Vec<InstrumentKind>>,
149    opener: Arc<dyn SessionOpener>,
150    opts: ConnectOptions,
151    policy: ProbePolicy,
152    concurrency: usize,
153) -> Result<Vec<DiscoveredDevice>> {
154    if candidates.is_empty() {
155        return Ok(Vec::new());
156    }
157
158    let chunk_size = (candidates.len() + concurrency - 1) / concurrency;
159    let mut handles = Vec::new();
160    for chunk in candidates.chunks(chunk_size.max(1)) {
161        let chunk = chunk.to_vec();
162        let registry = registry.clone();
163        let overrides = overrides.clone();
164        let opener = opener.clone();
165        let opts = opts.clone();
166        handles.push(thread::spawn(move || {
167            chunk
168                .into_iter()
169                .map(|raw| {
170                    let fallback = raw.clone();
171                    match catch_unwind(AssertUnwindSafe(|| {
172                        probe_one(raw, &registry, &overrides, opener.as_ref(), &opts, policy)
173                    })) {
174                        Ok(device) => device,
175                        Err(_) => panic_fallback_device(&registry, &overrides, fallback),
176                    }
177                })
178                .collect::<Vec<_>>()
179        }));
180    }
181
182    let mut devices = Vec::new();
183    for (handle_idx, handle) in handles.into_iter().enumerate() {
184        match handle.join() {
185            Ok(chunk) => devices.extend(chunk),
186            Err(_) => {
187                tracing::warn!(thread = handle_idx, "discovery probe thread panicked");
188            }
189        }
190    }
191
192    Ok(devices)
193}
194
195fn probe_one(
196    raw: RawResource,
197    registry: &ModelRegistry,
198    overrides: &HashMap<String, Vec<InstrumentKind>>,
199    opener: &dyn SessionOpener,
200    opts: &ConnectOptions,
201    policy: ProbePolicy,
202) -> DiscoveredDevice {
203    let override_kinds = overrides.get(&raw.address.raw).map(|v| v.as_slice());
204    let (mut identity, layer1) = classify_from_address(&raw.address, registry);
205    let (hint_identity, layer2) = classify_from_transport_hint(&raw.identity_hint, registry);
206    identity.merge(&hint_identity);
207    let mut layers = vec![layer1, layer2];
208
209    match opener.open(&raw.address, opts) {
210        Ok(transport) => {
211            match InstrumentSession::new(
212                raw.address.clone(),
213                transport,
214                opts.clone(),
215                identity.clone(),
216            ) {
217                Ok(mut session) => {
218                    let _ = session.clear_status();
219                    let _ = session.scpi_mut().flush();
220
221                    if let Ok(idn) = Ieee4882::new(session.scpi_mut()).idn() {
222                        let (idn_identity, layer4) = classify_from_identity(&idn, registry);
223                        identity.merge(&idn_identity);
224                        layers.push(layer4);
225
226                        if let Ok(opt) = Ieee4882::new(session.scpi_mut()).options() {
227                            identity.options = Some(opt);
228                        }
229                    }
230
231                    if policy != ProbePolicy::None {
232                        let probe_kinds = classify_with_policy(session.scpi_mut(), policy);
233                        if !probe_kinds.is_empty() {
234                            layers.push(probe_kinds);
235                        }
236                    }
237
238                    let (supported_kinds, classification) =
239                        merge_classifications(layers, override_kinds);
240
241                    DiscoveredDevice {
242                        address: raw.address,
243                        identity,
244                        supported_kinds,
245                        classification,
246                        reachable: true,
247                        error: None,
248                    }
249                }
250                Err(e) => unreachable_device(raw, identity, layers, override_kinds, e.to_string()),
251            }
252        }
253        Err(e) => unreachable_device(raw, identity, layers, override_kinds, e.to_string()),
254    }
255}
256
257fn panic_fallback_device(
258    registry: &ModelRegistry,
259    overrides: &HashMap<String, Vec<InstrumentKind>>,
260    raw: RawResource,
261) -> DiscoveredDevice {
262    let override_kinds = overrides.get(&raw.address.raw).map(|v| v.as_slice());
263    let (mut identity, layer1) = classify_from_address(&raw.address, registry);
264    let (hint_identity, layer2) = classify_from_transport_hint(&raw.identity_hint, registry);
265    identity.merge(&hint_identity);
266    unreachable_device(
267        raw,
268        identity,
269        vec![layer1, layer2],
270        override_kinds,
271        "probe panicked".into(),
272    )
273}
274
275fn unreachable_device(
276    raw: RawResource,
277    identity: DeviceIdentity,
278    layers: Vec<Vec<instrument_core::classifier::ClassifiedKind>>,
279    override_kinds: Option<&[InstrumentKind]>,
280    error: String,
281) -> DiscoveredDevice {
282    let (supported_kinds, classification) = merge_classifications(layers, override_kinds);
283    DiscoveredDevice {
284        address: raw.address,
285        identity,
286        supported_kinds,
287        classification,
288        reachable: false,
289        error: Some(error),
290    }
291}