Skip to main content

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}