Skip to main content

rusty_tip/
signal_registry.rs

1use serde::{Deserialize, Serialize};
2
3use crate::NanonisError;
4use std::{collections::HashMap, ops::Deref};
5
6/// Generate common aliases for signal names
7fn generate_signal_aliases(name: &str) -> Vec<String> {
8    let mut aliases = Vec::new();
9    let name_lower = name.to_lowercase();
10
11    // Common signal name patterns and their aliases
12    let alias_patterns = [
13        // Current signal variations
14        ("current", vec!["i", "cur", "amp"]),
15        ("bias", vec!["u", "voltage", "v"]),
16        // Position signals
17        ("x", vec!["x pos", "x position", "xpos"]),
18        ("y", vec!["y pos", "y position", "ypos"]),
19        ("z", vec!["z pos", "z position", "zpos", "height"]),
20        // Frequency shift signals (common in AFM)
21        (
22            "oc m1 freq. shift",
23            vec!["freq shift", "frequency shift", "df", "oc freq shift"],
24        ),
25        ("oc m1 amplitude", vec!["amplitude", "amp", "oc amp"]),
26        ("oc m1 phase", vec!["phase", "oc phase"]),
27        // Lock-in amplifier signals
28        ("li demod 1 x", vec!["li1x", "demod1x", "x1"]),
29        ("li demod 1 y", vec!["li1y", "demod1y", "y1"]),
30        ("li demod 2 x", vec!["li2x", "demod2x", "x2"]),
31        ("li demod 2 y", vec!["li2y", "demod2y", "y2"]),
32        // Z controller
33        ("z ctrl shift", vec!["z shift", "zshift"]),
34        // Generic patterns
35        ("frequency", vec!["freq", "f"]),
36        ("amplitude", vec!["amp", "a"]),
37        ("phase", vec!["ph"]),
38        ("shift", vec!["sh"]),
39    ];
40
41    // Check for exact matches and add their aliases
42    for (pattern, pattern_aliases) in &alias_patterns {
43        if name_lower == *pattern {
44            aliases.extend(pattern_aliases.iter().map(|s| s.to_string()));
45        }
46    }
47
48    // Check for partial matches and create shortened versions
49    for (pattern, pattern_aliases) in &alias_patterns {
50        if name_lower.contains(pattern) {
51            aliases.extend(pattern_aliases.iter().map(|s| s.to_string()));
52        }
53    }
54
55    // Create abbreviated versions by removing common words
56    let words_to_remove = ["the", "signal", "channel", "ch", "ctrl", "control"];
57    let mut abbreviated = name_lower.clone();
58    for word in &words_to_remove {
59        abbreviated = abbreviated.replace(word, "").trim().to_string();
60    }
61    if abbreviated != name_lower && !abbreviated.is_empty() {
62        aliases.push(abbreviated);
63    }
64
65    // Create initials-based aliases for multi-word signals
66    let words: Vec<&str> = name_lower.split_whitespace().collect();
67    if words.len() > 1 {
68        let initials: String = words
69            .iter()
70            .map(|w| w.chars().next().unwrap_or('_'))
71            .collect();
72        if initials.len() > 1 {
73            aliases.push(initials);
74        }
75    }
76
77    // Remove duplicates and empty strings
78    aliases.retain(|s| !s.is_empty());
79    aliases.sort();
80    aliases.dedup();
81
82    aliases
83}
84
85/// Signal registry with case-insensitive lookup and TCP/Nanonis index mapping
86#[derive(Debug, Clone, Default)]
87pub struct SignalRegistry(HashMap<String, Signal>);
88
89impl Deref for SignalRegistry {
90    type Target = HashMap<String, Signal>;
91    fn deref(&self) -> &Self::Target {
92        &self.0
93    }
94}
95/// Signal representation with both Nanonis index and optional TCP channel
96///
97/// This struct replaces the old SignalIndex/NanonisIndex/ChannelIndex wrapper types.
98/// It carries both the Nanonis signal index (0-127) and the optional TCP channel
99/// mapping (0-23) in one place, eliminating the need for verbose conversions.
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
101pub struct Signal {
102    /// Original Name from Machine
103    pub name: String,
104    /// Nanonis signal index (0-127)
105    pub index: u8,
106    /// TCP channel index (0-23) if this signal has TCP logger mapping
107    pub tcp_channel: Option<u8>,
108}
109
110impl Signal {
111    /// Create a new Signal with validation
112    pub fn new(name: String, index: u8, tcp_channel: Option<u8>) -> Result<Self, NanonisError> {
113        if index > 127 {
114            return Err(NanonisError::Protocol(format!(
115                "Nanonis index {index} out of range (0-127)"
116            )));
117        }
118
119        if let Some(ch) = tcp_channel {
120            if ch > 23 {
121                return Err(NanonisError::Protocol(format!(
122                    "TCP Channel {ch} is out of range (0-23) "
123                )));
124            }
125        }
126
127        Ok(Self {
128            name,
129            index,
130            tcp_channel,
131        })
132    }
133
134    pub fn new_unchecked(name: &str, index: u8, tcp_channel: Option<u8>) -> Self {
135        let name = String::from(name);
136
137        Signal {
138            name,
139            index,
140            tcp_channel,
141        }
142    }
143
144    /// Create a Signal with only Nanonis index (no TCP mapping)
145    pub fn index_only(name: &str, index: u8) -> Self {
146        let name = String::from(name);
147        Signal {
148            name,
149            index,
150            tcp_channel: None,
151        }
152    }
153
154    /// Create a Signal with both Nanonis index and TCP channel
155    pub fn with_tcp(name: &str, index: u8, tcp_channel: u8) -> Result<Self, NanonisError> {
156        let name = String::from(name);
157        Self::new(name, index, Some(tcp_channel))
158    }
159}
160
161#[derive(Default)]
162pub struct SignalRegistryBuilder {
163    signals: HashMap<String, Signal>,
164    nanonis_to_tcp: HashMap<u8, u8>,
165}
166
167impl SignalRegistryBuilder {
168    pub fn add_tcp_mapping(mut self, nanonis_index: u8, tcp_channel: u8) -> Self {
169        self.nanonis_to_tcp.insert(nanonis_index, tcp_channel);
170
171        self
172    }
173
174    pub fn add_tcp_map(mut self, nanonis_to_tcp: &[(u8, u8)]) -> Self {
175        nanonis_to_tcp.iter().for_each(|(n, t)| {
176            self.nanonis_to_tcp.insert(*n, *t);
177        });
178
179        self
180    }
181
182    pub fn with_standard_map(mut self) -> Self {
183        let standard_map: HashMap<u8, u8> = [
184            (0, 0),
185            (1, 1),
186            (2, 2),
187            (3, 3),
188            (4, 4),
189            (5, 5),
190            (6, 6),
191            (7, 7),
192            (24, 8),
193            (25, 9),
194            (26, 10),
195            (27, 11),
196            (28, 12),
197            (29, 13),
198            (30, 14),
199            (31, 15),
200            (74, 16),
201            (75, 17),
202            (76, 18),
203            (77, 19),
204            (78, 20),
205            (79, 21),
206            (80, 22),
207            (81, 23),
208        ]
209        .iter()
210        .cloned()
211        .collect();
212
213        self.nanonis_to_tcp.extend(standard_map);
214
215        self
216    }
217
218    pub fn from_signal_names(mut self, signal_names: &[String]) -> Self {
219        for (index, name) in signal_names.iter().enumerate() {
220            let clean_name = name.split('(').next().unwrap().trim();
221
222            let signal;
223
224            if let Some(tcp_channel) = self.nanonis_to_tcp.get(&(index as u8)).copied() {
225                signal = Signal {
226                    name: name.clone(),
227                    index: index as u8,
228                    tcp_channel: Some(tcp_channel),
229                };
230            } else {
231                signal = Signal {
232                    name: name.clone(),
233                    index: index as u8,
234                    tcp_channel: None,
235                };
236            }
237            self.signals.insert(name.to_lowercase(), signal.clone());
238
239            if clean_name != name {
240                self.signals
241                    .insert(clean_name.to_lowercase(), signal.clone());
242            }
243        }
244        self
245    }
246
247    pub fn add_signal(mut self, name: String, index: u8) -> Self {
248        let tcp_channel = self.nanonis_to_tcp.get(&index).copied();
249        let clean_name = name.split('(').next().unwrap().trim();
250
251        let signal = Signal {
252            name: name.clone(),
253            index,
254            tcp_channel,
255        };
256
257        self.signals.insert(name.to_lowercase(), signal.clone());
258
259        if clean_name != name {
260            self.signals
261                .insert(clean_name.to_lowercase(), signal.clone());
262        };
263
264        self
265    }
266
267    pub fn create_aliases(mut self) -> Self {
268        let mut new_aliases = Vec::new();
269
270        // Create aliases for existing signals
271        for (existing_key, signal) in &self.signals {
272            let name = &signal.name;
273            let clean_name = name.split('(').next().unwrap_or(name).trim();
274
275            // Create common aliases based on signal patterns
276            let aliases = generate_signal_aliases(clean_name);
277
278            for alias in aliases {
279                let alias_key = alias.to_lowercase();
280                // Only add if it doesn't already exist
281                if !self.signals.contains_key(&alias_key) && alias_key != *existing_key {
282                    new_aliases.push((alias_key, signal.clone()));
283                }
284            }
285        }
286
287        // Insert all new aliases
288        for (alias_key, signal) in new_aliases {
289            self.signals.insert(alias_key, signal);
290        }
291
292        self
293    }
294
295    pub fn build(self) -> SignalRegistry {
296        SignalRegistry(self.signals)
297    }
298}
299
300impl SignalRegistry {
301    pub fn builder() -> SignalRegistryBuilder {
302        SignalRegistryBuilder::default()
303    }
304
305    pub fn with_hardcoded_tcp_mapping(signal_names: &[String]) -> Self {
306        Self::builder()
307            .with_standard_map()
308            .from_signal_names(signal_names)
309            .create_aliases()
310            .build()
311    }
312
313    pub fn all_names(&self) -> Vec<String> {
314        self.0
315            .values()
316            .map(|s| s.name.clone())
317            .collect::<std::collections::HashSet<_>>()
318            .into_iter()
319            .collect()
320    }
321
322    pub fn tcp_signals(&self) -> Vec<&Signal> {
323        self.0
324            .values()
325            .filter(|s| s.tcp_channel.is_some())
326            .collect()
327    }
328
329    pub fn find_signals_like(&self, query: &str) -> Vec<&Signal> {
330        let query_lower = query.to_lowercase();
331        self.0
332            .values()
333            .filter(|s| s.name.to_lowercase().contains(&query_lower))
334            .collect()
335    }
336
337    /// Get signal by name, returning the new Signal type
338    pub fn get_by_name(&self, name: &str) -> Option<&Signal> {
339        self.get(name)
340    }
341
342    /// Get signal by Nanonis index, returning the new Signal type
343    pub fn get_by_index(&self, index: u8) -> Option<&Signal> {
344        self.0.values().find(|signal| signal.index == index)
345    }
346}