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}