1#[cfg(feature = "async")]
2mod async_probes;
3mod probes;
4
5use crate::address::ResourceAddress;
6use crate::identity::{DeviceId, DeviceIdentity, Idn};
7use crate::kind::InstrumentKind;
8use crate::probe_policy::ProbePolicy;
9use crate::registry::ModelRegistry;
10use crate::scpi::ScpiSession;
11use crate::transport::TransportIdentity;
12pub use probes::valid_probe_reply;
13use probes::{
14 probe_any, COUNTER_READONLY_COMMANDS, DMM_ACQUISITION_COMMANDS, DMM_READONLY_COMMANDS,
15 FGEN_READONLY_COMMANDS, PROBE_TIMEOUT, PSU_READONLY_COMMANDS, PWRMETER_READONLY_COMMANDS,
16 SCOPE_READONLY_COMMANDS, SPECAN_READONLY_COMMANDS, SWITCH_READONLY_COMMANDS,
17};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21#[cfg(feature = "async")]
22use crate::scpi::AsyncScpiSession;
23#[cfg(feature = "async")]
24use async_probes::probe_any_async;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ClassifySource {
29 ResourceParse,
30 VisaAttributes,
31 ModelRegistry,
32 ScpiIdn,
33 CapabilityProbe,
34 UserOverride,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct ClassifiedKind {
40 pub kind: InstrumentKind,
41 pub confidence: u8,
42 pub source: ClassifySource,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DiscoveredDevice {
48 pub address: ResourceAddress,
49 pub identity: DeviceIdentity,
50 pub supported_kinds: Vec<InstrumentKind>,
51 pub classification: Vec<ClassifiedKind>,
52 pub reachable: bool,
53 pub error: Option<String>,
54}
55
56impl DiscoveredDevice {
57 pub fn device_id(&self) -> DeviceId {
59 DeviceId::from_identity(&self.identity, &self.address.raw)
60 }
61}
62
63pub fn classify_from_address(
65 address: &ResourceAddress,
66 registry: &ModelRegistry,
67) -> (DeviceIdentity, Vec<ClassifiedKind>) {
68 let mut identity = DeviceIdentity::default();
69 let mut kinds = Vec::new();
70
71 if let (Some(vid), Some(pid)) = (
72 address.components.vid.as_deref(),
73 address.components.pid.as_deref(),
74 ) {
75 if let Some(hint) = registry.lookup_usb(vid, pid) {
76 identity.manufacturer = hint.manufacturer.clone();
77 identity.model = hint.model.clone();
78 for kind in &hint.kinds {
79 kinds.push(ClassifiedKind {
80 kind: *kind,
81 confidence: 40,
82 source: ClassifySource::ModelRegistry,
83 });
84 }
85 }
86 }
87
88 if identity.serial.is_none() {
89 identity.serial = address.components.serial.clone();
90 }
91
92 if kinds.is_empty() {
93 kinds.push(ClassifiedKind {
94 kind: InstrumentKind::Unknown,
95 confidence: 10,
96 source: ClassifySource::ResourceParse,
97 });
98 }
99
100 (identity, kinds)
101}
102
103pub fn classify_from_transport_hint(
105 hint: &TransportIdentity,
106 registry: &ModelRegistry,
107) -> (DeviceIdentity, Vec<ClassifiedKind>) {
108 let identity = DeviceIdentity {
109 manufacturer: hint.manufacturer.clone(),
110 model: hint.model.clone(),
111 serial: hint.serial.clone(),
112 firmware: None,
113 options: None,
114 };
115 let mut kinds = Vec::new();
116
117 if let (Some(m), Some(model)) = (&hint.manufacturer, &hint.model) {
118 if let Some(registry_kinds) = registry.lookup_model(m, model) {
119 for kind in registry_kinds {
120 kinds.push(ClassifiedKind {
121 kind,
122 confidence: 45,
123 source: ClassifySource::ModelRegistry,
124 });
125 }
126 }
127 }
128
129 if kinds.is_empty() && hint.manf_id.is_some() {
130 kinds.push(ClassifiedKind {
131 kind: InstrumentKind::Unknown,
132 confidence: 15,
133 source: ClassifySource::VisaAttributes,
134 });
135 }
136
137 (identity, kinds)
138}
139
140pub fn classify_from_identity(
142 idn: &Idn,
143 registry: &ModelRegistry,
144) -> (DeviceIdentity, Vec<ClassifiedKind>) {
145 let identity = DeviceIdentity::from_idn(idn);
146 let mut kinds = Vec::new();
147
148 if let Some(registry_kinds) = registry.lookup_model(&idn.manufacturer, &idn.model) {
149 for kind in registry_kinds {
150 kinds.push(ClassifiedKind {
151 kind,
152 confidence: 60,
153 source: ClassifySource::ModelRegistry,
154 });
155 }
156 }
157
158 kinds.push(ClassifiedKind {
159 kind: InstrumentKind::Unknown,
160 confidence: 30,
161 source: ClassifySource::ScpiIdn,
162 });
163
164 (identity, kinds)
165}
166
167pub fn classify_with_policy(session: &mut ScpiSession, policy: ProbePolicy) -> Vec<ClassifiedKind> {
169 match policy {
170 ProbePolicy::None => Vec::new(),
171 ProbePolicy::ReadOnly => classify_readonly_probes(session),
172 ProbePolicy::Full => {
173 let mut kinds = classify_readonly_probes(session);
174 kinds.extend(classify_acquisition_probes(session));
175 kinds
176 }
177 }
178}
179
180pub fn classify_deep(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
182 classify_with_policy(session, ProbePolicy::Full)
183}
184
185fn classify_readonly_probes(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
186 let mut kinds = Vec::new();
187
188 if probe_any(session, DMM_READONLY_COMMANDS, PROBE_TIMEOUT) {
189 kinds.push(ClassifiedKind {
190 kind: InstrumentKind::Dmm,
191 confidence: 80,
192 source: ClassifySource::CapabilityProbe,
193 });
194 }
195
196 if probe_any(session, PSU_READONLY_COMMANDS, PROBE_TIMEOUT) {
197 kinds.push(ClassifiedKind {
198 kind: InstrumentKind::DcPowerSupply,
199 confidence: 85,
200 source: ClassifySource::CapabilityProbe,
201 });
202 }
203
204 if probe_any(session, FGEN_READONLY_COMMANDS, PROBE_TIMEOUT) {
205 kinds.push(ClassifiedKind {
206 kind: InstrumentKind::FunctionGenerator,
207 confidence: 85,
208 source: ClassifySource::CapabilityProbe,
209 });
210 }
211
212 if probe_any(session, SCOPE_READONLY_COMMANDS, PROBE_TIMEOUT) {
213 kinds.push(ClassifiedKind {
214 kind: InstrumentKind::Oscilloscope,
215 confidence: 85,
216 source: ClassifySource::CapabilityProbe,
217 });
218 }
219
220 if probe_any(session, SWITCH_READONLY_COMMANDS, PROBE_TIMEOUT) {
221 kinds.push(ClassifiedKind {
222 kind: InstrumentKind::Switch,
223 confidence: 85,
224 source: ClassifySource::CapabilityProbe,
225 });
226 }
227
228 if probe_any(session, COUNTER_READONLY_COMMANDS, PROBE_TIMEOUT) {
229 kinds.push(ClassifiedKind {
230 kind: InstrumentKind::Counter,
231 confidence: 85,
232 source: ClassifySource::CapabilityProbe,
233 });
234 }
235
236 if probe_any(session, PWRMETER_READONLY_COMMANDS, PROBE_TIMEOUT) {
237 kinds.push(ClassifiedKind {
238 kind: InstrumentKind::PowerMeter,
239 confidence: 85,
240 source: ClassifySource::CapabilityProbe,
241 });
242 }
243
244 if probe_any(session, SPECAN_READONLY_COMMANDS, PROBE_TIMEOUT) {
245 kinds.push(ClassifiedKind {
246 kind: InstrumentKind::SpectrumAnalyzer,
247 confidence: 85,
248 source: ClassifySource::CapabilityProbe,
249 });
250 }
251
252 kinds
253}
254
255fn classify_acquisition_probes(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
256 let mut kinds = Vec::new();
257
258 if probe_any(session, DMM_ACQUISITION_COMMANDS, PROBE_TIMEOUT) {
259 kinds.push(ClassifiedKind {
260 kind: InstrumentKind::Dmm,
261 confidence: 90,
262 source: ClassifySource::CapabilityProbe,
263 });
264 }
265
266 kinds
267}
268
269#[cfg(feature = "async")]
271pub async fn classify_with_policy_async(
272 session: &mut AsyncScpiSession,
273 policy: ProbePolicy,
274) -> Vec<ClassifiedKind> {
275 match policy {
276 ProbePolicy::None => Vec::new(),
277 ProbePolicy::ReadOnly => classify_readonly_probes_async(session).await,
278 ProbePolicy::Full => {
279 let mut kinds = classify_readonly_probes_async(session).await;
280 kinds.extend(classify_acquisition_probes_async(session).await);
281 kinds
282 }
283 }
284}
285
286#[cfg(feature = "async")]
288pub async fn classify_deep_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
289 classify_with_policy_async(session, ProbePolicy::Full).await
290}
291
292#[cfg(feature = "async")]
293async fn classify_readonly_probes_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
294 let mut kinds = Vec::new();
295
296 if probe_any_async(session, DMM_READONLY_COMMANDS, PROBE_TIMEOUT).await {
297 kinds.push(ClassifiedKind {
298 kind: InstrumentKind::Dmm,
299 confidence: 80,
300 source: ClassifySource::CapabilityProbe,
301 });
302 }
303
304 if probe_any_async(session, PSU_READONLY_COMMANDS, PROBE_TIMEOUT).await {
305 kinds.push(ClassifiedKind {
306 kind: InstrumentKind::DcPowerSupply,
307 confidence: 85,
308 source: ClassifySource::CapabilityProbe,
309 });
310 }
311
312 if probe_any_async(session, FGEN_READONLY_COMMANDS, PROBE_TIMEOUT).await {
313 kinds.push(ClassifiedKind {
314 kind: InstrumentKind::FunctionGenerator,
315 confidence: 85,
316 source: ClassifySource::CapabilityProbe,
317 });
318 }
319
320 if probe_any_async(session, SCOPE_READONLY_COMMANDS, PROBE_TIMEOUT).await {
321 kinds.push(ClassifiedKind {
322 kind: InstrumentKind::Oscilloscope,
323 confidence: 85,
324 source: ClassifySource::CapabilityProbe,
325 });
326 }
327
328 if probe_any_async(session, SWITCH_READONLY_COMMANDS, PROBE_TIMEOUT).await {
329 kinds.push(ClassifiedKind {
330 kind: InstrumentKind::Switch,
331 confidence: 85,
332 source: ClassifySource::CapabilityProbe,
333 });
334 }
335
336 if probe_any_async(session, COUNTER_READONLY_COMMANDS, PROBE_TIMEOUT).await {
337 kinds.push(ClassifiedKind {
338 kind: InstrumentKind::Counter,
339 confidence: 85,
340 source: ClassifySource::CapabilityProbe,
341 });
342 }
343
344 if probe_any_async(session, PWRMETER_READONLY_COMMANDS, PROBE_TIMEOUT).await {
345 kinds.push(ClassifiedKind {
346 kind: InstrumentKind::PowerMeter,
347 confidence: 85,
348 source: ClassifySource::CapabilityProbe,
349 });
350 }
351
352 if probe_any_async(session, SPECAN_READONLY_COMMANDS, PROBE_TIMEOUT).await {
353 kinds.push(ClassifiedKind {
354 kind: InstrumentKind::SpectrumAnalyzer,
355 confidence: 85,
356 source: ClassifySource::CapabilityProbe,
357 });
358 }
359
360 kinds
361}
362
363#[cfg(feature = "async")]
364async fn classify_acquisition_probes_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
365 let mut kinds = Vec::new();
366
367 if probe_any_async(session, DMM_ACQUISITION_COMMANDS, PROBE_TIMEOUT).await {
368 kinds.push(ClassifiedKind {
369 kind: InstrumentKind::Dmm,
370 confidence: 90,
371 source: ClassifySource::CapabilityProbe,
372 });
373 }
374
375 kinds
376}
377
378pub fn merge_classifications(
380 layers: impl IntoIterator<Item = Vec<ClassifiedKind>>,
381 user_override: Option<&[InstrumentKind]>,
382) -> (Vec<InstrumentKind>, Vec<ClassifiedKind>) {
383 if let Some(override_kinds) = user_override {
384 let classified: Vec<_> = override_kinds
385 .iter()
386 .map(|kind| ClassifiedKind {
387 kind: *kind,
388 confidence: 100,
389 source: ClassifySource::UserOverride,
390 })
391 .collect();
392 return (override_kinds.to_vec(), classified);
393 }
394
395 let mut by_kind: HashMap<InstrumentKind, ClassifiedKind> = HashMap::new();
396 let mut all = Vec::new();
397
398 for layer in layers {
399 for entry in layer {
400 all.push(entry.clone());
401 by_kind
402 .entry(entry.kind)
403 .and_modify(|existing| {
404 if entry.confidence > existing.confidence {
405 *existing = entry.clone();
406 }
407 })
408 .or_insert(entry);
409 }
410 }
411
412 let mut supported: Vec<_> = by_kind
413 .values()
414 .filter(|k| k.kind != InstrumentKind::Unknown && k.confidence >= 40)
415 .map(|k| k.kind)
416 .collect();
417 supported.sort_by_key(|k| format!("{k:?}"));
418 supported.dedup();
419
420 if supported.is_empty() {
421 supported.push(InstrumentKind::Unknown);
422 }
423
424 (supported, all)
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use crate::registry::ModelRegistry;
431
432 #[test]
433 fn registry_hint_for_usb_vid_pid() {
434 let registry = ModelRegistry::embedded();
435 let addr = ResourceAddress::parse("USB0::0x0957::0x0607::SN::INSTR").unwrap();
436 let (_, kinds) = classify_from_address(&addr, ®istry);
437 assert!(kinds.iter().any(|k| k.kind == InstrumentKind::Dmm));
438 }
439
440 #[test]
441 fn probe_policy_none_skips_capability_queries() {
442 use crate::connect::ConnectOptions;
443 use crate::mock::{MockTransport, ScriptStep};
444 use crate::probe_policy::ProbePolicy;
445
446 let transport = MockTransport::from_script(vec![
447 ScriptStep::Write {
448 data: ":SENS:FUNC?\n".into(),
449 },
450 ScriptStep::Read {
451 data: "VOLT\n".into(),
452 },
453 ]);
454 let mut session = ScpiSession::new(Box::new(transport), ConnectOptions::default()).unwrap();
455 let kinds = super::classify_with_policy(&mut session, ProbePolicy::None);
456 assert!(kinds.is_empty());
457 }
458
459 #[test]
460 fn user_override_wins() {
461 let (kinds, _) = merge_classifications(
462 vec![vec![ClassifiedKind {
463 kind: InstrumentKind::Unknown,
464 confidence: 10,
465 source: ClassifySource::ResourceParse,
466 }]],
467 Some(&[InstrumentKind::Dmm]),
468 );
469 assert_eq!(kinds, vec![InstrumentKind::Dmm]);
470 }
471
472 #[test]
473 fn scope_readonly_probe_succeeds() {
474 use crate::connect::ConnectOptions;
475 use crate::mock::{MockTransport, ScriptStep};
476 use probes::{probe_any, PROBE_TIMEOUT, SCOPE_READONLY_COMMANDS};
477
478 let transport = MockTransport::from_script(vec![
479 ScriptStep::Write {
480 data: ":TIMebase:SCALe?\n".into(),
481 },
482 ScriptStep::Read {
483 data: "1e-3\n".into(),
484 },
485 ]);
486 let mut session = ScpiSession::new(Box::new(transport), ConnectOptions::default()).unwrap();
487 assert!(probe_any(
488 &mut session,
489 SCOPE_READONLY_COMMANDS,
490 PROBE_TIMEOUT
491 ));
492 }
493
494 #[test]
495 fn switch_readonly_probe_succeeds() {
496 use crate::connect::ConnectOptions;
497 use crate::mock::{MockTransport, ScriptStep};
498 use probes::{probe_any, PROBE_TIMEOUT, SWITCH_READONLY_COMMANDS};
499
500 let transport = MockTransport::from_script(vec![
501 ScriptStep::Write {
502 data: ":ROUTe:CLOS?\n".into(),
503 },
504 ScriptStep::Read { data: "0\n".into() },
505 ]);
506 let mut session = ScpiSession::new(Box::new(transport), ConnectOptions::default()).unwrap();
507 assert!(probe_any(
508 &mut session,
509 SWITCH_READONLY_COMMANDS,
510 PROBE_TIMEOUT
511 ));
512 }
513
514 #[test]
515 fn counter_readonly_probe_succeeds() {
516 use crate::connect::ConnectOptions;
517 use crate::mock::{MockTransport, ScriptStep};
518 use probes::{probe_any, COUNTER_READONLY_COMMANDS, PROBE_TIMEOUT};
519
520 let transport = MockTransport::from_script(vec![
521 ScriptStep::Write {
522 data: ":COUNter:DATA?\n".into(),
523 },
524 ScriptStep::Read {
525 data: "42\n".into(),
526 },
527 ]);
528 let mut session = ScpiSession::new(Box::new(transport), ConnectOptions::default()).unwrap();
529 assert!(probe_any(
530 &mut session,
531 COUNTER_READONLY_COMMANDS,
532 PROBE_TIMEOUT
533 ));
534 }
535}