Skip to main content

nanonis_rs/client/
mod.rs

1use super::protocol::{HEADER_SIZE, Protocol};
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/// Connection configuration for the Nanonis TCP client.
54///
55/// Contains timeout settings for different phases of the TCP connection lifecycle.
56/// All timeouts have sensible defaults but can be customized for specific network conditions.
57///
58/// # Examples
59///
60/// ```
61/// use std::time::Duration;
62/// use nanonis_rs::ConnectionConfig;
63///
64/// // Use default timeouts
65/// let config = ConnectionConfig::default();
66///
67/// // Customize timeouts for slow network
68/// let config = ConnectionConfig {
69///     connect_timeout: Duration::from_secs(30),
70///     read_timeout: Duration::from_secs(60),
71///     write_timeout: Duration::from_secs(10),
72/// };
73/// ```
74#[derive(Debug, Clone)]
75pub struct ConnectionConfig {
76    /// Timeout for establishing the initial TCP connection
77    pub connect_timeout: Duration,
78    /// Timeout for reading data from the Nanonis server
79    pub read_timeout: Duration,
80    /// Timeout for writing data to the Nanonis server
81    pub write_timeout: Duration,
82}
83
84impl Default for ConnectionConfig {
85    fn default() -> Self {
86        Self {
87            connect_timeout: Duration::from_secs(5),
88            read_timeout: Duration::from_secs(10),
89            write_timeout: Duration::from_secs(5),
90        }
91    }
92}
93
94/// Builder for constructing [`NanonisClient`] instances with flexible configuration.
95///
96/// The builder pattern allows you to configure various aspects of the client
97/// before establishing the connection. This is more ergonomic than having
98/// multiple constructor variants.
99///
100/// # Examples
101///
102/// Basic usage:
103/// ```no_run
104/// use nanonis_rs::NanonisClient;
105///
106/// let client = NanonisClient::builder()
107///     .address("127.0.0.1")
108///     .port(6501)
109///     .debug(true)
110///     .build()?;
111/// # Ok::<(), Box<dyn std::error::Error>>(())
112/// ```
113///
114/// With custom timeouts:
115/// ```no_run
116/// use std::time::Duration;
117/// use nanonis_rs::NanonisClient;
118///
119/// let client = NanonisClient::builder()
120///     .address("192.168.1.100")
121///     .port(6501)
122///     .connect_timeout(Duration::from_secs(30))
123///     .read_timeout(Duration::from_secs(60))
124///     .debug(false)
125///     .build()?;
126/// # Ok::<(), Box<dyn std::error::Error>>(())
127/// ```
128#[derive(Default)]
129pub struct NanonisClientBuilder {
130    address: Option<String>,
131    port: Option<u16>,
132    config: ConnectionConfig,
133    debug: bool,
134    safe_tip_on_drop: bool,
135}
136
137impl NanonisClientBuilder {
138    pub fn address(mut self, addr: &str) -> Self {
139        self.address = Some(addr.to_string());
140        self
141    }
142
143    pub fn port(mut self, port: u16) -> Self {
144        self.port = Some(port);
145        self
146    }
147
148    /// Enable or disable debug logging
149    pub fn debug(mut self, debug: bool) -> Self {
150        self.debug = debug;
151        self
152    }
153
154    /// Set the full connection configuration
155    pub fn config(mut self, config: ConnectionConfig) -> Self {
156        self.config = config;
157        self
158    }
159
160    /// Set connect timeout
161    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
162        self.config.connect_timeout = timeout;
163        self
164    }
165
166    /// Set read timeout
167    pub fn read_timeout(mut self, timeout: Duration) -> Self {
168        self.config.read_timeout = timeout;
169        self
170    }
171
172    /// Set write timeout
173    pub fn write_timeout(mut self, timeout: Duration) -> Self {
174        self.config.write_timeout = timeout;
175        self
176    }
177
178    /// Enable automatic tip safety on client drop.
179    ///
180    /// When enabled, the client will automatically withdraw the Z-controller
181    /// and move motors to a safe position when dropped. This is a safety feature
182    /// to protect the tip if the program exits unexpectedly.
183    ///
184    /// **Warning**: This will move hardware on every client drop, including normal
185    /// program termination. Only enable if you want this behavior.
186    ///
187    /// Default: `false` (disabled)
188    pub fn safe_tip_on_drop(mut self, enabled: bool) -> Self {
189        self.safe_tip_on_drop = enabled;
190        self
191    }
192
193    /// Build the NanonisClient
194    pub fn build(self) -> Result<NanonisClient, NanonisError> {
195        let address = self
196            .address
197            .ok_or_else(|| NanonisError::Protocol("Address must be specified".to_string()))?;
198
199        let port = self
200            .port
201            .ok_or_else(|| NanonisError::Protocol("Port must be specified".to_string()))?;
202
203        let socket_addr: SocketAddr = format!("{address}:{port}")
204            .parse()
205            .map_err(|_| NanonisError::Protocol(format!("Invalid address: {address}")))?;
206
207        debug!("Connecting to Nanonis at {address}");
208
209        let stream = TcpStream::connect_timeout(&socket_addr, self.config.connect_timeout)
210            .map_err(|e| {
211                warn!("Failed to connect to {address}: {e}");
212                NanonisError::from_io(e, format!("Failed to connect to {address}"))
213            })?;
214
215        // Set socket timeouts
216        stream.set_read_timeout(Some(self.config.read_timeout))?;
217        stream.set_write_timeout(Some(self.config.write_timeout))?;
218
219        debug!("Successfully connected to Nanonis");
220
221        Ok(NanonisClient {
222            stream,
223            address,
224            port,
225            debug: self.debug,
226            config: self.config,
227            safe_tip_on_drop: self.safe_tip_on_drop,
228            poisoned: false,
229        })
230    }
231}
232
233/// High-level client for communicating with Nanonis SPM systems via TCP.
234///
235/// `NanonisClient` provides a type-safe, Rust-friendly interface to the Nanonis
236/// TCP protocol. It handles connection management, protocol serialization/deserialization,
237/// and provides convenient methods for common operations like reading signals,
238/// controlling bias voltage, and managing the scanning probe.
239///
240/// # Connection Management
241///
242/// The client maintains a persistent TCP connection to the Nanonis server.
243/// If an I/O error occurs during a command, the client is **poisoned** to
244/// prevent desynchronized reads on a corrupted stream. Call
245/// [`reconnect()`](Self::reconnect) to re-establish the connection, or check
246/// [`is_poisoned()`](Self::is_poisoned) to inspect the state.
247///
248/// # Protocol Support
249///
250/// Supports the standard Nanonis TCP command set including:
251/// - Signal reading (`Signals.ValsGet`, `Signals.NamesGet`)
252/// - Bias control (`Bias.Set`, `Bias.Get`)
253/// - Position control (`FolMe.XYPosSet`, `FolMe.XYPosGet`)
254/// - Motor control (`Motor.*` commands)
255/// - Auto-approach (`AutoApproach.*` commands)
256///
257/// # Examples
258///
259/// Basic usage:
260/// ```no_run
261/// use nanonis_rs::NanonisClient;
262///
263/// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
264///
265/// // Read signal names
266/// let signals = client.signal_names_get()?;
267///
268/// // Set bias voltage
269/// client.bias_set(1.0)?;
270///
271/// // Read signal values
272/// let values = client.signals_vals_get(vec![0, 1, 2], true)?;
273/// # Ok::<(), Box<dyn std::error::Error>>(())
274/// ```
275///
276/// With builder pattern:
277/// ```no_run
278/// use std::time::Duration;
279/// use nanonis_rs::NanonisClient;
280///
281/// let mut client = NanonisClient::builder()
282///     .address("192.168.1.100")
283///     .port(6501)
284///     .debug(true)
285///     .connect_timeout(Duration::from_secs(30))
286///     .build()?;
287/// # Ok::<(), Box<dyn std::error::Error>>(())
288/// ```
289pub struct NanonisClient {
290    stream: TcpStream,
291    address: String,
292    port: u16,
293    debug: bool,
294    config: ConnectionConfig,
295    safe_tip_on_drop: bool,
296    poisoned: bool,
297}
298
299impl NanonisClient {
300    /// Create a new client with default configuration.
301    ///
302    /// This is the most convenient way to create a client for basic usage.
303    /// Uses default timeouts and disables debug logging.
304    ///
305    /// # Arguments
306    /// * `addr` - Server address (e.g., "127.0.0.1")
307    /// * `port` - Server port (e.g., 6501)
308    ///
309    /// # Returns
310    /// A connected `NanonisClient` ready for use.
311    ///
312    /// # Errors
313    /// Returns `NanonisError` if:
314    /// - The address format is invalid
315    /// - Connection to the server fails
316    /// - Connection times out
317    ///
318    /// # Examples
319    /// ```no_run
320    /// use nanonis_rs::NanonisClient;
321    ///
322    /// let client = NanonisClient::new("127.0.0.1", 6501)?;
323    /// # Ok::<(), Box<dyn std::error::Error>>(())
324    /// ```
325    pub fn new(addr: &str, port: u16) -> Result<Self, NanonisError> {
326        Self::builder().address(addr).port(port).build()
327    }
328
329    /// Create a builder for flexible configuration.
330    ///
331    /// Use this when you need to customize timeouts, enable debug logging,
332    /// or other advanced configuration options.
333    ///
334    /// # Returns
335    /// A `NanonisClientBuilder` with default settings that can be customized.
336    ///
337    /// # Examples
338    /// ```no_run
339    /// use std::time::Duration;
340    /// use nanonis_rs::NanonisClient;
341    ///
342    /// let client = NanonisClient::builder()
343    ///     .address("192.168.1.100")
344    ///     .port(6501)
345    ///     .debug(true)
346    ///     .connect_timeout(Duration::from_secs(30))
347    ///     .build()?;
348    /// # Ok::<(), Box<dyn std::error::Error>>(())
349    /// ```
350    pub fn builder() -> NanonisClientBuilder {
351        NanonisClientBuilder::default()
352    }
353
354    /// Create a new client with custom configuration (legacy method).
355    ///
356    /// **Deprecated**: Use [`NanonisClient::builder()`] instead for more flexibility.
357    ///
358    /// # Arguments
359    /// * `addr` - Server address in format "host:port"
360    /// * `config` - Connection configuration with custom timeouts
361    #[deprecated(since = "0.2.0", note = "Use NanonisClient::builder() instead")]
362    pub fn with_config(addr: &str, config: ConnectionConfig) -> Result<Self, NanonisError> {
363        let (host, port_str) = addr.rsplit_once(':').ok_or_else(|| {
364            NanonisError::Protocol(format!(
365                "Invalid address format '{addr}': expected 'host:port'"
366            ))
367        })?;
368        let port: u16 = port_str
369            .parse()
370            .map_err(|_| NanonisError::Protocol(format!("Invalid port in address '{addr}'")))?;
371        Self::builder()
372            .address(host)
373            .port(port)
374            .config(config)
375            .build()
376    }
377
378    /// Enable or disable debug output
379    pub fn set_debug(&mut self, debug: bool) {
380        self.debug = debug;
381    }
382
383    /// Get the current connection configuration
384    pub fn config(&self) -> &ConnectionConfig {
385        &self.config
386    }
387
388    /// Returns `true` if the connection has been poisoned by an I/O error.
389    ///
390    /// After a failed read or write, the TCP stream may be in a desynchronized
391    /// state where subsequent commands would parse garbage data. The client
392    /// refuses further commands until [`reconnect()`](Self::reconnect) is called.
393    pub fn is_poisoned(&self) -> bool {
394        self.poisoned
395    }
396
397    /// Re-establish the TCP connection using the original address and configuration.
398    ///
399    /// This drops the old stream, connects a new one, and clears the poisoned flag.
400    /// Use this after an I/O error has poisoned the client, or when the Nanonis
401    /// application has been restarted.
402    pub fn reconnect(&mut self) -> Result<(), NanonisError> {
403        let socket_addr: SocketAddr = format!("{}:{}", self.address, self.port)
404            .parse()
405            .map_err(|_| NanonisError::Protocol(format!("Invalid address: {}", self.address)))?;
406
407        debug!("Reconnecting to Nanonis at {}:{}", self.address, self.port);
408
409        let stream = TcpStream::connect_timeout(&socket_addr, self.config.connect_timeout)
410            .map_err(|e| {
411                warn!("Failed to reconnect to {}:{}: {e}", self.address, self.port);
412                NanonisError::from_io(
413                    e,
414                    format!("Failed to reconnect to {}:{}", self.address, self.port),
415                )
416            })?;
417
418        stream.set_read_timeout(Some(self.config.read_timeout))?;
419        stream.set_write_timeout(Some(self.config.write_timeout))?;
420
421        self.stream = stream;
422        self.poisoned = false;
423
424        debug!("Successfully reconnected to Nanonis");
425        Ok(())
426    }
427
428    /// Send a quick command with minimal response handling.
429    ///
430    /// This is a low-level method for sending custom commands that don't fit
431    /// the standard method patterns. Most users should use the specific
432    /// command methods instead.
433    pub fn quick_send(
434        &mut self,
435        command: &str,
436        args: Vec<NanonisValue>,
437        argument_types: Vec<&str>,
438        return_types: Vec<&str>,
439    ) -> Result<Vec<NanonisValue>, NanonisError> {
440        debug!("Return types: {:?}", return_types);
441        let response_body = self.quick_send_raw(command, args, argument_types)?;
442
443        // Parse response with error checking
444        debug!("Parsing response with types: {:?}", return_types);
445        let result = Protocol::parse_response_with_error_check(&response_body, &return_types)
446            .map_err(|e| {
447                debug!("Failed to parse response: {}", e);
448                e
449            })?;
450
451        // Validate that the parsed result has the expected number of values.
452        // parse_response should always produce exactly one value per type descriptor,
453        // but this guard prevents panics in callers if there's ever a mismatch.
454        if result.len() < return_types.len() {
455            return Err(NanonisError::Protocol(format!(
456                "{command}: expected {} return values, got {}",
457                return_types.len(),
458                result.len()
459            )));
460        }
461
462        debug!("=== COMMAND SUCCESS: {} ===", command);
463        debug!("Parsed result: {:?}", result);
464
465        Ok(result)
466    }
467
468    /// Send a command and return the raw, unparsed response body.
469    ///
470    /// This is the lower half of [`quick_send`]: it serializes and sends the
471    /// command, then reads the full response body **without** applying a
472    /// return-type format string. Useful for commands whose reply layout
473    /// varies across Nanonis firmware versions (e.g. `Scan.PropsGet`), where
474    /// the caller needs to parse the body defensively.
475    pub fn quick_send_raw(
476        &mut self,
477        command: &str,
478        args: Vec<NanonisValue>,
479        argument_types: Vec<&str>,
480    ) -> Result<Vec<u8>, NanonisError> {
481        // Refuse commands on a poisoned connection to prevent desynchronized reads
482        if self.poisoned {
483            return Err(NanonisError::Io {
484                source: std::io::Error::new(
485                    std::io::ErrorKind::NotConnected,
486                    "connection poisoned after previous I/O error",
487                ),
488                context: format!(
489                    "Cannot send '{command}': call reconnect() to re-establish the connection"
490                ),
491            });
492        }
493
494        debug!("=== COMMAND START: {} ===", command);
495        debug!("Arguments: {:?}", args);
496        debug!("Argument types: {:?}", argument_types);
497
498        // Serialize arguments
499        let mut body = Vec::new();
500        for (arg, arg_type) in args.iter().zip(argument_types.iter()) {
501            debug!("Serializing {:?} as {}", arg, arg_type);
502            Protocol::serialize_value(arg, arg_type, &mut body)?;
503        }
504
505        // Create command header
506        let header = Protocol::create_command_header(command, body.len() as u32);
507
508        debug!("Header size: {}, Body size: {}", header.len(), body.len());
509        debug!("Full header bytes: {:02x?}", header);
510        debug!(
511            "Command in header: {:?}",
512            String::from_utf8_lossy(&header[0..32]).trim_end_matches('\0')
513        );
514        debug!(
515            "Body size in header: {}",
516            u32::from_be_bytes([header[32], header[33], header[34], header[35]])
517        );
518
519        if !body.is_empty() {
520            debug!("Body bytes: {:02x?}", body);
521        }
522
523        // Send command
524        // Any I/O failure from here on poisons the connection, because the
525        // stream may be left in a partially-written or partially-read state.
526        debug!("Sending header ({} bytes)...", header.len());
527        self.stream.write_all(&header).map_err(|e| {
528            debug!("Failed to write header: {}", e);
529            self.poisoned = true;
530            NanonisError::from_io(e, "Writing command header")
531        })?;
532
533        if !body.is_empty() {
534            debug!("Sending body ({} bytes)...", body.len());
535            self.stream.write_all(&body).map_err(|e| {
536                debug!("Failed to write body: {}", e);
537                self.poisoned = true;
538                NanonisError::from_io(e, "Writing command body")
539            })?;
540        }
541
542        debug!("Command data sent successfully");
543
544        // Read response header with improved error handling
545        debug!("Reading response header ({} bytes)...", HEADER_SIZE);
546        let response_header =
547            Protocol::read_exact_bytes::<HEADER_SIZE>(&mut self.stream).map_err(|e| {
548                debug!("Failed to read response header: {}", e);
549                self.poisoned = true;
550                e
551            })?;
552
553        debug!("Response header received: {:02x?}", response_header);
554        debug!(
555            "Response command: {:?}",
556            String::from_utf8_lossy(&response_header[0..32]).trim_end_matches('\0')
557        );
558
559        // Validate and get body size
560        let body_size = Protocol::validate_response_header(&response_header, command)?;
561        debug!("Expected response body size: {}", body_size);
562
563        // Read response body with size validation
564        let response_body = if body_size > 0 {
565            debug!("Reading response body ({} bytes)...", body_size);
566            let body = Protocol::read_variable_bytes(&mut self.stream, body_size as usize)
567                .map_err(|e| {
568                    debug!("Failed to read response body: {}", e);
569                    self.poisoned = true;
570                    e
571                })?;
572            debug!(
573                "Response body received ({} bytes): {:02x?}",
574                body.len(),
575                if body.len() <= 100 {
576                    &body[..]
577                } else {
578                    &body[..100]
579                }
580            );
581            body
582        } else {
583            debug!("No response body expected");
584            Vec::new()
585        };
586
587        Ok(response_body)
588    }
589}
590
591impl Drop for NanonisClient {
592    fn drop(&mut self) {
593        if self.safe_tip_on_drop {
594            if self.poisoned {
595                // A poisoned connection has a desynchronized TCP stream.
596                // Sending commands on it could be interpreted as garbage by
597                // the server, potentially triggering unintended hardware
598                // actions. Skip safety commands and rely on the hardware's
599                // own safe-tip protection instead.
600                warn!(
601                    "Skipping safe-tip-on-drop: connection is poisoned \
602                     (rely on Nanonis safe-tip hardware protection)"
603                );
604                return;
605            }
606            use motor::{MotorDirection, MotorGroup};
607            let _ = self.z_ctrl_withdraw(false, Duration::from_secs(2));
608            let _ = self.motor_start_move(MotorDirection::ZMinus, 15u16, MotorGroup::Group1, false);
609        }
610    }
611}