Skip to main content

nanonis_rs/client/
mod.rs

1use super::protocol::{Protocol, HEADER_SIZE};
2use crate::error::NanonisError;
3use crate::types::NanonisValue;
4use log::{debug, warn};
5use std::io::Write;
6use std::net::{SocketAddr, TcpStream};
7use std::time::Duration;
8
9pub mod atom_track;
10pub mod auto_approach;
11pub mod beam_defl;
12pub mod bias;
13pub mod bias_spectr;
14pub mod bias_sweep;
15pub mod cpd_comp;
16pub mod current;
17pub mod data_log;
18pub mod dig_lines;
19pub mod folme;
20pub mod gen_pi_ctrl;
21pub mod gen_swp;
22pub mod hs_swp;
23pub mod interf;
24pub mod kelvin_ctrl;
25pub mod laser;
26pub mod lockin;
27pub mod lockin_freq_swp;
28pub mod marks;
29pub mod motor;
30pub mod mpass;
31pub mod oc_sync;
32pub mod oscilloscope;
33pub mod pattern;
34pub mod pi_ctrl;
35pub mod piezo;
36pub mod pll;
37pub mod pll_freq_swp;
38pub mod pll_signal_anlzr;
39pub mod safe_tip;
40pub mod scan;
41pub mod script;
42pub mod signal_chart;
43pub mod signals;
44pub mod spectrum_anlzr;
45pub mod tcplog;
46pub mod tip_recovery;
47pub mod user_in;
48pub mod user_out;
49pub mod util;
50pub mod z_ctrl;
51pub mod z_spectr;
52
53
54/// Connection configuration for the Nanonis TCP client.
55///
56/// Contains timeout settings for different phases of the TCP connection lifecycle.
57/// All timeouts have sensible defaults but can be customized for specific network conditions.
58///
59/// # Examples
60///
61/// ```
62/// use std::time::Duration;
63/// use nanonis_rs::ConnectionConfig;
64///
65/// // Use default timeouts
66/// let config = ConnectionConfig::default();
67///
68/// // Customize timeouts for slow network
69/// let config = ConnectionConfig {
70///     connect_timeout: Duration::from_secs(30),
71///     read_timeout: Duration::from_secs(60),
72///     write_timeout: Duration::from_secs(10),
73/// };
74/// ```
75#[derive(Debug, Clone)]
76pub struct ConnectionConfig {
77    /// Timeout for establishing the initial TCP connection
78    pub connect_timeout: Duration,
79    /// Timeout for reading data from the Nanonis server
80    pub read_timeout: Duration,
81    /// Timeout for writing data to the Nanonis server
82    pub write_timeout: Duration,
83}
84
85impl Default for ConnectionConfig {
86    fn default() -> Self {
87        Self {
88            connect_timeout: Duration::from_secs(5),
89            read_timeout: Duration::from_secs(10),
90            write_timeout: Duration::from_secs(5),
91        }
92    }
93}
94
95/// Builder for constructing [`NanonisClient`] instances with flexible configuration.
96///
97/// The builder pattern allows you to configure various aspects of the client
98/// before establishing the connection. This is more ergonomic than having
99/// multiple constructor variants.
100///
101/// # Examples
102///
103/// Basic usage:
104/// ```no_run
105/// use nanonis_rs::NanonisClient;
106///
107/// let client = NanonisClient::builder()
108///     .address("127.0.0.1")
109///     .port(6501)
110///     .debug(true)
111///     .build()?;
112/// # Ok::<(), Box<dyn std::error::Error>>(())
113/// ```
114///
115/// With custom timeouts:
116/// ```no_run
117/// use std::time::Duration;
118/// use nanonis_rs::NanonisClient;
119///
120/// let client = NanonisClient::builder()
121///     .address("192.168.1.100")
122///     .port(6501)
123///     .connect_timeout(Duration::from_secs(30))
124///     .read_timeout(Duration::from_secs(60))
125///     .debug(false)
126///     .build()?;
127/// # Ok::<(), Box<dyn std::error::Error>>(())
128/// ```
129#[derive(Default)]
130pub struct NanonisClientBuilder {
131    address: Option<String>,
132    port: Option<u16>,
133    config: ConnectionConfig,
134    debug: bool,
135    safe_tip_on_drop: bool,
136}
137
138impl NanonisClientBuilder {
139    pub fn address(mut self, addr: &str) -> Self {
140        self.address = Some(addr.to_string());
141        self
142    }
143
144    pub fn port(mut self, port: u16) -> Self {
145        self.port = Some(port);
146        self
147    }
148
149    /// Enable or disable debug logging
150    pub fn debug(mut self, debug: bool) -> Self {
151        self.debug = debug;
152        self
153    }
154
155    /// Set the full connection configuration
156    pub fn config(mut self, config: ConnectionConfig) -> Self {
157        self.config = config;
158        self
159    }
160
161    /// Set connect timeout
162    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
163        self.config.connect_timeout = timeout;
164        self
165    }
166
167    /// Set read timeout
168    pub fn read_timeout(mut self, timeout: Duration) -> Self {
169        self.config.read_timeout = timeout;
170        self
171    }
172
173    /// Set write timeout
174    pub fn write_timeout(mut self, timeout: Duration) -> Self {
175        self.config.write_timeout = timeout;
176        self
177    }
178
179    /// Enable automatic tip safety on client drop.
180    ///
181    /// When enabled, the client will automatically withdraw the Z-controller
182    /// and move motors to a safe position when dropped. This is a safety feature
183    /// to protect the tip if the program exits unexpectedly.
184    ///
185    /// **Warning**: This will move hardware on every client drop, including normal
186    /// program termination. Only enable if you want this behavior.
187    ///
188    /// Default: `false` (disabled)
189    pub fn safe_tip_on_drop(mut self, enabled: bool) -> Self {
190        self.safe_tip_on_drop = enabled;
191        self
192    }
193
194    /// Build the NanonisClient
195    pub fn build(self) -> Result<NanonisClient, NanonisError> {
196        let address = self
197            .address
198            .ok_or_else(|| NanonisError::Protocol("Address must be specified".to_string()))?;
199
200        let port = self
201            .port
202            .ok_or_else(|| NanonisError::Protocol("Port must be specified".to_string()))?;
203
204        let socket_addr: SocketAddr = format!("{address}:{port}")
205            .parse()
206            .map_err(|_| NanonisError::Protocol(format!("Invalid address: {address}")))?;
207
208        debug!("Connecting to Nanonis at {address}");
209
210        let stream = TcpStream::connect_timeout(&socket_addr, self.config.connect_timeout)
211            .map_err(|e| {
212                warn!("Failed to connect to {address}: {e}");
213                if e.kind() == std::io::ErrorKind::TimedOut {
214                    NanonisError::Timeout(format!("Connection to {address} timed out"))
215                } else {
216                    NanonisError::Io {
217                        source: e,
218                        context: format!("Failed to connect to {address}"),
219                    }
220                }
221            })?;
222
223        // Set socket timeouts
224        stream.set_read_timeout(Some(self.config.read_timeout))?;
225        stream.set_write_timeout(Some(self.config.write_timeout))?;
226
227        debug!("Successfully connected to Nanonis");
228
229        Ok(NanonisClient {
230            stream,
231            debug: self.debug,
232            config: self.config,
233            safe_tip_on_drop: self.safe_tip_on_drop,
234        })
235    }
236}
237
238/// High-level client for communicating with Nanonis SPM systems via TCP.
239///
240/// `NanonisClient` provides a type-safe, Rust-friendly interface to the Nanonis
241/// TCP protocol. It handles connection management, protocol serialization/deserialization,
242/// and provides convenient methods for common operations like reading signals,
243/// controlling bias voltage, and managing the scanning probe.
244///
245/// # Connection Management
246///
247/// The client maintains a persistent TCP connection to the Nanonis server.
248/// Connection timeouts and retry logic are handled automatically.
249///
250/// # Protocol Support
251///
252/// Supports the standard Nanonis TCP command set including:
253/// - Signal reading (`Signals.ValsGet`, `Signals.NamesGet`)
254/// - Bias control (`Bias.Set`, `Bias.Get`)
255/// - Position control (`FolMe.XYPosSet`, `FolMe.XYPosGet`)
256/// - Motor control (`Motor.*` commands)
257/// - Auto-approach (`AutoApproach.*` commands)
258///
259/// # Examples
260///
261/// Basic usage:
262/// ```no_run
263/// use nanonis_rs::NanonisClient;
264///
265/// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
266///
267/// // Read signal names
268/// let signals = client.signal_names_get()?;
269///
270/// // Set bias voltage
271/// client.bias_set(1.0)?;
272///
273/// // Read signal values
274/// let values = client.signals_vals_get(vec![0, 1, 2], true)?;
275/// # Ok::<(), Box<dyn std::error::Error>>(())
276/// ```
277///
278/// With builder pattern:
279/// ```no_run
280/// use std::time::Duration;
281/// use nanonis_rs::NanonisClient;
282///
283/// let mut client = NanonisClient::builder()
284///     .address("192.168.1.100")
285///     .port(6501)
286///     .debug(true)
287///     .connect_timeout(Duration::from_secs(30))
288///     .build()?;
289/// # Ok::<(), Box<dyn std::error::Error>>(())
290/// ```
291pub struct NanonisClient {
292    stream: TcpStream,
293    debug: bool,
294    config: ConnectionConfig,
295    safe_tip_on_drop: bool,
296}
297
298impl NanonisClient {
299    /// Create a new client with default configuration.
300    ///
301    /// This is the most convenient way to create a client for basic usage.
302    /// Uses default timeouts and disables debug logging.
303    ///
304    /// # Arguments
305    /// * `addr` - Server address (e.g., "127.0.0.1")
306    /// * `port` - Server port (e.g., 6501)
307    ///
308    /// # Returns
309    /// A connected `NanonisClient` ready for use.
310    ///
311    /// # Errors
312    /// Returns `NanonisError` if:
313    /// - The address format is invalid
314    /// - Connection to the server fails
315    /// - Connection times out
316    ///
317    /// # Examples
318    /// ```no_run
319    /// use nanonis_rs::NanonisClient;
320    ///
321    /// let client = NanonisClient::new("127.0.0.1", 6501)?;
322    /// # Ok::<(), Box<dyn std::error::Error>>(())
323    /// ```
324    pub fn new(addr: &str, port: u16) -> Result<Self, NanonisError> {
325        Self::builder().address(addr).port(port).build()
326    }
327
328    /// Create a builder for flexible configuration.
329    ///
330    /// Use this when you need to customize timeouts, enable debug logging,
331    /// or other advanced configuration options.
332    ///
333    /// # Returns
334    /// A `NanonisClientBuilder` with default settings that can be customized.
335    ///
336    /// # Examples
337    /// ```no_run
338    /// use std::time::Duration;
339    /// use nanonis_rs::NanonisClient;
340    ///
341    /// let client = NanonisClient::builder()
342    ///     .address("192.168.1.100")
343    ///     .port(6501)
344    ///     .debug(true)
345    ///     .connect_timeout(Duration::from_secs(30))
346    ///     .build()?;
347    /// # Ok::<(), Box<dyn std::error::Error>>(())
348    /// ```
349    pub fn builder() -> NanonisClientBuilder {
350        NanonisClientBuilder::default()
351    }
352
353    /// Create a new client with custom configuration (legacy method).
354    ///
355    /// **Deprecated**: Use [`NanonisClient::builder()`] instead for more flexibility.
356    ///
357    /// # Arguments
358    /// * `addr` - Server address in format "host:port"
359    /// * `config` - Connection configuration with custom timeouts
360    pub fn with_config(addr: &str, config: ConnectionConfig) -> Result<Self, NanonisError> {
361        Self::builder().address(addr).config(config).build()
362    }
363
364    /// Enable or disable debug output
365    pub fn set_debug(&mut self, debug: bool) {
366        self.debug = debug;
367    }
368
369    /// Get the current connection configuration
370    pub fn config(&self) -> &ConnectionConfig {
371        &self.config
372    }
373
374    /// Send a quick command with minimal response handling.
375    ///
376    /// This is a low-level method for sending custom commands that don't fit
377    /// the standard method patterns. Most users should use the specific
378    /// command methods instead.
379    pub fn quick_send(
380        &mut self,
381        command: &str,
382        args: Vec<NanonisValue>,
383        argument_types: Vec<&str>,
384        return_types: Vec<&str>,
385    ) -> Result<Vec<NanonisValue>, NanonisError> {
386        debug!("=== COMMAND START: {} ===", command);
387        debug!("Arguments: {:?}", args);
388        debug!("Argument types: {:?}", argument_types);
389        debug!("Return types: {:?}", return_types);
390
391        // Serialize arguments
392        let mut body = Vec::new();
393        for (arg, arg_type) in args.iter().zip(argument_types.iter()) {
394            debug!("Serializing {:?} as {}", arg, arg_type);
395            Protocol::serialize_value(arg, arg_type, &mut body)?;
396        }
397
398        // Create command header
399        let header = Protocol::create_command_header(command, body.len() as u32);
400
401        debug!("Header size: {}, Body size: {}", header.len(), body.len());
402        debug!("Full header bytes: {:02x?}", header);
403        debug!(
404            "Command in header: {:?}",
405            String::from_utf8_lossy(&header[0..32]).trim_end_matches('\0')
406        );
407        debug!(
408            "Body size in header: {}",
409            u32::from_be_bytes([header[32], header[33], header[34], header[35]])
410        );
411
412        if !body.is_empty() {
413            debug!("Body bytes: {:02x?}", body);
414        }
415
416        // Send command
417        debug!("Sending header ({} bytes)...", header.len());
418        self.stream.write_all(&header).map_err(|e| {
419            debug!("Failed to write header: {}", e);
420            NanonisError::Io {
421                source: e,
422                context: "Writing command header".to_string(),
423            }
424        })?;
425
426        if !body.is_empty() {
427            debug!("Sending body ({} bytes)...", body.len());
428            self.stream.write_all(&body).map_err(|e| {
429                debug!("Failed to write body: {}", e);
430                NanonisError::Io {
431                    source: e,
432                    context: "Writing command body".to_string(),
433                }
434            })?;
435        }
436
437        debug!("Command data sent successfully");
438
439        // Read response header with improved error handling
440        debug!("Reading response header ({} bytes)...", HEADER_SIZE);
441        let response_header =
442            Protocol::read_exact_bytes::<HEADER_SIZE>(&mut self.stream).map_err(|e| {
443                debug!("Failed to read response header: {}", e);
444                e
445            })?;
446
447        debug!("Response header received: {:02x?}", response_header);
448        debug!(
449            "Response command: {:?}",
450            String::from_utf8_lossy(&response_header[0..32]).trim_end_matches('\0')
451        );
452
453        // Validate and get body size
454        let body_size = Protocol::validate_response_header(&response_header, command)?;
455        debug!("Expected response body size: {}", body_size);
456
457        // Read response body with size validation
458        let response_body = if body_size > 0 {
459            debug!("Reading response body ({} bytes)...", body_size);
460            let body = Protocol::read_variable_bytes(&mut self.stream, body_size as usize)
461                .map_err(|e| {
462                    debug!("Failed to read response body: {}", e);
463                    e
464                })?;
465            debug!(
466                "Response body received ({} bytes): {:02x?}",
467                body.len(),
468                if body.len() <= 100 {
469                    &body[..]
470                } else {
471                    &body[..100]
472                }
473            );
474            body
475        } else {
476            debug!("No response body expected");
477            Vec::new()
478        };
479
480        // Parse response with error checking
481        debug!("Parsing response with types: {:?}", return_types);
482        let result = Protocol::parse_response_with_error_check(&response_body, &return_types)
483            .map_err(|e| {
484                debug!("Failed to parse response: {}", e);
485                e
486            })?;
487
488        debug!("=== COMMAND SUCCESS: {} ===", command);
489        debug!("Parsed result: {:?}", result);
490
491        Ok(result)
492    }
493}
494
495impl Drop for NanonisClient {
496    fn drop(&mut self) {
497        if self.safe_tip_on_drop {
498            use motor::{MotorDirection, MotorGroup};
499            let _ = self.z_ctrl_withdraw(false, Duration::from_secs(2));
500            let _ = self.motor_start_move(MotorDirection::ZMinus, 15u16, MotorGroup::Group1, false);
501        }
502    }
503}