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}