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 = if parts.len() == 4 && parts[3].eq_ignore_ascii_case("SOCKET") {
131        parts[2].parse().ok()
132    } else {
133        None
134    };
135    Ok((
136        InterfaceKind::Tcpip,
137        AddressParts {
138            host,
139            port,
140            ..Default::default()
141        },
142    ))
143}
144
145fn parse_serial(raw: &str) -> Result<(InterfaceKind, AddressParts)> {
146    let board = raw
147        .split("::")
148        .next()
149        .and_then(|s| s.strip_prefix("ASRL"))
150        .and_then(|s| s.parse().ok());
151    Ok((
152        InterfaceKind::Serial,
153        AddressParts {
154            board,
155            ..Default::default()
156        },
157    ))
158}
159
160fn normalize_hex_id(s: &str) -> String {
161    s.trim()
162        .trim_start_matches("0x")
163        .trim_start_matches("0X")
164        .to_uppercase()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn parses_usb_address() {
173        let addr = ResourceAddress::parse("USB0::0x0957::0x0607::MY123::INSTR").unwrap();
174        assert_eq!(addr.interface, InterfaceKind::Usb);
175        assert_eq!(addr.components.vid.as_deref(), Some("0957"));
176        assert_eq!(addr.components.pid.as_deref(), Some("0607"));
177    }
178
179    #[test]
180    fn dedup_is_case_insensitive() {
181        let a = ResourceAddress::parse("usb0::0x0957::0x0607::SN::INSTR").unwrap();
182        let b = ResourceAddress::parse("USB0::0x0957::0x0607::SN::INSTR").unwrap();
183        assert_eq!(a.dedup_key(), b.dedup_key());
184    }
185}