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(
101    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default,
102)]
103pub struct Signal {
104    /// Original Name from Machine
105    pub name: String,
106    /// Nanonis signal index (0-127)
107    pub index: u8,
108    /// TCP channel index (0-23) if this signal has TCP logger mapping
109    pub tcp_channel: Option<u8>,
110}
111
112impl Signal {
113    /// Create a new Signal with validation
114    pub fn new(
115        name: String,
116        index: u8,
117        tcp_channel: Option<u8>,
118    ) -> Result<Self, NanonisError> {
119        if index > 127 {
120            return Err(NanonisError::Protocol(format!(
121                "Nanonis index {index} out of range (0-127)"
122            )));
123        }
124
125        if let Some(ch) = tcp_channel {
126            if ch > 23 {
127                return Err(NanonisError::Protocol(format!(
128                    "TCP Channel {ch} is out of range (0-23) "
129                )));
130            }
131        }
132
133        Ok(Self {
134            name,
135            index,
136            tcp_channel,
137        })
138    }
139
140    pub fn new_unchecked(
141        name: &str,
142        index: u8,
143        tcp_channel: Option<u8>,
144    ) -> Self {
145        let name = String::from(name);
146
147        Signal {
148            name,
149            index,
150            tcp_channel,
151        }
152    }
153
154    /// Create a Signal with only Nanonis index (no TCP mapping)
155    pub fn index_only(name: &str, index: u8) -> Self {
156        let name = String::from(name);
157        Signal {
158            name,
159            index,
160            tcp_channel: None,
161        }
162    }
163
164    /// Create a Signal with both Nanonis index and TCP channel
165    pub fn with_tcp(
166        name: &str,
167        index: u8,
168        tcp_channel: u8,
169    ) -> Result<Self, NanonisError> {
170        let name = String::from(name);
171        Self::new(name, index, Some(tcp_channel))
172    }
173}
174
175#[derive(Default)]
176pub struct SignalRegistryBuilder {
177    signals: HashMap<String, Signal>,
178    nanonis_to_tcp: HashMap<u8, u8>,
179}
180
181impl SignalRegistryBuilder {
182    pub fn add_tcp_mapping(
183        mut self,
184        nanonis_index: u8,
185        tcp_channel: u8,
186    ) -> Self {
187        self.nanonis_to_tcp.insert(nanonis_index, tcp_channel);
188
189        self
190    }
191
192    pub fn add_tcp_map(mut self, nanonis_to_tcp: &[(u8, u8)]) -> Self {
193        nanonis_to_tcp.iter().for_each(|(n, t)| {
194            self.nanonis_to_tcp.insert(*n, *t);
195        });
196
197        self
198    }
199
200    pub fn with_standard_map(mut self) -> Self {
201        let standard_map: HashMap<u8, u8> = [
202            (0, 0),
203            (1, 1),
204            (2, 2),
205            (3, 3),
206            (4, 4),
207            (5, 5),
208            (6, 6),
209            (7, 7),
210            (24, 8),
211            (25, 9),
212            (26, 10),
213            (27, 11),
214            (28, 12),
215            (29, 13),
216            (30, 14),
217            (31, 15),
218            (74, 16),
219            (75, 17),
220            (76, 18),
221            (77, 19),
222            (78, 20),
223            (79, 21),
224            (80, 22),
225            (81, 23),
226        ]
227        .iter()
228        .cloned()
229        .collect();
230
231        self.nanonis_to_tcp.extend(standard_map);
232
233        self
234    }
235
236    pub fn from_signal_names(mut self, signal_names: &[String]) -> Self {
237        for (index, name) in signal_names.iter().enumerate() {
238            let clean_name = name.split('(').next().unwrap().trim();
239
240            let signal;
241
242            if let Some(tcp_channel) =
243                self.nanonis_to_tcp.get(&(index as u8)).copied()
244            {
245                signal = Signal {
246                    name: name.clone(),
247                    index: index as u8,
248                    tcp_channel: Some(tcp_channel),
249                };
250            } else {
251                signal = Signal {
252                    name: name.clone(),
253                    index: index as u8,
254                    tcp_channel: None,
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 add_signal(mut self, name: String, index: u8) -> Self {
268        let tcp_channel = self.nanonis_to_tcp.get(&index).copied();
269        let clean_name = name.split('(').next().unwrap().trim();
270
271        let signal = Signal {
272            name: name.clone(),
273            index,
274            tcp_channel,
275        };
276
277        self.signals.insert(name.to_lowercase(), signal.clone());
278
279        if clean_name != name {
280            self.signals
281                .insert(clean_name.to_lowercase(), signal.clone());
282        };
283
284        self
285    }
286
287    pub fn create_aliases(mut self) -> Self {
288        let mut new_aliases = Vec::new();
289
290        // Create aliases for existing signals
291        for (existing_key, signal) in &self.signals {
292            let name = &signal.name;
293            let clean_name = name.split('(').next().unwrap_or(name).trim();
294
295            // Create common aliases based on signal patterns
296            let aliases = generate_signal_aliases(clean_name);
297
298            for alias in aliases {
299                let alias_key = alias.to_lowercase();
300                // Only add if it doesn't already exist
301                if !self.signals.contains_key(&alias_key)
302                    && alias_key != *existing_key
303                {
304                    new_aliases.push((alias_key, signal.clone()));
305                }
306            }
307        }
308
309        // Insert all new aliases
310        for (alias_key, signal) in new_aliases {
311            self.signals.insert(alias_key, signal);
312        }
313
314        self
315    }
316
317    pub fn build(self) -> SignalRegistry {
318        SignalRegistry(self.signals)
319    }
320}
321
322impl SignalRegistry {
323    pub fn builder() -> SignalRegistryBuilder {
324        SignalRegistryBuilder::default()
325    }
326
327    pub fn with_hardcoded_tcp_mapping(signal_names: &[String]) -> Self {
328        Self::builder()
329            .with_standard_map()
330            .from_signal_names(signal_names)
331            .create_aliases()
332            .build()
333    }
334
335    pub fn all_names(&self) -> Vec<String> {
336        self.0
337            .values()
338            .map(|s| s.name.clone())
339            .collect::<std::collections::HashSet<_>>()
340            .into_iter()
341            .collect()
342    }
343
344    pub fn tcp_signals(&self) -> Vec<&Signal> {
345        self.0
346            .values()
347            .filter(|s| s.tcp_channel.is_some())
348            .collect()
349    }
350
351    pub fn find_signals_like(&self, query: &str) -> Vec<&Signal> {
352        let query_lower = query.to_lowercase();
353        self.0
354            .values()
355            .filter(|s| s.name.to_lowercase().contains(&query_lower))
356            .collect()
357    }
358
359    /// Get signal by name, returning the new Signal type
360    pub fn get_by_name(&self, name: &str) -> Option<&Signal> {
361        self.get(name)
362    }
363
364    /// Get signal by Nanonis index, returning the new Signal type
365    pub fn get_by_index(&self, index: u8) -> Option<&Signal> {
366        self.0.values().find(|signal| signal.index == index)
367    }
368}