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