Skip to main content

instrument_core/
address.rs

1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5
6/// VISA interface kind.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
8#[non_exhaustive]
9pub enum InterfaceKind {
10    Usb,
11    Gpib,
12    Tcpip,
13    Serial,
14    Vxi,
15    Pxi,
16    #[default]
17    Unknown,
18}
19
20/// Parsed components of a VISA resource string.
21#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
22pub struct AddressParts {
23    pub board: Option<u32>,
24    pub primary_address: Option<u32>,
25    pub secondary_address: Option<u32>,
26    pub vid: Option<String>,
27    pub pid: Option<String>,
28    pub serial: Option<String>,
29    pub host: Option<String>,
30    pub port: Option<u16>,
31    pub lane: Option<String>,
32}
33
34/// Typed VISA resource address with canonical dedup key.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct ResourceAddress {
37    pub interface: InterfaceKind,
38    pub raw: String,
39    pub components: AddressParts,
40    dedup_key: u64,
41}
42
43impl ResourceAddress {
44    /// Parses a VISA resource string into a typed address.
45    pub fn parse(raw: &str) -> Result<Self> {
46        let trimmed = raw.trim();
47        if trimmed.is_empty() {
48            return Err(Error::InvalidAddress("empty address".into()));
49        }
50
51        let upper = trimmed.to_uppercase();
52        let (interface, components) = if upper.starts_with("USB") {
53            parse_usb(trimmed)?
54        } else if upper.starts_with("GPIB") {
55            parse_gpib(trimmed)?
56        } else if upper.starts_with("TCPIP") || upper.starts_with("TCP") {
57            parse_tcpip(trimmed)?
58        } else if upper.starts_with("ASRL") {
59            parse_serial(trimmed)?
60        } else if upper.starts_with("VXI") {
61            (InterfaceKind::Vxi, AddressParts::default())
62        } else if upper.starts_with("PXI") {
63            (InterfaceKind::Pxi, AddressParts::default())
64        } else {
65            (InterfaceKind::Unknown, AddressParts::default())
66        };
67
68        let mut addr = Self {
69            interface,
70            raw: trimmed.to_string(),
71            components,
72            dedup_key: 0,
73        };
74        addr.dedup_key = addr.compute_dedup_key();
75        Ok(addr)
76    }
77
78    /// Returns the canonical dedup key for merge operations.
79    pub fn dedup_key(&self) -> u64 {
80        self.dedup_key
81    }
82
83    fn compute_dedup_key(&self) -> u64 {
84        let mut hasher = DefaultHasher::new();
85        self.interface.hash(&mut hasher);
86        self.raw.to_uppercase().hash(&mut hasher);
87        hasher.finish()
88    }
89}
90
91fn parse_usb(raw: &str) -> Result<(InterfaceKind, AddressParts)> {
92    // USB0::0x0957::0x0607::SN123::INSTR
93    let parts: Vec<&str> = raw.split("::").collect();
94    let vid = parts.get(1).map(|s| normalize_hex_id(s));
95    let pid = parts.get(2).map(|s| normalize_hex_id(s));
96    let serial = parts.get(3).map(|s| s.to_string());
97    Ok((
98        InterfaceKind::Usb,
99        AddressParts {
100            vid,
101            pid,
102            serial,
103            ..Default::default()
104        },
105    ))
106}
107
108fn parse_gpib(raw: &str) -> Result<(InterfaceKind, AddressParts)> {
109    let parts: Vec<&str> = raw.split("::").collect();
110    let board = parts
111        .first()
112        .and_then(|s| s.strip_prefix("GPIB"))
113        .and_then(|s| s.parse().ok());
114    let primary_address = parts.get(1).and_then(|s| s.parse().ok());
115    let secondary_address = parts.get(2).and_then(|s| s.parse().ok());
116    Ok((
117        InterfaceKind::Gpib,
118        AddressParts {
119            board,
120            primary_address,
121            secondary_address,
122            ..Default::default()
123        },
124    ))
125}
126
127fn parse_tcpip(raw: &str) -> Result<(InterfaceKind, AddressParts)> {
128    let parts: Vec<&str> = raw.split("::").collect();
129    let host = parts.get(1).map(|s| s.to_string());
130    let port = parts.get(3).and_then(|s| s.parse().ok());
131    Ok((
132        InterfaceKind::Tcpip,
133        AddressParts {
134            host,
135            port,
136            ..Default::default()
137        },
138    ))
139}
140
141fn parse_serial(raw: &str) -> Result<(InterfaceKind, AddressParts)> {
142    let board = raw
143        .split("::")
144        .next()
145        .and_then(|s| s.strip_prefix("ASRL"))
146        .and_then(|s| s.parse().ok());
147    Ok((
148        InterfaceKind::Serial,
149        AddressParts {
150            board,
151            ..Default::default()
152        },
153    ))
154}
155
156fn normalize_hex_id(s: &str) -> String {
157    s.trim()
158        .trim_start_matches("0x")
159        .trim_start_matches("0X")
160        .to_uppercase()
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn parses_usb_address() {
169        let addr = ResourceAddress::parse("USB0::0x0957::0x0607::MY123::INSTR").unwrap();
170        assert_eq!(addr.interface, InterfaceKind::Usb);
171        assert_eq!(addr.components.vid.as_deref(), Some("0957"));
172        assert_eq!(addr.components.pid.as_deref(), Some("0607"));
173    }
174
175    #[test]
176    fn dedup_is_case_insensitive() {
177        let a = ResourceAddress::parse("usb0::0x0957::0x0607::SN::INSTR").unwrap();
178        let b = ResourceAddress::parse("USB0::0x0957::0x0607::SN::INSTR").unwrap();
179        assert_eq!(a.dedup_key(), b.dedup_key());
180    }
181}