Skip to main content

rusty_tip/
actions.rs

1use crate::{
2    types::{DataToGet, MotorDisplacement, OsciData, TipShape, TriggerConfig},
3    MotorDirection, MovementMode, Position, Position3D, ScanAction, Signal, TipShaperConfig,
4};
5use nanonis_rs::signals::SignalIndex;
6use std::{collections::HashMap, time::Duration};
7
8/// Method for determining tip state
9#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
10pub enum TipCheckMethod {
11    /// Check single signal against bounds
12    SignalBounds { signal: Signal, bounds: (f32, f32) },
13    /// Check multiple signals (all must be in bounds)
14    MultiSignalBounds { signals: Vec<(Signal, (f32, f32))> },
15}
16
17/// Method for determining signal stability for GetStableSignal action
18#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19pub enum SignalStabilityMethod {
20    /// Standard deviation threshold
21    StandardDeviation { threshold: f32 },
22    /// Relative standard deviation (coefficient of variation)
23    RelativeStandardDeviation { threshold_percent: f32 },
24    /// Moving window - signal must be stable within sliding window
25    MovingWindow {
26        window_size: usize,
27        max_variation: f32,
28    },
29    /// Trend analysis - ensure no consistent drift.
30    /// `max_slope` is the maximum allowed drift rate in Hz/s.
31    TrendAnalysis { max_slope: f32 },
32    /// Combined: checks both noise (`max_std_dev`, Hz) AND drift
33    /// (`max_slope`, Hz/s). Both conditions must hold 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)) if vs.len() == 1 => {
442                vs.pop().unwrap()
443            }
444            (Action::ReadBias, ActionResult::Value(v)) => v,
445            (action, result) => panic!(
446                "Expected signal value from action {:?}, got {:?}",
447                action, result
448            ),
449        }
450    }
451
452    /// Extract multiple values with action validation (panics on type mismatch)
453    pub fn expect_values(self, action: &Action) -> Vec<f64> {
454        match (action, self) {
455            (Action::ReadSignals { .. }, ActionResult::Values(values)) => values,
456            (Action::ReadSignal { .. }, ActionResult::Value(v)) => vec![v],
457            (action, result) => {
458                panic!("Expected values from action {:?}, got {:?}", action, result)
459            }
460        }
461    }
462
463    /// Extract position with action validation (panics on type mismatch)
464    pub fn expect_position(self, action: &Action) -> Position {
465        match (action, self) {
466            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => pos,
467            (action, result) => panic!(
468                "Expected position from action {:?}, got {:?}",
469                action, result
470            ),
471        }
472    }
473
474    /// Extract bias voltage with action validation (panics on type mismatch)
475    pub fn expect_bias_voltage(self, action: &Action) -> f32 {
476        match (action, self) {
477            (Action::ReadBias, ActionResult::Value(v)) => v as f32,
478            (action, result) => panic!(
479                "Expected bias voltage from action {:?}, got {:?}",
480                action, result
481            ),
482        }
483    }
484
485    /// Extract signal names with action validation (panics on type mismatch)
486    pub fn expect_signal_names(self, action: &Action) -> Vec<String> {
487        match (action, self) {
488            (Action::ReadSignalNames, ActionResult::Text(names)) => names,
489            (action, result) => panic!(
490                "Expected signal names from action {:?}, got {:?}",
491                action, result
492            ),
493        }
494    }
495
496    /// Extract status with action validation (panics on type mismatch)
497    pub fn expect_status(self, action: &Action) -> bool {
498        match (action, self) {
499            (Action::ReadScanStatus, ActionResult::Status(status)) => status,
500            (action, result) => {
501                panic!("Expected status from action {:?}, got {:?}", action, result)
502            }
503        }
504    }
505
506    /// Extract tip shape enum with action validation (panics on type mismatch)
507    pub fn expect_tip_shape(self, action: &Action) -> TipShape {
508        match (action, self) {
509            (Action::CheckTipState { .. }, ActionResult::TipState(tip_state)) => tip_state.shape,
510            (action, result) => {
511                panic!(
512                    "Expected tip state from action {:?}, got {:?}",
513                    action, result
514                )
515            }
516        }
517    }
518
519    /// Extract full tip state result with action validation (panics on type mismatch)
520    pub fn expect_tip_state(self, action: &Action) -> TipState {
521        match (action, self) {
522            (Action::CheckTipState { .. }, ActionResult::TipState(tip_state)) => tip_state,
523            (action, result) => {
524                panic!(
525                    "Expected tip state from action {:?}, got {:?}",
526                    action, result
527                )
528            }
529        }
530    }
531
532    /// Extract stability result (panics on type mismatch)
533    pub fn expect_stability_result(self, action: &Action) -> StabilityResult {
534        match (action, self) {
535            (Action::CheckTipStability { .. }, ActionResult::StabilityResult(result)) => result,
536            (action, result) => {
537                panic!(
538                    "Expected stability result from action {:?}, got {:?}",
539                    action, result
540                )
541            }
542        }
543    }
544
545    /// Extract stable signal value (panics on type mismatch)
546    pub fn expect_stable_signal_value(self, action: &Action) -> f32 {
547        match (action, self) {
548            (Action::ReadStableSignal { .. }, ActionResult::StableSignal(stable)) => {
549                stable.stable_value
550            }
551            (action, result) => {
552                panic!(
553                    "Expected stable signal from action {:?}, got {:?}",
554                    action, result
555                )
556            }
557        }
558    }
559
560    /// Extract full stable signal result with action validation (panics on type mismatch)
561    pub fn expect_stable_signal(self, action: &Action) -> StableSignal {
562        match (action, self) {
563            (Action::ReadStableSignal { .. }, ActionResult::StableSignal(stable)) => stable,
564            (action, result) => {
565                panic!(
566                    "Expected stable signal from action {:?}, got {:?}",
567                    action, result
568                )
569            }
570        }
571    }
572
573    /// Extract TCP reader status with action validation (panics on type mismatch)
574    pub fn expect_tcp_reader_status(self, action: &Action) -> TCPReaderStatus {
575        match (action, self) {
576            (Action::GetTCPLoggerStatus, ActionResult::TCPReaderStatus(status)) => status,
577            (action, result) => {
578                panic!(
579                    "Expected TCP reader status from action {:?}, got {:?}",
580                    action, result
581                )
582            }
583        }
584    }
585
586    // === Safe Extraction Methods (non-panicking) ===
587
588    /// Try to extract OsciData with action validation
589    pub fn try_into_osci_data(self, action: &Action) -> Result<OsciData, String> {
590        match (action, self) {
591            (Action::ReadOsci { .. }, ActionResult::OsciData(data)) => Ok(data),
592            (action, result) => Err(format!(
593                "Expected OsciData from action {:?}, got {:?}",
594                action, result
595            )),
596        }
597    }
598
599    /// Try to extract signal value with action validation
600    pub fn try_into_signal_value(self, action: &Action) -> Result<f64, String> {
601        match (action, self) {
602            (Action::ReadSignal { .. }, ActionResult::Value(v)) => Ok(v),
603            (Action::ReadSignal { .. }, ActionResult::Values(mut vs)) if vs.len() == 1 => {
604                Ok(vs.pop().unwrap())
605            }
606            (Action::ReadBias, ActionResult::Value(v)) => Ok(v),
607            (action, result) => Err(format!(
608                "Expected signal value from action {:?}, got {:?}",
609                action, result
610            )),
611        }
612    }
613
614    /// Try to extract position with action validation
615    pub fn try_into_position(self, action: &Action) -> Result<Position, String> {
616        match (action, self) {
617            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => Ok(pos),
618            (action, result) => Err(format!(
619                "Expected position from action {:?}, got {:?}",
620                action, result
621            )),
622        }
623    }
624
625    /// Try to extract status with action validation
626    pub fn try_into_status(self, action: &Action) -> Result<bool, String> {
627        match (action, self) {
628            (Action::ReadScanStatus, ActionResult::Status(status)) => Ok(status),
629            (action, result) => Err(format!(
630                "Expected status from action {:?}, got {:?}",
631                action, result
632            )),
633        }
634    }
635
636    /// Try to extract stability result with action validation
637    pub fn try_into_stability_result(self, action: &Action) -> Result<StabilityResult, String> {
638        match (action, self) {
639            (Action::CheckTipStability { .. }, ActionResult::StabilityResult(result)) => Ok(result),
640            (action, result) => Err(format!(
641                "Expected stability result from action {:?}, got {:?}",
642                action, result
643            )),
644        }
645    }
646
647    /// Try to extract stable signal value with action validation
648    pub fn try_into_stable_signal_value(self, action: &Action) -> Result<f32, String> {
649        match (action, self) {
650            (Action::ReadStableSignal { .. }, ActionResult::StableSignal(stable)) => {
651                Ok(stable.stable_value)
652            }
653            (action, result) => Err(format!(
654                "Expected stable signal from action {:?}, got {:?}",
655                action, result
656            )),
657        }
658    }
659}
660
661// === Trait for Generic Type Extraction ===
662
663/// Trait for extracting specific types from ActionResult with action validation
664pub trait ExpectFromAction<T> {
665    fn expect_from_action(self, action: &Action) -> T;
666}
667
668impl ExpectFromAction<OsciData> for ActionResult {
669    fn expect_from_action(self, action: &Action) -> OsciData {
670        self.expect_osci_data(action)
671    }
672}
673
674impl ExpectFromAction<f64> for ActionResult {
675    fn expect_from_action(self, action: &Action) -> f64 {
676        self.expect_signal_value(action)
677    }
678}
679
680impl ExpectFromAction<Vec<f64>> for ActionResult {
681    fn expect_from_action(self, action: &Action) -> Vec<f64> {
682        self.expect_values(action)
683    }
684}
685
686impl ExpectFromAction<Position> for ActionResult {
687    fn expect_from_action(self, action: &Action) -> Position {
688        self.expect_position(action)
689    }
690}
691
692impl ExpectFromAction<Vec<String>> for ActionResult {
693    fn expect_from_action(self, action: &Action) -> Vec<String> {
694        self.expect_signal_names(action)
695    }
696}
697
698impl ExpectFromAction<bool> for ActionResult {
699    fn expect_from_action(self, action: &Action) -> bool {
700        self.expect_status(action)
701    }
702}
703
704impl ExpectFromAction<StabilityResult> for ActionResult {
705    fn expect_from_action(self, action: &Action) -> StabilityResult {
706        self.expect_stability_result(action)
707    }
708}
709
710impl ExpectFromAction<f32> for ActionResult {
711    fn expect_from_action(self, action: &Action) -> f32 {
712        match action {
713            Action::ReadStableSignal { .. } => self.expect_stable_signal_value(action),
714            _ => self.expect_bias_voltage(action),
715        }
716    }
717}
718
719// === Action Categorization ===
720
721impl Action {
722    /// Check if this is a positioning action
723    pub fn is_positioning_action(&self) -> bool {
724        matches!(
725            self,
726            Action::SetPiezoPosition { .. }
727                | Action::MovePiezoRelative { .. }
728                | Action::MoveMotorAxis { .. }
729                | Action::MoveMotor3D { .. }
730                | Action::MoveMotorClosedLoop { .. }
731        )
732    }
733
734    /// Check if this is a read-only action
735    pub fn is_read_action(&self) -> bool {
736        matches!(
737            self,
738            Action::ReadSignal { .. }
739                | Action::ReadSignals { .. }
740                | Action::ReadSignalNames
741                | Action::ReadBias
742                | Action::ReadPiezoPosition { .. }
743                | Action::ReadScanStatus
744                | Action::Retrieve { .. }
745        )
746    }
747
748    /// Check if this is a control action
749    pub fn is_control_action(&self) -> bool {
750        matches!(
751            self,
752            Action::AutoApproach { .. }
753                | Action::Withdraw { .. }
754                | Action::SafeReposition { .. }
755                | Action::ScanControl { .. }
756                | Action::StopMotor
757        )
758    }
759
760    /// Check if this action modifies bias voltage
761    pub fn modifies_bias(&self) -> bool {
762        matches!(self, Action::SetBias { .. } | Action::BiasPulse { .. })
763    }
764
765    /// Check if this action involves motor movement
766    pub fn involves_motor(&self) -> bool {
767        matches!(
768            self,
769            Action::MoveMotorAxis { .. }
770                | Action::MoveMotor3D { .. }
771                | Action::MoveMotorClosedLoop { .. }
772                | Action::SafeReposition { .. }
773                | Action::StopMotor
774        )
775    }
776
777    /// Check if this action involves piezo movement
778    pub fn involves_piezo(&self) -> bool {
779        matches!(
780            self,
781            Action::SetPiezoPosition { .. }
782                | Action::MovePiezoRelative { .. }
783                | Action::ReadPiezoPosition { .. }
784        )
785    }
786
787    /// Get a human-readable description of the action
788    pub fn description(&self) -> String {
789        match self {
790            Action::ReadSignal { signal, .. } => {
791                format!("Read signal {}", signal.index)
792            }
793            Action::ReadSignals { signals, .. } => {
794                let indices: Vec<i32> =
795                    signals.iter().map(|s| s.index as i32).collect();
796                format!("Read signals: {:?}", indices)
797            }
798            Action::SetBias { voltage } => {
799                format!("Set bias to {:.3}V", voltage)
800            }
801            Action::SetPiezoPosition { position, .. } => {
802                format!(
803                    "Set piezo position to ({:.3e}, {:.3e})",
804                    position.x, position.y
805                )
806            }
807            Action::MoveMotorAxis {
808                direction,
809                steps,
810                blocking,
811            } => {
812                format!("Move motor {direction:?} {steps} steps with blocking {blocking}")
813            }
814            Action::MoveMotor3D {
815                displacement,
816                blocking,
817            } => {
818                format!(
819                    "Move motor 3D displacement ({}, {}, {}) with blocking {blocking}",
820                    displacement.x, displacement.y, displacement.z
821                )
822            }
823            Action::AutoApproach {
824                wait_until_finished,
825                timeout,
826                center_freq_shift,
827            } => format!(
828                "Auto approach blocking: {wait_until_finished}, timeout: {:?}, center freq: {center_freq_shift}",
829                timeout
830            ),
831            Action::Withdraw { timeout, .. } => {
832                format!("Withdraw tip (timeout: {}ms)", timeout.as_micros())
833            }
834            Action::SafeReposition { x_steps, y_steps } => {
835                format!(
836                    "Safe reposition: move ({}, {}) steps",
837                    x_steps, y_steps
838                )
839            }
840            Action::SetZSetpoint { setpoint } => {
841                format!("Set Z setpoint: {:.3e}", setpoint)
842            }
843            Action::Wait { duration } => {
844                format!("Wait {:.1}s", duration.as_secs_f64())
845            }
846            Action::BiasPulse {
847                wait_until_done: _,
848                pulse_width,
849                bias_value_v,
850                z_controller_hold: _,
851                pulse_mode: _,
852            } => {
853                format!(
854                    "Bias pulse {:.3}V for {:?}ms",
855                    bias_value_v, pulse_width
856                )
857            }
858            Action::TipShaper {
859                config,
860                wait_until_finished,
861                timeout,
862            } => {
863                format!(
864                    "Tip shaper: bias {:.1}V, lift {:.0}nm, times {:.1?}s/{:.1?}s (wait: {}, timeout: {:?}ms)",
865                    config.bias_v,
866                    config.tip_lift_m * 1e9,
867                    config.lift_time_1.as_secs_f32(),
868                    config.lift_time_2.as_secs_f32(),
869                    wait_until_finished,
870                    timeout
871                )
872            }
873            Action::PulseRetract {
874                pulse_width,
875                pulse_height_v,
876            } => {
877                format!(
878                    "Pulse retract {:.1}V for {:.0?}ms",
879                    pulse_height_v, pulse_width
880                )
881            }
882            Action::ReadOsci {
883                signal,
884                trigger,
885                data_to_get,
886                is_stable,
887            } => {
888                let trigger_desc = match trigger {
889                    Some(config) => format!("trigger: {:?}", config.mode),
890                    None => "no trigger config".to_string(),
891                };
892                let stability_desc = match is_stable {
893                    Some(_) => " with custom stability",
894                    None => "",
895                };
896                format!(
897                    "Read oscilloscope signal {} with {} (mode: {:?}){}",
898                    signal.index, trigger_desc, data_to_get, stability_desc
899                )
900            }
901            Action::CheckTipState { method } => match method {
902                TipCheckMethod::SignalBounds { signal, bounds } => {
903                    format!(
904                        "Check tip state: signal {} bounds ({:.3e}, {:.3e})",
905                        signal.index, bounds.0, bounds.1
906                    )
907                }
908                TipCheckMethod::MultiSignalBounds { signals } => {
909                    format!("Check tip state: {} signal bounds", signals.len())
910                }
911            },
912            Action::CheckTipStability {
913                method,
914                max_duration,
915            } => {
916                let duration_desc =
917                    format!("{:.1}s", max_duration.as_secs_f32());
918                match method {
919                    TipStabilityMethod::ExtendedMonitoring {
920                        signal,
921                        duration,
922                        ..
923                    } => {
924                        format!("Check tip stability: extended monitoring signal {} for {:.1}s (max: {})",
925                               signal.index, duration.as_secs_f32(), duration_desc)
926                    }
927                    TipStabilityMethod::BiasSweepResponse {
928                        signal,
929                        bias_range,
930                        bias_steps,
931                        step_duration,
932                        allowed_signal_change,
933                    } => {
934                        format!("Check tip stability: bias sweep signal {} from {:.2}V to {:.2}V ({} steps, {:.1}ms period, {:.1}% change allowed, max: {})",
935                               signal.index, bias_range.0, bias_range.1, bias_steps, step_duration.as_millis(), allowed_signal_change * 100.0, duration_desc)
936                    }
937                }
938            }
939            Action::ReadStableSignal {
940                signal,
941                data_points,
942                use_new_data,
943                stability_method,
944                timeout,
945                retry_count,
946            } => {
947                let points_desc = data_points
948                    .map_or("default".to_string(), |p| p.to_string());
949                let data_desc = if *use_new_data {
950                    "new data"
951                } else {
952                    "buffered data"
953                };
954                let method_desc = match stability_method {
955                    SignalStabilityMethod::StandardDeviation { threshold } => {
956                        format!("std dev {:.3e}", threshold)
957                    }
958                    SignalStabilityMethod::RelativeStandardDeviation {
959                        threshold_percent,
960                    } => {
961                        format!("rel std {:.1}%", threshold_percent)
962                    }
963                    SignalStabilityMethod::MovingWindow {
964                        window_size,
965                        max_variation,
966                    } => {
967                        format!(
968                            "window {}pts, max var {:.3e}",
969                            window_size, max_variation
970                        )
971                    }
972                    SignalStabilityMethod::TrendAnalysis { max_slope } => {
973                        format!(
974                            "trend analysis, max slope {:.3e} Hz/s",
975                            max_slope
976                        )
977                    }
978                    SignalStabilityMethod::Combined {
979                        max_std_dev,
980                        max_slope,
981                    } => {
982                        format!(
983                            "combined: std dev {:.3e} Hz, slope {:.3e} Hz/s",
984                            max_std_dev, max_slope
985                        )
986                    }
987                };
988                let retry_desc = retry_count
989                    .map_or("no retry".to_string(), |r| {
990                        format!("{} retries", r)
991                    });
992                format!(
993                    "Get stable signal {} ({} points, {}, {}, timeout {:.1}s, {})",
994                    signal.index,
995                    points_desc,
996                    data_desc,
997                    method_desc,
998                    timeout.as_secs_f32(),
999                    retry_desc
1000                )
1001            }
1002            _ => format!("{:?}", self),
1003        }
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010
1011    #[test]
1012    fn test_action_result_extraction() {
1013        let bias_result = ActionResult::Value(2.5);
1014        assert_eq!(bias_result.as_f64(), Some(2.5));
1015
1016        let position_result = ActionResult::Position(Position { x: 1e-9, y: 2e-9 });
1017        assert_eq!(
1018            position_result.as_position(),
1019            Some(Position { x: 1e-9, y: 2e-9 })
1020        );
1021    }
1022}
1023
1024/// A sequence of actions with simple Vec<Action> foundation
1025#[derive(Debug, Clone)]
1026pub struct ActionChain {
1027    actions: Vec<Action>,
1028    name: Option<String>,
1029}
1030
1031impl ActionChain {
1032    /// Create a new ActionChain from a vector of actions
1033    pub fn new(actions: Vec<Action>) -> Self {
1034        Self {
1035            actions,
1036            name: None,
1037        }
1038    }
1039
1040    /// Create a new ActionChain from any iterator of actions
1041    pub fn from_actions(actions: impl IntoIterator<Item = Action>) -> Self {
1042        Self::new(actions.into_iter().collect())
1043    }
1044
1045    /// Create a new ActionChain with a name
1046    pub fn named(actions: Vec<Action>, name: impl Into<String>) -> Self {
1047        Self {
1048            actions,
1049            name: Some(name.into()),
1050        }
1051    }
1052
1053    /// Create an empty ActionChain
1054    pub fn empty() -> Self {
1055        Self::new(vec![])
1056    }
1057
1058    // === Direct Vec<Action> Access ===
1059
1060    /// Get immutable reference to actions
1061    pub fn actions(&self) -> &[Action] {
1062        &self.actions
1063    }
1064
1065    /// Get mutable reference to actions vector for direct manipulation
1066    pub fn actions_mut(&mut self) -> &mut Vec<Action> {
1067        &mut self.actions
1068    }
1069
1070    /// Add an action to the end of the chain
1071    pub fn push(&mut self, action: Action) {
1072        self.actions.push(action);
1073    }
1074
1075    /// Add multiple actions to the end of the chain
1076    pub fn extend(&mut self, actions: impl IntoIterator<Item = Action>) {
1077        self.actions.extend(actions);
1078    }
1079
1080    /// Insert an action at a specific index
1081    pub fn insert(&mut self, index: usize, action: Action) {
1082        self.actions.insert(index, action);
1083    }
1084
1085    /// Remove and return the action at index
1086    pub fn remove(&mut self, index: usize) -> Action {
1087        self.actions.remove(index)
1088    }
1089
1090    /// Remove the last action and return it
1091    pub fn pop(&mut self) -> Option<Action> {
1092        self.actions.pop()
1093    }
1094
1095    /// Clear all actions
1096    pub fn clear(&mut self) {
1097        self.actions.clear();
1098    }
1099
1100    /// Create a new chain by appending another chain
1101    pub fn chain_with(mut self, other: ActionChain) -> Self {
1102        self.actions.extend(other.actions);
1103        self
1104    }
1105
1106    /// Get an iterator over actions
1107    pub fn iter(&self) -> std::slice::Iter<'_, Action> {
1108        self.actions.iter()
1109    }
1110
1111    /// Get a mutable iterator over actions
1112    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Action> {
1113        self.actions.iter_mut()
1114    }
1115
1116    // === Metadata Access ===
1117
1118    /// Get the name of this chain
1119    pub fn name(&self) -> Option<&str> {
1120        self.name.as_deref()
1121    }
1122
1123    /// Set the name of this chain
1124    pub fn set_name(&mut self, name: impl Into<String>) {
1125        self.name = Some(name.into());
1126    }
1127
1128    /// Get the number of actions in this chain
1129    pub fn len(&self) -> usize {
1130        self.actions.len()
1131    }
1132
1133    /// Check if this chain is empty
1134    pub fn is_empty(&self) -> bool {
1135        self.actions.is_empty()
1136    }
1137
1138    // === Analysis Methods ===
1139
1140    /// Get actions that match a specific category
1141    pub fn positioning_actions(&self) -> Vec<&Action> {
1142        self.actions
1143            .iter()
1144            .filter(|a| a.is_positioning_action())
1145            .collect()
1146    }
1147
1148    pub fn read_actions(&self) -> Vec<&Action> {
1149        self.actions.iter().filter(|a| a.is_read_action()).collect()
1150    }
1151
1152    pub fn control_actions(&self) -> Vec<&Action> {
1153        self.actions
1154            .iter()
1155            .filter(|a| a.is_control_action())
1156            .collect()
1157    }
1158
1159    /// Check if chain contains any motor movements
1160    pub fn involves_motor(&self) -> bool {
1161        self.actions.iter().any(|a| a.involves_motor())
1162    }
1163
1164    /// Check if chain contains any piezo movements
1165    pub fn involves_piezo(&self) -> bool {
1166        self.actions.iter().any(|a| a.involves_piezo())
1167    }
1168
1169    /// Check if chain contains any bias modifications
1170    pub fn modifies_bias(&self) -> bool {
1171        self.actions.iter().any(|a| a.modifies_bias())
1172    }
1173
1174    /// Get a summary description of the chain
1175    pub fn summary(&self) -> String {
1176        if let Some(name) = &self.name {
1177            format!("{} ({} actions)", name, self.len())
1178        } else {
1179            format!("Action chain with {} actions", self.len())
1180        }
1181    }
1182
1183    /// Get detailed analysis of the chain
1184    pub fn analysis(&self) -> ChainAnalysis {
1185        ChainAnalysis {
1186            total_actions: self.len(),
1187            positioning_actions: self.positioning_actions().len(),
1188            read_actions: self.read_actions().len(),
1189            control_actions: self.control_actions().len(),
1190            involves_motor: self.involves_motor(),
1191            involves_piezo: self.involves_piezo(),
1192            modifies_bias: self.modifies_bias(),
1193        }
1194    }
1195}
1196
1197/// Analysis result for an ActionChain
1198#[derive(Debug, Clone)]
1199pub struct ChainAnalysis {
1200    pub total_actions: usize,
1201    pub positioning_actions: usize,
1202    pub read_actions: usize,
1203    pub control_actions: usize,
1204    pub involves_motor: bool,
1205    pub involves_piezo: bool,
1206    pub modifies_bias: bool,
1207}
1208
1209// === Iterator Support ===
1210
1211impl IntoIterator for ActionChain {
1212    type Item = Action;
1213    type IntoIter = std::vec::IntoIter<Action>;
1214
1215    fn into_iter(self) -> Self::IntoIter {
1216        self.actions.into_iter()
1217    }
1218}
1219
1220impl<'a> IntoIterator for &'a ActionChain {
1221    type Item = &'a Action;
1222    type IntoIter = std::slice::Iter<'a, Action>;
1223
1224    fn into_iter(self) -> Self::IntoIter {
1225        self.actions.iter()
1226    }
1227}
1228
1229impl FromIterator<Action> for ActionChain {
1230    fn from_iter<T: IntoIterator<Item = Action>>(iter: T) -> Self {
1231        Self::from_actions(iter)
1232    }
1233}
1234
1235impl From<Vec<Action>> for ActionChain {
1236    fn from(actions: Vec<Action>) -> Self {
1237        Self::new(actions)
1238    }
1239}
1240
1241// ==================== Pre-built Common Patterns ====================
1242
1243impl ActionChain {
1244    /// Comprehensive system status check
1245    pub fn system_status_check() -> Self {
1246        ActionChain::named(
1247            vec![
1248                Action::ReadSignalNames,
1249                Action::ReadBias,
1250                Action::ReadPiezoPosition {
1251                    wait_for_newest_data: true,
1252                },
1253            ],
1254            "System status check",
1255        )
1256    }
1257
1258    /// Safe tip approach with verification
1259    pub fn safe_tip_approach() -> Self {
1260        ActionChain::named(
1261            vec![
1262                Action::ReadPiezoPosition {
1263                    wait_for_newest_data: true,
1264                },
1265                Action::AutoApproach {
1266                    wait_until_finished: true,
1267                    timeout: Duration::from_secs(300),
1268                    center_freq_shift: false,
1269                },
1270                Action::Wait {
1271                    duration: Duration::from_millis(500),
1272                },
1273                Action::ReadSignal {
1274                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1275                    wait_for_newest: true,
1276                }, // Typical bias voltage
1277                Action::ReadSignal {
1278                    signal: Signal::new("Current".to_string(), 0, None).unwrap(),
1279                    wait_for_newest: true,
1280                }, // Typical current
1281            ],
1282            "Safe tip approach",
1283        )
1284    }
1285
1286    /// Move to position and approach
1287    pub fn move_and_approach(target: Position) -> Self {
1288        ActionChain::named(
1289            vec![
1290                Action::SetPiezoPosition {
1291                    position: target,
1292                    wait_until_finished: true,
1293                },
1294                Action::Wait {
1295                    duration: Duration::from_millis(100),
1296                },
1297                Action::AutoApproach {
1298                    wait_until_finished: true,
1299                    timeout: Duration::from_secs(300),
1300                    center_freq_shift: false,
1301                },
1302                Action::ReadSignal {
1303                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1304                    wait_for_newest: true,
1305                },
1306            ],
1307            format!("Move to ({:.1e}, {:.1e}) and approach", target.x, target.y),
1308        )
1309    }
1310
1311    /// Bias pulse sequence with restoration
1312    pub fn bias_pulse_sequence(voltage: f32, duration_ms: u32) -> Self {
1313        ActionChain::named(
1314            vec![
1315                Action::ReadBias,
1316                Action::SetBias { voltage },
1317                Action::Wait {
1318                    duration: Duration::from_millis(50),
1319                },
1320                Action::Wait {
1321                    duration: Duration::from_millis(duration_ms as u64),
1322                },
1323                Action::SetBias { voltage: 0.0 },
1324            ],
1325            format!("Bias pulse {:.3}V for {}ms", voltage, duration_ms),
1326        )
1327    }
1328
1329    /// Survey multiple positions
1330    pub fn position_survey(positions: Vec<Position>) -> Self {
1331        let position_count = positions.len(); // Store length before moving
1332        let mut actions = Vec::new();
1333
1334        for pos in positions {
1335            actions.extend([
1336                Action::SetPiezoPosition {
1337                    position: pos,
1338                    wait_until_finished: true,
1339                },
1340                Action::Wait {
1341                    duration: Duration::from_millis(100),
1342                },
1343                Action::AutoApproach {
1344                    wait_until_finished: true,
1345                    timeout: Duration::from_secs(300),
1346                    center_freq_shift: false,
1347                },
1348                Action::ReadSignal {
1349                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1350                    wait_for_newest: true,
1351                }, // Bias voltage
1352                Action::ReadSignal {
1353                    signal: Signal::new("Current".to_string(), 0, None).unwrap(),
1354                    wait_for_newest: true,
1355                }, // Current
1356                Action::Withdraw {
1357                    wait_until_finished: true,
1358                    timeout: Duration::from_secs(5),
1359                },
1360            ]);
1361        }
1362
1363        ActionChain::named(
1364            actions,
1365            format!("Position survey ({} points)", position_count),
1366        )
1367    }
1368
1369    /// Complete tip recovery sequence
1370    pub fn tip_recovery_sequence() -> Self {
1371        ActionChain::named(
1372            vec![
1373                Action::Withdraw {
1374                    wait_until_finished: true,
1375                    timeout: Duration::from_secs(5),
1376                },
1377                Action::MovePiezoRelative {
1378                    delta: Position { x: 3e-9, y: 3e-9 },
1379                },
1380                Action::Wait {
1381                    duration: Duration::from_millis(200),
1382                },
1383                Action::AutoApproach {
1384                    wait_until_finished: true,
1385                    timeout: Duration::from_secs(300),
1386                    center_freq_shift: false,
1387                },
1388                Action::ReadSignal {
1389                    signal: Signal::new("Bias".to_string(), 24, None).unwrap(),
1390                    wait_for_newest: true,
1391                },
1392            ],
1393            "Tip recovery sequence",
1394        )
1395    }
1396}
1397
1398#[cfg(test)]
1399mod chain_tests {
1400    use super::*;
1401    use crate::types::MotorDirection;
1402
1403    #[test]
1404    fn test_vec_foundation() {
1405        // Test direct Vec<Action> usage
1406        let mut chain = ActionChain::new(vec![Action::ReadBias, Action::SetBias { voltage: 1.0 }]);
1407
1408        assert_eq!(chain.len(), 2);
1409
1410        // Test Vec operations
1411        chain.push(Action::AutoApproach {
1412            wait_until_finished: true,
1413            timeout: Duration::from_secs(300),
1414            center_freq_shift: false,
1415        });
1416        assert_eq!(chain.len(), 3);
1417
1418        let action = chain.pop().unwrap();
1419        assert!(matches!(
1420            action,
1421            Action::AutoApproach {
1422                wait_until_finished: true,
1423                timeout: _,
1424                center_freq_shift: _,
1425            }
1426        ));
1427        assert_eq!(chain.len(), 2);
1428
1429        // Test extension
1430        chain.extend([
1431            Action::Wait {
1432                duration: Duration::from_millis(100),
1433            },
1434            Action::ReadBias,
1435        ]);
1436        assert_eq!(chain.len(), 4);
1437    }
1438
1439    #[test]
1440    fn test_simple_construction() {
1441        let chain = ActionChain::named(
1442            vec![
1443                Action::ReadBias,
1444                Action::SetBias { voltage: 1.0 },
1445                Action::Wait {
1446                    duration: Duration::from_millis(100),
1447                },
1448                Action::AutoApproach {
1449                    wait_until_finished: true,
1450                    timeout: Duration::from_secs(300),
1451                    center_freq_shift: false,
1452                },
1453            ],
1454            "Test chain",
1455        );
1456
1457        assert_eq!(chain.name(), Some("Test chain"));
1458        assert_eq!(chain.len(), 4);
1459
1460        let analysis = chain.analysis();
1461        assert_eq!(analysis.total_actions, 4);
1462        assert_eq!(analysis.read_actions, 1);
1463        assert_eq!(analysis.control_actions, 1);
1464        assert!(analysis.modifies_bias);
1465    }
1466
1467    #[test]
1468    fn test_programmatic_generation() {
1469        // Test building chains programmatically
1470        let mut chain = ActionChain::empty();
1471
1472        for _ in 0..3 {
1473            chain.push(Action::MoveMotorAxis {
1474                direction: MotorDirection::XPlus,
1475                steps: 10,
1476                blocking: true,
1477            });
1478            chain.push(Action::Wait {
1479                duration: Duration::from_millis(100),
1480            });
1481        }
1482
1483        assert_eq!(chain.len(), 6);
1484        assert!(chain.involves_motor());
1485
1486        // Test iterator construction
1487        let actions: Vec<Action> = (0..5).map(|_| Action::ReadBias).collect();
1488
1489        let iter_chain: ActionChain = actions.into_iter().collect();
1490        assert_eq!(iter_chain.len(), 5);
1491    }
1492
1493    #[test]
1494    fn test_pre_built_patterns() {
1495        let status_check = ActionChain::system_status_check();
1496        assert!(status_check.name().is_some());
1497        assert!(!status_check.is_empty());
1498
1499        let approach = ActionChain::safe_tip_approach();
1500        assert!(!approach.control_actions().is_empty());
1501
1502        let positions = vec![Position { x: 1e-9, y: 1e-9 }, Position { x: 2e-9, y: 2e-9 }];
1503        let survey = ActionChain::position_survey(positions);
1504        assert_eq!(survey.len(), 12); // 6 actions per position × 2 positions
1505    }
1506
1507    #[test]
1508    fn test_chain_analysis() {
1509        let chain = ActionChain::new(vec![
1510            Action::MoveMotorAxis {
1511                direction: MotorDirection::XPlus,
1512                steps: 100,
1513                blocking: true,
1514            },
1515            Action::SetPiezoPosition {
1516                position: Position { x: 1e-9, y: 1e-9 },
1517                wait_until_finished: true,
1518            },
1519            Action::ReadBias,
1520            Action::AutoApproach {
1521                wait_until_finished: true,
1522                timeout: Duration::from_secs(1),
1523                center_freq_shift: false,
1524            },
1525            Action::SetBias { voltage: 1.5 },
1526        ]);
1527
1528        let analysis = chain.analysis();
1529        assert_eq!(analysis.total_actions, 5);
1530        assert_eq!(analysis.positioning_actions, 2);
1531        assert_eq!(analysis.read_actions, 1);
1532        assert_eq!(analysis.control_actions, 1);
1533        assert!(analysis.involves_motor);
1534        assert!(analysis.involves_piezo);
1535        assert!(analysis.modifies_bias);
1536    }
1537
1538    #[test]
1539    fn test_iteration() {
1540        let chain = ActionChain::new(vec![
1541            Action::ReadBias,
1542            Action::AutoApproach {
1543                wait_until_finished: true,
1544                timeout: Duration::from_secs(1),
1545                center_freq_shift: false,
1546            },
1547            Action::Wait {
1548                duration: Duration::from_millis(100),
1549            },
1550        ]);
1551
1552        // Test iterator
1553        let mut count = 0;
1554        for _ in &chain {
1555            count += 1;
1556            // Can access action here
1557        }
1558        assert_eq!(count, 3);
1559
1560        // Test into_iter
1561        let actions: Vec<Action> = chain.into_iter().collect();
1562        assert_eq!(actions.len(), 3);
1563    }
1564
1565    #[test]
1566    fn test_from_vec_action() {
1567        // Test From<Vec<Action>> trait
1568        let actions = vec![
1569            Action::ReadBias,
1570            Action::SetBias { voltage: 1.5 },
1571            Action::AutoApproach {
1572                wait_until_finished: true,
1573                timeout: Duration::from_secs(1),
1574                center_freq_shift: false,
1575            },
1576        ];
1577
1578        let chain: ActionChain = actions.into();
1579        assert_eq!(chain.len(), 3);
1580        assert!(chain.name().is_none());
1581
1582        // Test that it's usable with Into<ActionChain> parameters
1583        let vec_actions = vec![
1584            Action::ReadBias,
1585            Action::Wait {
1586                duration: Duration::from_millis(50),
1587            },
1588        ];
1589
1590        // This should compile thanks to Into<ActionChain>
1591        fn accepts_into_action_chain(_chain: impl Into<ActionChain>) {
1592            // This function would be called by execute methods
1593        }
1594
1595        accepts_into_action_chain(vec_actions);
1596    }
1597}
1598
1599// ==================== Action Logging Support ====================
1600
1601/// Log entry for action execution with timing information
1602#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1603pub struct ActionLogEntry {
1604    /// The action that was executed
1605    pub action: String, // Action description for JSON serialization
1606    /// The result of the action execution
1607    pub result: ActionLogResult,
1608    /// When the action started executing
1609    pub start_time: chrono::DateTime<chrono::Utc>,
1610    /// How long the action took to execute
1611    pub duration_ms: u64,
1612    /// Optional metadata for debugging
1613    pub metadata: Option<std::collections::HashMap<String, String>>,
1614}
1615
1616/// Comprehensive action result for logging (JSON-serializable)
1617/// Captures all possible data types without simplification
1618#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1619pub enum ActionLogResult {
1620    /// Single numeric value
1621    Value(f64),
1622    /// Multiple numeric values
1623    Values(Vec<f64>),
1624    /// String data
1625    Text(Vec<String>),
1626    /// Boolean status
1627    Status(bool),
1628    /// Position data
1629    Position { x: f64, y: f64 },
1630    /// Complete oscilloscope data with timing and statistics
1631    OsciData {
1632        t0: f64,
1633        dt: f64,
1634        size: i32,
1635        data: Vec<f64>,
1636        signal_stats: Option<LoggableSignalStats>,
1637        is_stable: bool,
1638        fallback_value: Option<f64>,
1639    },
1640    /// Experiment data with action result and TCP signal collection
1641    ExperimentData {
1642        action_result: Box<ActionLogResult>,
1643        signal_frames: Vec<LoggableTimestampedSignalFrame>,
1644        tcp_config: LoggableTCPLoggerConfig,
1645        action_start_ms: u64, // Timestamp as milliseconds since epoch
1646        action_end_ms: u64,
1647        total_duration_ms: u64,
1648    },
1649    /// Chain experiment data with per-action timing and results
1650    ChainExperimentData {
1651        action_results: Vec<ActionLogResult>,
1652        signal_frames: Vec<LoggableTimestampedSignalFrame>,
1653        tcp_config: LoggableTCPLoggerConfig,
1654        action_timings: Vec<(u64, u64)>, // (start_ms, end_ms) for each action
1655        chain_start_ms: u64,
1656        chain_end_ms: u64,
1657        total_duration_ms: u64,
1658    },
1659    /// TCP Logger Status
1660    TCPLoggerStatus {
1661        status: String, // TCPLogStatus serialized as string
1662        channels: Vec<i32>,
1663        oversampling: i32,
1664    },
1665    /// Comprehensive tip state check result
1666    TipState {
1667        shape: TipShape,
1668        measured_signals: std::collections::HashMap<u8, f32>, // SignalIndex as u8 for JSON serialization
1669        bounds_info: Option<std::collections::HashMap<String, String>>, // Bounds and margins for analysis
1670    },
1671    /// Comprehensive stable signal result with full TCP dataset for debugging
1672    StableSignal {
1673        stable_value: f32,
1674        data_points_used: usize,
1675        analysis_duration_ms: u64,
1676        stability_metrics: std::collections::HashMap<String, f32>,
1677        raw_data: Vec<f32>, // Full TCP dataset for debugging stability measures
1678    },
1679    /// Operation completed successfully
1680    Success,
1681    /// Operation completed but no data returned
1682    None,
1683    /// Error occurred during execution
1684    Error(String),
1685}
1686
1687/// JSON-serializable version of SignalStats
1688#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1689pub struct LoggableSignalStats {
1690    pub mean: f64,
1691    pub std_dev: f64,
1692    pub relative_std: f64,
1693    pub window_size: usize,
1694    pub stability_method: String,
1695}
1696
1697/// JSON-serializable version of TimestampedSignalFrame
1698#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1699pub struct LoggableTimestampedSignalFrame {
1700    pub signal_frame: LoggableSignalFrame,
1701    pub timestamp_ms: u64,     // Milliseconds since epoch
1702    pub relative_time_ms: u64, // Milliseconds relative to collection start
1703}
1704
1705/// JSON-serializable version of SignalFrame
1706#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1707pub struct LoggableSignalFrame {
1708    pub counter: u64,
1709    pub data: Vec<f32>,
1710}
1711
1712/// JSON-serializable version of TCPLoggerConfig
1713#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1714pub struct LoggableTCPLoggerConfig {
1715    pub stream_port: u16,
1716    pub channels: Vec<i32>,
1717    pub oversampling: i32,
1718    pub auto_start: bool,
1719    pub buffer_size: Option<usize>,
1720}
1721
1722impl ActionLogEntry {
1723    /// Create a new log entry from action execution
1724    pub fn new(
1725        action: &Action,
1726        result: &ActionResult,
1727        start_time: chrono::DateTime<chrono::Utc>,
1728        duration: std::time::Duration,
1729    ) -> Self {
1730        Self {
1731            action: action.description(),
1732            result: ActionLogResult::from_action_result(result),
1733            start_time,
1734            duration_ms: duration.as_millis() as u64,
1735            metadata: None,
1736        }
1737    }
1738
1739    /// Create a new log entry with error
1740    pub fn new_error(
1741        action: &Action,
1742        error: &crate::NanonisError,
1743        start_time: chrono::DateTime<chrono::Utc>,
1744        duration: std::time::Duration,
1745    ) -> Self {
1746        Self {
1747            action: action.description(),
1748            result: ActionLogResult::Error(error.to_string()),
1749            start_time,
1750            duration_ms: duration.as_millis() as u64,
1751            metadata: None,
1752        }
1753    }
1754
1755    /// Add metadata to this log entry
1756    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1757        if self.metadata.is_none() {
1758            self.metadata = Some(std::collections::HashMap::new());
1759        }
1760        self.metadata
1761            .as_mut()
1762            .unwrap()
1763            .insert(key.into(), value.into());
1764        self
1765    }
1766}
1767
1768impl ActionLogResult {
1769    /// Convert ActionResult to ActionLogResult for comprehensive logging
1770    /// No data simplification - captures everything in full detail
1771    pub fn from_action_result(result: &ActionResult) -> Self {
1772        match result {
1773            ActionResult::Value(v) => ActionLogResult::Value(*v),
1774            ActionResult::Values(values) => ActionLogResult::Values(values.clone()),
1775            ActionResult::Text(text) => ActionLogResult::Text(text.clone()),
1776            ActionResult::Status(status) => ActionLogResult::Status(*status),
1777            ActionResult::Position(pos) => ActionLogResult::Position { x: pos.x, y: pos.y },
1778            ActionResult::OsciData(osci_data) => ActionLogResult::OsciData {
1779                t0: osci_data.t0,
1780                dt: osci_data.dt,
1781                size: osci_data.size,
1782                data: osci_data.data.clone(),
1783                signal_stats: osci_data
1784                    .signal_stats
1785                    .as_ref()
1786                    .map(|stats| LoggableSignalStats {
1787                        mean: stats.mean,
1788                        std_dev: stats.std_dev,
1789                        relative_std: stats.relative_std,
1790                        window_size: stats.window_size,
1791                        stability_method: stats.stability_method.clone(),
1792                    }),
1793                is_stable: osci_data.is_stable,
1794                fallback_value: osci_data.fallback_value,
1795            },
1796            ActionResult::Success => ActionLogResult::Success,
1797            ActionResult::None => ActionLogResult::None,
1798            ActionResult::TCPReaderStatus(tcp_status) => {
1799                ActionLogResult::TCPLoggerStatus {
1800                    status: format!("{:?}", tcp_status.status), // Serialize enum as string
1801                    channels: tcp_status.channels.clone(),
1802                    oversampling: tcp_status.oversampling,
1803                }
1804            }
1805            ActionResult::TipState(tip_state) => {
1806                // Convert SignalIndex to u8 for JSON serialization
1807                let measured_signals = tip_state
1808                    .measured_signals
1809                    .iter()
1810                    .map(|(signal_idx, value)| (signal_idx.get(), *value))
1811                    .collect();
1812
1813                // Extract bounds information from metadata if available
1814                let bounds_info = if !tip_state.metadata.is_empty() {
1815                    Some(tip_state.metadata.clone())
1816                } else {
1817                    None
1818                };
1819
1820                ActionLogResult::TipState {
1821                    shape: tip_state.shape,
1822                    measured_signals,
1823                    bounds_info,
1824                }
1825            }
1826            ActionResult::StabilityResult(result) => {
1827                // Convert stability result to comprehensive TipState for logging
1828                let tip_shape = if result.is_stable {
1829                    TipShape::Stable
1830                } else {
1831                    TipShape::Blunt
1832                };
1833
1834                // Convert measured values from stability check
1835                let measured_signals = result
1836                    .measured_values
1837                    .iter()
1838                    .flat_map(|(signal_idx, values)| {
1839                        // Use the last (most recent) measured value for each signal
1840                        values.last().map(|&value| (signal_idx.index, value))
1841                    })
1842                    .collect();
1843
1844                // Create bounds info with stability metrics
1845                let mut bounds_info = std::collections::HashMap::new();
1846                bounds_info.insert("is_stable".to_string(), result.is_stable.to_string());
1847                bounds_info.insert("method".to_string(), "stability_check".to_string());
1848
1849                ActionLogResult::TipState {
1850                    shape: tip_shape,
1851                    measured_signals,
1852                    bounds_info: Some(bounds_info),
1853                }
1854            }
1855            ActionResult::StableSignal(stable) => {
1856                // Convert stable signal with full dataset for debugging stability measures
1857                ActionLogResult::StableSignal {
1858                    stable_value: stable.stable_value,
1859                    data_points_used: stable.data_points_used,
1860                    analysis_duration_ms: stable.analysis_duration.as_millis() as u64,
1861                    stability_metrics: stable.stability_metrics.clone(),
1862                    raw_data: stable.raw_data.clone(), // Full TCP dataset for debugging
1863                }
1864            }
1865        }
1866    }
1867
1868    /// Convert ExperimentData to ActionLogResult for comprehensive logging
1869    pub fn from_experiment_data(exp_data: &crate::types::ExperimentData) -> Self {
1870        let action_result = Box::new(Self::from_action_result(&exp_data.action_result));
1871
1872        let signal_frames: Vec<LoggableTimestampedSignalFrame> = exp_data
1873            .signal_frames
1874            .iter()
1875            .map(|frame| LoggableTimestampedSignalFrame {
1876                signal_frame: LoggableSignalFrame {
1877                    counter: frame.signal_frame.counter,
1878                    data: frame.signal_frame.data.clone(),
1879                },
1880                timestamp_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate current time
1881                relative_time_ms: frame.relative_time.as_millis() as u64,
1882            })
1883            .collect();
1884
1885        let tcp_config = LoggableTCPLoggerConfig {
1886            stream_port: exp_data.tcp_config.stream_port,
1887            channels: exp_data.tcp_config.channels.clone(),
1888            oversampling: exp_data.tcp_config.oversampling,
1889            auto_start: exp_data.tcp_config.auto_start,
1890            buffer_size: exp_data.tcp_config.buffer_size,
1891        };
1892
1893        ActionLogResult::ExperimentData {
1894            action_result,
1895            signal_frames,
1896            tcp_config,
1897            action_start_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate timing
1898            action_end_ms: chrono::Utc::now().timestamp_millis() as u64,
1899            total_duration_ms: exp_data.total_duration.as_millis() as u64,
1900        }
1901    }
1902
1903    /// Convert ChainExperimentData to ActionLogResult for comprehensive logging
1904    pub fn from_chain_experiment_data(chain_data: &crate::types::ChainExperimentData) -> Self {
1905        let action_results: Vec<ActionLogResult> = chain_data
1906            .action_results
1907            .iter()
1908            .map(Self::from_action_result)
1909            .collect();
1910
1911        let signal_frames: Vec<LoggableTimestampedSignalFrame> = chain_data
1912            .signal_frames
1913            .iter()
1914            .map(|frame| LoggableTimestampedSignalFrame {
1915                signal_frame: LoggableSignalFrame {
1916                    counter: frame.signal_frame.counter,
1917                    data: frame.signal_frame.data.clone(),
1918                },
1919                timestamp_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate current time
1920                relative_time_ms: frame.relative_time.as_millis() as u64,
1921            })
1922            .collect();
1923
1924        let tcp_config = LoggableTCPLoggerConfig {
1925            stream_port: chain_data.tcp_config.stream_port,
1926            channels: chain_data.tcp_config.channels.clone(),
1927            oversampling: chain_data.tcp_config.oversampling,
1928            auto_start: chain_data.tcp_config.auto_start,
1929            buffer_size: chain_data.tcp_config.buffer_size,
1930        };
1931
1932        let action_timings: Vec<(u64, u64)> = chain_data
1933            .action_timings
1934            .iter()
1935            .map(|(_, _)| {
1936                let now = chrono::Utc::now().timestamp_millis() as u64;
1937                (now, now) // Approximate timing
1938            })
1939            .collect();
1940
1941        ActionLogResult::ChainExperimentData {
1942            action_results,
1943            signal_frames,
1944            tcp_config,
1945            action_timings,
1946            chain_start_ms: chrono::Utc::now().timestamp_millis() as u64, // Approximate timing
1947            chain_end_ms: chrono::Utc::now().timestamp_millis() as u64,
1948            total_duration_ms: chain_data.total_duration.as_millis() as u64,
1949        }
1950    }
1951}