rusty_tip/
actions.rs

1use crate::{
2    types::{
3        DataToGet, MovementMode, OsciData, Position, Position3D, ScanAction, SignalIndex,
4        TriggerConfig,
5    },
6    MotorDirection, TipShaperConfig,
7};
8use std::time::Duration;
9
10/// Enhanced Action enum representing all possible SPM operations
11/// Properly separates motor (step-based) and piezo (continuous) movements
12#[derive(Debug, Clone)]
13pub enum Action {
14    /// Read single signal value
15    ReadSignal {
16        signal: SignalIndex,
17        wait_for_newest: bool,
18    },
19
20    /// Read multiple signal values
21    ReadSignals {
22        signals: Vec<SignalIndex>,
23        wait_for_newest: bool,
24    },
25
26    /// Read all available signal names
27    ReadSignalNames,
28
29    /// Read current bias voltage
30    ReadBias,
31
32    /// Set bias voltage to specific value
33    SetBias { voltage: f32 },
34
35    // Osci functions
36    ReadOsci {
37        signal: SignalIndex,
38        trigger: Option<TriggerConfig>,
39        data_to_get: DataToGet,
40        is_stable: Option<fn(&[f64]) -> bool>,
41    },
42
43    /// Read current piezo position (continuous coordinates)
44    ReadPiezoPosition { wait_for_newest_data: bool },
45
46    /// Set piezo position (absolute)
47    SetPiezoPosition {
48        position: Position,
49        wait_until_finished: bool,
50    },
51
52    /// Move piezo position (relative to current)
53    MovePiezoRelative { delta: Position },
54
55    // === Coarse Positioning Operations (Motor) ===
56    /// Move motor in steps (discrete positioning)
57    MoveMotor {
58        direction: MotorDirection,
59        steps: u16,
60    },
61
62    /// Move motor using closed-loop to target position
63    MoveMotorClosedLoop {
64        target: Position3D,
65        mode: MovementMode,
66    },
67
68    /// Stop all motor movement
69    StopMotor,
70
71    // === Control Operations ===
72    /// Perform auto-approach with timeout
73    AutoApproach {
74        wait_until_finished: bool,
75        timeout: Duration,
76    },
77
78    /// Withdraw tip with timeout
79    Withdraw {
80        wait_until_finished: bool,
81        timeout: Duration,
82    },
83
84    /// Set Z-controller setpoint
85    SetZSetpoint { setpoint: f32 },
86
87    // === Scan Operations ===
88    /// Control scan operations
89    ScanControl { action: ScanAction },
90
91    /// Read scan status
92    ReadScanStatus,
93
94    // === Advanced Operations ===
95    /// Execute bias pulse with parameters
96    BiasPulse {
97        wait_until_done: bool,
98        pulse_width: Duration,
99        bias_value_v: f32,
100        z_controller_hold: u16,
101        pulse_mode: u16,
102    },
103
104    /// Full tip shaper control with all parameters
105    TipShaper {
106        config: TipShaperConfig,
107        wait_until_finished: bool,
108        timeout: Duration,
109    },
110
111    /// Simple pulse-retract with predefined safe values
112    PulseRetract {
113        pulse_width: Duration,
114        pulse_height_v: f32,
115    },
116
117    /// Wait for a specific duration
118    Wait { duration: Duration },
119
120    // === Data Management ===
121    /// Store result value with key for later retrieval
122    Store { key: String, action: Box<Action> },
123
124    /// Retrieve previously stored value
125    Retrieve { key: String },
126}
127
128/// Simplified ActionResult with clear semantic separation
129#[derive(Debug, Clone)]
130pub enum ActionResult {
131    /// Single numeric value (signals, bias, etc.)
132    Value(f64),
133
134    /// Multiple numeric values (signal arrays)
135    Values(Vec<f64>),
136
137    /// String data (signal names, error messages, etc.)
138    Text(Vec<String>),
139
140    /// Boolean status (scanning/idle, running/stopped, etc.)
141    Status(bool),
142
143    /// Position data (meaningful x,y structure)
144    Position(Position),
145
146    /// Complex oscilloscope data (timing + data + metadata)
147    OsciData(OsciData),
148
149    /// Operation completed successfully (no data returned)
150    Success,
151
152    /// No result/waiting state
153    None,
154}
155
156impl ActionResult {
157    /// Convert to f64 if possible (for numerical results)
158    pub fn as_f64(&self) -> Option<f64> {
159        match self {
160            ActionResult::Value(v) => Some(*v),
161            ActionResult::Values(values) => {
162                if values.len() == 1 {
163                    Some(values[0])
164                } else {
165                    None
166                }
167            }
168            _ => None,
169        }
170    }
171
172    /// Convert to bool if possible (for status results)
173    pub fn as_bool(&self) -> Option<bool> {
174        match self {
175            ActionResult::Status(b) => Some(*b),
176            _ => None,
177        }
178    }
179
180    /// Convert to Position if possible
181    pub fn as_position(&self) -> Option<Position> {
182        match self {
183            ActionResult::Position(pos) => Some(*pos),
184            _ => None,
185        }
186    }
187
188    /// Convert to OsciData if possible
189    pub fn as_osci_data(&self) -> Option<&OsciData> {
190        match self {
191            ActionResult::OsciData(data) => Some(data),
192            _ => None,
193        }
194    }
195
196    // === Action-Aware Type Extractors ===
197    // These methods validate that the result type matches what the action should produce
198
199    /// Extract OsciData with action validation (panics on type mismatch)
200    pub fn expect_osci_data(self, action: &Action) -> OsciData {
201        match (action, self) {
202            (Action::ReadOsci { .. }, ActionResult::OsciData(data)) => data,
203            (action, result) => panic!(
204                "Expected OsciData from action {:?}, got {:?}",
205                action, result
206            ),
207        }
208    }
209
210    /// Extract signal value with action validation (panics on type mismatch)
211    pub fn expect_signal_value(self, action: &Action) -> f64 {
212        match (action, self) {
213            (Action::ReadSignal { .. }, ActionResult::Value(v)) => v,
214            (Action::ReadSignal { .. }, ActionResult::Values(mut vs)) if vs.len() == 1 => {
215                vs.pop().unwrap()
216            }
217            (Action::ReadBias, ActionResult::Value(v)) => v,
218            (action, result) => panic!(
219                "Expected signal value from action {:?}, got {:?}",
220                action, result
221            ),
222        }
223    }
224
225    /// Extract multiple values with action validation (panics on type mismatch)
226    pub fn expect_values(self, action: &Action) -> Vec<f64> {
227        match (action, self) {
228            (Action::ReadSignals { .. }, ActionResult::Values(values)) => values,
229            (Action::ReadSignal { .. }, ActionResult::Value(v)) => vec![v],
230            (action, result) => {
231                panic!("Expected values from action {:?}, got {:?}", action, result)
232            }
233        }
234    }
235
236    /// Extract position with action validation (panics on type mismatch)
237    pub fn expect_position(self, action: &Action) -> Position {
238        match (action, self) {
239            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => pos,
240            (action, result) => panic!(
241                "Expected position from action {:?}, got {:?}",
242                action, result
243            ),
244        }
245    }
246
247    /// Extract bias voltage with action validation (panics on type mismatch)
248    pub fn expect_bias_voltage(self, action: &Action) -> f32 {
249        match (action, self) {
250            (Action::ReadBias, ActionResult::Value(v)) => v as f32,
251            (action, result) => panic!(
252                "Expected bias voltage from action {:?}, got {:?}",
253                action, result
254            ),
255        }
256    }
257
258    /// Extract signal names with action validation (panics on type mismatch)
259    pub fn expect_signal_names(self, action: &Action) -> Vec<String> {
260        match (action, self) {
261            (Action::ReadSignalNames, ActionResult::Text(names)) => names,
262            (action, result) => panic!(
263                "Expected signal names from action {:?}, got {:?}",
264                action, result
265            ),
266        }
267    }
268
269    /// Extract status with action validation (panics on type mismatch)
270    pub fn expect_status(self, action: &Action) -> bool {
271        match (action, self) {
272            (Action::ReadScanStatus, ActionResult::Status(status)) => status,
273            (action, result) => {
274                panic!("Expected status from action {:?}, got {:?}", action, result)
275            }
276        }
277    }
278
279    // === Safe Extraction Methods (non-panicking) ===
280
281    /// Try to extract OsciData with action validation
282    pub fn try_into_osci_data(self, action: &Action) -> Result<OsciData, String> {
283        match (action, self) {
284            (Action::ReadOsci { .. }, ActionResult::OsciData(data)) => Ok(data),
285            (action, result) => Err(format!(
286                "Expected OsciData from action {:?}, got {:?}",
287                action, result
288            )),
289        }
290    }
291
292    /// Try to extract signal value with action validation
293    pub fn try_into_signal_value(self, action: &Action) -> Result<f64, String> {
294        match (action, self) {
295            (Action::ReadSignal { .. }, ActionResult::Value(v)) => Ok(v),
296            (Action::ReadSignal { .. }, ActionResult::Values(mut vs)) if vs.len() == 1 => {
297                Ok(vs.pop().unwrap())
298            }
299            (Action::ReadBias, ActionResult::Value(v)) => Ok(v),
300            (action, result) => Err(format!(
301                "Expected signal value from action {:?}, got {:?}",
302                action, result
303            )),
304        }
305    }
306
307    /// Try to extract position with action validation
308    pub fn try_into_position(self, action: &Action) -> Result<Position, String> {
309        match (action, self) {
310            (Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => Ok(pos),
311            (action, result) => Err(format!(
312                "Expected position from action {:?}, got {:?}",
313                action, result
314            )),
315        }
316    }
317
318    /// Try to extract status with action validation
319    pub fn try_into_status(self, action: &Action) -> Result<bool, String> {
320        match (action, self) {
321            (Action::ReadScanStatus, ActionResult::Status(status)) => Ok(status),
322            (action, result) => Err(format!(
323                "Expected status from action {:?}, got {:?}",
324                action, result
325            )),
326        }
327    }
328}
329
330// === Trait for Generic Type Extraction ===
331
332/// Trait for extracting specific types from ActionResult with action validation
333pub trait ExpectFromAction<T> {
334    fn expect_from_action(self, action: &Action) -> T;
335}
336
337impl ExpectFromAction<OsciData> for ActionResult {
338    fn expect_from_action(self, action: &Action) -> OsciData {
339        self.expect_osci_data(action)
340    }
341}
342
343impl ExpectFromAction<f64> for ActionResult {
344    fn expect_from_action(self, action: &Action) -> f64 {
345        self.expect_signal_value(action)
346    }
347}
348
349impl ExpectFromAction<Vec<f64>> for ActionResult {
350    fn expect_from_action(self, action: &Action) -> Vec<f64> {
351        self.expect_values(action)
352    }
353}
354
355impl ExpectFromAction<Position> for ActionResult {
356    fn expect_from_action(self, action: &Action) -> Position {
357        self.expect_position(action)
358    }
359}
360
361impl ExpectFromAction<f32> for ActionResult {
362    fn expect_from_action(self, action: &Action) -> f32 {
363        self.expect_bias_voltage(action)
364    }
365}
366
367impl ExpectFromAction<Vec<String>> for ActionResult {
368    fn expect_from_action(self, action: &Action) -> Vec<String> {
369        self.expect_signal_names(action)
370    }
371}
372
373impl ExpectFromAction<bool> for ActionResult {
374    fn expect_from_action(self, action: &Action) -> bool {
375        self.expect_status(action)
376    }
377}
378
379// === Action Categorization ===
380
381impl Action {
382    /// Check if this is a positioning action
383    pub fn is_positioning_action(&self) -> bool {
384        matches!(
385            self,
386            Action::SetPiezoPosition { .. }
387                | Action::MovePiezoRelative { .. }
388                | Action::MoveMotor { .. }
389                | Action::MoveMotorClosedLoop { .. }
390        )
391    }
392
393    /// Check if this is a read-only action
394    pub fn is_read_action(&self) -> bool {
395        matches!(
396            self,
397            Action::ReadSignal { .. }
398                | Action::ReadSignals { .. }
399                | Action::ReadSignalNames
400                | Action::ReadBias
401                | Action::ReadPiezoPosition { .. }
402                | Action::ReadScanStatus
403                | Action::Retrieve { .. }
404        )
405    }
406
407    /// Check if this is a control action
408    pub fn is_control_action(&self) -> bool {
409        matches!(
410            self,
411            Action::AutoApproach { .. }
412                | Action::Withdraw { .. }
413                | Action::ScanControl { .. }
414                | Action::StopMotor
415        )
416    }
417
418    /// Check if this action modifies bias voltage
419    pub fn modifies_bias(&self) -> bool {
420        matches!(self, Action::SetBias { .. } | Action::BiasPulse { .. })
421    }
422
423    /// Check if this action involves motor movement
424    pub fn involves_motor(&self) -> bool {
425        matches!(
426            self,
427            Action::MoveMotor { .. } | Action::MoveMotorClosedLoop { .. } | Action::StopMotor
428        )
429    }
430
431    /// Check if this action involves piezo movement
432    pub fn involves_piezo(&self) -> bool {
433        matches!(
434            self,
435            Action::SetPiezoPosition { .. }
436                | Action::MovePiezoRelative { .. }
437                | Action::ReadPiezoPosition { .. }
438        )
439    }
440
441    /// Get a human-readable description of the action
442    pub fn description(&self) -> String {
443        match self {
444            Action::ReadSignal { signal, .. } => {
445                format!("Read signal {}", signal.0)
446            }
447            Action::ReadSignals { signals, .. } => {
448                let indices: Vec<i32> = signals.iter().map(|s| s.0).collect();
449                format!("Read signals: {:?}", indices)
450            }
451            Action::SetBias { voltage } => {
452                format!("Set bias to {:.3}V", voltage)
453            }
454            Action::SetPiezoPosition { position, .. } => {
455                format!(
456                    "Set piezo position to ({:.3e}, {:.3e})",
457                    position.x, position.y
458                )
459            }
460            Action::MoveMotor { direction, steps } => {
461                format!("Move motor {:?} {} steps", direction, steps)
462            }
463            Action::AutoApproach {
464                wait_until_finished,
465                timeout,
466            } => format!(
467                "Auto approach blocking: {wait_until_finished}, timeout: {:?}",
468                timeout
469            ),
470            Action::Withdraw { timeout, .. } => {
471                format!("Withdraw tip (timeout: {}ms)", timeout.as_micros())
472            }
473            Action::SetZSetpoint { setpoint } => {
474                format!("Set Z setpoint: {:.3e}", setpoint)
475            }
476            Action::Wait { duration } => {
477                format!("Wait {:.1}s", duration.as_secs_f64())
478            }
479            Action::BiasPulse {
480                wait_until_done: _,
481                pulse_width,
482                bias_value_v,
483                z_controller_hold: _,
484                pulse_mode: _,
485            } => {
486                format!("Bias pulse {:.3}V for {:?}ms", bias_value_v, pulse_width)
487            }
488            Action::TipShaper {
489                config,
490                wait_until_finished,
491                timeout,
492            } => {
493                format!(
494                    "Tip shaper: bias {:.1}V, lift {:.0}nm, times {:.1?}s/{:.1?}s (wait: {}, timeout: {:?}ms)",
495                    config.bias_v,
496                    config.tip_lift_m * 1e9,
497                    config.lift_time_1.as_secs_f32(),
498                    config.lift_time_2.as_secs_f32(),
499                    wait_until_finished,
500                    timeout
501                )
502            }
503            Action::PulseRetract {
504                pulse_width,
505                pulse_height_v,
506            } => {
507                format!(
508                    "Pulse retract {:.1}V for {:.0?}ms",
509                    pulse_height_v, pulse_width
510                )
511            }
512            Action::ReadOsci {
513                signal,
514                trigger,
515                data_to_get,
516                is_stable,
517            } => {
518                let trigger_desc = match trigger {
519                    Some(config) => format!("trigger: {:?}", config.mode),
520                    None => "no trigger config".to_string(),
521                };
522                let stability_desc = match is_stable {
523                    Some(_) => " with custom stability",
524                    None => "",
525                };
526                format!(
527                    "Read oscilloscope signal {} with {} (mode: {:?}){}",
528                    signal.0, trigger_desc, data_to_get, stability_desc
529                )
530            }
531            _ => format!("{:?}", self),
532        }
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_action_result_extraction() {
542        let bias_result = ActionResult::Value(2.5);
543        assert_eq!(bias_result.as_f64(), Some(2.5));
544
545        let position_result = ActionResult::Position(Position { x: 1e-9, y: 2e-9 });
546        assert_eq!(
547            position_result.as_position(),
548            Some(Position { x: 1e-9, y: 2e-9 })
549        );
550    }
551}
552
553/// A sequence of actions with simple Vec<Action> foundation
554#[derive(Debug, Clone)]
555pub struct ActionChain {
556    actions: Vec<Action>,
557    name: Option<String>,
558}
559
560impl ActionChain {
561    /// Create a new ActionChain from a vector of actions
562    pub fn new(actions: Vec<Action>) -> Self {
563        Self {
564            actions,
565            name: None,
566        }
567    }
568
569    /// Create a new ActionChain from any iterator of actions
570    pub fn from_actions(actions: impl IntoIterator<Item = Action>) -> Self {
571        Self::new(actions.into_iter().collect())
572    }
573
574    /// Create a new ActionChain with a name
575    pub fn named(actions: Vec<Action>, name: impl Into<String>) -> Self {
576        Self {
577            actions,
578            name: Some(name.into()),
579        }
580    }
581
582    /// Create an empty ActionChain
583    pub fn empty() -> Self {
584        Self::new(vec![])
585    }
586
587    // === Direct Vec<Action> Access ===
588
589    /// Get immutable reference to actions
590    pub fn actions(&self) -> &[Action] {
591        &self.actions
592    }
593
594    /// Get mutable reference to actions vector for direct manipulation
595    pub fn actions_mut(&mut self) -> &mut Vec<Action> {
596        &mut self.actions
597    }
598
599    /// Add an action to the end of the chain
600    pub fn push(&mut self, action: Action) {
601        self.actions.push(action);
602    }
603
604    /// Add multiple actions to the end of the chain
605    pub fn extend(&mut self, actions: impl IntoIterator<Item = Action>) {
606        self.actions.extend(actions);
607    }
608
609    /// Insert an action at a specific index
610    pub fn insert(&mut self, index: usize, action: Action) {
611        self.actions.insert(index, action);
612    }
613
614    /// Remove and return the action at index
615    pub fn remove(&mut self, index: usize) -> Action {
616        self.actions.remove(index)
617    }
618
619    /// Remove the last action and return it
620    pub fn pop(&mut self) -> Option<Action> {
621        self.actions.pop()
622    }
623
624    /// Clear all actions
625    pub fn clear(&mut self) {
626        self.actions.clear();
627    }
628
629    /// Create a new chain by appending another chain
630    pub fn chain_with(mut self, other: ActionChain) -> Self {
631        self.actions.extend(other.actions);
632        self
633    }
634
635    /// Get an iterator over actions
636    pub fn iter(&self) -> std::slice::Iter<'_, Action> {
637        self.actions.iter()
638    }
639
640    /// Get a mutable iterator over actions
641    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Action> {
642        self.actions.iter_mut()
643    }
644
645    // === Metadata Access ===
646
647    /// Get the name of this chain
648    pub fn name(&self) -> Option<&str> {
649        self.name.as_deref()
650    }
651
652    /// Set the name of this chain
653    pub fn set_name(&mut self, name: impl Into<String>) {
654        self.name = Some(name.into());
655    }
656
657    /// Get the number of actions in this chain
658    pub fn len(&self) -> usize {
659        self.actions.len()
660    }
661
662    /// Check if this chain is empty
663    pub fn is_empty(&self) -> bool {
664        self.actions.is_empty()
665    }
666
667    // === Analysis Methods ===
668
669    /// Get actions that match a specific category
670    pub fn positioning_actions(&self) -> Vec<&Action> {
671        self.actions
672            .iter()
673            .filter(|a| a.is_positioning_action())
674            .collect()
675    }
676
677    pub fn read_actions(&self) -> Vec<&Action> {
678        self.actions.iter().filter(|a| a.is_read_action()).collect()
679    }
680
681    pub fn control_actions(&self) -> Vec<&Action> {
682        self.actions
683            .iter()
684            .filter(|a| a.is_control_action())
685            .collect()
686    }
687
688    /// Check if chain contains any motor movements
689    pub fn involves_motor(&self) -> bool {
690        self.actions.iter().any(|a| a.involves_motor())
691    }
692
693    /// Check if chain contains any piezo movements
694    pub fn involves_piezo(&self) -> bool {
695        self.actions.iter().any(|a| a.involves_piezo())
696    }
697
698    /// Check if chain contains any bias modifications
699    pub fn modifies_bias(&self) -> bool {
700        self.actions.iter().any(|a| a.modifies_bias())
701    }
702
703    /// Get a summary description of the chain
704    pub fn summary(&self) -> String {
705        if let Some(name) = &self.name {
706            format!("{} ({} actions)", name, self.len())
707        } else {
708            format!("Action chain with {} actions", self.len())
709        }
710    }
711
712    /// Get detailed analysis of the chain
713    pub fn analysis(&self) -> ChainAnalysis {
714        ChainAnalysis {
715            total_actions: self.len(),
716            positioning_actions: self.positioning_actions().len(),
717            read_actions: self.read_actions().len(),
718            control_actions: self.control_actions().len(),
719            involves_motor: self.involves_motor(),
720            involves_piezo: self.involves_piezo(),
721            modifies_bias: self.modifies_bias(),
722        }
723    }
724}
725
726/// Analysis result for an ActionChain
727#[derive(Debug, Clone)]
728pub struct ChainAnalysis {
729    pub total_actions: usize,
730    pub positioning_actions: usize,
731    pub read_actions: usize,
732    pub control_actions: usize,
733    pub involves_motor: bool,
734    pub involves_piezo: bool,
735    pub modifies_bias: bool,
736}
737
738// === Iterator Support ===
739
740impl IntoIterator for ActionChain {
741    type Item = Action;
742    type IntoIter = std::vec::IntoIter<Action>;
743
744    fn into_iter(self) -> Self::IntoIter {
745        self.actions.into_iter()
746    }
747}
748
749impl<'a> IntoIterator for &'a ActionChain {
750    type Item = &'a Action;
751    type IntoIter = std::slice::Iter<'a, Action>;
752
753    fn into_iter(self) -> Self::IntoIter {
754        self.actions.iter()
755    }
756}
757
758impl FromIterator<Action> for ActionChain {
759    fn from_iter<T: IntoIterator<Item = Action>>(iter: T) -> Self {
760        Self::from_actions(iter)
761    }
762}
763
764impl From<Vec<Action>> for ActionChain {
765    fn from(actions: Vec<Action>) -> Self {
766        Self::new(actions)
767    }
768}
769
770// ==================== Pre-built Common Patterns ====================
771
772impl ActionChain {
773    /// Comprehensive system status check
774    pub fn system_status_check() -> Self {
775        ActionChain::named(
776            vec![
777                Action::ReadSignalNames,
778                Action::ReadBias,
779                Action::ReadPiezoPosition {
780                    wait_for_newest_data: true,
781                },
782            ],
783            "System status check",
784        )
785    }
786
787    /// Safe tip approach with verification
788    pub fn safe_tip_approach() -> Self {
789        ActionChain::named(
790            vec![
791                Action::ReadPiezoPosition {
792                    wait_for_newest_data: true,
793                },
794                Action::AutoApproach {
795                    wait_until_finished: true,
796                    timeout: Duration::from_secs(300),
797                },
798                Action::Wait {
799                    duration: Duration::from_millis(500),
800                },
801                Action::ReadSignal {
802                    signal: SignalIndex(24),
803                    wait_for_newest: true,
804                }, // Typical bias voltage
805                Action::ReadSignal {
806                    signal: SignalIndex(0),
807                    wait_for_newest: true,
808                }, // Typical current
809            ],
810            "Safe tip approach",
811        )
812    }
813
814    /// Move to position and approach
815    pub fn move_and_approach(target: Position) -> Self {
816        ActionChain::named(
817            vec![
818                Action::SetPiezoPosition {
819                    position: target,
820                    wait_until_finished: true,
821                },
822                Action::Wait {
823                    duration: Duration::from_millis(100),
824                },
825                Action::AutoApproach {
826                    wait_until_finished: true,
827                    timeout: Duration::from_secs(300),
828                },
829                Action::ReadSignal {
830                    signal: SignalIndex(24),
831                    wait_for_newest: true,
832                },
833            ],
834            format!("Move to ({:.1e}, {:.1e}) and approach", target.x, target.y),
835        )
836    }
837
838    /// Bias pulse sequence with restoration
839    pub fn bias_pulse_sequence(voltage: f32, duration_ms: u32) -> Self {
840        ActionChain::named(
841            vec![
842                Action::ReadBias,
843                Action::SetBias { voltage },
844                Action::Wait {
845                    duration: Duration::from_millis(50),
846                },
847                Action::Wait {
848                    duration: Duration::from_millis(duration_ms as u64),
849                },
850                Action::SetBias { voltage: 0.0 },
851            ],
852            format!("Bias pulse {:.3}V for {}ms", voltage, duration_ms),
853        )
854    }
855
856    /// Survey multiple positions
857    pub fn position_survey(positions: Vec<Position>) -> Self {
858        let position_count = positions.len(); // Store length before moving
859        let mut actions = Vec::new();
860
861        for pos in positions {
862            actions.extend([
863                Action::SetPiezoPosition {
864                    position: pos,
865                    wait_until_finished: true,
866                },
867                Action::Wait {
868                    duration: Duration::from_millis(100),
869                },
870                Action::AutoApproach {
871                    wait_until_finished: true,
872                    timeout: Duration::from_secs(300),
873                },
874                Action::ReadSignal {
875                    signal: SignalIndex(24),
876                    wait_for_newest: true,
877                }, // Bias voltage
878                Action::ReadSignal {
879                    signal: SignalIndex(0),
880                    wait_for_newest: true,
881                }, // Current
882                Action::Withdraw {
883                    wait_until_finished: true,
884                    timeout: Duration::from_secs(5),
885                },
886            ]);
887        }
888
889        ActionChain::named(
890            actions,
891            format!("Position survey ({} points)", position_count),
892        )
893    }
894
895    /// Complete tip recovery sequence
896    pub fn tip_recovery_sequence() -> Self {
897        ActionChain::named(
898            vec![
899                Action::Withdraw {
900                    wait_until_finished: true,
901                    timeout: Duration::from_secs(5),
902                },
903                Action::MovePiezoRelative {
904                    delta: Position { x: 3e-9, y: 3e-9 },
905                },
906                Action::Wait {
907                    duration: Duration::from_millis(200),
908                },
909                Action::AutoApproach {
910                    wait_until_finished: true,
911                    timeout: Duration::from_secs(300),
912                },
913                Action::ReadSignal {
914                    signal: SignalIndex(24),
915                    wait_for_newest: true,
916                },
917            ],
918            "Tip recovery sequence",
919        )
920    }
921}
922
923#[cfg(test)]
924mod chain_tests {
925    use super::*;
926    use crate::types::MotorDirection;
927
928    #[test]
929    fn test_vec_foundation() {
930        // Test direct Vec<Action> usage
931        let mut chain = ActionChain::new(vec![Action::ReadBias, Action::SetBias { voltage: 1.0 }]);
932
933        assert_eq!(chain.len(), 2);
934
935        // Test Vec operations
936        chain.push(Action::AutoApproach {
937            wait_until_finished: true,
938            timeout: Duration::from_secs(300),
939        });
940        assert_eq!(chain.len(), 3);
941
942        let action = chain.pop().unwrap();
943        assert!(matches!(
944            action,
945            Action::AutoApproach {
946                wait_until_finished: true,
947                timeout: _
948            }
949        ));
950        assert_eq!(chain.len(), 2);
951
952        // Test extension
953        chain.extend([
954            Action::Wait {
955                duration: Duration::from_millis(100),
956            },
957            Action::ReadBias,
958        ]);
959        assert_eq!(chain.len(), 4);
960    }
961
962    #[test]
963    fn test_simple_construction() {
964        let chain = ActionChain::named(
965            vec![
966                Action::ReadBias,
967                Action::SetBias { voltage: 1.0 },
968                Action::Wait {
969                    duration: Duration::from_millis(100),
970                },
971                Action::AutoApproach {
972                    wait_until_finished: true,
973                    timeout: Duration::from_secs(300),
974                },
975            ],
976            "Test chain",
977        );
978
979        assert_eq!(chain.name(), Some("Test chain"));
980        assert_eq!(chain.len(), 4);
981
982        let analysis = chain.analysis();
983        assert_eq!(analysis.total_actions, 4);
984        assert_eq!(analysis.read_actions, 1);
985        assert_eq!(analysis.control_actions, 1);
986        assert!(analysis.modifies_bias);
987    }
988
989    #[test]
990    fn test_programmatic_generation() {
991        // Test building chains programmatically
992        let mut chain = ActionChain::empty();
993
994        for _ in 0..3 {
995            chain.push(Action::MoveMotor {
996                direction: MotorDirection::XPlus,
997                steps: 10,
998            });
999            chain.push(Action::Wait {
1000                duration: Duration::from_millis(100),
1001            });
1002        }
1003
1004        assert_eq!(chain.len(), 6);
1005        assert!(chain.involves_motor());
1006
1007        // Test iterator construction
1008        let actions: Vec<Action> = (0..5).map(|_| Action::ReadBias).collect();
1009
1010        let iter_chain: ActionChain = actions.into_iter().collect();
1011        assert_eq!(iter_chain.len(), 5);
1012    }
1013
1014    #[test]
1015    fn test_pre_built_patterns() {
1016        let status_check = ActionChain::system_status_check();
1017        assert!(status_check.name().is_some());
1018        assert!(!status_check.is_empty());
1019
1020        let approach = ActionChain::safe_tip_approach();
1021        assert!(!approach.control_actions().is_empty());
1022
1023        let positions = vec![Position { x: 1e-9, y: 1e-9 }, Position { x: 2e-9, y: 2e-9 }];
1024        let survey = ActionChain::position_survey(positions);
1025        assert_eq!(survey.len(), 12); // 6 actions per position × 2 positions
1026    }
1027
1028    #[test]
1029    fn test_chain_analysis() {
1030        let chain = ActionChain::new(vec![
1031            Action::MoveMotor {
1032                direction: MotorDirection::XPlus,
1033                steps: 100,
1034            },
1035            Action::SetPiezoPosition {
1036                position: Position { x: 1e-9, y: 1e-9 },
1037                wait_until_finished: true,
1038            },
1039            Action::ReadBias,
1040            Action::AutoApproach {
1041                wait_until_finished: true,
1042                timeout: Duration::from_secs(1),
1043            },
1044            Action::SetBias { voltage: 1.5 },
1045        ]);
1046
1047        let analysis = chain.analysis();
1048        assert_eq!(analysis.total_actions, 5);
1049        assert_eq!(analysis.positioning_actions, 2);
1050        assert_eq!(analysis.read_actions, 1);
1051        assert_eq!(analysis.control_actions, 1);
1052        assert!(analysis.involves_motor);
1053        assert!(analysis.involves_piezo);
1054        assert!(analysis.modifies_bias);
1055    }
1056
1057    #[test]
1058    fn test_iteration() {
1059        let chain = ActionChain::new(vec![
1060            Action::ReadBias,
1061            Action::AutoApproach {
1062                wait_until_finished: true,
1063                timeout: Duration::from_secs(1),
1064            },
1065            Action::Wait {
1066                duration: Duration::from_millis(100),
1067            },
1068        ]);
1069
1070        // Test iterator
1071        let mut count = 0;
1072        for _ in &chain {
1073            count += 1;
1074            // Can access action here
1075        }
1076        assert_eq!(count, 3);
1077
1078        // Test into_iter
1079        let actions: Vec<Action> = chain.into_iter().collect();
1080        assert_eq!(actions.len(), 3);
1081    }
1082
1083    #[test]
1084    fn test_from_vec_action() {
1085        // Test From<Vec<Action>> trait
1086        let actions = vec![
1087            Action::ReadBias,
1088            Action::SetBias { voltage: 1.5 },
1089            Action::AutoApproach {
1090                wait_until_finished: true,
1091                timeout: Duration::from_secs(1),
1092            },
1093        ];
1094
1095        let chain: ActionChain = actions.into();
1096        assert_eq!(chain.len(), 3);
1097        assert!(chain.name().is_none());
1098
1099        // Test that it's usable with Into<ActionChain> parameters
1100        let vec_actions = vec![
1101            Action::ReadBias,
1102            Action::Wait {
1103                duration: Duration::from_millis(50),
1104            },
1105        ];
1106
1107        // This should compile thanks to Into<ActionChain>
1108        fn accepts_into_action_chain(_chain: impl Into<ActionChain>) {
1109            // This function would be called by execute methods
1110        }
1111
1112        accepts_into_action_chain(vec_actions);
1113    }
1114}