instrument_core/classifier/
mod.rs1#[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;
12use probes::{
13 probe_any, DMM_ACQUISITION_COMMANDS, DMM_READONLY_COMMANDS, FGEN_READONLY_COMMANDS,
14 PROBE_TIMEOUT, PSU_READONLY_COMMANDS,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[cfg(feature = "async")]
20use crate::scpi::AsyncScpiSession;
21#[cfg(feature = "async")]
22use async_probes::probe_any_async;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum ClassifySource {
27 ResourceParse,
28 VisaAttributes,
29 ModelRegistry,
30 ScpiIdn,
31 CapabilityProbe,
32 UserOverride,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ClassifiedKind {
38 pub kind: InstrumentKind,
39 pub confidence: u8,
40 pub source: ClassifySource,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct DiscoveredDevice {
46 pub address: ResourceAddress,
47 pub identity: DeviceIdentity,
48 pub supported_kinds: Vec<InstrumentKind>,
49 pub classification: Vec<ClassifiedKind>,
50 pub reachable: bool,
51 pub error: Option<String>,
52}
53
54impl DiscoveredDevice {
55 pub fn device_id(&self) -> DeviceId {
57 DeviceId::from_identity(&self.identity, &self.address.raw)
58 }
59}
60
61pub fn classify_from_address(
63 address: &ResourceAddress,
64 registry: &ModelRegistry,
65) -> (DeviceIdentity, Vec<ClassifiedKind>) {
66 let mut identity = DeviceIdentity::default();
67 let mut kinds = Vec::new();
68
69 if let (Some(vid), Some(pid)) = (
70 address.components.vid.as_deref(),
71 address.components.pid.as_deref(),
72 ) {
73 if let Some(hint) = registry.lookup_usb(vid, pid) {
74 identity.manufacturer = hint.manufacturer.clone();
75 identity.model = hint.model.clone();
76 for kind in &hint.kinds {
77 kinds.push(ClassifiedKind {
78 kind: *kind,
79 confidence: 40,
80 source: ClassifySource::ModelRegistry,
81 });
82 }
83 }
84 }
85
86 if identity.serial.is_none() {
87 identity.serial = address.components.serial.clone();
88 }
89
90 if kinds.is_empty() {
91 kinds.push(ClassifiedKind {
92 kind: InstrumentKind::Unknown,
93 confidence: 10,
94 source: ClassifySource::ResourceParse,
95 });
96 }
97
98 (identity, kinds)
99}
100
101pub fn classify_from_transport_hint(
103 hint: &TransportIdentity,
104 registry: &ModelRegistry,
105) -> (DeviceIdentity, Vec<ClassifiedKind>) {
106 let identity = DeviceIdentity {
107 manufacturer: hint.manufacturer.clone(),
108 model: hint.model.clone(),
109 serial: hint.serial.clone(),
110 firmware: None,
111 options: None,
112 };
113 let mut kinds = Vec::new();
114
115 if let (Some(m), Some(model)) = (&hint.manufacturer, &hint.model) {
116 if let Some(registry_kinds) = registry.lookup_model(m, model) {
117 for kind in registry_kinds {
118 kinds.push(ClassifiedKind {
119 kind,
120 confidence: 45,
121 source: ClassifySource::ModelRegistry,
122 });
123 }
124 }
125 }
126
127 if kinds.is_empty() && hint.manf_id.is_some() {
128 kinds.push(ClassifiedKind {
129 kind: InstrumentKind::Unknown,
130 confidence: 15,
131 source: ClassifySource::VisaAttributes,
132 });
133 }
134
135 (identity, kinds)
136}
137
138pub fn classify_from_identity(
140 idn: &Idn,
141 registry: &ModelRegistry,
142) -> (DeviceIdentity, Vec<ClassifiedKind>) {
143 let identity = DeviceIdentity::from_idn(idn);
144 let mut kinds = Vec::new();
145
146 if let Some(registry_kinds) = registry.lookup_model(&idn.manufacturer, &idn.model) {
147 for kind in registry_kinds {
148 kinds.push(ClassifiedKind {
149 kind,
150 confidence: 60,
151 source: ClassifySource::ModelRegistry,
152 });
153 }
154 }
155
156 kinds.push(ClassifiedKind {
157 kind: InstrumentKind::Unknown,
158 confidence: 30,
159 source: ClassifySource::ScpiIdn,
160 });
161
162 (identity, kinds)
163}
164
165pub fn classify_with_policy(session: &mut ScpiSession, policy: ProbePolicy) -> Vec<ClassifiedKind> {
167 match policy {
168 ProbePolicy::None => Vec::new(),
169 ProbePolicy::ReadOnly => classify_readonly_probes(session),
170 ProbePolicy::Full => {
171 let mut kinds = classify_readonly_probes(session);
172 kinds.extend(classify_acquisition_probes(session));
173 kinds
174 }
175 }
176}
177
178pub fn classify_deep(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
180 classify_with_policy(session, ProbePolicy::Full)
181}
182
183fn classify_readonly_probes(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
184 let mut kinds = Vec::new();
185
186 if probe_any(session, DMM_READONLY_COMMANDS, PROBE_TIMEOUT) {
187 kinds.push(ClassifiedKind {
188 kind: InstrumentKind::Dmm,
189 confidence: 80,
190 source: ClassifySource::CapabilityProbe,
191 });
192 }
193
194 if probe_any(session, PSU_READONLY_COMMANDS, PROBE_TIMEOUT) {
195 kinds.push(ClassifiedKind {
196 kind: InstrumentKind::DcPowerSupply,
197 confidence: 85,
198 source: ClassifySource::CapabilityProbe,
199 });
200 }
201
202 if probe_any(session, FGEN_READONLY_COMMANDS, PROBE_TIMEOUT) {
203 kinds.push(ClassifiedKind {
204 kind: InstrumentKind::FunctionGenerator,
205 confidence: 85,
206 source: ClassifySource::CapabilityProbe,
207 });
208 }
209
210 kinds
211}
212
213fn classify_acquisition_probes(session: &mut ScpiSession) -> Vec<ClassifiedKind> {
214 let mut kinds = Vec::new();
215
216 if probe_any(session, DMM_ACQUISITION_COMMANDS, PROBE_TIMEOUT) {
217 kinds.push(ClassifiedKind {
218 kind: InstrumentKind::Dmm,
219 confidence: 90,
220 source: ClassifySource::CapabilityProbe,
221 });
222 }
223
224 kinds
225}
226
227#[cfg(feature = "async")]
229pub async fn classify_with_policy_async(
230 session: &mut AsyncScpiSession,
231 policy: ProbePolicy,
232) -> Vec<ClassifiedKind> {
233 match policy {
234 ProbePolicy::None => Vec::new(),
235 ProbePolicy::ReadOnly => classify_readonly_probes_async(session).await,
236 ProbePolicy::Full => {
237 let mut kinds = classify_readonly_probes_async(session).await;
238 kinds.extend(classify_acquisition_probes_async(session).await);
239 kinds
240 }
241 }
242}
243
244#[cfg(feature = "async")]
246pub async fn classify_deep_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
247 classify_with_policy_async(session, ProbePolicy::Full).await
248}
249
250#[cfg(feature = "async")]
251async fn classify_readonly_probes_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
252 let mut kinds = Vec::new();
253
254 if probe_any_async(session, DMM_READONLY_COMMANDS, PROBE_TIMEOUT).await {
255 kinds.push(ClassifiedKind {
256 kind: InstrumentKind::Dmm,
257 confidence: 80,
258 source: ClassifySource::CapabilityProbe,
259 });
260 }
261
262 if probe_any_async(session, PSU_READONLY_COMMANDS, PROBE_TIMEOUT).await {
263 kinds.push(ClassifiedKind {
264 kind: InstrumentKind::DcPowerSupply,
265 confidence: 85,
266 source: ClassifySource::CapabilityProbe,
267 });
268 }
269
270 if probe_any_async(session, FGEN_READONLY_COMMANDS, PROBE_TIMEOUT).await {
271 kinds.push(ClassifiedKind {
272 kind: InstrumentKind::FunctionGenerator,
273 confidence: 85,
274 source: ClassifySource::CapabilityProbe,
275 });
276 }
277
278 kinds
279}
280
281#[cfg(feature = "async")]
282async fn classify_acquisition_probes_async(session: &mut AsyncScpiSession) -> Vec<ClassifiedKind> {
283 let mut kinds = Vec::new();
284
285 if probe_any_async(session, DMM_ACQUISITION_COMMANDS, PROBE_TIMEOUT).await {
286 kinds.push(ClassifiedKind {
287 kind: InstrumentKind::Dmm,
288 confidence: 90,
289 source: ClassifySource::CapabilityProbe,
290 });
291 }
292
293 kinds
294}
295
296pub fn merge_classifications(
298 layers: impl IntoIterator<Item = Vec<ClassifiedKind>>,
299 user_override: Option<&[InstrumentKind]>,
300) -> (Vec<InstrumentKind>, Vec<ClassifiedKind>) {
301 if let Some(override_kinds) = user_override {
302 let classified: Vec<_> = override_kinds
303 .iter()
304 .map(|kind| ClassifiedKind {
305 kind: *kind,
306 confidence: 100,
307 source: ClassifySource::UserOverride,
308 })
309 .collect();
310 return (override_kinds.to_vec(), classified);
311 }
312
313 let mut by_kind: HashMap<InstrumentKind, ClassifiedKind> = HashMap::new();
314 let mut all = Vec::new();
315
316 for layer in layers {
317 for entry in layer {
318 all.push(entry.clone());
319 by_kind
320 .entry(entry.kind)
321 .and_modify(|existing| {
322 if entry.confidence > existing.confidence {
323 *existing = entry.clone();
324 }
325 })
326 .or_insert(entry);
327 }
328 }
329
330 let mut supported: Vec<_> = by_kind
331 .values()
332 .filter(|k| k.kind != InstrumentKind::Unknown && k.confidence >= 40)
333 .map(|k| k.kind)
334 .collect();
335 supported.sort_by_key(|k| format!("{k:?}"));
336 supported.dedup();
337
338 if supported.is_empty() {
339 supported.push(InstrumentKind::Unknown);
340 }
341
342 (supported, all)
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use crate::registry::ModelRegistry;
349
350 #[test]
351 fn registry_hint_for_usb_vid_pid() {
352 let registry = ModelRegistry::embedded();
353 let addr = ResourceAddress::parse("USB0::0x0957::0x0607::SN::INSTR").unwrap();
354 let (_, kinds) = classify_from_address(&addr, ®istry);
355 assert!(kinds.iter().any(|k| k.kind == InstrumentKind::Dmm));
356 }
357
358 #[test]
359 fn probe_policy_none_skips_capability_queries() {
360 use crate::connect::ConnectOptions;
361 use crate::mock::{MockTransport, ScriptStep};
362 use crate::probe_policy::ProbePolicy;
363
364 let transport = MockTransport::from_script(vec![
365 ScriptStep::Write {
366 data: ":SENS:FUNC?\n".into(),
367 },
368 ScriptStep::Read {
369 data: "VOLT\n".into(),
370 },
371 ]);
372 let mut session = ScpiSession::new(Box::new(transport), ConnectOptions::default()).unwrap();
373 let kinds = super::classify_with_policy(&mut session, ProbePolicy::None);
374 assert!(kinds.is_empty());
375 }
376
377 #[test]
378 fn user_override_wins() {
379 let (kinds, _) = merge_classifications(
380 vec![vec![ClassifiedKind {
381 kind: InstrumentKind::Unknown,
382 confidence: 10,
383 source: ClassifySource::ResourceParse,
384 }]],
385 Some(&[InstrumentKind::Dmm]),
386 );
387 assert_eq!(kinds, vec![InstrumentKind::Dmm]);
388 }
389}