rusty_tip/
action_driver.rs

1use log::info;
2use ndarray::Array1;
3
4use crate::actions::{Action, ActionChain, ActionResult, ExpectFromAction};
5use crate::error::NanonisError;
6use crate::nanonis::NanonisClient;
7use crate::types::{
8    DataToGet, MotorGroup, OsciData, Position, PulseMode, ScanDirection, SignalIndex, SignalStats,
9    TriggerConfig, ZControllerHold,
10};
11use crate::utils::{poll_until, poll_with_timeout, PollError};
12use crate::TipShaperConfig;
13use std::collections::HashMap;
14use std::thread;
15use std::time::Duration;
16
17/// Direct 1:1 translation layer between Actions and NanonisClient calls
18/// No safety checks, no validation - maximum performance and flexibility
19pub struct ActionDriver {
20    client: NanonisClient,
21    /// Storage for Store/Retrieve actions
22    stored_values: HashMap<String, ActionResult>,
23}
24
25impl ActionDriver {
26    /// Create a new ActionDriver with the given client
27    pub fn new(addr: &str, port: u16) -> Result<Self, NanonisError> {
28        let client = NanonisClient::new(addr, port)?;
29
30        Ok(Self {
31            client,
32            stored_values: HashMap::new(),
33        })
34    }
35
36    /// Convenience method to create with NanonisClient
37    pub fn with_nanonis_client(client: NanonisClient) -> Self {
38        Self {
39            client,
40            stored_values: HashMap::new(),
41        }
42    }
43
44    /// Get a reference to the underlying NanonisClient
45    pub fn client(&self) -> &NanonisClient {
46        &self.client
47    }
48
49    /// Get a mutable reference to the underlying NanonisClient
50    pub fn client_mut(&mut self) -> &mut NanonisClient {
51        &mut self.client
52    }
53
54    /// Execute a single action with direct 1:1 mapping to client methods
55    pub fn execute(&mut self, action: Action) -> Result<ActionResult, NanonisError> {
56        match action {
57            // === Signal Operations ===
58            Action::ReadSignal {
59                signal,
60                wait_for_newest,
61            } => {
62                let value = self
63                    .client
64                    .signals_vals_get(vec![signal.into()], wait_for_newest)?;
65                Ok(ActionResult::Value(value[0] as f64))
66            }
67
68            Action::ReadSignals {
69                signals,
70                wait_for_newest,
71            } => {
72                let indices: Vec<i32> = signals.iter().map(|s| (*s).into()).collect();
73                let values = self.client.signals_vals_get(indices, wait_for_newest)?;
74                Ok(ActionResult::Values(
75                    values.into_iter().map(|v| v as f64).collect(),
76                ))
77            }
78
79            Action::ReadSignalNames => {
80                let names = self.client.signal_names_get(false)?;
81                Ok(ActionResult::Text(names))
82            }
83
84            // === Bias Operations ===
85            Action::ReadBias => {
86                let bias = self.client.get_bias()?;
87                Ok(ActionResult::Value(bias as f64))
88            }
89
90            Action::SetBias { voltage } => {
91                self.client.set_bias(voltage)?;
92                Ok(ActionResult::Success)
93            }
94
95            // === Oscilloscope Operations ===
96            Action::ReadOsci {
97                signal,
98                trigger,
99                data_to_get,
100                is_stable,
101            } => {
102                self.client.osci1t_run()?;
103
104                self.client.osci1t_ch_set(signal.0)?;
105
106                if let Some(trigger) = trigger {
107                    self.client.osci1t_trig_set(
108                        trigger.mode.into(),
109                        trigger.slope.into(),
110                        trigger.level,
111                        trigger.hysteresis,
112                    )?;
113                }
114
115                match data_to_get {
116                    crate::types::DataToGet::Stable { readings, timeout } => {
117                        let osci_data = self.find_stable_oscilloscope_data_with_fallback(
118                            data_to_get,
119                            readings,
120                            timeout,
121                            0.01,
122                            50e-15,
123                            0.8,
124                            is_stable,
125                        )?;
126                        Ok(ActionResult::OsciData(osci_data))
127                    }
128                    _ => {
129                        // Use NextTrigger for actual data reading - Stable is just for our algorithm
130                        let data_mode = match data_to_get {
131                            DataToGet::Current => 0,
132                            DataToGet::NextTrigger => 1,
133                            DataToGet::Wait2Triggers => 2,
134                            DataToGet::Stable { .. } => 1, // Use NextTrigger for stable
135                        };
136                        let (t0, dt, size, data) = self.client.osci1t_data_get(data_mode)?;
137                        let osci_data = OsciData::new_stable(t0, dt, size, data);
138                        Ok(ActionResult::OsciData(osci_data))
139                    }
140                }
141            }
142
143            // === Fine Positioning Operations (Piezo) ===
144            Action::ReadPiezoPosition {
145                wait_for_newest_data,
146            } => {
147                let pos = self.client.folme_xy_pos_get(wait_for_newest_data)?;
148                Ok(ActionResult::Position(pos))
149            }
150
151            Action::SetPiezoPosition {
152                position,
153                wait_until_finished,
154            } => {
155                self.client
156                    .folme_xy_pos_set(position, wait_until_finished)?;
157                Ok(ActionResult::Success)
158            }
159
160            Action::MovePiezoRelative { delta } => {
161                // Get current position and add delta
162                let current = self.client.folme_xy_pos_get(true)?;
163                info!("Current position: {current:?}");
164                let new_position = Position {
165                    x: current.x + delta.x,
166                    y: current.y + delta.y,
167                };
168                self.client.folme_xy_pos_set(new_position, true)?;
169                Ok(ActionResult::Success)
170            }
171
172            // === Coarse Positioning Operations (Motor) ===
173            Action::MoveMotor { direction, steps } => {
174                self.client.motor_start_move(
175                    direction,
176                    steps,
177                    MotorGroup::Group1,
178                    true, // wait_until_finished
179                )?;
180                Ok(ActionResult::Success)
181            }
182
183            Action::MoveMotorClosedLoop { target, mode } => {
184                self.client.motor_start_closed_loop(
185                    mode,
186                    target,
187                    true, // wait_until_finished
188                    MotorGroup::Group1,
189                )?;
190                Ok(ActionResult::Success)
191            }
192
193            Action::StopMotor => {
194                self.client.motor_stop_move()?;
195                Ok(ActionResult::Success)
196            }
197
198            // === Control Operations ===
199            Action::AutoApproach {
200                wait_until_finished,
201                timeout,
202            } => {
203                log::debug!(
204                    "Starting auto-approach (wait: {}, timeout: {:?})",
205                    wait_until_finished,
206                    timeout
207                );
208
209                // Check if already running
210                match self.client.auto_approach_on_off_get() {
211                    Ok(true) => {
212                        log::warn!("Auto-approach already running");
213                        return Ok(ActionResult::Success); // Consider already running as success
214                    }
215                    Ok(false) => {
216                        log::debug!("Auto-approach is idle, proceeding to start");
217                    }
218                    Err(_) => {
219                        log::warn!("Auto-approach status unknown, attempting to proceed");
220                    }
221                }
222
223                // Open auto-approach module
224                if let Err(e) = self.client.auto_approach_open() {
225                    log::error!("Failed to open auto-approach module: {}", e);
226                    return Err(NanonisError::InvalidCommand(format!(
227                        "Failed to open auto-approach module: {}",
228                        e
229                    )));
230                }
231
232                // Wait for module initialization
233                std::thread::sleep(std::time::Duration::from_millis(500));
234
235                // Start auto-approach
236                if let Err(e) = self.client.auto_approach_on_off_set(true) {
237                    log::error!("Failed to start auto-approach: {}", e);
238                    return Err(NanonisError::InvalidCommand(format!(
239                        "Failed to start auto-approach: {}",
240                        e
241                    )));
242                }
243
244                if !wait_until_finished {
245                    log::debug!("Auto-approach started, not waiting for completion");
246                    return Ok(ActionResult::Success);
247                }
248
249                // Wait for completion with timeout
250                log::debug!("Waiting for auto-approach to complete...");
251                let poll_interval = std::time::Duration::from_millis(100);
252
253                match poll_until(
254                    || {
255                        // Returns Ok(true) when auto-approach is complete (not running)
256                        self.client
257                            .auto_approach_on_off_get()
258                            .map(|running| !running)
259                    },
260                    timeout,
261                    poll_interval,
262                ) {
263                    Ok(()) => {
264                        log::debug!("Auto-approach completed successfully");
265                        Ok(ActionResult::Success)
266                    }
267                    Err(PollError::Timeout) => {
268                        log::warn!("Auto-approach timed out after {:?}", timeout);
269                        // Try to stop the auto-approach
270                        let _ = self.client.auto_approach_on_off_set(false);
271                        Err(NanonisError::InvalidCommand(
272                            "Auto-approach timed out".to_string(),
273                        ))
274                    }
275                    Err(PollError::ConditionError(e)) => {
276                        log::error!("Error checking auto-approach status: {}", e);
277                        Err(NanonisError::InvalidCommand(format!(
278                            "Status check error: {}",
279                            e
280                        )))
281                    }
282                }
283            }
284
285            Action::Withdraw {
286                wait_until_finished,
287                timeout,
288            } => {
289                self.client.z_ctrl_withdraw(wait_until_finished, timeout)?;
290                Ok(ActionResult::Success)
291            }
292
293            Action::SetZSetpoint { setpoint } => {
294                self.client.z_ctrl_setpoint_set(setpoint)?;
295                Ok(ActionResult::Success)
296            }
297
298            // === Scan Operations ===
299            Action::ScanControl { action } => {
300                self.client.scan_action(action, ScanDirection::Up)?;
301                Ok(ActionResult::Success)
302            }
303
304            Action::ReadScanStatus => {
305                let is_scanning = self.client.scan_status_get()?;
306                Ok(ActionResult::Status(is_scanning))
307            }
308
309            // === Advanced Operations ===
310            Action::BiasPulse {
311                wait_until_done,
312                pulse_width,
313                bias_value_v,
314                z_controller_hold,
315                pulse_mode,
316            } => {
317                // Convert u16 parameters to enums (safe conversion with fallback)
318                let hold_enum = match z_controller_hold {
319                    0 => ZControllerHold::NoChange,
320                    1 => ZControllerHold::Hold,
321                    2 => ZControllerHold::Release,
322                    _ => ZControllerHold::NoChange, // Safe fallback
323                };
324
325                let mode_enum = match pulse_mode {
326                    0 => PulseMode::Keep,
327                    1 => PulseMode::Relative,
328                    2 => PulseMode::Absolute,
329                    _ => PulseMode::Keep, // Safe fallback
330                };
331
332                self.client.bias_pulse(
333                    wait_until_done,
334                    pulse_width.as_secs_f32(),
335                    bias_value_v,
336                    hold_enum.into(),
337                    mode_enum.into(),
338                )?;
339
340                Ok(ActionResult::Success)
341            }
342
343            Action::TipShaper {
344                config,
345                wait_until_finished,
346                timeout,
347            } => {
348                // Set tip shaper configuration
349                self.client.tip_shaper_props_set(config)?;
350
351                // Start tip shaper
352                self.client.tip_shaper_start(wait_until_finished, timeout)?;
353
354                Ok(ActionResult::Success)
355            }
356
357            Action::PulseRetract {
358                pulse_width,
359                pulse_height_v,
360            } => {
361                let current_bias = self.client_mut().get_bias().unwrap_or(500e-3);
362                let config = TipShaperConfig {
363                    switch_off_delay: std::time::Duration::from_millis(10),
364                    change_bias: true,
365                    bias_v: pulse_height_v,
366                    tip_lift_m: 0.0,
367                    lift_time_1: pulse_width,
368                    bias_lift_v: current_bias,
369                    bias_settling_time: std::time::Duration::from_millis(50),
370                    lift_height_m: 10e-9,
371                    lift_time_2: std::time::Duration::from_millis(100),
372                    end_wait_time: std::time::Duration::from_millis(50),
373                    restore_feedback: false,
374                };
375
376                // Set tip shaper configuration and start
377                self.client_mut().tip_shaper_props_set(config)?;
378                self.client_mut()
379                    .tip_shaper_start(true, Duration::from_secs(5))?;
380
381                Ok(ActionResult::Success)
382            }
383
384            Action::Wait { duration } => {
385                thread::sleep(duration);
386                Ok(ActionResult::None)
387            }
388
389            // === Data Management ===
390            Action::Store { key, action } => {
391                let result = self.execute(*action)?;
392                self.stored_values.insert(key, result.clone());
393                Ok(result) // Return the original result directly
394            }
395
396            Action::Retrieve { key } => match self.stored_values.get(&key) {
397                Some(value) => Ok(value.clone()), // Return the stored result directly
398                None => Err(NanonisError::InvalidCommand(format!(
399                    "No stored value found for key: {}",
400                    key
401                ))),
402            },
403        }
404    }
405
406    /// Execute action and extract specific type with validation
407    ///
408    /// This is a convenience method that combines execute() with type extraction,
409    /// providing better ergonomics while preserving type safety.
410    ///
411    /// # Example
412    /// ```no_run
413    /// use rusty_tip::{ActionDriver, Action, SignalIndex};
414    /// use rusty_tip::types::{DataToGet, OsciData};
415    ///
416    /// let mut driver = ActionDriver::new("127.0.0.1", 6501)?;
417    /// let osci_data: OsciData = driver.execute_expecting(Action::ReadOsci {
418    ///     signal: SignalIndex(24),
419    ///     trigger: None,
420    ///     data_to_get: DataToGet::Current,
421    ///     is_stable: None,
422    /// })?;
423    /// # Ok::<(), Box<dyn std::error::Error>>(())
424    /// ```
425    pub fn execute_expecting<T>(&mut self, action: Action) -> Result<T, NanonisError>
426    where
427        ActionResult: ExpectFromAction<T>,
428    {
429        let result = self.execute(action.clone())?;
430        Ok(result.expect_from_action(&action))
431    }
432
433    /// Find stable oscilloscope data with proper timeout handling
434    ///
435    /// This method implements stability detection logic with dual-threshold
436    /// approach and timeout handling. It repeatedly reads oscilloscope data until
437    /// stable values are found or timeout is reached.
438    fn find_stable_oscilloscope_data(
439        &mut self,
440        _data_to_get: DataToGet,
441        readings: u32,
442        timeout: std::time::Duration,
443        relative_threshold: f64,
444        absolute_threshold: f64,
445        min_window_percent: f64,
446        stability_fn: Option<fn(&[f64]) -> bool>,
447    ) -> Result<Option<OsciData>, NanonisError> {
448        match poll_with_timeout(
449            || {
450                // Try to find stable data in a batch of readings
451                for _attempt in 0..readings {
452                    let (t0, dt, size, data) = self.client.osci1t_data_get(2)?; // Wait2Triggers = 2
453
454                    if let Some(stable_osci_data) = self.analyze_stability_window(
455                        t0,
456                        dt,
457                        size,
458                        data,
459                        relative_threshold,
460                        absolute_threshold,
461                        min_window_percent,
462                        stability_fn,
463                    )? {
464                        return Ok(Some(stable_osci_data));
465                    }
466
467                    // Small delay between attempts to avoid overwhelming the system
468                    std::thread::sleep(std::time::Duration::from_millis(100));
469                }
470
471                // No stable data found in this batch, continue polling
472                Ok(None)
473            },
474            timeout,
475            std::time::Duration::from_millis(50), // Brief pause between reading cycles
476        ) {
477            Ok(Some(result)) => Ok(Some(result)),
478            Ok(None) => Ok(None), // Timeout reached
479            Err(PollError::ConditionError(e)) => Err(e),
480            Err(PollError::Timeout) => unreachable!(), // poll_with_timeout returns Ok(None) on timeout
481        }
482    }
483
484    /// Analyze a single oscilloscope data window for stability
485    fn analyze_stability_window(
486        &self,
487        t0: f64,
488        dt: f64,
489        size: i32,
490        data: Vec<f64>,
491        relative_threshold: f64,
492        absolute_threshold: f64,
493        min_window_percent: f64,
494        stability_fn: Option<fn(&[f64]) -> bool>,
495    ) -> Result<Option<OsciData>, NanonisError> {
496        let min_window = (size as f64 * min_window_percent) as usize;
497        let mut start = 0;
498        let mut end = size as usize;
499
500        while (end - start) > min_window {
501            let window = &data[start..end];
502            let arr = Array1::from_vec(window.to_vec());
503            let mean = arr.mean().expect(
504                "There must be an non-empty array, osci1t_data_get would have returned early.",
505            );
506            let std_dev = arr.std(0.0);
507            let relative_std = std_dev / mean.abs();
508
509            // Use custom stability function if provided, otherwise default dual-threshold
510            let is_stable = if let Some(stability_fn) = stability_fn {
511                stability_fn(window)
512            } else {
513                // Default dual-threshold approach: relative OR absolute
514                let is_relative_stable = relative_std < relative_threshold;
515                let is_absolute_stable = std_dev < absolute_threshold;
516                is_relative_stable || is_absolute_stable
517            };
518
519            if is_stable {
520                let stable_data = window.to_vec();
521                let stability_method = if stability_fn.is_some() {
522                    "custom".to_string()
523                } else {
524                    // Default dual-threshold logic
525                    let is_relative_stable = relative_std < relative_threshold;
526                    let is_absolute_stable = std_dev < absolute_threshold;
527                    match (is_relative_stable, is_absolute_stable) {
528                        (true, true) => "both".to_string(),
529                        (true, false) => "relative".to_string(),
530                        (false, true) => "absolute".to_string(),
531                        (false, false) => unreachable!(),
532                    }
533                };
534
535                let stats = SignalStats {
536                    mean,
537                    std_dev,
538                    relative_std,
539                    window_size: stable_data.len(),
540                    stability_method,
541                };
542
543                let mut osci_data =
544                    OsciData::new_with_stats(t0, dt, stable_data.len() as i32, stable_data, stats);
545                osci_data.is_stable = true; // Mark as stable since we found stable data
546                return Ok(Some(osci_data));
547            }
548
549            let shrink = ((end - start) / 10).max(1);
550            start += shrink;
551            end -= shrink;
552        }
553
554        // No stable window found in this data
555        Ok(None)
556    }
557
558    /// Find stable oscilloscope data with fallback to single value
559    ///
560    /// This method attempts to find stable oscilloscope data. If successful,
561    /// it returns OsciData with is_stable=true. If no stable data is found
562    /// within the timeout, it returns OsciData with is_stable=false and
563    /// a fallback single value reading.
564    fn find_stable_oscilloscope_data_with_fallback(
565        &mut self,
566        data_to_get: DataToGet,
567        readings: u32,
568        timeout: std::time::Duration,
569        relative_threshold: f64,
570        absolute_threshold: f64,
571        min_window_percent: f64,
572        stability_fn: Option<fn(&[f64]) -> bool>,
573    ) -> Result<OsciData, NanonisError> {
574        // First try to find stable data
575        if let Some(stable_osci_data) = self.find_stable_oscilloscope_data(
576            data_to_get,
577            readings,
578            timeout,
579            relative_threshold,
580            absolute_threshold,
581            min_window_percent,
582            stability_fn,
583        )? {
584            return Ok(stable_osci_data);
585        }
586
587        // If no stable data found, get a single reading as fallback
588        let (t0, dt, size, data) = self.client.osci1t_data_get(1)?; // NextTrigger = 1
589
590        // Calculate fallback value (mean of the data)
591        let fallback_value = if !data.is_empty() {
592            data.iter().sum::<f64>() / data.len() as f64
593        } else {
594            0.0
595        };
596
597        Ok(OsciData::new_unstable_with_fallback(
598            t0,
599            dt,
600            size,
601            data,
602            fallback_value,
603        ))
604    }
605
606    /// Execute a chain of actions sequentially
607    pub fn execute_chain(
608        &mut self,
609        chain: impl Into<ActionChain>,
610    ) -> Result<Vec<ActionResult>, NanonisError> {
611        let chain = chain.into();
612        let mut results = Vec::with_capacity(chain.len());
613
614        for action in chain.into_iter() {
615            let result = self.execute(action)?;
616            results.push(result);
617        }
618
619        Ok(results)
620    }
621
622    /// Execute chain and return only the final result
623    pub fn execute_chain_final(
624        &mut self,
625        chain: impl Into<ActionChain>,
626    ) -> Result<ActionResult, NanonisError> {
627        let results = self.execute_chain(chain)?;
628        Ok(results.into_iter().last().unwrap_or(ActionResult::None))
629    }
630
631    /// Execute chain with early termination on error, returning partial results
632    pub fn execute_chain_partial(
633        &mut self,
634        chain: impl Into<ActionChain>,
635    ) -> Result<Vec<ActionResult>, (Vec<ActionResult>, NanonisError)> {
636        let chain = chain.into();
637        let mut results = Vec::new();
638
639        for action in chain.into_iter() {
640            match self.execute(action) {
641                Ok(result) => results.push(result),
642                Err(error) => return Err((results, error)),
643            }
644        }
645
646        Ok(results)
647    }
648
649    /// Clear all stored values
650    pub fn clear_storage(&mut self) {
651        self.stored_values.clear();
652    }
653
654    /// Get all stored value keys
655    pub fn stored_keys(&self) -> Vec<&String> {
656        self.stored_values.keys().collect()
657    }
658
659    /// Convenience method to read oscilloscope data directly
660    pub fn read_oscilloscope(
661        &mut self,
662        signal: SignalIndex,
663        trigger: Option<TriggerConfig>,
664        data_to_get: DataToGet,
665    ) -> Result<Option<OsciData>, NanonisError> {
666        match self.execute(Action::ReadOsci {
667            signal,
668            trigger,
669            data_to_get,
670            is_stable: None,
671        })? {
672            ActionResult::OsciData(osci_data) => Ok(Some(osci_data)),
673            ActionResult::None => Ok(None),
674            _ => Err(NanonisError::InvalidCommand(
675                "Expected oscilloscope data".into(),
676            )),
677        }
678    }
679
680    /// Convenience method to read oscilloscope data with custom stability function
681    pub fn read_oscilloscope_with_stability(
682        &mut self,
683        signal: SignalIndex,
684        trigger: Option<TriggerConfig>,
685        data_to_get: DataToGet,
686        is_stable: fn(&[f64]) -> bool,
687    ) -> Result<Option<OsciData>, NanonisError> {
688        match self.execute(Action::ReadOsci {
689            signal,
690            trigger,
691            data_to_get,
692            is_stable: Some(is_stable),
693        })? {
694            ActionResult::OsciData(osci_data) => Ok(Some(osci_data)),
695            ActionResult::None => Ok(None),
696            _ => Err(NanonisError::InvalidCommand(
697                "Expected oscilloscope data".into(),
698            )),
699        }
700    }
701}
702
703/// Simple stability detection functions for oscilloscope windows
704pub mod stability {
705    /// Dual threshold stability (current default behavior)
706    /// Uses relative (1%) OR absolute (50fA) thresholds
707    pub fn dual_threshold_stability(window: &[f64]) -> bool {
708        if window.len() < 3 {
709            return false;
710        }
711
712        let mean = window.iter().sum::<f64>() / window.len() as f64;
713        let variance = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / window.len() as f64;
714        let std_dev = variance.sqrt();
715        let relative_std = std_dev / mean.abs();
716
717        // Stable if EITHER relative OR absolute threshold is met
718        relative_std < 0.05 || std_dev < 50e-15
719    }
720
721    /// Trend analysis stability detector
722    /// Checks for low slope (no trend) and good signal-to-noise ratio
723    pub fn trend_analysis_stability(window: &[f64]) -> bool {
724        if window.len() < 5 {
725            return false;
726        }
727
728        // Calculate linear regression slope
729        let n = window.len() as f64;
730        let x_mean = (n - 1.0) / 2.0; // 0, 1, 2, ... n-1 mean
731        let y_mean = window.iter().sum::<f64>() / n;
732
733        let mut numerator = 0.0;
734        let mut denominator = 0.0;
735
736        for (i, &y) in window.iter().enumerate() {
737            let x = i as f64;
738            numerator += (x - x_mean) * (y - y_mean);
739            denominator += (x - x_mean).powi(2);
740        }
741
742        let slope = if denominator != 0.0 {
743            numerator / denominator
744        } else {
745            0.0
746        };
747
748        // Calculate signal-to-noise ratio
749        let signal_level = y_mean.abs();
750        let noise_level = {
751            let variance = window.iter().map(|y| (y - y_mean).powi(2)).sum::<f64>() / n;
752            variance.sqrt()
753        };
754
755        let snr = if noise_level != 0.0 {
756            signal_level / noise_level
757        } else {
758            f64::INFINITY
759        };
760
761        // Thresholds: very low slope and decent SNR
762        slope.abs() < 0.001 && snr > 10.0
763    }
764}
765
766/// Statistics about action execution
767#[derive(Debug, Clone)]
768pub struct ExecutionStats {
769    pub total_actions: usize,
770    pub successful_actions: usize,
771    pub failed_actions: usize,
772    pub total_duration: std::time::Duration,
773}
774
775impl ExecutionStats {
776    pub fn success_rate(&self) -> f64 {
777        if self.total_actions == 0 {
778            0.0
779        } else {
780            self.successful_actions as f64 / self.total_actions as f64
781        }
782    }
783}
784
785/// Extension for ActionDriver with execution statistics
786impl ActionDriver {
787    /// Execute chain with detailed statistics
788    pub fn execute_chain_with_stats(
789        &mut self,
790        chain: impl Into<ActionChain>,
791    ) -> Result<(Vec<ActionResult>, ExecutionStats), NanonisError> {
792        let chain = chain.into();
793        let start_time = std::time::Instant::now();
794        let mut results = Vec::with_capacity(chain.len());
795        let mut successful = 0;
796        let failed = 0;
797
798        for action in chain.into_iter() {
799            match self.execute(action) {
800                Ok(result) => {
801                    results.push(result);
802                    successful += 1;
803                }
804                Err(e) => {
805                    // For stats purposes, we want to continue executing but track failures
806                    // In a real application, you might want to decide whether to continue or stop
807                    // For now, return the error to maintain proper error handling
808                    return Err(e);
809                }
810            }
811        }
812
813        let stats = ExecutionStats {
814            total_actions: results.len(),
815            successful_actions: successful,
816            failed_actions: failed,
817            total_duration: start_time.elapsed(),
818        };
819
820        Ok((results, stats))
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use std::time::Duration;
827
828    use super::*;
829    // Note: These tests will fail without actual Nanonis hardware
830    // They're included to show the intended interface
831
832    #[test]
833    fn test_action_translator_interface() {
834        // This test shows how the translator would be used
835        // It will fail without actual hardware, but demonstrates the API
836
837        let driver_result = ActionDriver::new("127.0.0.1", 6501);
838        match driver_result {
839            Ok(mut driver) => {
840                // Test single action
841                let action = Action::ReadBias;
842                let _result = driver.execute(action);
843
844                // With real hardware, this would succeed
845                // Without hardware, it will error, which is expected
846
847                // Test chain
848                let chain = ActionChain::new(vec![
849                    Action::ReadBias,
850                    Action::Wait {
851                        duration: Duration::from_millis(500),
852                    },
853                    Action::SetBias { voltage: 1.0 },
854                ]);
855
856                let _chain_result = driver.execute_chain(chain);
857            }
858            Err(_) => {
859                // Expected when signals can't be discovered
860                println!("Signal discovery failed - this is expected without hardware");
861            }
862        }
863    }
864}