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
26pub 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, ®istry, &overrides, opener.as_ref(), &opts, policy)
173 })) {
174 Ok(device) => device,
175 Err(_) => panic_fallback_device(®istry, &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}