Skip to main content

rusty_tip/
actions.rs

1use crate::{
2    types::{DataToGet, MotorDisplacement, OsciData, TipShape, TriggerConfig},
3    MotorDirection, MovementMode, Position, Position3D, ScanAction, Signal,
4    TipShaperConfig,
5};
6use nanonis_rs::signals::SignalIndex;
7use std::{collections::HashMap, time::Duration};
8
9/// Method for determining tip state
10#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
11pub enum TipCheckMethod {
12    /// Check single signal against bounds
13    SignalBounds { signal: Signal, bounds: (f32, f32) },
14    /// Check multiple signals (all must be in bounds)
15    MultiSignalBounds { signals: Vec<(Signal, (f32, f32))> },
16}
17
18/// Method for determining signal stability for GetStableSignal action
19#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
20pub enum SignalStabilityMethod {
21    /// Standard deviation threshold
22    StandardDeviation { threshold: f32 },
23    /// Relative standard deviation (coefficient of variation)
24    RelativeStandardDeviation { threshold_percent: f32 },
25    /// Moving window - signal must be stable within sliding window
26    MovingWindow {
27        window_size: usize,
28        max_variation: f32,
29    },
30    /// Trend analysis - ensure no consistent drift
31    TrendAnalysis { max_slope: f32 },
32    /// Combined: checks both noise (std dev) AND drift (slope)
33    /// Both conditions must be satisfied for signal to be stable
34    Combined { max_std_dev: f32, max_slope: f32 },
35}
36
37impl Default for SignalStabilityMethod {
38    fn default() -> Self {
39        Self::RelativeStandardDeviation {
40            threshold_percent: 5.0,
41        } // 5% variation
42    }
43}
44
45/// Method for determining tip stability with potentially invasive operations
46#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
47pub enum TipStabilityMethod {
48    /// Extended signal monitoring over time with statistical analysis
49    ExtendedMonitoring {
50        signal: Signal,
51        duration: Duration,
52        sampling_interval: Duration,
53        stability_threshold: f32,
54    },
55    /// Bias sweep response analysis (potentially destructive)
56    ///
57    /// This method starts a scan, then manually sweeps the bias voltage while monitoring
58    /// the specified signal (typically frequency shift) for sudden changes that indicate
59    /// tip instability. Unlike BiasSweepResponse, this keeps Z-controller ON and uses
60    /// manual bias control instead of the Nanonis bias sweep module.
61    BiasSweepResponse {
62        /// Signal to monitor (typically frequency shift)
63        signal: Signal,
64        /// Bias voltage range to sweep (e.g., (-2.0, 2.0) V)
65        bias_range: (f32, f32),
66        /// Number of bias steps in the sweep
67        bias_steps: u16,
68        /// Duration to wait at each bias step for signal to settle
69        step_duration: Duration,
70        /// Absolute threshold for sudden change detection (e.g., 0.5 Hz)
71        /// If abs(signal - baseline) > threshold, mark as unstable
72        allowed_signal_change: f32,
73    },
74}
75
76/// Configuration for bias sweep during stability testing
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub struct BiasSweepConfig {
79    pub lower_limit: f32,
80    pub upper_limit: f32,
81    pub steps: u16,
82    pub period_ms: u16,
83    pub reset_bias_after: bool,
84    pub z_controller_behavior: u16, // 0=no change, 1=turn off, 2=don't turn off
85}
86
87/// Information about bounds checking results
88#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
89pub struct BoundsCheckInfo {
90    pub bounds_used: Vec<(SignalIndex, (f32, f32))>,
91    pub violations: Vec<(SignalIndex, f32, (f32, f32))>, // signal, value, bounds
92    pub all_passed: bool,
93}
94
95/// Detailed stability analysis result
96#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
97pub struct StabilityResult {
98    pub is_stable: bool,
99    pub method_used: String,
100    pub measured_values: HashMap<Signal, Vec<f32>>, // Time series data
101    pub analysis_duration: Duration,
102    pub metrics: HashMap<String, f32>, // Method-specific metrics
103    pub potential_damage_detected: bool,
104    pub recommendations: Vec<String>,
105}
106
107/// Tip state determination result with measured values
108#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
109pub struct TipState {
110    pub shape: TipShape, // the enum value
111    pub measured_signals: HashMap<SignalIndex, f32>, // Always populated, empty for simple checks
112    pub metadata: HashMap<String, String>,
113}
114
115/// TCP Logger status information  
116#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
117pub struct TCPReaderStatus {
118    pub status: crate::types::TCPLogStatus,
119    pub channels: Vec<i32>,
120    pub oversampling: i32,
121}
122
123/// Stable signal analysis result
124#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
125pub struct StableSignal {
126    pub stable_value: f32,
127    pub data_points_used: usize,
128    pub analysis_duration: Duration,
129    pub stability_metrics: HashMap<String, f32>,
130    pub raw_data: Vec<f32>,
131}
132
133/// Enhanced Action enum representing all possible SPM operations
134/// Properly separates motor (step-based) and piezo (continuous) movements
135#[derive(Debug, Clone)]
136pub enum Action {
137    /// Read single signal value
138    ReadSignal {
139        signal: Signal,
140        wait_for_newest: bool,
141    },
142
143    /// Read multiple signal values
144    ReadSignals {
145        signals: Vec<Signal>,
146        wait_for_newest: bool,
147    },
148
149    /// Read all available signal names
150    ReadSignalNames,
151
152    /// Read current bias voltage
153    ReadBias,
154
155    /// Set bias voltage to specific value
156    SetBias { voltage: f32 },
157
158    // Osci functions
159    ReadOsci {
160        signal: Signal,
161        trigger: Option<TriggerConfig>,
162        data_to_get: DataToGet,
163        is_stable: Option<fn(&[f64]) -> bool>,
164    },
165
166    /// Read current piezo position (continuous coordinates)
167    ReadPiezoPosition { wait_for_newest_data: bool },
168
169    /// Set piezo position (absolute)
170    SetPiezoPosition {
171        position: Position,
172        wait_until_finished: bool,
173    },
174
175    /// Move piezo position (relative to current)
176    MovePiezoRelative { delta: Position },
177
178    // === Coarse Positioning Operations (Motor) ===
179    /// Move motor along a single axis (discrete positioning)
180    MoveMotorAxis {
181        direction: MotorDirection,
182        steps: u16,
183        blocking: bool,
184    },
185
186    /// Move motor in 3D space with single displacement vector
187    MoveMotor3D {
188        displacement: MotorDisplacement,
189        blocking: bool,
190    },
191
192    /// Move motor using closed-loop to target position
193    MoveMotorClosedLoop {
194        target: Position3D,
195        mode: MovementMode,
196    },
197
198    /// Stop all motor movement
199    StopMotor,
200
201    // === Control Operations ===
202    /// Perform auto-approach with timeout
203    AutoApproach {
204        wait_until_finished: bool,
205        timeout: Duration,
206        /// If true, center the frequency shift before starting auto-approach
207        center_freq_shift: bool,
208    },
209
210    /// Withdraw tip with timeout
211    Withdraw {
212        wait_until_finished: bool,
213        timeout: Duration,
214    },
215
216    /// Safely reposition tip: withdraw → move → approach → stabilize
217    SafeReposition { x_steps: i16, y_steps: i16 },
218
219    /// Set Z-controller setpoint
220    SetZSetpoint { setpoint: f32 },
221
222    // === Scan Operations ===
223    /// Control scan operations
224    ScanControl { action: ScanAction },
225
226    /// Read scan status
227    ReadScanStatus,
228
229    // === Advanced Operations ===
230    /// Execute bias pulse with parameters
231    BiasPulse {
232        wait_until_done: bool,
233        pulse_width: Duration,
234        bias_value_v: f32,
235        z_controller_hold: u16,
236        pulse_mode: u16,
237    },
238
239    /// Full tip shaper control with all parameters
240    TipShaper {
241        config: TipShaperConfig,
242        wait_until_finished: bool,
243        timeout: Duration,
244    },
245
246    /// Simple pulse-retract with predefined safe values
247    PulseRetract {
248        pulse_width: Duration,
249        pulse_height_v: f32,
250    },
251
252    /// Wait for a specific duration
253    Wait { duration: Duration },
254
255    // === Data Management ===
256    /// Store result value with key for later retrieval
257    Store { key: String, action: Box<Action> },
258
259    /// Retrieve previously stored value
260    Retrieve { key: String },
261
262    // === TCP Logger Operations ===
263    /// Start TCP logger (must be configured first)
264    StartTCPLogger,
265
266    /// Stop TCP logger
267    StopTCPLogger,
268
269    /// Get TCP logger status and configuration
270    GetTCPLoggerStatus,
271
272    /// Configure TCP logger channels and oversampling
273    ConfigureTCPLogger {
274        channels: Vec<i32>,
275        oversampling: i32,
276    },
277
278    // === Tip State Operations ===
279    /// Check tip state using specified method (non-invasive)
280    CheckTipState { method: TipCheckMethod },
281
282    /// Check tip stability using potentially invasive methods
283    /// WARNING: This action may damage the tip through bias sweeps or extended testing
284    CheckTipStability {
285        method: TipStabilityMethod,
286        max_duration: Duration,
287    },
288
289    /// Get a stable signal value using TCP logger data and stability analysis
290    ReadStableSignal {
291        signal: Signal,
292        data_points: Option<usize>,
293        use_new_data: bool,
294        stability_method: SignalStabilityMethod,
295        timeout: Duration,
296        retry_count: Option<u32>,
297    },
298
299    /// Check if oscillation amplitude is reached
300    ReachedTargedAmplitude,
301}
302
303/// Simplified ActionResult with clear semantic separation
304#[derive(Debug, Clone)]
305pub enum ActionResult {
306    /// Single numeric value (signals, bias, etc.)
307    Value(f64),
308
309    /// Multiple numeric values (signal arrays)
310    Values(Vec<f64>),
311
312    /// String data (signal names, error messages, etc.)
313    Text(Vec<String>),
314
315    /// Boolean status (scanning/idle, running/stopped, etc.)
316    Status(bool),
317
318    /// Position data (meaningful x,y structure)
319    Position(Position),
320
321    /// Complex oscilloscope data (timing + data + metadata)
322    OsciData(OsciData),
323
324    /// Operation completed successfully (no data returned)
325    Success,
326
327    /// TCP Logger status information
328    TCPReaderStatus(TCPReaderStatus),
329
330    /// Tip state determination result
331    TipState(TipState),
332
333    /// Detailed stability analysis result
334    StabilityResult(StabilityResult),
335
336    /// Stable signal value with analysis metadata
337    StableSignal(StableSignal),
338
339    /// No result/waiting state
340    None,
341}
342
343impl ActionResult {
344    /// Convert to f64 if possible (for numerical results)
345    pub fn as_f64(&self) -> Option<f64> {
346        match self {
347            ActionResult::Value(v) => Some(*v),
348            ActionResult::Values(values) => {
349                if values.len() == 1 {
350                    Some(values[0])
351                } else {
352                    None
353                }
354            }
355            _ => None,
356        }
357    }
358
359    /// Convert to bool if possible (for status results)
360    pub fn as_bool(&self) -> Option<bool> {
361        match self {
362            ActionResult::Status(b) => Some(*b),
363            _ => None,
364        }
365    }
366
367    /// Convert to Position if possible
368    pub fn as_position(&self) -> Option<Position> {
369        match self {
370            ActionResult::Position(pos) => Some(*pos),
371            _ => None,
372        }
373    }
374
375    /// Convert to OsciData if possible
376    pub fn as_osci_data(&self) -> Option<&OsciData> {
377        match self {
378            ActionResult::OsciData(data) => Some(data),
379            _ => None,
380        }
381    }
382
383    /// Convert to TipShape if possible
384    pub fn as_tip_shape(&self) -> Option<TipShape> {
385        match self {
386            ActionResult::TipState(tip_state) => Some(tip_state.shape),
387            _ => None,
388        }
389    }
390
391    /// Convert to full TipState if possible
392    pub fn as_tip_state(&self) -> Option<&TipState> {
393        match self {
394            ActionResult::TipState(tip_state) => Some(tip_state),
395            _ => None,
396        }
397    }
398
399    /// Convert to StabilityResult if possible
400    pub fn as_stability_result(&self) -> Option<&StabilityResult> {
401        match self {
402            ActionResult::StabilityResult(result) => Some(result),
403            _ => None,
404        }
405    }
406
407    /// Convert to stable signal value if possible
408    pub fn as_stable_signal_value(&self) -> Option<f32> {
409        match self {
410            ActionResult::StableSignal(stable) => Some(stable.stable_value),
411            _ => None,
412        }
413    }
414
415    /// Convert to full StableSignal if possible
416    pub fn as_stable_signal(&self) -> Option<&StableSignal> {
417        match self {
418            ActionResult::StableSignal(stable) => Some(stable),
419            _ => None,
420        }
421    }
422
423    // === Action-Aware Type Extractors ===
424    // These methods validate that the result type matches what the action should produce
425
426    /// Extract OsciData with action validation (panics on type mismatch)
427    pub fn expect_osci_data(self, action: &Action) -> OsciData {
428        match (action, self) {
429            (Action::ReadOsci { .. }, ActionResult::OsciData(data)) => data,
430            (action, result) => panic!(
431                "Expected OsciData from action {:?}, got {:?}",
432                action, result
433            ),
434        }
435    }
436
437    /// Extract signal value with action validation (panics on type mismatch)
438    pub fn expect_signal_value(self, action: &Action) -> f64 {
439        match (action, self) {
440            (Action::ReadSignal { .. }, ActionResult::Value(v)) => v,
441            (Action::ReadSignal { .. }, ActionResult::Values(mut vs))
442                if vs.len() == 1 =>
443            {
444                vs.pop().unwrap()
445            }
446            (Action::ReadBias, ActionResult::Value(v)) => v,
447            (action, result) => panic!(
448                "Expected signal value from action {:?}, got {:?}",
449                action, result
450            ),
451        }
452    }
453
454    /// Extract multiple values with action validation (panics on type mismatch)
455    pub fn expect_values(self, action: &Action) -> Vec<f64> {
456        match (action, self) {
457            (Action::ReadSignals { .. }, ActionResult::Values(values)) => {
458                values
459            }
460            (Action::ReadSignal { .. }, ActionResult::Value(v)) => vec![v],
461            (action, result) => {
462                panic!(
463                    "Expected values from action {:?}, got {:?}",
464                    action, result
465                )
466            }
467        }
468    }
469
470    /// Extract position with action validation (panics on type mismatch)
471    pub fn expect_position(self, action: &Action) -> Position {
472        match (action, self) {
473            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => {
474                pos
475            }
476            (action, result) => panic!(
477                "Expected position from action {:?}, got {:?}",
478                action, result
479            ),
480        }
481    }
482
483    /// Extract bias voltage with action validation (panics on type mismatch)
484    pub fn expect_bias_voltage(self, action: &Action) -> f32 {
485        match (action, self) {
486            (Action::ReadBias, ActionResult::Value(v)) => v as f32,
487            (action, result) => panic!(
488                "Expected bias voltage from action {:?}, got {:?}",
489                action, result
490            ),
491        }
492    }
493
494    /// Extract signal names with action validation (panics on type mismatch)
495    pub fn expect_signal_names(self, action: &Action) -> Vec<String> {
496        match (action, self) {
497            (Action::ReadSignalNames, ActionResult::Text(names)) => names,
498            (action, result) => panic!(
499                "Expected signal names from action {:?}, got {:?}",
500                action, result
501            ),
502        }
503    }
504
505    /// Extract status with action validation (panics on type mismatch)
506    pub fn expect_status(self, action: &Action) -> bool {
507        match (action, self) {
508            (Action::ReadScanStatus, ActionResult::Status(status)) => status,
509            (action, result) => {
510                panic!(
511                    "Expected status from action {:?}, got {:?}",
512                    action, result
513                )
514            }
515        }
516    }
517
518    /// Extract tip shape enum with action validation (panics on type mismatch)
519    pub fn expect_tip_shape(self, action: &Action) -> TipShape {
520        match (action, self) {
521            (
522                Action::CheckTipState { .. },
523                ActionResult::TipState(tip_state),
524            ) => tip_state.shape,
525            (action, result) => {
526                panic!(
527                    "Expected tip state from action {:?}, got {:?}",
528                    action, result
529                )
530            }
531        }
532    }
533
534    /// Extract full tip state result with action validation (panics on type mismatch)
535    pub fn expect_tip_state(self, action: &Action) -> TipState {
536        match (action, self) {
537            (
538                Action::CheckTipState { .. },
539                ActionResult::TipState(tip_state),
540            ) => tip_state,
541            (action, result) => {
542                panic!(
543                    "Expected tip state from action {:?}, got {:?}",
544                    action, result
545                )
546            }
547        }
548    }
549
550    /// Extract stability result (panics on type mismatch)
551    pub fn expect_stability_result(self, action: &Action) -> StabilityResult {
552        match (action, self) {
553            (
554                Action::CheckTipStability { .. },
555                ActionResult::StabilityResult(result),
556            ) => result,
557            (action, result) => {
558                panic!(
559                    "Expected stability result from action {:?}, got {:?}",
560                    action, result
561                )
562            }
563        }
564    }
565
566    /// Extract stable signal value (panics on type mismatch)
567    pub fn expect_stable_signal_value(self, action: &Action) -> f32 {
568        match (action, self) {
569            (
570                Action::ReadStableSignal { .. },
571                ActionResult::StableSignal(stable),
572            ) => stable.stable_value,
573            (action, result) => {
574                panic!(
575                    "Expected stable signal from action {:?}, got {:?}",
576                    action, result
577                )
578            }
579        }
580    }
581
582    /// Extract full stable signal result with action validation (panics on type mismatch)
583    pub fn expect_stable_signal(self, action: &Action) -> StableSignal {
584        match (action, self) {
585            (
586                Action::ReadStableSignal { .. },
587                ActionResult::StableSignal(stable),
588            ) => stable,
589            (action, result) => {
590                panic!(
591                    "Expected stable signal from action {:?}, got {:?}",
592                    action, result
593                )
594            }
595        }
596    }
597
598    /// Extract TCP reader status with action validation (panics on type mismatch)
599    pub fn expect_tcp_reader_status(self, action: &Action) -> TCPReaderStatus {
600        match (action, self) {
601            (
602                Action::GetTCPLoggerStatus,
603                ActionResult::TCPReaderStatus(status),
604            ) => status,
605            (action, result) => {
606                panic!(
607                    "Expected TCP reader status from action {:?}, got {:?}",
608                    action, result
609                )
610            }
611        }
612    }
613
614    // === Safe Extraction Methods (non-panicking) ===
615
616    /// Try to extract OsciData with action validation
617    pub fn try_into_osci_data(
618        self,
619        action: &Action,
620    ) -> Result<OsciData, String> {
621        match (action, self) {
622            (Action::ReadOsci { .. }, ActionResult::OsciData(data)) => Ok(data),
623            (action, result) => Err(format!(
624                "Expected OsciData from action {:?}, got {:?}",
625                action, result
626            )),
627        }
628    }
629
630    /// Try to extract signal value with action validation
631    pub fn try_into_signal_value(self, action: &Action) -> Result<f64, String> {
632        match (action, self) {
633            (Action::ReadSignal { .. }, ActionResult::Value(v)) => Ok(v),
634            (Action::ReadSignal { .. }, ActionResult::Values(mut vs))
635                if vs.len() == 1 =>
636            {
637                Ok(vs.pop().unwrap())
638            }
639            (Action::ReadBias, ActionResult::Value(v)) => Ok(v),
640            (action, result) => Err(format!(
641                "Expected signal value from action {:?}, got {:?}",
642                action, result
643            )),
644        }
645    }
646
647    /// Try to extract position with action validation
648    pub fn try_into_position(
649        self,
650        action: &Action,
651    ) -> Result<Position, String> {
652        match (action, self) {
653            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => {
654                Ok(pos)
655            }
656            (action, result) => Err(format!(
657                "Expected position from action {:?}, got {:?}",
658                action, result
659            )),
660        }
661    }
662
663    /// Try to extract status with action validation
664    pub fn try_into_status(self, action: &Action) -> Result<bool, String> {
665        match (action, self) {
666            (Action::ReadScanStatus, ActionResult::Status(status)) => {
667                Ok(status)
668            }
669            (action, result) => Err(format!(
670                "Expected status from action {:?}, got {:?}",
671                action, result
672            )),
673        }
674    }
675
676    /// Try to extract stability result with action validation
677    pub fn try_into_stability_result(
678        self,
679        action: &Action,
680    ) -> Result<StabilityResult, String> {
681        match (action, self) {
682            (
683                Action::CheckTipStability { .. },
684                ActionResult::StabilityResult(result),
685            ) => Ok(result),
686            (action, result) => Err(format!(
687                "Expected stability result from action {:?}, got {:?}",
688                action, result
689            )),
690        }
691    }
692
693    /// Try to extract stable signal value with action validation
694    pub fn try_into_stable_signal_value(
695        self,
696        action: &Action,
697    ) -> Result<f32, String> {
698        match (action, self) {
699            (
700                Action::ReadStableSignal { .. },
701                ActionResult::StableSignal(stable),
702            ) => Ok(stable.stable_value),
703            (action, result) => Err(format!(
704                "Expected stable signal from action {:?}, got {:?}",
705                action, result
706            )),
707        }
708    }
709}
710
711// === Trait for Generic Type Extraction ===
712
713/// Trait for extracting specific types from ActionResult with action validation
714pub trait ExpectFromAction<T> {
715    fn expect_from_action(self, action: &Action) -> T;
716}
717
718impl ExpectFromAction<OsciData> for ActionResult {
719    fn expect_from_action(self, action: &Action) -> OsciData {
720        self.expect_osci_data(action)
721    }
722}
723
724impl ExpectFromAction<f64> for ActionResult {
725    fn expect_from_action(self, action: &Action) -> f64 {
726        self.expect_signal_value(action)
727    }
728}
729
730impl ExpectFromAction<Vec<f64>> for ActionResult {
731    fn expect_from_action(self, action: &Action) -> Vec<f64> {
732        self.expect_values(action)
733    }
734}
735
736impl ExpectFromAction<Position> for ActionResult {
737    fn expect_from_action(self, action: &Action) -> Position {
738        self.expect_position(action)
739    }
740}
741
742impl ExpectFromAction<Vec<String>> for ActionResult {
743    fn expect_from_action(self, action: &Action) -> Vec<String> {
744        self.expect_signal_names(action)
745    }
746}
747
748impl ExpectFromAction<bool> for ActionResult {
749    fn expect_from_action(self, action: &Action) -> bool {
750        self.expect_status(action)
751    }
752}
753
754impl ExpectFromAction<StabilityResult> for ActionResult {
755    fn expect_from_action(self, action: &Action) -> StabilityResult {
756        self.expect_stability_result(action)
757    }
758}
759
760impl ExpectFromAction<f32> for ActionResult {
761    fn expect_from_action(self, action: &Action) -> f32 {
762        match action {
763            Action::ReadStableSignal { .. } => {
764                self.expect_stable_signal_value(action)
765            }
766            _ => self.expect_bias_voltage(action),
767        }
768    }
769}
770
771// === Action Categorization ===
772
773impl Action {
774    /// Check if this is a positioning action
775    pub fn is_positioning_action(&self) -> bool {
776        matches!(
777            self,
778            Action::SetPiezoPosition { .. }
779                | Action::MovePiezoRelative { .. }
780                | Action::MoveMotorAxis { .. }
781                | Action::MoveMotor3D { .. }
782                | Action::MoveMotorClosedLoop { .. }
783        )
784    }
785
786    /// Check if this is a read-only action
787    pub fn is_read_action(&self) -> bool {
788        matches!(
789            self,
790            Action::ReadSignal { .. }
791                | Action::ReadSignals { .. }
792                | Action::ReadSignalNames
793                | Action::ReadBias
794                | Action::ReadPiezoPosition { .. }
795                | Action::ReadScanStatus
796                | Action::Retrieve { .. }
797        )
798    }
799
800    /// Check if this is a control action
801    pub fn is_control_action(&self) -> bool {
802        matches!(
803            self,
804            Action::AutoApproach { .. }
805                | Action::Withdraw { .. }
806                | Action::SafeReposition { .. }
807                | Action::ScanControl { .. }
808                | Action::StopMotor
809        )
810    }
811
812    /// Check if this action modifies bias voltage
813    pub fn modifies_bias(&self) -> bool {
814        matches!(self, Action::SetBias { .. } | Action::BiasPulse { .. })
815    }
816
817    /// Check if this action involves motor movement
818    pub fn involves_motor(&self) -> bool {
819        matches!(
820            self,
821            Action::MoveMotorAxis { .. }
822                | Action::MoveMotor3D { .. }
823                | Action::MoveMotorClosedLoop { .. }
824                | Action::SafeReposition { .. }
825                | Action::StopMotor
826        )
827    }
828
829    /// Check if this action involves piezo movement
830    pub fn involves_piezo(&self) -> bool {
831        matches!(
832            self,
833            Action::SetPiezoPosition { .. }
834                | Action::MovePiezoRelative { .. }
835                | Action::ReadPiezoPosition { .. }
836        )
837    }
838
839    /// Get a human-readable description of the action
840    pub fn description(&self) -> String {
841        match self {
842            Action::ReadSignal { signal, .. } => {
843                format!("Read signal {}", signal.index)
844            }
845            Action::ReadSignals { signals, .. } => {
846                let indices: Vec<i32> =
847                    signals.iter().map(|s| s.index as i32).collect();
848                format!("Read signals: {:?}", indices)
849            }
850            Action::SetBias { voltage } => {
851                format!("Set bias to {:.3}V", voltage)
852            }
853            Action::SetPiezoPosition { position, .. } => {
854                format!(
855                    "Set piezo position to ({:.3e}, {:.3e})",
856                    position.x, position.y
857                )
858            }
859            Action::MoveMotorAxis {
860                direction,
861                steps,
862                blocking,
863            } => {
864                format!("Move motor {direction:?} {steps} steps with blocking {blocking}")
865            }
866            Action::MoveMotor3D {
867                displacement,
868                blocking,
869            } => {
870                format!(
871                    "Move motor 3D displacement ({}, {}, {}) with blocking {blocking}",
872                    displacement.x, displacement.y, displacement.z
873                )
874            }
875            Action::AutoApproach {
876                wait_until_finished,
877                timeout,
878                center_freq_shift,
879            } => format!(
880                "Auto approach blocking: {wait_until_finished}, timeout: {:?}, center freq: {center_freq_shift}",
881                timeout
882            ),
883            Action::Withdraw { timeout, .. } => {
884                format!("Withdraw tip (timeout: {}ms)", timeout.as_micros())
885            }
886            Action::SafeReposition { x_steps, y_steps } => {
887                format!(
888                    "Safe reposition: move ({}, {}) steps",
889                    x_steps, y_steps
890                )
891            }
892            Action::SetZSetpoint { setpoint } => {
893                format!("Set Z setpoint: {:.3e}", setpoint)
894            }
895            Action::Wait { duration } => {
896                format!("Wait {:.1}s", duration.as_secs_f64())
897            }
898            Action::BiasPulse {
899                wait_until_done: _,
900                pulse_width,
901                bias_value_v,
902                z_controller_hold: _,
903                pulse_mode: _,
904            } => {
905                format!(
906                    "Bias pulse {:.3}V for {:?}ms",
907                    bias_value_v, pulse_width
908                )
909            }
910            Action::TipShaper {
911                config,
912                wait_until_finished,
913                timeout,
914            } => {
915                format!(
916                    "Tip shaper: bias {:.1}V, lift {:.0}nm, times {:.1?}s/{:.1?}s (wait: {}, timeout: {:?}ms)",
917                    config.bias_v,
918                    config.tip_lift_m * 1e9,
919                    config.lift_time_1.as_secs_f32(),
920                    config.lift_time_2.as_secs_f32(),
921                    wait_until_finished,
922                    timeout
923                )
924            }
925            Action::PulseRetract {
926                pulse_width,
927                pulse_height_v,
928            } => {
929                format!(
930                    "Pulse retract {:.1}V for {:.0?}ms",
931                    pulse_height_v, pulse_width
932                )
933            }
934            Action::ReadOsci {
935                signal,
936                trigger,
937                data_to_get,
938                is_stable,
939            } => {
940                let trigger_desc = match trigger {
941                    Some(config) => format!("trigger: {:?}", config.mode),
942                    None => "no trigger config".to_string(),
943                };
944                let stability_desc = match is_stable {
945                    Some(_) => " with custom stability",
946                    None => "",
947                };
948                format!(
949                    "Read oscilloscope signal {} with {} (mode: {:?}){}",
950                    signal.index, trigger_desc, data_to_get, stability_desc
951                )
952            }
953            Action::CheckTipState { method } => match method {
954                TipCheckMethod::SignalBounds { signal, bounds } => {
955                    format!(
956                        "Check tip state: signal {} bounds ({:.3e}, {:.3e})",
957                        signal.index, bounds.0, bounds.1
958                    )
959                }
960                TipCheckMethod::MultiSignalBounds { signals } => {
961                    format!("Check tip state: {} signal bounds", signals.len())
962                }
963            },
964            Action::CheckTipStability {
965                method,
966                max_duration,
967            } => {
968                let duration_desc =
969                    format!("{:.1}s", max_duration.as_secs_f32());
970                match method {
971                    TipStabilityMethod::ExtendedMonitoring {
972                        signal,
973                        duration,
974                        ..
975                    } => {
976                        format!("Check tip stability: extended monitoring signal {} for {:.1}s (max: {})",
977                               signal.index, duration.as_secs_f32(), duration_desc)
978                    }
979                    TipStabilityMethod::BiasSweepResponse {
980                        signal,
981                        bias_range,
982                        bias_steps,
983                        step_duration,
984                        allowed_signal_change,
985                    } => {
986                        format!("Check tip stability: bias sweep signal {} from {:.2}V to {:.2}V ({} steps, {:.1}ms period, {:.1}% change allowed, max: {})",
987                               signal.index, bias_range.0, bias_range.1, bias_steps, step_duration.as_millis(), allowed_signal_change * 100.0, duration_desc)
988                    }
989                }
990            }
991            Action::ReadStableSignal {
992                signal,
993                data_points,
994                use_new_data,
995                stability_method,
996                timeout,
997                retry_count,
998            } => {
999                let points_desc = data_points
1000                    .map_or("default".to_string(), |p| p.to_string());
1001                let data_desc = if *use_new_data {
1002                    "new data"
1003                } else {
1004                    "buffered data"
1005                };
1006                let method_desc = match stability_method {
1007                    SignalStabilityMethod::StandardDeviation { threshold } => {
1008                        format!("std dev {:.3e}", threshold)
1009                    }
1010                    SignalStabilityMethod::RelativeStandardDeviation {
1011                        threshold_percent,
1012                    } => {
1013                        format!("rel std {:.1}%", threshold_percent)
1014                    }
1015                    SignalStabilityMethod::MovingWindow {
1016                        window_size,
1017                        max_variation,
1018                    } => {
1019                        format!(
1020                            "window {}pts, max var {:.3e}",
1021                            window_size, max_variation
1022                        )
1023                    }
1024                    SignalStabilityMethod::TrendAnalysis { max_slope } => {
1025                        format!("trend analysis, max slope {:.3e}", max_slope)
1026                    }
1027                    SignalStabilityMethod::Combined {
1028                        max_std_dev,
1029                        max_slope,
1030                    } => {
1031                        format!(
1032                            "combined: std dev {:.3e}, slope {:.3e}",
1033                            max_std_dev, max_slope
1034                        )
1035                    }
1036                };
1037                let retry_desc = retry_count
1038                    .map_or("no retry".to_string(), |r| {
1039                        format!("{} retries", r)
1040                    });
1041                format!(
1042                    "Get stable signal {} ({} points, {}, {}, timeout {:.1}s, {})",
1043                    signal.index,
1044                    points_desc,
1045                    data_desc,
1046                    method_desc,
1047                    timeout.as_secs_f32(),
1048                    retry_desc
1049                )
1050            }
1051            _ => format!("{:?}", self),
1052        }
1053    }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058    use super::*;
1059
1060    #[test]
1061    fn test_action_result_extraction() {
1062        let bias_result = ActionResult::Value(2.5);
1063        assert_eq!(bias_result.as_f64(), Some(2.5));
1064
1065        let position_result =
1066            ActionResult::Position(Position { x: 1e-9, y: 2e-9 });
1067        assert_eq!(
1068            position_result.as_position(),
1069            Some(Position { x: 1e-9, y: 2e-9 })
1070        );
1071    }
1072}
1073
1074/// A sequence of actions with simple Vec<Action> foundation
1075#[derive(Debug, Clone)]
1076pub struct ActionChain {
1077    actions: Vec<Action>,
1078    name: Option<String>,
1079}
1080
1081impl ActionChain {
1082    /// Create a new ActionChain from a vector of actions
1083    pub fn new(actions: Vec<Action>) -> Self {
1084        Self {
1085            actions,
1086            name: None,
1087        }
1088    }
1089
1090    /// Create a new ActionChain from any iterator of actions
1091    pub fn from_actions(actions: impl IntoIterator<Item = Action>) -> Self {
1092        Self::new(actions.into_iter().collect())
1093    }
1094
1095    /// Create a new ActionChain with a name
1096    pub fn named(actions: Vec<Action>, name: impl Into<String>) -> Self {
1097        Self {
1098            actions,
1099            name: Some(name.into()),
1100        }
1101    }
1102
1103    /// Create an empty ActionChain
1104    pub fn empty() -> Self {
1105        Self::new(vec![])
1106    }
1107
1108    // === Direct Vec<Action> Access ===
1109
1110    /// Get immutable reference to actions
1111    pub fn actions(&self) -> &[Action] {
1112        &self.actions
1113    }
1114
1115    /// Get mutable reference to actions vector for direct manipulation
1116    pub fn actions_mut(&mut self) -> &mut Vec<Action> {
1117        &mut self.actions
1118    }
1119
1120    /// Add an action to the end of the chain
1121    pub fn push(&mut self, action: Action) {
1122        self.actions.push(action);
1123    }
1124
1125    /// Add multiple actions to the end of the chain
1126    pub fn extend(&mut self, actions: impl IntoIterator<Item = Action>) {
1127        self.actions.extend(actions);
1128    }
1129
1130    /// Insert an action at a specific index
1131    pub fn insert(&mut self, index: usize, action: Action) {
1132        self.actions.insert(index, action);
1133    }
1134
1135    /// Remove and return the action at index
1136    pub fn remove(&mut self, index: usize) -> Action {
1137        self.actions.remove(index)
1138    }
1139
1140    /// Remove the last action and return it
1141    pub fn pop(&mut self) -> Option<Action> {
1142        self.actions.pop()
1143    }
1144
1145    /// Clear all actions
1146    pub fn clear(&mut self) {
1147        self.actions.clear();
1148    }
1149
1150    /// Create a new chain by appending another chain
1151    pub fn chain_with(mut self, other: ActionChain) -> Self {
1152        self.actions.extend(other.actions);
1153        self
1154    }
1155
1156    /// Get an iterator over actions
1157    pub fn iter(&self) -> std::slice::Iter<'_, Action> {
1158        self.actions.iter()
1159    }
1160
1161    /// Get a mutable iterator over actions
1162    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Action> {
1163        self.actions.iter_mut()
1164    }
1165
1166    // === Metadata Access ===
1167
1168    /// Get the name of this chain
1169    pub fn name(&self) -> Option<&str> {
1170        self.name.as_deref()
1171    }
1172
1173    /// Set the name of this chain
1174    pub fn set_name(&mut self, name: impl Into<String>) {
1175        self.name = Some(name.into());
1176    }
1177
1178    /// Get the number of actions in this chain
1179    pub fn len(&self) -> usize {
1180        self.actions.len()
1181    }
1182
1183    /// Check if this chain is empty
1184    pub fn is_empty(&self) -> bool {
1185        self.actions.is_empty()
1186    }
1187
1188    // === Analysis Methods ===
1189
1190    /// Get actions that match a specific category
1191    pub fn positioning_actions(&self) -> Vec<&Action> {
1192        self.actions
1193            .iter()
1194            .filter(|a| a.is_positioning_action())
1195            .collect()
1196    }
1197
1198    pub fn read_actions(&self) -> Vec<&Action> {
1199        self.actions.iter().filter(|a| a.is_read_action()).collect()
1200    }
1201
1202    pub fn control_actions(&self) -> Vec<&Action> {
1203        self.actions
1204            .iter()
1205            .filter(|a| a.is_control_action())
1206            .collect()
1207    }
1208
1209    /// Check if chain contains any motor movements
1210    pub fn involves_motor(&self) -> bool {
1211        self.actions.iter().any(|a| a.involves_motor())
1212    }
1213
1214    /// Check if chain contains any piezo movements
1215    pub fn involves_piezo(&self) -> bool {
1216        self.actions.iter().any(|a| a.involves_piezo())
1217    }
1218
1219    /// Check if chain contains any bias modifications
1220    pub fn modifies_bias(&self) -> bool {
1221        self.actions.iter().any(|a| a.modifies_bias())
1222    }
1223
1224    /// Get a summary description of the chain
1225    pub fn summary(&self) -> String {
1226        if let Some(name) = &self.name {
1227            format!("{} ({} actions)", name, self.len())
1228        } else {
1229            format!("Action chain with {} actions", self.len())
1230        }
1231    }
1232
1233    /// Get detailed analysis of the chain
1234    pub fn analysis(&self) -> ChainAnalysis {
1235        ChainAnalysis {
1236            total_actions: self.len(),
1237            positioning_actions: self.positioning_actions().len(),
1238            read_actions: self.read_actions().len(),
1239            control_actions: self.control_actions().len(),
1240            involves_motor: self.involves_motor(),
1241            involves_piezo: self.involves_piezo(),
1242            modifies_bias: self.modifies_bias(),
1243        }
1244    }
1245}
1246
1247/// Analysis result for an ActionChain
1248#[derive(Debug, Clone)]
1249pub struct ChainAnalysis {
1250    pub total_actions: usize,
1251    pub positioning_actions: usize,
1252    pub read_actions: usize,
1253    pub control_actions: usize,
1254    pub involves_motor: bool,
1255    pub involves_piezo: bool,
1256    pub modifies_bias: bool,
1257}
1258
1259// === Iterator Support ===
1260
1261impl IntoIterator for ActionChain {
1262    type Item = Action;
1263    type IntoIter = std::vec::IntoIter<Action>;
1264
1265    fn into_iter(self) -> Self::IntoIter {
1266        self.actions.into_iter()
1267    }
1268}
1269
1270impl<'a> IntoIterator for &'a ActionChain {
1271    type Item = &'a Action;
1272    type IntoIter = std::slice::Iter<'a, Action>;
1273
1274    fn into_iter(self) -> Self::IntoIter {
1275        self.actions.iter()
1276    }
1277}
1278
1279impl FromIterator<Action> for ActionChain {
1280    fn from_iter<T: IntoIterator<Item = Action>>(iter: T) -> Self {
1281        Self::from_actions(iter)
1282    }
1283}
1284
1285impl From<Vec<Action>> for ActionChain {
1286    fn from(actions: Vec<Action>) -> Self {
1287        Self::new(actions)
1288    }
1289}
1290
1291// ==================== Pre-built Common Patterns ====================
1292
1293impl ActionChain {
1294    /// Comprehensive system status check
1295    pub fn system_status_check() -> Self {
1296        ActionChain::named(
1297            vec![
1298                Action::ReadSignalNames,
1299                Action::ReadBias,
1300                Action::ReadPiezoPosition {
1301                    wait_for_newest_data: true,
1302                },
1303            ],
1304            "System status check",
1305        )
1306    }
1307
1308    /// Safe tip approach with verification
1309    pub fn safe_tip_approach() -> Self {
1310        ActionChain::named(
1311            vec![
1312                Action::ReadPiezoPosition {
1313                    wait_for_newest_data: true,
1314                },
1315                Action::AutoApproach {
1316                    wait_until_finished: true,
1317                    timeout: Duration::from_secs(300),
1318                    center_freq_shift: false,
1319                },
1320                Action::Wait {
1321                    duration: Duration::from_millis(500),
1322                },
1323                Action::ReadSignal {
1324                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1325                    wait_for_newest: true,
1326                }, // Typical bias voltage
1327                Action::ReadSignal {
1328                    signal: Signal::new("Current".to_string(), 0, None)
1329                        .unwrap(),
1330                    wait_for_newest: true,
1331                }, // Typical current
1332            ],
1333            "Safe tip approach",
1334        )
1335    }
1336
1337    /// Move to position and approach
1338    pub fn move_and_approach(target: Position) -> Self {
1339        ActionChain::named(
1340            vec![
1341                Action::SetPiezoPosition {
1342                    position: target,
1343                    wait_until_finished: true,
1344                },
1345                Action::Wait {
1346                    duration: Duration::from_millis(100),
1347                },
1348                Action::AutoApproach {
1349                    wait_until_finished: true,
1350                    timeout: Duration::from_secs(300),
1351                    center_freq_shift: false,
1352                },
1353                Action::ReadSignal {
1354                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1355                    wait_for_newest: true,
1356                },
1357            ],
1358            format!(
1359                "Move to ({:.1e}, {:.1e}) and approach",
1360                target.x, target.y
1361            ),
1362        )
1363    }
1364
1365    /// Bias pulse sequence with restoration
1366    pub fn bias_pulse_sequence(voltage: f32, duration_ms: u32) -> Self {
1367        ActionChain::named(
1368            vec![
1369                Action::ReadBias,
1370                Action::SetBias { voltage },
1371                Action::Wait {
1372                    duration: Duration::from_millis(50),
1373                },
1374                Action::Wait {
1375                    duration: Duration::from_millis(duration_ms as u64),
1376                },
1377                Action::SetBias { voltage: 0.0 },
1378            ],
1379            format!("Bias pulse {:.3}V for {}ms", voltage, duration_ms),
1380        )
1381    }
1382
1383    /// Survey multiple positions
1384    pub fn position_survey(positions: Vec<Position>) -> Self {
1385        let position_count = positions.len(); // Store length before moving
1386        let mut actions = Vec::new();
1387
1388        for pos in positions {
1389            actions.extend([
1390                Action::SetPiezoPosition {
1391                    position: pos,
1392                    wait_until_finished: true,
1393                },
1394                Action::Wait {
1395                    duration: Duration::from_millis(100),
1396                },
1397                Action::AutoApproach {
1398                    wait_until_finished: true,
1399                    timeout: Duration::from_secs(300),
1400                    center_freq_shift: false,
1401                },
1402                Action::ReadSignal {
1403                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1404                    wait_for_newest: true,
1405                }, // Bias voltage
1406                Action::ReadSignal {
1407                    signal: Signal::new("Current".to_string(), 0, None)
1408                        .unwrap(),
1409                    wait_for_newest: true,
1410                }, // Current
1411                Action::Withdraw {
1412                    wait_until_finished: true,
1413                    timeout: Duration::from_secs(5),
1414                },
1415            ]);
1416        }
1417
1418        ActionChain::named(
1419            actions,
1420            format!("Position survey ({} points)", position_count),
1421        )
1422    }
1423
1424    /// Complete tip recovery sequence
1425    pub fn tip_recovery_sequence() -> Self {
1426        ActionChain::named(
1427            vec![
1428                Action::Withdraw {
1429                    wait_until_finished: true,
1430                    timeout: Duration::from_secs(5),
1431                },
1432                Action::MovePiezoRelative {
1433                    delta: Position { x: 3e-9, y: 3e-9 },
1434                },
1435                Action::Wait {
1436                    duration: Duration::from_millis(200),
1437                },
1438                Action::AutoApproach {
1439                    wait_until_finished: true,
1440                    timeout: Duration::from_secs(300),
1441                    center_freq_shift: false,
1442                },
1443                Action::ReadSignal {
1444                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1445                    wait_for_newest: true,
1446                },
1447            ],
1448            "Tip recovery sequence",
1449        )
1450    }
1451}
1452
1453#[cfg(test)]
1454mod chain_tests {
1455    use super::*;
1456    use crate::types::MotorDirection;
1457
1458    #[test]
1459    fn test_vec_foundation() {
1460        // Test direct Vec<Action> usage
1461        let mut chain = ActionChain::new(vec![
1462            Action::ReadBias,
1463            Action::SetBias { voltage: 1.0 },
1464        ]);
1465
1466        assert_eq!(chain.len(), 2);
1467
1468        // Test Vec operations
1469        chain.push(Action::AutoApproach {
1470            wait_until_finished: true,
1471            timeout: Duration::from_secs(300),
1472            center_freq_shift: false,
1473        });
1474        assert_eq!(chain.len(), 3);
1475
1476        let action = chain.pop().unwrap();
1477        assert!(matches!(
1478            action,
1479            Action::AutoApproach {
1480                wait_until_finished: true,
1481                timeout: _,
1482                center_freq_shift: _,
1483            }
1484        ));
1485        assert_eq!(chain.len(), 2);
1486
1487        // Test extension
1488        chain.extend([
1489            Action::Wait {
1490                duration: Duration::from_millis(100),
1491            },
1492            Action::ReadBias,
1493        ]);
1494        assert_eq!(chain.len(), 4);
1495    }
1496
1497    #[test]
1498    fn test_simple_construction() {
1499        let chain = ActionChain::named(
1500            vec![
1501                Action::ReadBias,
1502                Action::SetBias { voltage: 1.0 },
1503                Action::Wait {
1504                    duration: Duration::from_millis(100),
1505                },
1506                Action::AutoApproach {
1507                    wait_until_finished: true,
1508                    timeout: Duration::from_secs(300),
1509                    center_freq_shift: false,
1510                },
1511            ],
1512            "Test chain",
1513        );
1514
1515        assert_eq!(chain.name(), Some("Test chain"));
1516        assert_eq!(chain.len(), 4);
1517
1518        let analysis = chain.analysis();
1519        assert_eq!(analysis.total_actions, 4);
1520        assert_eq!(analysis.read_actions, 1);
1521        assert_eq!(analysis.control_actions, 1);
1522        assert!(analysis.modifies_bias);
1523    }
1524
1525    #[test]
1526    fn test_programmatic_generation() {
1527        // Test building chains programmatically
1528        let mut chain = ActionChain::empty();
1529
1530        for _ in 0..3 {
1531            chain.push(Action::MoveMotorAxis {
1532                direction: MotorDirection::XPlus,
1533                steps: 10,
1534                blocking: true,
1535            });
1536            chain.push(Action::Wait {
1537                duration: Duration::from_millis(100),
1538            });
1539        }
1540
1541        assert_eq!(chain.len(), 6);
1542        assert!(chain.involves_motor());
1543
1544        // Test iterator construction
1545        let actions: Vec<Action> = (0..5).map(|_| Action::ReadBias).collect();
1546
1547        let iter_chain: ActionChain = actions.into_iter().collect();
1548        assert_eq!(iter_chain.len(), 5);
1549    }
1550
1551    #[test]
1552    fn test_pre_built_patterns() {
1553        let status_check = ActionChain::system_status_check();
1554        assert!(status_check.name().is_some());
1555        assert!(!status_check.is_empty());
1556
1557        let approach = ActionChain::safe_tip_approach();
1558        assert!(!approach.control_actions().is_empty());
1559
1560        let positions =
1561            vec![Position { x: 1e-9, y: 1e-9 }, Position { x: 2e-9, y: 2e-9 }];
1562        let survey = ActionChain::position_survey(positions);
1563        assert_eq!(survey.len(), 12); // 6 actions per position × 2 positions
1564    }
1565
1566    #[test]
1567    fn test_chain_analysis() {
1568        let chain = ActionChain::new(vec![
1569            Action::MoveMotorAxis {
1570                direction: MotorDirection::XPlus,
1571                steps: 100,
1572                blocking: true,
1573            },
1574            Action::SetPiezoPosition {
1575                position: Position { x: 1e-9, y: 1e-9 },
1576                wait_until_finished: true,
1577            },
1578            Action::ReadBias,
1579            Action::AutoApproach {
1580                wait_until_finished: true,
1581                timeout: Duration::from_secs(1),
1582                center_freq_shift: false,
1583            },
1584            Action::SetBias { voltage: 1.5 },
1585        ]);
1586
1587        let analysis = chain.analysis();
1588        assert_eq!(analysis.total_actions, 5);
1589        assert_eq!(analysis.positioning_actions, 2);
1590        assert_eq!(analysis.read_actions, 1);
1591        assert_eq!(analysis.control_actions, 1);
1592        assert!(analysis.involves_motor);
1593        assert!(analysis.involves_piezo);
1594        assert!(analysis.modifies_bias);
1595    }
1596
1597    #[test]
1598    fn test_iteration() {
1599        let chain = ActionChain::new(vec![
1600            Action::ReadBias,
1601            Action::AutoApproach {
1602                wait_until_finished: true,
1603                timeout: Duration::from_secs(1),
1604                center_freq_shift: false,
1605            },
1606            Action::Wait {
1607                duration: Duration::from_millis(100),
1608            },
1609        ]);
1610
1611        // Test iterator
1612        let mut count = 0;
1613        for _ in &chain {
1614            count += 1;
1615            // Can access action here
1616        }
1617        assert_eq!(count, 3);
1618
1619        // Test into_iter
1620        let actions: Vec<Action> = chain.into_iter().collect();
1621        assert_eq!(actions.len(), 3);
1622    }
1623
1624    #[test]
1625    fn test_from_vec_action() {
1626        // Test From<Vec<Action>> trait
1627        let actions = vec![
1628            Action::ReadBias,
1629            Action::SetBias { voltage: 1.5 },
1630            Action::AutoApproach {
1631                wait_until_finished: true,
1632                timeout: Duration::from_secs(1),
1633                center_freq_shift: false,
1634            },
1635        ];
1636
1637        let chain: ActionChain = actions.into();
1638        assert_eq!(chain.len(), 3);
1639        assert!(chain.name().is_none());
1640
1641        // Test that it's usable with Into<ActionChain> parameters
1642        let vec_actions = vec![
1643            Action::ReadBias,
1644            Action::Wait {
1645                duration: Duration::from_millis(50),
1646            },
1647        ];
1648
1649        // This should compile thanks to Into<ActionChain>
1650        fn accepts_into_action_chain(_chain: impl Into<ActionChain>) {
1651            // This function would be called by execute methods
1652        }
1653
1654        accepts_into_action_chain(vec_actions);
1655    }
1656}
1657
1658// ==================== Action Logging Support ====================
1659
1660/// Log entry for action execution with timing information
1661#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1662pub struct ActionLogEntry {
1663    /// The action that was executed
1664    pub action: String, // Action description for JSON serialization
1665    /// The result of the action execution
1666    pub result: ActionLogResult,
1667    /// When the action started executing
1668    pub start_time: chrono::DateTime<chrono::Utc>,
1669    /// How long the action took to execute
1670    pub duration_ms: u64,
1671    /// Optional metadata for debugging
1672    pub metadata: Option<std::collections::HashMap<String, String>>,
1673}
1674
1675/// Comprehensive action result for logging (JSON-serializable)
1676/// Captures all possible data types without simplification
1677#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1678pub enum ActionLogResult {
1679    /// Single numeric value
1680    Value(f64),
1681    /// Multiple numeric values
1682    Values(Vec<f64>),
1683    /// String data
1684    Text(Vec<String>),
1685    /// Boolean status
1686    Status(bool),
1687    /// Position data
1688    Position { x: f64, y: f64 },
1689    /// Complete oscilloscope data with timing and statistics
1690    OsciData {
1691        t0: f64,
1692        dt: f64,
1693        size: i32,
1694        data: Vec<f64>,
1695        signal_stats: Option<LoggableSignalStats>,
1696        is_stable: bool,
1697        fallback_value: Option<f64>,
1698    },
1699    /// Experiment data with action result and TCP signal collection
1700    ExperimentData {
1701        action_result: Box<ActionLogResult>,
1702        signal_frames: Vec<LoggableTimestampedSignalFrame>,
1703        tcp_config: LoggableTCPLoggerConfig,
1704        action_start_ms: u64, // Timestamp as milliseconds since epoch
1705        action_end_ms: u64,
1706        total_duration_ms: u64,
1707    },
1708    /// Chain experiment data with per-action timing and results
1709    ChainExperimentData {
1710        action_results: Vec<ActionLogResult>,
1711        signal_frames: Vec<LoggableTimestampedSignalFrame>,
1712        tcp_config: LoggableTCPLoggerConfig,
1713        action_timings: Vec<(u64, u64)>, // (start_ms, end_ms) for each action
1714        chain_start_ms: u64,
1715        chain_end_ms: u64,
1716        total_duration_ms: u64,
1717    },
1718    /// TCP Logger Status
1719    TCPLoggerStatus {
1720        status: String, // TCPLogStatus serialized as string
1721        channels: Vec<i32>,
1722        oversampling: i32,
1723    },
1724    /// Comprehensive tip state check result
1725    TipState {
1726        shape: TipShape,
1727        measured_signals: std::collections::HashMap<u8, f32>, // SignalIndex as u8 for JSON serialization
1728        bounds_info: Option<std::collections::HashMap<String, String>>, // Bounds and margins for analysis
1729    },
1730    /// Comprehensive stable signal result with full TCP dataset for debugging
1731    StableSignal {
1732        stable_value: f32,
1733        data_points_used: usize,
1734        analysis_duration_ms: u64,
1735        stability_metrics: std::collections::HashMap<String, f32>,
1736        raw_data: Vec<f32>, // Full TCP dataset for debugging stability measures
1737    },
1738    /// Operation completed successfully
1739    Success,
1740    /// Operation completed but no data returned
1741    None,
1742    /// Error occurred during execution
1743    Error(String),
1744}
1745
1746/// JSON-serializable version of SignalStats
1747#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1748pub struct LoggableSignalStats {
1749    pub mean: f64,
1750    pub std_dev: f64,
1751    pub relative_std: f64,
1752    pub window_size: usize,
1753    pub stability_method: String,
1754}
1755
1756/// JSON-serializable version of TimestampedSignalFrame
1757#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1758pub struct LoggableTimestampedSignalFrame {
1759    pub signal_frame: LoggableSignalFrame,
1760    pub timestamp_ms: u64,     // Milliseconds since epoch
1761    pub relative_time_ms: u64, // Milliseconds relative to collection start
1762}
1763
1764/// JSON-serializable version of SignalFrame
1765#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1766pub struct LoggableSignalFrame {
1767    pub counter: u64,
1768    pub data: Vec<f32>,
1769}
1770
1771/// JSON-serializable version of TCPLoggerConfig
1772#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1773pub struct LoggableTCPLoggerConfig {
1774    pub stream_port: u16,
1775    pub channels: Vec<i32>,
1776    pub oversampling: i32,
1777    pub auto_start: bool,
1778    pub buffer_size: Option<usize>,
1779}
1780
1781impl ActionLogEntry {
1782    /// Create a new log entry from action execution
1783    pub fn new(
1784        action: &Action,
1785        result: &ActionResult,
1786        start_time: chrono::DateTime<chrono::Utc>,
1787        duration: std::time::Duration,
1788    ) -> Self {
1789        Self {
1790            action: action.description(),
1791            result: ActionLogResult::from_action_result(result),
1792            start_time,
1793            duration_ms: duration.as_millis() as u64,
1794            metadata: None,
1795        }
1796    }
1797
1798    /// Create a new log entry with error
1799    pub fn new_error(
1800        action: &Action,
1801        error: &crate::NanonisError,
1802        start_time: chrono::DateTime<chrono::Utc>,
1803        duration: std::time::Duration,
1804    ) -> Self {
1805        Self {
1806            action: action.description(),
1807            result: ActionLogResult::Error(error.to_string()),
1808            start_time,
1809            duration_ms: duration.as_millis() as u64,
1810            metadata: None,
1811        }
1812    }
1813
1814    /// Add metadata to this log entry
1815    pub fn with_metadata(
1816        mut self,
1817        key: impl Into<String>,
1818        value: impl Into<String>,
1819    ) -> Self {
1820        if self.metadata.is_none() {
1821            self.metadata = Some(std::collections::HashMap::new());
1822        }
1823        self.metadata
1824            .as_mut()
1825            .unwrap()
1826            .insert(key.into(), value.into());
1827        self
1828    }
1829}
1830
1831impl ActionLogResult {
1832    /// Convert ActionResult to ActionLogResult for comprehensive logging
1833    /// No data simplification - captures everything in full detail
1834    pub fn from_action_result(result: &ActionResult) -> Self {
1835        match result {
1836            ActionResult::Value(v) => ActionLogResult::Value(*v),
1837            ActionResult::Values(values) => {
1838                ActionLogResult::Values(values.clone())
1839            }
1840            ActionResult::Text(text) => ActionLogResult::Text(text.clone()),
1841            ActionResult::Status(status) => ActionLogResult::Status(*status),
1842            ActionResult::Position(pos) => {
1843                ActionLogResult::Position { x: pos.x, y: pos.y }
1844            }
1845            ActionResult::OsciData(osci_data) => ActionLogResult::OsciData {
1846                t0: osci_data.t0,
1847                dt: osci_data.dt,
1848                size: osci_data.size,
1849                data: osci_data.data.clone(),
1850                signal_stats: osci_data.signal_stats.as_ref().map(|stats| {
1851                    LoggableSignalStats {
1852                        mean: stats.mean,
1853                        std_dev: stats.std_dev,
1854                        relative_std: stats.relative_std,
1855                        window_size: stats.window_size,
1856                        stability_method: stats.stability_method.clone(),
1857                    }
1858                }),
1859                is_stable: osci_data.is_stable,
1860                fallback_value: osci_data.fallback_value,
1861            },
1862            ActionResult::Success => ActionLogResult::Success,
1863            ActionResult::None => ActionLogResult::None,
1864            ActionResult::TCPReaderStatus(tcp_status) => {
1865                ActionLogResult::TCPLoggerStatus {
1866                    status: format!("{:?}", tcp_status.status), // Serialize enum as string
1867                    channels: tcp_status.channels.clone(),
1868                    oversampling: tcp_status.oversampling,
1869                }
1870            }
1871            ActionResult::TipState(tip_state) => {
1872                // Convert SignalIndex to u8 for JSON serialization
1873                let measured_signals = tip_state
1874                    .measured_signals
1875                    .iter()
1876                    .map(|(signal_idx, value)| (signal_idx.get(), *value))
1877                    .collect();
1878
1879                // Extract bounds information from metadata if available
1880                let bounds_info = if !tip_state.metadata.is_empty() {
1881                    Some(tip_state.metadata.clone())
1882                } else {
1883                    None
1884                };
1885
1886                ActionLogResult::TipState {
1887                    shape: tip_state.shape,
1888                    measured_signals,
1889                    bounds_info,
1890                }
1891            }
1892            ActionResult::StabilityResult(result) => {
1893                // Convert stability result to comprehensive TipState for logging
1894                let tip_shape = if result.is_stable {
1895                    TipShape::Stable
1896                } else {
1897                    TipShape::Blunt
1898                };
1899
1900                // Convert measured values from stability check
1901                let measured_signals = result
1902                    .measured_values
1903                    .iter()
1904                    .flat_map(|(signal_idx, values)| {
1905                        // Use the last (most recent) measured value for each signal
1906                        values.last().map(|&value| (signal_idx.index, value))
1907                    })
1908                    .collect();
1909
1910                // Create bounds info with stability metrics
1911                let mut bounds_info = std::collections::HashMap::new();
1912                bounds_info.insert(
1913                    "is_stable".to_string(),
1914                    result.is_stable.to_string(),
1915                );
1916                bounds_info.insert(
1917                    "method".to_string(),
1918                    "stability_check".to_string(),
1919                );
1920
1921                ActionLogResult::TipState {
1922                    shape: tip_shape,
1923                    measured_signals,
1924                    bounds_info: Some(bounds_info),
1925                }
1926            }
1927            ActionResult::StableSignal(stable) => {
1928                // Convert stable signal with full dataset for debugging stability measures
1929                ActionLogResult::StableSignal {
1930                    stable_value: stable.stable_value,
1931                    data_points_used: stable.data_points_used,
1932                    analysis_duration_ms: stable.analysis_duration.as_millis()
1933                        as u64,
1934                    stability_metrics: stable.stability_metrics.clone(),
1935                    raw_data: stable.raw_data.clone(), // Full TCP dataset for debugging
1936                }
1937            }
1938        }
1939    }
1940
1941    /// Convert ExperimentData to ActionLogResult for comprehensive logging
1942    pub fn from_experiment_data(
1943        exp_data: &crate::types::ExperimentData,
1944    ) -> Self {
1945        let action_result =
1946            Box::new(Self::from_action_result(&exp_data.action_result));
1947
1948        let signal_frames: Vec<LoggableTimestampedSignalFrame> = exp_data
1949            .signal_frames
1950            .iter()
1951            .map(|frame| LoggableTimestampedSignalFrame {
1952                signal_frame: LoggableSignalFrame {
1953                    counter: frame.signal_frame.counter,
1954                    data: frame.signal_frame.data.clone(),
1955                },
1956                timestamp_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate current time
1957                relative_time_ms: frame.relative_time.as_millis() as u64,
1958            })
1959            .collect();
1960
1961        let tcp_config = LoggableTCPLoggerConfig {
1962            stream_port: exp_data.tcp_config.stream_port,
1963            channels: exp_data.tcp_config.channels.clone(),
1964            oversampling: exp_data.tcp_config.oversampling,
1965            auto_start: exp_data.tcp_config.auto_start,
1966            buffer_size: exp_data.tcp_config.buffer_size,
1967        };
1968
1969        ActionLogResult::ExperimentData {
1970            action_result,
1971            signal_frames,
1972            tcp_config,
1973            action_start_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate timing
1974            action_end_ms: chrono::Utc::now().timestamp_millis() as u64,
1975            total_duration_ms: exp_data.total_duration.as_millis() as u64,
1976        }
1977    }
1978
1979    /// Convert ChainExperimentData to ActionLogResult for comprehensive logging
1980    pub fn from_chain_experiment_data(
1981        chain_data: &crate::types::ChainExperimentData,
1982    ) -> Self {
1983        let action_results: Vec<ActionLogResult> = chain_data
1984            .action_results
1985            .iter()
1986            .map(Self::from_action_result)
1987            .collect();
1988
1989        let signal_frames: Vec<LoggableTimestampedSignalFrame> = chain_data
1990            .signal_frames
1991            .iter()
1992            .map(|frame| LoggableTimestampedSignalFrame {
1993                signal_frame: LoggableSignalFrame {
1994                    counter: frame.signal_frame.counter,
1995                    data: frame.signal_frame.data.clone(),
1996                },
1997                timestamp_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate current time
1998                relative_time_ms: frame.relative_time.as_millis() as u64,
1999            })
2000            .collect();
2001
2002        let tcp_config = LoggableTCPLoggerConfig {
2003            stream_port: chain_data.tcp_config.stream_port,
2004            channels: chain_data.tcp_config.channels.clone(),
2005            oversampling: chain_data.tcp_config.oversampling,
2006            auto_start: chain_data.tcp_config.auto_start,
2007            buffer_size: chain_data.tcp_config.buffer_size,
2008        };
2009
2010        let action_timings: Vec<(u64, u64)> = chain_data
2011            .action_timings
2012            .iter()
2013            .map(|(_, _)| {
2014                let now = chrono::Utc::now().timestamp_millis() as u64;
2015                (now, now) // Approximate timing
2016            })
2017            .collect();
2018
2019        ActionLogResult::ChainExperimentData {
2020            action_results,
2021            signal_frames,
2022            tcp_config,
2023            action_timings,
2024            chain_start_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate timing
2025            chain_end_ms: chrono::Utc::now().timestamp_millis() as u64,
2026            total_duration_ms: chain_data.total_duration.as_millis() as u64,
2027        }
2028    }
2029}