nanonis_rs/client/scan/mod.rs
1mod types;
2pub use types::*;
3
4use super::NanonisClient;
5use crate::error::NanonisError;
6use crate::types::{NanonisValue, Position};
7use std::time::Duration;
8
9use crate::protocol::Protocol;
10
11/// `Scan.PropsGet` reply layout for the newest documented firmware:
12/// continuous, bouncy, autosave, series-name(size+str), comment(size+str),
13/// modules-names(size+count+array), num-params-per-module(size+array),
14/// parameters(rows+cols+2-D array), autopaste.
15const SCAN_PROPS_FULL: &[&str] = &[
16 "I", "I", "I", "i", "*-c", "i", "*-c", "i", "i", "*+c", "i", "*+i", "i", "i", "*+c", "I",
17];
18
19/// Core fields returned by every firmware version. Older Nanonis releases omit
20/// the modules-params block and autopaste that were added later (see the
21/// TCPProtocol_SPM.pdf changelog), so their reply ends here.
22const SCAN_PROPS_CORE: &[&str] = &["I", "I", "I", "i", "*-c", "i", "*-c"];
23
24/// Parse a `Scan.PropsGet` response body, tolerant of older firmware.
25///
26/// The reply layout is purely additive across Nanonis versions, so we try the
27/// full documented layout first and fall back to the core fields when the body
28/// is too short. Without this, the cursor overruns the data into the error
29/// trailer and the read fails with `UnexpectedEof`.
30fn parse_scan_props(body: &[u8]) -> Result<ScanProps, NanonisError> {
31 let (result, data_end, full) = match Protocol::parse_response(body, SCAN_PROPS_FULL) {
32 Ok((values, cursor)) => (values, cursor, true),
33 Err(_) => {
34 let (values, cursor) = Protocol::parse_response(body, SCAN_PROPS_CORE)?;
35 (values, cursor, false)
36 }
37 };
38
39 // The error trailer sits immediately after the data we consumed.
40 Protocol::parse_error_info(body, data_end)?;
41
42 let continuous_scan = result[0].as_u32()? == 1;
43 let bouncy_scan = result[1].as_u32()? == 1;
44 let autosave = AutosaveMode::try_from(result[2].as_u32()?)?;
45 let series_name = result[4].as_string()?.to_string();
46 let comment = result[6].as_string()?.to_string();
47
48 let (modules_names, num_params_per_module, parameters, autopaste) = if full {
49 let modules_names = result[9].as_string_array()?.to_vec();
50 let num_params_per_module = result[11].as_i32_array()?.to_vec();
51 let rows = result[12].as_i32()?;
52 let cols = result[13].as_i32()?;
53 let params_flat = result[14].as_string_array()?;
54
55 // Convert flat array to 2D (rows x cols)
56 let mut parameters = Vec::new();
57 for row in 0..rows as usize {
58 let mut row_params = Vec::new();
59 for col in 0..cols as usize {
60 let idx = row * (cols as usize) + col;
61 if idx < params_flat.len() {
62 row_params.push(params_flat[idx].clone());
63 }
64 }
65 parameters.push(row_params);
66 }
67
68 let autopaste = AutopasteMode::try_from(result[15].as_u32()?)?;
69 (modules_names, num_params_per_module, parameters, autopaste)
70 } else {
71 // Older firmware does not report these fields; leave them empty.
72 (Vec::new(), Vec::new(), Vec::new(), AutopasteMode::Off)
73 };
74
75 Ok(ScanProps {
76 continuous_scan,
77 bouncy_scan,
78 autosave,
79 series_name,
80 comment,
81 modules_names,
82 num_params_per_module,
83 parameters,
84 autopaste,
85 })
86}
87
88impl NanonisClient {
89 /// Start, stop, pause or resume a scan
90 pub fn scan_action(
91 &mut self,
92 scan_action: ScanAction,
93 scan_direction: ScanDirection,
94 ) -> Result<(), NanonisError> {
95 self.quick_send(
96 "Scan.Action",
97 vec![
98 NanonisValue::U16(scan_action.into()),
99 NanonisValue::U32(scan_direction.into()),
100 ],
101 vec!["H", "I"],
102 vec![],
103 )?;
104 Ok(())
105 }
106
107 /// Configure the scan frame parameters
108 pub fn scan_frame_set(&mut self, frame: ScanFrame) -> Result<(), NanonisError> {
109 self.quick_send(
110 "Scan.FrameSet",
111 vec![
112 NanonisValue::F32(frame.center.x as f32),
113 NanonisValue::F32(frame.center.y as f32),
114 NanonisValue::F32(frame.width_m),
115 NanonisValue::F32(frame.height_m),
116 NanonisValue::F32(frame.angle_deg),
117 ],
118 vec!["f", "f", "f", "f", "f"],
119 vec![],
120 )?;
121 Ok(())
122 }
123
124 /// Get the scan frame parameters
125 pub fn scan_frame_get(&mut self) -> Result<ScanFrame, NanonisError> {
126 let result = self.quick_send(
127 "Scan.FrameGet",
128 vec![],
129 vec![],
130 vec!["f", "f", "f", "f", "f"],
131 )?;
132 if result.len() >= 5 {
133 let center_x = result[0].as_f32()? as f64;
134 let center_y = result[1].as_f32()? as f64;
135 let width = result[2].as_f32()?;
136 let height = result[3].as_f32()?;
137 let angle = result[4].as_f32()?;
138
139 Ok(ScanFrame::new(
140 Position::new(center_x, center_y),
141 width,
142 height,
143 angle,
144 ))
145 } else {
146 Err(NanonisError::Protocol(
147 "Invalid scan frame response".to_string(),
148 ))
149 }
150 }
151
152 /// Get the scan buffer parameters
153 /// Returns: (channel_indexes, pixels, lines)
154 pub fn scan_buffer_get(&mut self) -> Result<(Vec<i32>, i32, i32), NanonisError> {
155 let result =
156 self.quick_send("Scan.BufferGet", vec![], vec![], vec!["i", "*i", "i", "i"])?;
157 if result.len() >= 4 {
158 let channel_indexes = result[1].as_i32_array()?.to_vec();
159 let pixels = result[2].as_i32()?;
160 let lines = result[3].as_i32()?;
161 Ok((channel_indexes, pixels, lines))
162 } else {
163 Err(NanonisError::Protocol(
164 "Invalid scan buffer response".to_string(),
165 ))
166 }
167 }
168
169 /// Get the current scan status.
170 ///
171 /// Returns whether a scan is currently running or not.
172 ///
173 /// # Returns
174 /// `true` if scan is running, `false` if scan is not running.
175 ///
176 /// # Errors
177 /// Returns `NanonisError` if communication fails or protocol error occurs.
178 ///
179 /// # Examples
180 /// ```no_run
181 /// use nanonis_rs::NanonisClient;
182 ///
183 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
184 ///
185 /// if client.scan_status_get()? {
186 /// println!("Scan is currently running");
187 /// } else {
188 /// println!("Scan is stopped");
189 /// }
190 /// # Ok::<(), Box<dyn std::error::Error>>(())
191 /// ```
192 pub fn scan_status_get(&mut self) -> Result<bool, NanonisError> {
193 let result = self.quick_send("Scan.StatusGet", vec![], vec![], vec!["I"])?;
194
195 match result.first() {
196 Some(value) => Ok(value.as_u32()? == 1),
197 None => Err(NanonisError::Protocol(
198 "No scan status returned".to_string(),
199 )),
200 }
201 }
202
203 /// Configure the scan buffer parameters.
204 ///
205 /// Sets which channels to record during scanning and the scan resolution.
206 /// The channel indexes refer to the 24 signals assigned in the Signals Manager (0-23).
207 ///
208 /// **Important**: The number of pixels is coerced to the closest multiple of 16
209 /// because scan data is sent in packages of 16 pixels.
210 ///
211 /// # Arguments
212 /// * `channel_indexes` - Indexes of channels to record (0-23 for signals in Signals Manager)
213 /// * `pixels` - Number of pixels per line (coerced to multiple of 16)
214 /// * `lines` - Number of scan lines
215 ///
216 /// # Errors
217 /// Returns `NanonisError` if communication fails or invalid parameters provided.
218 ///
219 /// # Examples
220 /// ```no_run
221 /// use nanonis_rs::NanonisClient;
222 ///
223 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
224 ///
225 /// // Record channels 0, 1, and 2 with 512x512 resolution
226 /// client.scan_buffer_set(vec![0, 1, 2], 512, 512)?;
227 ///
228 /// // High resolution scan with multiple channels
229 /// client.scan_buffer_set(vec![0, 1, 2, 3, 4], 1024, 1024)?;
230 /// # Ok::<(), Box<dyn std::error::Error>>(())
231 /// ```
232 pub fn scan_buffer_set(
233 &mut self,
234 channel_indexes: Vec<i32>,
235 pixels: i32,
236 lines: i32,
237 ) -> Result<(), NanonisError> {
238 self.quick_send(
239 "Scan.BufferSet",
240 vec![
241 NanonisValue::ArrayI32(channel_indexes),
242 NanonisValue::I32(pixels),
243 NanonisValue::I32(lines),
244 ],
245 vec!["+*i", "i", "i"],
246 vec![],
247 )?;
248 Ok(())
249 }
250
251 /// Configure scan speed parameters.
252 ///
253 /// Sets the tip scanning speeds for both forward and backward scan directions.
254 /// You can specify either linear speed or time per line, and set speed ratios
255 /// between forward and backward scanning.
256 ///
257 /// # Arguments
258 /// * `forward_linear_speed_m_s` - Forward linear speed in m/s
259 /// * `backward_linear_speed_m_s` - Backward linear speed in m/s
260 /// * `forward_time_per_line_s` - Forward time per line in seconds
261 /// * `backward_time_per_line_s` - Backward time per line in seconds
262 /// * `keep_parameter_constant` - Which parameter to keep constant: 0=no change, 1=linear speed, 2=time per line
263 /// * `speed_ratio` - Backward tip speed relative to forward speed
264 ///
265 /// # Errors
266 /// Returns `NanonisError` if communication fails or invalid parameters provided.
267 ///
268 /// # Examples
269 /// ```no_run
270 /// use nanonis_rs::NanonisClient;
271 /// use nanonis_rs::scan::ScanConfig;
272 ///
273 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
274 ///
275 /// // Set 1 μm/s forward, 2 μm/s backward, keep linear speed constant
276 /// let config = ScanConfig {
277 /// forward_linear_speed_m_s: 1e-6,
278 /// backward_linear_speed_m_s: 2e-6,
279 /// forward_time_per_line_s: 0.1,
280 /// backward_time_per_line_s: 0.05,
281 /// keep_parameter_constant: 1,
282 /// speed_ratio: 2.0,
283 /// };
284 /// client.scan_config_set(config)?;
285 /// # Ok::<(), Box<dyn std::error::Error>>(())
286 /// ```
287 pub fn scan_config_set(&mut self, config: ScanConfig) -> Result<(), NanonisError> {
288 self.quick_send(
289 "Scan.SpeedSet",
290 vec![
291 NanonisValue::F32(config.forward_linear_speed_m_s),
292 NanonisValue::F32(config.backward_linear_speed_m_s),
293 NanonisValue::F32(config.forward_time_per_line_s),
294 NanonisValue::F32(config.backward_time_per_line_s),
295 NanonisValue::U16(config.keep_parameter_constant),
296 NanonisValue::F32(config.speed_ratio),
297 ],
298 vec!["f", "f", "f", "f", "H", "f"],
299 vec![],
300 )?;
301 Ok(())
302 }
303
304 /// Get the current scan speed parameters.
305 ///
306 /// Returns all scan speed configuration values including linear speeds,
307 /// time per line, and speed ratio settings.
308 ///
309 /// # Returns
310 /// A tuple containing:
311 /// - `f32` - Forward linear speed (m/s)
312 /// - `f32` - Backward linear speed (m/s)
313 /// - `f32` - Forward time per line (s)
314 /// - `f32` - Backward time per line (s)
315 /// - `u16` - Keep parameter constant (0=linear speed, 1=time per line)
316 /// - `f32` - Speed ratio (backward relative to forward)
317 ///
318 /// # Errors
319 /// Returns `NanonisError` if communication fails or protocol error occurs.
320 ///
321 /// # Examples
322 /// ```no_run
323 /// use nanonis_rs::NanonisClient;
324 ///
325 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
326 ///
327 /// let config = client.scan_speed_get()?;
328 ///
329 /// println!("Forward speed: {:.2e} m/s", config.forward_linear_speed_m_s);
330 /// println!("Backward speed: {:.2e} m/s", config.backward_linear_speed_m_s);
331 /// println!("Speed ratio: {:.1}", config.speed_ratio);
332 /// # Ok::<(), Box<dyn std::error::Error>>(())
333 /// ```
334 pub fn scan_speed_get(&mut self) -> Result<ScanConfig, NanonisError> {
335 let result = self.quick_send(
336 "Scan.SpeedGet",
337 vec![],
338 vec![],
339 vec!["f", "f", "f", "f", "H", "f"],
340 )?;
341
342 if result.len() >= 6 {
343 Ok(ScanConfig {
344 forward_linear_speed_m_s: result[0].as_f32()?,
345 backward_linear_speed_m_s: result[1].as_f32()?,
346 forward_time_per_line_s: result[2].as_f32()?,
347 backward_time_per_line_s: result[3].as_f32()?,
348 keep_parameter_constant: result[4].as_u16()?,
349 speed_ratio: result[5].as_f32()?,
350 })
351 } else {
352 Err(NanonisError::Protocol(
353 "Invalid scan speed response".to_string(),
354 ))
355 }
356 }
357
358 /// Get the current XY position during scanning.
359 ///
360 /// Returns the current values of the X and Y signals, useful for monitoring
361 /// tip position during scanning operations.
362 ///
363 /// # Arguments
364 /// * `wait_newest_data` - If `true`, discards first value and waits for fresh data
365 ///
366 /// # Returns
367 /// A tuple containing (X position in m, Y position in m)
368 ///
369 /// # Errors
370 /// Returns `NanonisError` if communication fails or protocol error occurs.
371 ///
372 /// # Examples
373 /// ```no_run
374 /// use nanonis_rs::NanonisClient;
375 ///
376 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
377 ///
378 /// // Get current position immediately
379 /// let (x, y) = client.scan_xy_pos_get(false)?;
380 /// println!("Current position: ({:.6}, {:.6}) m", x, y);
381 ///
382 /// // Wait for fresh position data
383 /// let (x, y) = client.scan_xy_pos_get(true)?;
384 /// # Ok::<(), Box<dyn std::error::Error>>(())
385 /// ```
386 pub fn scan_xy_pos_get(&mut self, wait_newest_data: bool) -> Result<(f32, f32), NanonisError> {
387 let wait_flag = if wait_newest_data { 1u32 } else { 0u32 };
388
389 let result = self.quick_send(
390 "Scan.XYPosGet",
391 vec![NanonisValue::U32(wait_flag)],
392 vec!["I"],
393 vec!["f", "f"],
394 )?;
395
396 if result.len() >= 2 {
397 Ok((result[0].as_f32()?, result[1].as_f32()?))
398 } else {
399 Err(NanonisError::Protocol(
400 "Invalid XY position response".to_string(),
401 ))
402 }
403 }
404
405 /// Save the current scan data buffer to file.
406 ///
407 /// Saves the current scan data into a file. If `wait_until_saved` is true,
408 /// the function waits for the save operation to complete before returning.
409 ///
410 /// # Arguments
411 /// * `wait_until_saved` - If `true`, waits for save completion before returning
412 /// * `timeout_ms` - Timeout in milliseconds (-1 for indefinite wait)
413 ///
414 /// # Returns
415 /// `true` if timeout occurred while waiting for save completion, `false` otherwise
416 ///
417 /// # Errors
418 /// Returns `NanonisError` if communication fails or protocol error occurs.
419 ///
420 /// # Examples
421 /// ```no_run
422 /// use nanonis_rs::NanonisClient;
423 ///
424 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
425 ///
426 /// // Save immediately without waiting
427 /// let timed_out = client.scan_save(false, 5000)?;
428 ///
429 /// // Save and wait up to 30 seconds for completion
430 /// let timed_out = client.scan_save(true, 30000)?;
431 /// if timed_out {
432 /// println!("Save operation timed out");
433 /// }
434 /// # Ok::<(), Box<dyn std::error::Error>>(())
435 /// ```
436 pub fn scan_save(
437 &mut self,
438 wait_until_saved: bool,
439 timeout_ms: i32,
440 ) -> Result<bool, NanonisError> {
441 let wait_flag = if wait_until_saved { 1u32 } else { 0u32 };
442
443 let result = self.quick_send(
444 "Scan.Save",
445 vec![NanonisValue::U32(wait_flag), NanonisValue::I32(timeout_ms)],
446 vec!["I", "i"],
447 vec!["I"],
448 )?;
449
450 match result.first() {
451 Some(value) => Ok(value.as_u32()? == 1),
452 None => Err(NanonisError::Protocol(
453 "No save status returned".to_string(),
454 )),
455 }
456 }
457
458 /// Get scan frame data for a specific channel and direction.
459 ///
460 /// Returns the complete 2D scan data array for the selected channel.
461 /// The channel must be one of the channels configured in the scan buffer.
462 ///
463 /// # Arguments
464 /// * `channel_index` - Index of channel to retrieve data from (must be in acquired channels)
465 /// * `data_direction` - Data direction: `true` for forward, `false` for backward
466 ///
467 /// # Returns
468 /// A tuple containing:
469 /// - `String` - Channel name
470 /// - `Vec<Vec<f32>>` - 2D scan data array \[rows\]\[columns\]
471 /// - `bool` - Scan direction: `true` for up, `false` for down
472 ///
473 /// # Errors
474 /// Returns `NanonisError` if:
475 /// - Invalid channel index (not in acquired channels)
476 /// - Communication fails or protocol error occurs
477 ///
478 /// # Examples
479 /// ```no_run
480 /// use nanonis_rs::NanonisClient;
481 ///
482 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
483 ///
484 /// // Get forward scan data for channel 0
485 /// let (channel_name, data, scan_up) = client.scan_frame_data_grab(0, true)?;
486 /// println!("Channel: {}, Direction: {}", channel_name, if scan_up { "up" } else { "down" });
487 /// println!("Data size: {}x{}", data.len(), data[0].len());
488 ///
489 /// // Get backward scan data
490 /// let (_, back_data, _) = client.scan_frame_data_grab(0, false)?;
491 /// # Ok::<(), Box<dyn std::error::Error>>(())
492 /// ```
493 pub fn scan_frame_data_grab(
494 &mut self,
495 channel_index: u32,
496 data_direction: bool,
497 ) -> Result<(String, Vec<Vec<f32>>, bool), NanonisError> {
498 let direction_flag = if data_direction { 1u32 } else { 0u32 };
499
500 let result = self.quick_send(
501 "Scan.FrameDataGrab",
502 vec![
503 NanonisValue::U32(channel_index),
504 NanonisValue::U32(direction_flag),
505 ],
506 vec!["I", "I"],
507 vec!["i", "*-c", "i", "i", "2f", "I"],
508 )?;
509
510 if result.len() >= 6 {
511 let channel_name = result[1].as_string()?.to_string();
512
513 // The protocol layer already parses "2f" into a 2D array
514 let data_2d = result[4].as_f32_2d_array()?.clone();
515
516 let scan_direction = result[5].as_u32()? == 1;
517 Ok((channel_name, data_2d, scan_direction))
518 } else {
519 Err(NanonisError::Protocol(
520 "Invalid frame data response".to_string(),
521 ))
522 }
523 }
524
525 /// Wait for the End-of-Scan.
526 ///
527 /// Waits for the current scan to complete or timeout to occur, whichever comes first.
528 /// This is useful for synchronizing operations with scan completion.
529 ///
530 /// # Arguments
531 /// * `timeout` - Timeout duration (-1 for indefinite wait)
532 ///
533 /// # Returns
534 /// A tuple containing:
535 /// - `bool` - `true` if timeout occurred, `false` if scan completed normally
536 /// - `String` - File path where data was auto-saved (empty if no auto-save)
537 ///
538 /// # Errors
539 /// Returns `NanonisError` if communication fails or protocol error occurs.
540 ///
541 /// # Examples
542 /// ```no_run
543 /// use nanonis_rs::NanonisClient;
544 /// use nanonis_rs::scan::{ScanAction, ScanDirection};
545 /// use std::time::Duration;
546 ///
547 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
548 ///
549 /// // Start a scan
550 /// client.scan_action(ScanAction::Start, ScanDirection::Up)?;
551 ///
552 /// // Wait for scan to complete (up to 5 minutes)
553 /// let (timed_out, file_path) = client.scan_wait_end_of_scan(Duration::from_secs(300))?;
554 ///
555 /// if timed_out {
556 /// println!("Scan timed out after 5 minutes");
557 /// } else {
558 /// println!("Scan completed");
559 /// if !file_path.is_empty() {
560 /// println!("Data saved to: {}", file_path);
561 /// }
562 /// }
563 /// # Ok::<(), Box<dyn std::error::Error>>(())
564 /// ```
565 pub fn scan_wait_end_of_scan(
566 &mut self,
567 timeout: Duration,
568 ) -> Result<(bool, String), NanonisError> {
569 let result = self.quick_send(
570 "Scan.WaitEndOfScan",
571 vec![NanonisValue::I32(
572 timeout.as_millis().min(i32::MAX as u128) as i32
573 )],
574 vec!["i"],
575 vec!["I", "I", "*-c"],
576 )?;
577
578 if result.len() >= 3 {
579 let timeout_occurred = result[0].as_u32()? == 1;
580 let file_path = result[2].as_string()?.to_string();
581 Ok((timeout_occurred, file_path))
582 } else {
583 Err(NanonisError::Protocol(
584 "Invalid scan wait response".to_string(),
585 ))
586 }
587 }
588
589 /// Get scan properties configuration.
590 ///
591 /// Returns current scan properties including continuous scan, bouncy scan,
592 /// autosave, series name, comment, modules names, and autopaste settings.
593 ///
594 /// # Returns
595 /// `ScanProps` structure containing all scan property settings
596 ///
597 /// # Errors
598 /// Returns `NanonisError` if communication fails or protocol error occurs.
599 ///
600 /// # Examples
601 /// ```no_run
602 /// use nanonis_rs::NanonisClient;
603 ///
604 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
605 ///
606 /// let props = client.scan_props_get()?;
607 /// println!("Continuous scan: {:?}", props.continuous_scan);
608 /// println!("Bouncy scan: {:?}", props.bouncy_scan);
609 /// println!("Series name: {}", props.series_name);
610 /// # Ok::<(), Box<dyn std::error::Error>>(())
611 /// ```
612 pub fn scan_props_get(&mut self) -> Result<ScanProps, NanonisError> {
613 // Read the raw body and parse defensively: the reply layout depends on
614 // the Nanonis firmware version (older firmware omits the modules-params
615 // block and autopaste). See `parse_scan_props`.
616 let body = self.quick_send_raw("Scan.PropsGet", vec![], vec![])?;
617 parse_scan_props(&body)
618 }
619
620 /// Set scan properties configuration.
621 ///
622 /// Configures scan parameters including continuous scan, bouncy scan,
623 /// autosave behavior, series name, comment, and autopaste settings.
624 /// Use `ScanPropsBuilder` to set only the properties you want to change.
625 ///
626 /// # Arguments
627 /// * `builder` - Builder with properties to set. Fields set to `None` will not be changed.
628 ///
629 /// # Errors
630 /// Returns `NanonisError` if communication fails or invalid parameters provided.
631 ///
632 /// # Examples
633 /// ```no_run
634 /// use nanonis_rs::NanonisClient;
635 /// use nanonis_rs::scan::{ScanPropsBuilder, AutosaveMode, AutopasteMode};
636 ///
637 /// let mut client = NanonisClient::new("127.0.0.1", 6501)?;
638 ///
639 /// // Set only specific properties using the builder
640 /// let builder = ScanPropsBuilder::new()
641 /// .continuous_scan(true) // Enable continuous scan
642 /// .bouncy_scan(true) // Enable bouncy scan
643 /// .autosave(AutosaveMode::Off); // Disable autosave
644 ///
645 /// client.scan_props_set(builder)?;
646 /// # Ok::<(), Box<dyn std::error::Error>>(())
647 /// ```
648 pub fn scan_props_set(&mut self, builder: ScanPropsBuilder) -> Result<(), NanonisError> {
649 // Convert boolean options to u32 (0=no change, 1=On, 2=Off)
650 let continuous_flag = match builder.continuous_scan {
651 None => 0u32,
652 Some(true) => 1u32,
653 Some(false) => 2u32,
654 };
655
656 let bouncy_flag = match builder.bouncy_scan {
657 None => 0u32,
658 Some(true) => 1u32,
659 Some(false) => 2u32,
660 };
661
662 let autosave_flag = match builder.autosave {
663 None => 0u32,
664 Some(mode) => mode.into(),
665 };
666
667 let autopaste_flag = match builder.autopaste {
668 None => 0u32,
669 Some(mode) => mode.into(),
670 };
671
672 // For strings/arrays: empty string/array means no change
673 let series_name = builder.series_name.unwrap_or_default();
674 let comment = builder.comment.unwrap_or_default();
675 let modules_names = builder.modules_names.unwrap_or_default();
676
677 self.quick_send(
678 "Scan.PropsSet",
679 vec![
680 NanonisValue::U32(continuous_flag),
681 NanonisValue::U32(bouncy_flag),
682 NanonisValue::U32(autosave_flag),
683 NanonisValue::String(series_name),
684 NanonisValue::String(comment),
685 NanonisValue::ArrayString(modules_names),
686 NanonisValue::U32(autopaste_flag),
687 ],
688 vec!["I", "I", "I", "+*c", "+*c", "+*c", "I"],
689 vec![],
690 )?;
691
692 Ok(())
693 }
694
695 /// Paste background image at specified location.
696 ///
697 /// Pastes a previously copied background image to the scan buffer at the
698 /// specified position. This is used for background subtraction operations.
699 ///
700 /// # Arguments
701 /// * `x` - X position in meters for paste location
702 /// * `y` - Y position in meters for paste location
703 ///
704 /// # Errors
705 /// Returns `NanonisError` if communication fails or no background is available.
706 pub fn scan_background_paste(&mut self, x: f64, y: f64) -> Result<(), NanonisError> {
707 self.quick_send(
708 "Scan.BackgroundPaste",
709 vec![NanonisValue::F64(x), NanonisValue::F64(y)],
710 vec!["d", "d"],
711 vec![],
712 )?;
713 Ok(())
714 }
715
716 /// Delete the stored background image.
717 ///
718 /// Removes the background image from memory, freeing resources and
719 /// disabling background subtraction until a new background is captured.
720 ///
721 /// # Errors
722 /// Returns `NanonisError` if communication fails.
723 pub fn scan_background_delete(&mut self) -> Result<(), NanonisError> {
724 self.quick_send("Scan.BackgroundDelete", vec![], vec![], vec![])?;
725 Ok(())
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 // Big-endian encoders for building synthetic Scan.PropsGet response bodies.
734 fn u32be(v: u32, out: &mut Vec<u8>) {
735 out.extend_from_slice(&v.to_be_bytes());
736 }
737 fn i32be(v: i32, out: &mut Vec<u8>) {
738 out.extend_from_slice(&v.to_be_bytes());
739 }
740 fn sized_str(s: &str, out: &mut Vec<u8>) {
741 i32be(s.len() as i32, out);
742 out.extend_from_slice(s.as_bytes());
743 }
744 // 8-byte success trailer: error status = 0, description size = 0.
745 fn ok_trailer(out: &mut Vec<u8>) {
746 i32be(0, out);
747 i32be(0, out);
748 }
749
750 /// Older firmware: the reply ends after the comment (no modules-params
751 /// block, no autopaste). This is the layout that previously overran into
752 /// the error trailer and failed with `UnexpectedEof`.
753 #[test]
754 fn parse_scan_props_core_only_layout() {
755 let mut body = Vec::new();
756 u32be(1, &mut body); // continuous = On
757 u32be(0, &mut body); // bouncy = Off
758 u32be(2, &mut body); // autosave = Off
759 sized_str("scan", &mut body);
760 sized_str("hello", &mut body);
761 ok_trailer(&mut body);
762
763 let props = parse_scan_props(&body).expect("core layout should parse");
764 assert!(props.continuous_scan);
765 assert!(!props.bouncy_scan);
766 assert_eq!(props.autosave, AutosaveMode::Off);
767 assert_eq!(props.series_name, "scan");
768 assert_eq!(props.comment, "hello");
769 assert!(props.modules_names.is_empty());
770 assert!(props.parameters.is_empty());
771 }
772
773 /// Newer firmware: full documented layout, with empty modules/params arrays.
774 #[test]
775 fn parse_scan_props_full_layout() {
776 let mut body = Vec::new();
777 u32be(0, &mut body); // continuous = Off
778 u32be(1, &mut body); // bouncy = On
779 u32be(0, &mut body); // autosave = All
780 sized_str("img", &mut body);
781 sized_str("", &mut body);
782 i32be(0, &mut body); // modules names size (bytes)
783 i32be(0, &mut body); // modules names count -> 0 strings
784 i32be(0, &mut body); // num-params-per-module count -> 0 ints
785 i32be(0, &mut body); // parameters rows
786 i32be(0, &mut body); // parameters cols -> 0 strings
787 u32be(2, &mut body); // autopaste = Off
788 ok_trailer(&mut body);
789
790 let props = parse_scan_props(&body).expect("full layout should parse");
791 assert!(!props.continuous_scan);
792 assert!(props.bouncy_scan);
793 assert_eq!(props.autosave, AutosaveMode::All);
794 assert_eq!(props.series_name, "img");
795 assert_eq!(props.comment, "");
796 assert_eq!(props.autopaste, AutopasteMode::Off);
797 }
798}