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}