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#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
10pub enum TipCheckMethod {
11 SignalBounds { signal: Signal, bounds: (f32, f32) },
13 MultiSignalBounds { signals: Vec<(Signal, (f32, f32))> },
15}
16
17#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19pub enum SignalStabilityMethod {
20 StandardDeviation { threshold: f32 },
22 RelativeStandardDeviation { threshold_percent: f32 },
24 MovingWindow {
26 window_size: usize,
27 max_variation: f32,
28 },
29 TrendAnalysis { max_slope: f32 },
32 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 } }
43}
44
45#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
47pub enum TipStabilityMethod {
48 ExtendedMonitoring {
50 signal: Signal,
51 duration: Duration,
52 sampling_interval: Duration,
53 stability_threshold: f32,
54 },
55 BiasSweepResponse {
62 signal: Signal,
64 bias_range: (f32, f32),
66 bias_steps: u16,
68 step_duration: Duration,
70 allowed_signal_change: f32,
73 },
74}
75
76#[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, }
86
87#[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))>, pub all_passed: bool,
93}
94
95#[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>>, pub analysis_duration: Duration,
102 pub metrics: HashMap<String, f32>, pub potential_damage_detected: bool,
104 pub recommendations: Vec<String>,
105}
106
107#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
109pub struct TipState {
110 pub shape: TipShape, pub measured_signals: HashMap<SignalIndex, f32>, pub metadata: HashMap<String, String>,
113}
114
115#[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#[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#[derive(Debug, Clone)]
136pub enum Action {
137 ReadSignal {
139 signal: Signal,
140 wait_for_newest: bool,
141 },
142
143 ReadSignals {
145 signals: Vec<Signal>,
146 wait_for_newest: bool,
147 },
148
149 ReadSignalNames,
151
152 ReadBias,
154
155 SetBias { voltage: f32 },
157
158 ReadOsci {
160 signal: Signal,
161 trigger: Option<TriggerConfig>,
162 data_to_get: DataToGet,
163 is_stable: Option<fn(&[f64]) -> bool>,
164 },
165
166 ReadPiezoPosition { wait_for_newest_data: bool },
168
169 SetPiezoPosition {
171 position: Position,
172 wait_until_finished: bool,
173 },
174
175 MovePiezoRelative { delta: Position },
177
178 MoveMotorAxis {
181 direction: MotorDirection,
182 steps: u16,
183 blocking: bool,
184 },
185
186 MoveMotor3D {
188 displacement: MotorDisplacement,
189 blocking: bool,
190 },
191
192 MoveMotorClosedLoop {
194 target: Position3D,
195 mode: MovementMode,
196 },
197
198 StopMotor,
200
201 AutoApproach {
204 wait_until_finished: bool,
205 timeout: Duration,
206 center_freq_shift: bool,
208 },
209
210 Withdraw {
212 wait_until_finished: bool,
213 timeout: Duration,
214 },
215
216 SafeReposition { x_steps: i16, y_steps: i16 },
218
219 SetZSetpoint { setpoint: f32 },
221
222 ScanControl { action: ScanAction },
225
226 ReadScanStatus,
228
229 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 TipShaper {
241 config: TipShaperConfig,
242 wait_until_finished: bool,
243 timeout: Duration,
244 },
245
246 PulseRetract {
248 pulse_width: Duration,
249 pulse_height_v: f32,
250 },
251
252 Wait { duration: Duration },
254
255 Store { key: String, action: Box<Action> },
258
259 Retrieve { key: String },
261
262 StartTCPLogger,
265
266 StopTCPLogger,
268
269 GetTCPLoggerStatus,
271
272 ConfigureTCPLogger {
274 channels: Vec<i32>,
275 oversampling: i32,
276 },
277
278 CheckTipState { method: TipCheckMethod },
281
282 CheckTipStability {
285 method: TipStabilityMethod,
286 max_duration: Duration,
287 },
288
289 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 ReachedTargedAmplitude,
301}
302
303#[derive(Debug, Clone)]
305pub enum ActionResult {
306 Value(f64),
308
309 Values(Vec<f64>),
311
312 Text(Vec<String>),
314
315 Status(bool),
317
318 Position(Position),
320
321 OsciData(OsciData),
323
324 Success,
326
327 TCPReaderStatus(TCPReaderStatus),
329
330 TipState(TipState),
332
333 StabilityResult(StabilityResult),
335
336 StableSignal(StableSignal),
338
339 None,
341}
342
343impl ActionResult {
344 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 pub fn as_bool(&self) -> Option<bool> {
361 match self {
362 ActionResult::Status(b) => Some(*b),
363 _ => None,
364 }
365 }
366
367 pub fn as_position(&self) -> Option<Position> {
369 match self {
370 ActionResult::Position(pos) => Some(*pos),
371 _ => None,
372 }
373 }
374
375 pub fn as_osci_data(&self) -> Option<&OsciData> {
377 match self {
378 ActionResult::OsciData(data) => Some(data),
379 _ => None,
380 }
381 }
382
383 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 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 pub fn as_stability_result(&self) -> Option<&StabilityResult> {
401 match self {
402 ActionResult::StabilityResult(result) => Some(result),
403 _ => None,
404 }
405 }
406
407 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 pub fn as_stable_signal(&self) -> Option<&StableSignal> {
417 match self {
418 ActionResult::StableSignal(stable) => Some(stable),
419 _ => None,
420 }
421 }
422
423 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
661pub 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
719impl Action {
722 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 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 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 pub fn modifies_bias(&self) -> bool {
762 matches!(self, Action::SetBias { .. } | Action::BiasPulse { .. })
763 }
764
765 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 pub fn involves_piezo(&self) -> bool {
779 matches!(
780 self,
781 Action::SetPiezoPosition { .. }
782 | Action::MovePiezoRelative { .. }
783 | Action::ReadPiezoPosition { .. }
784 )
785 }
786
787 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#[derive(Debug, Clone)]
1026pub struct ActionChain {
1027 actions: Vec<Action>,
1028 name: Option<String>,
1029}
1030
1031impl ActionChain {
1032 pub fn new(actions: Vec<Action>) -> Self {
1034 Self {
1035 actions,
1036 name: None,
1037 }
1038 }
1039
1040 pub fn from_actions(actions: impl IntoIterator<Item = Action>) -> Self {
1042 Self::new(actions.into_iter().collect())
1043 }
1044
1045 pub fn named(actions: Vec<Action>, name: impl Into<String>) -> Self {
1047 Self {
1048 actions,
1049 name: Some(name.into()),
1050 }
1051 }
1052
1053 pub fn empty() -> Self {
1055 Self::new(vec![])
1056 }
1057
1058 pub fn actions(&self) -> &[Action] {
1062 &self.actions
1063 }
1064
1065 pub fn actions_mut(&mut self) -> &mut Vec<Action> {
1067 &mut self.actions
1068 }
1069
1070 pub fn push(&mut self, action: Action) {
1072 self.actions.push(action);
1073 }
1074
1075 pub fn extend(&mut self, actions: impl IntoIterator<Item = Action>) {
1077 self.actions.extend(actions);
1078 }
1079
1080 pub fn insert(&mut self, index: usize, action: Action) {
1082 self.actions.insert(index, action);
1083 }
1084
1085 pub fn remove(&mut self, index: usize) -> Action {
1087 self.actions.remove(index)
1088 }
1089
1090 pub fn pop(&mut self) -> Option<Action> {
1092 self.actions.pop()
1093 }
1094
1095 pub fn clear(&mut self) {
1097 self.actions.clear();
1098 }
1099
1100 pub fn chain_with(mut self, other: ActionChain) -> Self {
1102 self.actions.extend(other.actions);
1103 self
1104 }
1105
1106 pub fn iter(&self) -> std::slice::Iter<'_, Action> {
1108 self.actions.iter()
1109 }
1110
1111 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Action> {
1113 self.actions.iter_mut()
1114 }
1115
1116 pub fn name(&self) -> Option<&str> {
1120 self.name.as_deref()
1121 }
1122
1123 pub fn set_name(&mut self, name: impl Into<String>) {
1125 self.name = Some(name.into());
1126 }
1127
1128 pub fn len(&self) -> usize {
1130 self.actions.len()
1131 }
1132
1133 pub fn is_empty(&self) -> bool {
1135 self.actions.is_empty()
1136 }
1137
1138 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 pub fn involves_motor(&self) -> bool {
1161 self.actions.iter().any(|a| a.involves_motor())
1162 }
1163
1164 pub fn involves_piezo(&self) -> bool {
1166 self.actions.iter().any(|a| a.involves_piezo())
1167 }
1168
1169 pub fn modifies_bias(&self) -> bool {
1171 self.actions.iter().any(|a| a.modifies_bias())
1172 }
1173
1174 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 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#[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
1209impl 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
1241impl ActionChain {
1244 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 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 }, Action::ReadSignal {
1278 signal: Signal::new("Current".to_string(), 0, None).unwrap(),
1279 wait_for_newest: true,
1280 }, ],
1282 "Safe tip approach",
1283 )
1284 }
1285
1286 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 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 pub fn position_survey(positions: Vec<Position>) -> Self {
1331 let position_count = positions.len(); 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 }, Action::ReadSignal {
1353 signal: Signal::new("Current".to_string(), 0, None).unwrap(),
1354 wait_for_newest: true,
1355 }, 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 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 let mut chain = ActionChain::new(vec![Action::ReadBias, Action::SetBias { voltage: 1.0 }]);
1407
1408 assert_eq!(chain.len(), 2);
1409
1410 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 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 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 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); }
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 let mut count = 0;
1554 for _ in &chain {
1555 count += 1;
1556 }
1558 assert_eq!(count, 3);
1559
1560 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 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 let vec_actions = vec![
1584 Action::ReadBias,
1585 Action::Wait {
1586 duration: Duration::from_millis(50),
1587 },
1588 ];
1589
1590 fn accepts_into_action_chain(_chain: impl Into<ActionChain>) {
1592 }
1594
1595 accepts_into_action_chain(vec_actions);
1596 }
1597}
1598
1599#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1603pub struct ActionLogEntry {
1604 pub action: String, pub result: ActionLogResult,
1608 pub start_time: chrono::DateTime<chrono::Utc>,
1610 pub duration_ms: u64,
1612 pub metadata: Option<std::collections::HashMap<String, String>>,
1614}
1615
1616#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1619pub enum ActionLogResult {
1620 Value(f64),
1622 Values(Vec<f64>),
1624 Text(Vec<String>),
1626 Status(bool),
1628 Position { x: f64, y: f64 },
1630 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 ExperimentData {
1642 action_result: Box<ActionLogResult>,
1643 signal_frames: Vec<LoggableTimestampedSignalFrame>,
1644 tcp_config: LoggableTCPLoggerConfig,
1645 action_start_ms: u64, action_end_ms: u64,
1647 total_duration_ms: u64,
1648 },
1649 ChainExperimentData {
1651 action_results: Vec<ActionLogResult>,
1652 signal_frames: Vec<LoggableTimestampedSignalFrame>,
1653 tcp_config: LoggableTCPLoggerConfig,
1654 action_timings: Vec<(u64, u64)>, chain_start_ms: u64,
1656 chain_end_ms: u64,
1657 total_duration_ms: u64,
1658 },
1659 TCPLoggerStatus {
1661 status: String, channels: Vec<i32>,
1663 oversampling: i32,
1664 },
1665 TipState {
1667 shape: TipShape,
1668 measured_signals: std::collections::HashMap<u8, f32>, bounds_info: Option<std::collections::HashMap<String, String>>, },
1671 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>, },
1679 Success,
1681 None,
1683 Error(String),
1685}
1686
1687#[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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1699pub struct LoggableTimestampedSignalFrame {
1700 pub signal_frame: LoggableSignalFrame,
1701 pub timestamp_ms: u64, pub relative_time_ms: u64, }
1704
1705#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1707pub struct LoggableSignalFrame {
1708 pub counter: u64,
1709 pub data: Vec<f32>,
1710}
1711
1712#[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 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 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 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 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), channels: tcp_status.channels.clone(),
1802 oversampling: tcp_status.oversampling,
1803 }
1804 }
1805 ActionResult::TipState(tip_state) => {
1806 let measured_signals = tip_state
1808 .measured_signals
1809 .iter()
1810 .map(|(signal_idx, value)| (signal_idx.get(), *value))
1811 .collect();
1812
1813 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 let tip_shape = if result.is_stable {
1829 TipShape::Stable
1830 } else {
1831 TipShape::Blunt
1832 };
1833
1834 let measured_signals = result
1836 .measured_values
1837 .iter()
1838 .flat_map(|(signal_idx, values)| {
1839 values.last().map(|&value| (signal_idx.index, value))
1841 })
1842 .collect();
1843
1844 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 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(), }
1864 }
1865 }
1866 }
1867
1868 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, 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, 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 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, 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) })
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, 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}