Skip to main content

rusty_tip/
action_driver.rs

1use std::{
2    collections::HashMap,
3    sync::{
4        atomic::{AtomicBool, Ordering},
5        Arc,
6    },
7    thread,
8    time::{Duration, Instant},
9};
10
11use log::{debug, info, warn};
12use nanonis_rs::signals::SignalIndex;
13use ndarray::Array1;
14
15use crate::{
16    actions::{
17        Action, ActionChain, ActionLogEntry, ActionLogResult, ActionResult, ExpectFromAction,
18    },
19    buffered_tcp_reader::BufferedTCPReader,
20    controller_types::TipStateConfig,
21    signal_registry::SignalRegistry,
22    types::{DataToGet, OsciData, SignalStats, TriggerConfig},
23    utils::{poll_until, poll_with_timeout, PollError},
24    MotorGroup, NanonisClient, NanonisError, Position, PulseMode, ScanAction, ScanDirection,
25    Signal, TipShaperConfig, ZControllerHold,
26};
27
28/// Configuration for TCP Logger integration with always-buffer support
29#[derive(Debug, Clone)]
30pub struct TCPReaderConfig {
31    /// TCP data stream port (typically 6590)
32    pub stream_port: u16,
33    /// Signal channel indices to record (0-23)
34    pub channels: Vec<i32>,
35    /// Oversampling rate multiplier (0-1000)
36    pub oversampling: i32,
37    /// Whether to start logging automatically on connection
38    pub auto_start: bool,
39    /// Buffer size for always-buffer mode (None = no buffering)
40    /// When Some(size), BufferedTCPReader starts automatically
41    pub buffer_size: Option<usize>,
42}
43
44impl Default for TCPReaderConfig {
45    fn default() -> Self {
46        Self {
47            stream_port: 6590,
48            channels: (0..=23).collect(),
49            oversampling: 20,
50            auto_start: true,
51            buffer_size: Some(10_000),
52        }
53    }
54}
55
56/// Unified input type for run() method - accepts single actions or chains
57#[derive(Debug, Clone)]
58pub enum ActionRequest {
59    /// Single action
60    Single(Action),
61    /// Multiple actions as chain
62    Chain(Vec<Action>),
63}
64
65impl From<Action> for ActionRequest {
66    fn from(action: Action) -> Self {
67        ActionRequest::Single(action)
68    }
69}
70
71impl From<Vec<Action>> for ActionRequest {
72    fn from(actions: Vec<Action>) -> Self {
73        ActionRequest::Chain(actions)
74    }
75}
76
77impl From<ActionChain> for ActionRequest {
78    fn from(chain: ActionChain) -> Self {
79        ActionRequest::Chain(chain.into_iter().collect())
80    }
81}
82
83impl ActionRequest {
84    pub fn is_single(&self) -> bool {
85        matches!(self, ActionRequest::Single(_))
86    }
87
88    pub fn is_chain(&self) -> bool {
89        matches!(self, ActionRequest::Chain(_))
90    }
91}
92
93/// Configuration for execution behavior in the unified run() method
94#[derive(Debug, Clone)]
95pub struct ExecutionConfig {
96    /// Enable data collection with pre/post durations
97    pub data_collection: Option<(Duration, Duration)>,
98    /// Chain execution behavior
99    pub chain_behavior: ChainBehavior,
100    /// Logging behavior
101    pub logging_behavior: LoggingBehavior,
102    /// Performance optimizations
103    pub performance_mode: PerformanceMode,
104}
105
106#[derive(Debug, Clone)]
107pub enum ChainBehavior {
108    /// Execute all actions, return all results (default)
109    Complete,
110    /// Execute all actions, return only final result
111    FinalOnly,
112    /// Execute until error, return partial results
113    Partial,
114}
115
116#[derive(Debug, Clone)]
117pub enum LoggingBehavior {
118    /// Normal logging (default)
119    Normal,
120    /// No per-action logging, single chain log
121    Deferred,
122    /// Disable logging completely for this execution
123    Disabled,
124}
125
126#[derive(Debug, Clone)]
127pub enum PerformanceMode {
128    /// Normal execution (default)
129    Normal,
130    /// Optimized for timing-critical operations
131    Fast,
132}
133
134impl Default for ExecutionConfig {
135    fn default() -> Self {
136        Self {
137            data_collection: None,
138            chain_behavior: ChainBehavior::Complete,
139            logging_behavior: LoggingBehavior::Normal,
140            performance_mode: PerformanceMode::Normal,
141        }
142    }
143}
144
145impl ExecutionConfig {
146    /// Create new config with default settings
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// Enable data collection with specified pre/post durations
152    pub fn with_data_collection(mut self, pre_duration: Duration, post_duration: Duration) -> Self {
153        self.data_collection = Some((pre_duration, post_duration));
154        self
155    }
156
157    /// Set chain to return only final result
158    pub fn final_only(mut self) -> Self {
159        self.chain_behavior = ChainBehavior::FinalOnly;
160        self
161    }
162
163    /// Set chain to allow partial execution on error
164    pub fn partial(mut self) -> Self {
165        self.chain_behavior = ChainBehavior::Partial;
166        self
167    }
168
169    /// Use deferred logging (single chain entry instead of per-action)
170    pub fn deferred_logging(mut self) -> Self {
171        self.logging_behavior = LoggingBehavior::Deferred;
172        self
173    }
174
175    /// Disable logging for this execution
176    pub fn no_logging(mut self) -> Self {
177        self.logging_behavior = LoggingBehavior::Disabled;
178        self
179    }
180
181    /// Enable fast performance mode
182    pub fn fast_mode(mut self) -> Self {
183        self.performance_mode = PerformanceMode::Fast;
184        self
185    }
186}
187
188/// Result container for unified run() method
189#[derive(Debug)]
190pub enum ExecutionResult {
191    /// Single action result
192    Single(ActionResult),
193    /// Multiple action results
194    Chain(Vec<ActionResult>),
195    /// Experiment data with signal collection
196    ExperimentData(crate::types::ExperimentData),
197    /// Chain experiment data with signal collection
198    ChainExperimentData(crate::types::ChainExperimentData),
199    /// Partial chain results (on error)
200    Partial(Vec<ActionResult>, NanonisError),
201}
202
203impl ExecutionResult {
204    /// Extract single result or error if not single
205    pub fn into_single(self) -> Result<ActionResult, NanonisError> {
206        match self {
207            ExecutionResult::Single(result) => Ok(result),
208            ExecutionResult::Chain(mut results) if results.len() == 1 => Ok(results.pop().unwrap()),
209            _ => Err(NanonisError::Protocol("Expected single result".to_string())),
210        }
211    }
212
213    /// Extract chain results or error if not chain
214    pub fn into_chain(self) -> Result<Vec<ActionResult>, NanonisError> {
215        match self {
216            ExecutionResult::Chain(results) => Ok(results),
217            ExecutionResult::Single(result) => Ok(vec![result]),
218            ExecutionResult::Partial(results, _) => Ok(results),
219            _ => Err(NanonisError::Protocol("Expected chain results".to_string())),
220        }
221    }
222
223    /// Extract experiment data or error if not experiment
224    pub fn into_experiment_data(self) -> Result<crate::types::ExperimentData, NanonisError> {
225        match self {
226            ExecutionResult::ExperimentData(data) => Ok(data),
227            _ => Err(NanonisError::Protocol(
228                "Expected experiment data".to_string(),
229            )),
230        }
231    }
232
233    /// Extract chain experiment data or error if not chain experiment
234    pub fn into_chain_experiment_data(
235        self,
236    ) -> Result<crate::types::ChainExperimentData, NanonisError> {
237        match self {
238            ExecutionResult::ChainExperimentData(data) => Ok(data),
239            _ => Err(NanonisError::Protocol(
240                "Expected chain experiment data".to_string(),
241            )),
242        }
243    }
244
245    /// Type-safe extraction with action validation
246    pub fn expecting<T>(self) -> Result<T, NanonisError>
247    where
248        Self: ExpectFromExecution<T>,
249    {
250        self.expect_from_execution()
251    }
252}
253
254/// Trait for type-safe extraction from ExecutionResult
255pub trait ExpectFromExecution<T> {
256    fn expect_from_execution(self) -> Result<T, NanonisError>;
257}
258
259/// Builder for fluent configuration of execution
260pub struct ExecutionBuilder<'a> {
261    driver: &'a mut ActionDriver,
262    request: ActionRequest,
263    config: ExecutionConfig,
264}
265
266impl<'a> ExecutionBuilder<'a> {
267    fn new(driver: &'a mut ActionDriver, request: ActionRequest) -> Self {
268        Self {
269            driver,
270            request,
271            config: ExecutionConfig::default(),
272        }
273    }
274
275    /// Enable data collection with specified durations
276    pub fn with_data_collection(mut self, pre_duration: Duration, post_duration: Duration) -> Self {
277        self.config = self
278            .config
279            .with_data_collection(pre_duration, post_duration);
280        self
281    }
282
283    /// Return only final result for chains
284    pub fn final_only(mut self) -> Self {
285        self.config = self.config.final_only();
286        self
287    }
288
289    /// Allow partial execution on error
290    pub fn partial(mut self) -> Self {
291        self.config = self.config.partial();
292        self
293    }
294
295    /// Use deferred logging
296    pub fn deferred_logging(mut self) -> Self {
297        self.config = self.config.deferred_logging();
298        self
299    }
300
301    /// Disable logging for this execution
302    pub fn no_logging(mut self) -> Self {
303        self.config = self.config.no_logging();
304        self
305    }
306
307    /// Enable fast performance mode
308    pub fn fast_mode(mut self) -> Self {
309        self.config = self.config.fast_mode();
310        self
311    }
312
313    /// Execute with type-safe result extraction
314    pub fn expecting<T>(self) -> Result<T, NanonisError>
315    where
316        ExecutionResult: ExpectFromExecution<T>,
317    {
318        let result = self.driver.run_with_config(self.request, self.config)?;
319        result.expecting()
320    }
321
322    /// Execute and return ExecutionResult
323    pub fn execute(self) -> Result<ExecutionResult, NanonisError> {
324        self.driver.run_with_config(self.request, self.config)
325    }
326}
327
328impl<'a> ExecutionBuilder<'a> {
329    /// Convenience method for single actions - returns ActionResult directly
330    pub fn go(self) -> Result<ActionResult, NanonisError> {
331        match self.request {
332            ActionRequest::Single(_) => {
333                let result = self.driver.run_with_config(self.request, self.config)?;
334                result.into_single()
335            }
336            ActionRequest::Chain(_) => Err(NanonisError::Protocol(
337                "Use .execute() for chains, .go() is only for single actions".to_string(),
338            )),
339        }
340    }
341}
342
343/// Builder for configuring ActionDriver with optional parameters
344#[derive(Debug, Clone)]
345pub struct ActionDriverBuilder {
346    addr: String,
347    port: u16,
348    connection_timeout: Option<Duration>,
349    initial_storage: HashMap<String, ActionResult>,
350    tcp_reader_config: Option<TCPReaderConfig>,
351    action_logger_config: Option<(std::path::PathBuf, usize, bool)>, // (file_path, buffer_size, final_format_json)
352    custom_tcp_mapping: Option<Vec<(u8, u8)>>, // Custom Nanonis to TCP channel mapping
353    shutdown_flag: Option<Arc<AtomicBool>>,    // Graceful shutdown support
354    tip_state_config: TipStateConfig,
355}
356
357impl ActionDriverBuilder {
358    /// Create a new builder with required connection parameters
359    pub fn new(addr: &str, port: u16) -> Self {
360        Self {
361            addr: addr.to_string(),
362            port,
363            connection_timeout: None,
364            initial_storage: HashMap::new(),
365            tcp_reader_config: None,
366            action_logger_config: None,
367            custom_tcp_mapping: None,
368            shutdown_flag: None,
369            tip_state_config: TipStateConfig::default(),
370        }
371    }
372
373    /// Set connection timeout for the underlying NanonisClient
374    pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
375        self.connection_timeout = Some(timeout);
376        self
377    }
378
379    /// Initialize with pre-stored values
380    pub fn with_initial_storage(mut self, storage: HashMap<String, ActionResult>) -> Self {
381        self.initial_storage = storage;
382        self
383    }
384
385    /// Add a single pre-stored value
386    pub fn with_stored_value(mut self, key: String, value: ActionResult) -> Self {
387        self.initial_storage.insert(key, value);
388        self
389    }
390
391    /// Configure TCP Logger with always-buffer mode (recommended)
392    /// This automatically starts BufferedTCPReader when ActionDriver is built
393    ///
394    /// # Arguments
395    /// * `config` - TCP logger configuration with buffer_size set
396    ///
397    /// # Usage
398    /// ```rust,ignore
399    /// let driver = ActionDriver::builder("127.0.0.1", 6501)
400    ///     .with_tcp_reader(TCPReaderConfig {
401    ///         stream_port: 6590,
402    ///         channels: vec![0, 8],
403    ///         oversampling: 100,
404    ///         auto_start: true,
405    ///         buffer_size: Some(10_000),
406    ///     })
407    ///     .build()?;
408    /// // Buffering is now active and ready for immediate data queries
409    /// ```
410    pub fn with_tcp_reader(mut self, config: TCPReaderConfig) -> Self {
411        if config.buffer_size.is_none() {
412            log::warn!("TCPLoggerConfig buffer_size is None - buffering disabled");
413        }
414        self.tcp_reader_config = Some(config);
415        self
416    }
417
418    /// Configure action logging with buffered file output
419    ///
420    /// # Arguments
421    /// * `file_path` - Base path where action logs will be written (extension added automatically)
422    /// * `buffer_size` - Number of actions to buffer before auto-flushing to file
423    /// * `final_format_json` - If true, convert to JSON array on final flush; if false, keep JSONL format
424    ///
425    /// # File Extensions
426    /// File extensions are added automatically based on the final format:
427    /// - `final_format_json = false` → `.jsonl` extension (efficient streaming)
428    /// - `final_format_json = true` → `.json` extension (post-analysis friendly)
429    ///
430    /// # Usage
431    /// ```rust,ignore
432    /// // JSONL format (efficient, streaming) → experiment_actions.jsonl
433    /// let driver = ActionDriver::builder("127.0.0.1", 6501)
434    ///     .with_action_logging("experiment_actions", 100, false)
435    ///     .build()?;
436    ///
437    /// // JSON format (better for post-analysis) → experiment_data.json
438    /// let driver = ActionDriver::builder("127.0.0.1", 6501)
439    ///     .with_action_logging("experiment_data", 100, true)
440    ///     .build()?;
441    /// ```
442    pub fn with_action_logging(
443        mut self,
444        file_path: impl Into<std::path::PathBuf>,
445        buffer_size: usize,
446        final_format_json: bool,
447    ) -> Self {
448        self.action_logger_config = Some((file_path.into(), buffer_size, final_format_json));
449        self
450    }
451
452    /// Provide custom Nanonis to TCP channel mapping
453    ///
454    /// Override the default hardcoded mappings with your own. This is useful when
455    /// your Nanonis configuration has different signal indices.
456    ///
457    /// # Arguments
458    /// * `mapping` - Array of (nanonis_index, tcp_channel) tuples
459    ///
460    /// # Example
461    /// ```rust,ignore
462    /// let custom_map = [
463    ///     (76, 18),  // Frequency shift
464    ///     (0, 0),    // Current
465    ///     (24, 8),   // Bias
466    /// ];
467    ///
468    /// let driver = ActionDriver::builder("127.0.0.1", 6501)
469    ///     .with_custom_tcp_mapping(&custom_map)
470    ///     .build()?;
471    /// ```
472    pub fn with_custom_tcp_mapping(mut self, mapping: &[(u8, u8)]) -> Self {
473        self.custom_tcp_mapping = Some(mapping.to_vec());
474        self
475    }
476
477    /// Set shutdown flag for graceful termination of long-running operations
478    ///
479    /// When set, operations like stability checks will periodically check this flag
480    /// and return early with `NanonisError::Protocol("Shutdown requested".to_string())` if it becomes true.
481    pub fn with_shutdown_flag(mut self, flag: Arc<AtomicBool>) -> Self {
482        self.shutdown_flag = Some(flag);
483        self
484    }
485
486    /// Configure tip state checking parameters
487    pub fn with_tip_state_config(mut self, config: TipStateConfig) -> Self {
488        self.tip_state_config = config;
489        self
490    }
491
492    /// Build the ActionDriver with configured parameters and optional automatic buffering
493    pub fn build(self) -> Result<ActionDriver, NanonisError> {
494        let mut client = {
495            let mut builder = NanonisClient::builder().address(&self.addr).port(self.port);
496
497            if let Some(timeout) = self.connection_timeout {
498                builder = builder.connect_timeout(timeout);
499            }
500
501            builder.build()?
502        };
503
504        let tcp_reader = if let Some(ref config) = self.tcp_reader_config {
505            if let Some(buffer_size) = config.buffer_size {
506                // 1. Configure TCP logger settings first
507                client.tcplog_chs_set(config.channels.clone())?;
508                client.tcplog_oversampl_set(config.oversampling)?;
509
510                // 2. Connect TCP stream BEFORE starting logger (critical sequence!)
511                let reader = crate::buffered_tcp_reader::BufferedTCPReader::new(
512                    "127.0.0.1",
513                    config.stream_port,
514                    buffer_size,
515                    config.channels.len() as u32,
516                    config.oversampling as f32,
517                )?;
518                log::debug!(
519                    "TCP stream connected, buffer capacity: {} frames",
520                    buffer_size
521                );
522
523                // 3. NOW start TCP logger (data flows to connected reader)
524                if config.auto_start {
525                    // Reset TCP logger state first to ensure clean start
526                    log::debug!("Stopping TCP logger to ensure clean state");
527                    let _ = client.tcplog_stop(); // Ignore errors - might not be running
528                    std::thread::sleep(std::time::Duration::from_millis(200)); // Give it time to stop
529
530                    // Now start TCP logger
531                    client.tcplog_start()?;
532                    log::debug!("TCP logger started, data collection active");
533                }
534
535                Some(reader)
536            } else {
537                None
538            }
539        } else {
540            None
541        };
542
543        // Create action logger if configured
544        let action_logger =
545            if let Some((file_path, buffer_size, final_format_json)) = self.action_logger_config {
546                Some(crate::logger::Logger::new(
547                    file_path,
548                    buffer_size,
549                    final_format_json,
550                ))
551            } else {
552                None
553            };
554
555        // Auto-initialize signal registry with custom or hardcoded mapping
556        let signal_names = client.signal_names_get()?;
557        let signal_registry = if let Some(ref custom_map) = self.custom_tcp_mapping {
558            log::debug!(
559                "Using custom TCP channel mapping with {} entries",
560                custom_map.len()
561            );
562            SignalRegistry::builder()
563                .with_standard_map()
564                .add_tcp_map(custom_map)
565                .from_signal_names(&signal_names)
566                .create_aliases()
567                .build()
568        } else {
569            SignalRegistry::with_hardcoded_tcp_mapping(&signal_names)
570        };
571
572        Ok(ActionDriver {
573            client,
574            stored_values: self.initial_storage,
575            tcp_reader_config: self.tcp_reader_config,
576            tcp_reader,
577            action_logger,
578            action_logging_enabled: true, // Default to enabled if logger is configured
579            signal_registry,
580            recent_stable_signals: std::collections::VecDeque::new(),
581            shutdown_flag: self.shutdown_flag,
582            tip_state_config: self.tip_state_config,
583        })
584    }
585}
586
587/// Direct 1:1 translation layer between Actions and NanonisClient calls
588/// Now with integrated always-buffer TCP data collection capability
589pub struct ActionDriver {
590    /// Nanonis control client for sending commands
591    client: NanonisClient,
592    /// Storage for Store/Retrieve actions
593    stored_values: HashMap<String, ActionResult>,
594    /// TCP Logger configuration for data collection
595    tcp_reader_config: Option<TCPReaderConfig>,
596    /// Buffered TCP reader for always-buffer mode (automatically started if configured)
597    tcp_reader: Option<crate::buffered_tcp_reader::BufferedTCPReader>,
598    /// Action logger for execution tracking
599    action_logger: Option<crate::logger::Logger<crate::actions::ActionLogEntry>>,
600    /// Enable/disable action logging at runtime
601    action_logging_enabled: bool,
602    /// Signal registry for name-based lookup and TCP mapping
603    signal_registry: SignalRegistry,
604    /// Recent ReadStableSignal results for correlation with CheckTipState
605    recent_stable_signals:
606        std::collections::VecDeque<(crate::actions::StableSignal, std::time::Instant)>,
607    /// Shutdown flag for graceful termination of long-running operations
608    shutdown_flag: Option<Arc<AtomicBool>>,
609    /// Configuration for tip state checking
610    tip_state_config: TipStateConfig,
611}
612
613impl ActionDriver {
614    /// Create a builder for configuring ActionDriver
615    pub fn builder(addr: &str, port: u16) -> ActionDriverBuilder {
616        ActionDriverBuilder::new(addr, port)
617    }
618
619    /// Create a new ActionDriver with default configuration (backward compatibility)
620    pub fn new(addr: &str, port: u16) -> Result<Self, NanonisError> {
621        Self::builder(addr, port).build()
622    }
623
624    /// Convenience method to create with existing NanonisClient (backward compatibility)
625    pub fn with_nanonis_client(mut client: NanonisClient) -> Self {
626        // Initialize signal registry even for this convenience method
627        let signal_names = client.signal_names_get().unwrap_or_default();
628        let signal_registry = SignalRegistry::with_hardcoded_tcp_mapping(&signal_names);
629
630        Self {
631            client,
632            stored_values: HashMap::new(),
633            tcp_reader_config: None,
634            tcp_reader: None,
635            action_logger: None,
636            action_logging_enabled: false,
637            signal_registry,
638            recent_stable_signals: std::collections::VecDeque::new(),
639            shutdown_flag: None,
640            tip_state_config: TipStateConfig::default(),
641        }
642    }
643
644    /// Get a reference to the underlying NanonisClient
645    pub fn client(&self) -> &NanonisClient {
646        &self.client
647    }
648
649    /// Get a mutable reference to the underlying NanonisClient
650    pub fn client_mut(&mut self) -> &mut NanonisClient {
651        &mut self.client
652    }
653
654    /// Set shutdown flag for graceful termination of long-running operations
655    pub fn set_shutdown_flag(&mut self, flag: Arc<AtomicBool>) {
656        self.shutdown_flag = Some(flag);
657    }
658
659    /// Check if shutdown has been requested
660    fn is_shutdown_requested(&self) -> bool {
661        self.shutdown_flag
662            .as_ref()
663            .map(|f| f.load(Ordering::SeqCst))
664            .unwrap_or(false)
665    }
666
667    /// Execute an auto-approach operation.
668    ///
669    /// If `wait_until_finished` is true, blocks until approach completes or timeout.
670    /// If false, starts the approach and returns immediately.
671    pub fn auto_approach(
672        &mut self,
673        wait_until_finished: bool,
674        timeout: Duration,
675    ) -> Result<(), NanonisError> {
676        // Check if already running
677        match self.client.auto_approach_on_off_get() {
678            Ok(true) => {
679                log::warn!("Auto-approach already running");
680                return Ok(());
681            }
682            Ok(false) => {
683                log::debug!("Auto-approach is idle, proceeding to start");
684            }
685            Err(_) => {
686                log::warn!("Auto-approach status unknown, attempting to proceed");
687            }
688        }
689
690        // Open auto-approach module
691        match self.client.auto_approach_open() {
692            Ok(_) => log::debug!("Opened the auto-approach module"),
693            Err(_) => {
694                log::debug!("Failed to open auto-approach module, already open")
695            }
696        }
697
698        // Wait for module initialization
699        std::thread::sleep(std::time::Duration::from_millis(500));
700
701        // Start auto-approach
702        if let Err(e) = self.client.auto_approach_on_off_set(true) {
703            log::error!("Failed to start auto-approach: {}", e);
704            return Err(NanonisError::Protocol(format!(
705                "Failed to start auto-approach: {}",
706                e
707            )));
708        }
709
710        if !wait_until_finished {
711            log::debug!("Auto-approach started, not waiting for completion");
712            return Ok(());
713        }
714
715        // Wait for completion with timeout
716        log::debug!("Waiting for auto-approach to complete...");
717        let poll_interval = std::time::Duration::from_millis(100);
718
719        match poll_until(
720            || {
721                self.client
722                    .auto_approach_on_off_get()
723                    .map(|running| !running)
724            },
725            timeout,
726            poll_interval,
727        ) {
728            Ok(()) => {
729                log::debug!("Auto-approach completed successfully");
730                Ok(())
731            }
732            Err(PollError::Timeout) => {
733                log::warn!("Auto-approach timed out after {:?}", timeout);
734                let _ = self.client.auto_approach_on_off_set(false);
735                Err(NanonisError::Protocol(
736                    "Auto-approach timed out".to_string(),
737                ))
738            }
739            Err(PollError::ConditionError(e)) => {
740                log::error!("Error checking auto-approach status: {}", e);
741                Err(NanonisError::Protocol(format!("Status check error: {}", e)))
742            }
743        }
744    }
745
746    /// Center the frequency shift using the PLL auto-center function.
747    pub fn center_freq_shift(&mut self) -> Result<(), NanonisError> {
748        let modulator_index = 1;
749        log::debug!("Centering frequency shift");
750        self.client.pll_freq_shift_auto_center(modulator_index)
751    }
752
753    /// Get TCP Logger configuration if set
754    pub fn tcp_reader_config(&self) -> Option<&TCPReaderConfig> {
755        self.tcp_reader_config.as_ref()
756    }
757
758    /// Check if TCP logger is configured and available
759    pub fn has_tcp_reader(&self) -> bool {
760        self.tcp_reader.is_some()
761    }
762
763    pub fn tcp_reader_mut(&mut self) -> Option<&mut BufferedTCPReader> {
764        self.tcp_reader.as_mut()
765    }
766
767    /// Clear the TCP reader buffer
768    ///
769    /// This removes all buffered data, which is useful to discard stale values
770    /// before starting a new measurement or tip preparation sequence.
771    pub fn clear_tcp_buffer(&self) {
772        if let Some(ref tcp_reader) = self.tcp_reader {
773            tcp_reader.clear_buffer();
774            debug!("TCP reader buffer cleared");
775        } else {
776            warn!("No TCP reader available to clear");
777        }
778    }
779
780    /// Get reference to the signal registry
781    pub fn signal_registry(&self) -> &SignalRegistry {
782        &self.signal_registry
783    }
784
785    /// Calculate number of data points needed for a target duration
786    ///
787    /// Based on the TCP reader configuration (oversampling), calculates how many
788    /// samples are needed to cover the specified duration.
789    ///
790    /// # Arguments
791    /// * `target_duration` - Desired time window for data collection
792    ///
793    /// # Returns
794    /// Number of samples, or None if TCP reader is not configured
795    ///
796    /// # Example
797    /// For 500ms at 2000 Hz effective rate: returns 1000 samples
798    fn calculate_samples_for_duration(&self, target_duration: Duration) -> Option<usize> {
799        if let Some(ref config) = self.tcp_reader_config {
800            // Effective sample rate = base_rate / oversampling
801            // For oversampling=1 at 2kHz base: 2000 samples/sec
802            // For 500ms: 2000 * 0.5 = 1000 samples
803            let base_rate = 2000.0; // Typical Nanonis base rate in Hz
804            let effective_rate = base_rate / config.oversampling as f64;
805            let samples = (effective_rate * target_duration.as_secs_f64()).ceil() as usize;
806            log::debug!(
807                "Calculated {} samples for {:.0}ms (base: {}Hz, oversampling: {}, effective: {:.1}Hz)",
808                samples,
809                target_duration.as_millis(),
810                base_rate,
811                config.oversampling,
812                effective_rate
813            );
814            Some(samples.max(50)) // Minimum 50 samples
815        } else {
816            None
817        }
818    }
819
820    // ==================== Unified Execution API ====================
821
822    /// Unified execution method with fluent configuration
823    ///
824    /// # Usage
825    /// ```rust,ignore
826    /// // Simple execution
827    /// let result = driver.run(action)?;
828    /// let results = driver.run(actions)?;
829    ///
830    /// // With data collection
831    /// let data = driver.run(action).with_data_collection(pre, post).execute()?;
832    ///
833    /// // Type-safe extraction
834    /// let signal: f64 = driver.run(read_signal).expecting()?;
835    ///
836    /// // Performance modes
837    /// let results = driver.run(actions).deferred_logging().execute()?;
838    /// let final_result = driver.run(actions).final_only().execute()?;
839    /// ```
840    pub fn run<R>(&mut self, request: R) -> ExecutionBuilder<'_>
841    where
842        R: Into<ActionRequest>,
843    {
844        ExecutionBuilder::new(self, request.into())
845    }
846
847    /// Execute with explicit configuration (for advanced use)
848    pub fn run_with_config(
849        &mut self,
850        request: ActionRequest,
851        config: ExecutionConfig,
852    ) -> Result<ExecutionResult, NanonisError> {
853        match (&request, &config.data_collection) {
854            // Single action with data collection
855            (ActionRequest::Single(action), Some((pre_duration, post_duration))) => {
856                let experiment_data = self.execute_with_data_collection(
857                    action.clone(),
858                    *pre_duration,
859                    *post_duration,
860                )?;
861                Ok(ExecutionResult::ExperimentData(experiment_data))
862            }
863
864            // Chain with data collection
865            (ActionRequest::Chain(actions), Some((pre_duration, post_duration))) => {
866                let chain_experiment_data = self.execute_chain_with_data_collection(
867                    actions.clone(),
868                    *pre_duration,
869                    *post_duration,
870                )?;
871                Ok(ExecutionResult::ChainExperimentData(chain_experiment_data))
872            }
873
874            // Single action without data collection
875            (ActionRequest::Single(action), None) => {
876                let result = match config.logging_behavior {
877                    LoggingBehavior::Disabled => {
878                        let previous_state = self.set_action_logging_enabled(false);
879                        let result = self.execute(action.clone());
880                        self.set_action_logging_enabled(previous_state);
881                        result
882                    }
883                    _ => self.execute(action.clone()),
884                }?;
885                Ok(ExecutionResult::Single(result))
886            }
887
888            // Chain without data collection
889            (ActionRequest::Chain(actions), None) => {
890                let results = match (&config.chain_behavior, &config.logging_behavior) {
891                    (ChainBehavior::Complete, LoggingBehavior::Normal) => {
892                        self.execute_chain(actions.clone())?
893                    }
894                    (ChainBehavior::Complete, LoggingBehavior::Deferred) => {
895                        self.execute_chain_deferred(actions.clone())?
896                    }
897                    (ChainBehavior::Complete, LoggingBehavior::Disabled) => {
898                        let previous_state = self.set_action_logging_enabled(false);
899                        let result = self.execute_chain(actions.clone());
900                        self.set_action_logging_enabled(previous_state);
901                        result?
902                    }
903                    (ChainBehavior::FinalOnly, _) => {
904                        let results = match config.logging_behavior {
905                            LoggingBehavior::Deferred => {
906                                self.execute_chain_deferred(actions.clone())?
907                            }
908                            LoggingBehavior::Disabled => {
909                                let previous_state = self.set_action_logging_enabled(false);
910                                let result = self.execute_chain(actions.clone());
911                                self.set_action_logging_enabled(previous_state);
912                                result?
913                            }
914                            _ => self.execute_chain(actions.clone())?,
915                        };
916                        vec![results.into_iter().last().unwrap_or(ActionResult::None)]
917                    }
918                    (ChainBehavior::Partial, _) => {
919                        match self.execute_chain_partial(actions.clone()) {
920                            Ok(results) => results,
921                            Err((partial_results, error)) => {
922                                return Ok(ExecutionResult::Partial(partial_results, error));
923                            }
924                        }
925                    }
926                };
927
928                Ok(ExecutionResult::Chain(results))
929            }
930        }
931    }
932
933    // ==================== Always-Buffer TCP Data Collection Methods ====================
934
935    /// Get recent TCP signal data (always available if buffering enabled)
936    ///
937    /// # Arguments
938    /// * `duration` - How far back to collect data from current time
939    ///
940    /// # Returns
941    /// Vector of recent timestamped signal frames, empty if buffering not active
942    ///
943    /// # Usage
944    /// Perfect for real-time monitoring and checking recent signal trends without
945    /// needing to plan data collection in advance
946    pub fn get_recent_tcp_data(
947        &self,
948        duration: Duration,
949    ) -> Vec<crate::types::TimestampedSignalFrame> {
950        self.tcp_reader
951            .as_ref()
952            .map(|reader| reader.get_recent_data(duration))
953            .unwrap_or_default()
954    }
955
956    /// Execute action with time-windowed data collection
957    ///
958    /// This is the core method for synchronized data collection during SPM operations.
959    /// It captures data before, during, and after action execution using the always-buffer.
960    ///
961    /// # Arguments
962    /// * `action` - The SPM action to execute
963    /// * `pre_duration` - How much data to collect before action starts
964    /// * `post_duration` - How much data to collect after action ends
965    ///
966    /// # Returns
967    /// ExperimentData containing both action result and time-windowed signal data
968    ///
969    /// # Errors
970    /// Returns error if buffering is not active or action execution fails
971    pub fn execute_with_data_collection(
972        &mut self,
973        action: Action,
974        pre_duration: Duration,
975        post_duration: Duration,
976    ) -> Result<crate::types::ExperimentData, NanonisError> {
977        if self.tcp_reader.is_none() {
978            return Err(NanonisError::Protocol(
979                "TCP buffering not active".to_string(),
980            ));
981        }
982
983        let action_start = Instant::now();
984        let action_result = self.execute(action.clone())?;
985        let action_end = Instant::now();
986
987        std::thread::sleep(post_duration);
988
989        let window_start = action_start - pre_duration;
990        let window_end = action_end + post_duration;
991
992        let signal_frames = self
993            .tcp_reader
994            .as_ref()
995            .unwrap()
996            .get_data_between(window_start, window_end);
997        let tcp_config = self.tcp_reader_config.as_ref().unwrap().clone();
998
999        let experiment_data = crate::types::ExperimentData {
1000            action_result,
1001            signal_frames,
1002            tcp_config,
1003            action_start,
1004            action_end,
1005            total_duration: action_end.duration_since(action_start),
1006        };
1007
1008        // Log the complete experiment data if logging is enabled
1009        if self.action_logging_enabled && self.action_logger.is_some() {
1010            let log_entry = ActionLogEntry {
1011                action: format!("Data Collection: {}", action.description()),
1012                result: ActionLogResult::from_experiment_data(&experiment_data),
1013                start_time: chrono::Utc::now(),
1014                duration_ms: experiment_data.total_duration.as_millis() as u64,
1015                metadata: Some(
1016                    [
1017                        ("type".to_string(), "experiment_data_collection".to_string()),
1018                        (
1019                            "pre_duration_ms".to_string(),
1020                            pre_duration.as_millis().to_string(),
1021                        ),
1022                        (
1023                            "post_duration_ms".to_string(),
1024                            post_duration.as_millis().to_string(),
1025                        ),
1026                        (
1027                            "signal_frame_count".to_string(),
1028                            experiment_data.signal_frames.len().to_string(),
1029                        ),
1030                    ]
1031                    .into_iter()
1032                    .collect(),
1033                ),
1034            };
1035
1036            if let Err(log_error) = self.action_logger.as_mut().unwrap().add(log_entry) {
1037                log::warn!("Failed to log experiment data: {}", log_error);
1038            }
1039        }
1040
1041        Ok(experiment_data)
1042    }
1043
1044    /// Convenience method for bias pulse with data collection
1045    ///
1046    /// # Arguments
1047    /// * `pulse_voltage` - Bias voltage for the pulse (V)
1048    /// * `pulse_duration` - Duration of the pulse
1049    /// * `pre_duration` - Data collection before pulse
1050    /// * `post_duration` - Data collection after pulse
1051    ///
1052    /// # Returns
1053    /// ExperimentData with pulse results and synchronized signal data
1054    pub fn pulse_with_data_collection(
1055        &mut self,
1056        pulse_voltage: f32,
1057        pulse_duration: Duration,
1058        pre_duration: Duration,
1059        post_duration: Duration,
1060    ) -> Result<crate::types::ExperimentData, NanonisError> {
1061        self.execute_with_data_collection(
1062            Action::BiasPulse {
1063                wait_until_done: true,
1064                bias_value_v: pulse_voltage,
1065                pulse_width: pulse_duration,
1066                z_controller_hold: crate::types::ZControllerHold::Hold as u16,
1067                pulse_mode: crate::types::PulseMode::Absolute as u16,
1068            },
1069            pre_duration,
1070            post_duration,
1071        )
1072    }
1073
1074    /// Get current buffer statistics if buffering is active
1075    ///
1076    /// # Returns
1077    /// Optional tuple of (current_count, max_capacity, time_span) or None if no buffering
1078    ///
1079    /// # Usage
1080    /// Monitor buffer health, detect overruns, check data collection status
1081    pub fn tcp_buffer_stats(&self) -> Option<(usize, usize, Duration)> {
1082        self.tcp_reader.as_ref().map(|reader| reader.buffer_stats())
1083    }
1084
1085    /// Stop TCP buffering and return final buffer state
1086    ///
1087    /// # Returns
1088    /// Vector containing all buffered data, or empty if buffering wasn't active
1089    ///
1090    /// # Usage
1091    /// Optional manual cleanup - this happens automatically via Drop trait.
1092    /// Call this only if you need to access the final buffered data before ActionDriver is dropped.
1093    pub fn stop_tcp_buffering(
1094        &mut self,
1095    ) -> Result<Vec<crate::types::TimestampedSignalFrame>, NanonisError> {
1096        if let Some(mut reader) = self.tcp_reader.take() {
1097            let final_data = reader.get_all_data();
1098            reader.stop()?;
1099            log::info!(
1100                "Manually stopped TCP buffering, collected {} frames",
1101                final_data.len()
1102            );
1103            Ok(final_data)
1104        } else {
1105            Ok(Vec::new())
1106        }
1107    }
1108
1109    /// Execute action chain with time-windowed data collection
1110    ///
1111    /// This executes a sequence of actions while continuously collecting signal data,
1112    /// providing precise timing information for each action in the chain.
1113    ///
1114    /// # Arguments
1115    /// * `actions` - Vector of actions to execute in sequence
1116    /// * `pre_duration` - How much data to collect before chain starts
1117    /// * `post_duration` - How much data to collect after chain ends
1118    ///
1119    /// # Returns
1120    /// ChainExperimentData containing results and timing for each action plus synchronized signal data
1121    ///
1122    /// # Errors
1123    /// Returns error if buffering is not active or any action execution fails
1124    pub fn execute_chain_with_data_collection(
1125        &mut self,
1126        actions: Vec<Action>,
1127        pre_duration: Duration,
1128        post_duration: Duration,
1129    ) -> Result<crate::types::ChainExperimentData, NanonisError> {
1130        if self.tcp_reader.is_none() {
1131            return Err(NanonisError::Protocol(
1132                "TCP buffering not active".to_string(),
1133            ));
1134        }
1135
1136        let chain_start = Instant::now();
1137        let mut action_results = Vec::with_capacity(actions.len());
1138        let mut action_timings = Vec::with_capacity(actions.len());
1139
1140        // Execute each action and track timing
1141        for action in actions {
1142            let action_start = Instant::now();
1143            let action_result = self.execute(action)?;
1144            let action_end = Instant::now();
1145
1146            action_results.push(action_result);
1147            action_timings.push((action_start, action_end));
1148        }
1149
1150        let chain_end = Instant::now();
1151
1152        // Wait for post-chain data to be collected
1153        std::thread::sleep(post_duration);
1154
1155        // Query buffered data for the entire time window
1156        let window_start = chain_start - pre_duration;
1157        let window_end = chain_end + post_duration;
1158
1159        let signal_frames = self
1160            .tcp_reader
1161            .as_ref()
1162            .unwrap()
1163            .get_data_between(window_start, window_end);
1164        let tcp_config = self.tcp_reader_config.as_ref().unwrap().clone();
1165
1166        let chain_experiment_data = crate::types::ChainExperimentData {
1167            action_results,
1168            signal_frames,
1169            tcp_config,
1170            action_timings,
1171            chain_start,
1172            chain_end,
1173            total_duration: chain_end.duration_since(chain_start),
1174        };
1175
1176        // Log the complete chain experiment data if logging is enabled
1177        if self.action_logging_enabled && self.action_logger.is_some() {
1178            let log_entry = ActionLogEntry {
1179                action: format!(
1180                    "Chain Data Collection: {} actions",
1181                    chain_experiment_data.action_results.len()
1182                ),
1183                result: ActionLogResult::from_chain_experiment_data(&chain_experiment_data),
1184                start_time: chrono::Utc::now(),
1185                duration_ms: chain_experiment_data.total_duration.as_millis() as u64,
1186                metadata: Some(
1187                    [
1188                        (
1189                            "type".to_string(),
1190                            "chain_experiment_data_collection".to_string(),
1191                        ),
1192                        (
1193                            "pre_duration_ms".to_string(),
1194                            pre_duration.as_millis().to_string(),
1195                        ),
1196                        (
1197                            "post_duration_ms".to_string(),
1198                            post_duration.as_millis().to_string(),
1199                        ),
1200                        (
1201                            "action_count".to_string(),
1202                            chain_experiment_data.action_results.len().to_string(),
1203                        ),
1204                        (
1205                            "signal_frame_count".to_string(),
1206                            chain_experiment_data.signal_frames.len().to_string(),
1207                        ),
1208                    ]
1209                    .into_iter()
1210                    .collect(),
1211                ),
1212            };
1213
1214            if let Err(log_error) = self.action_logger.as_mut().unwrap().add(log_entry) {
1215                log::warn!("Failed to log chain experiment data: {}", log_error);
1216            }
1217        }
1218
1219        Ok(chain_experiment_data)
1220    }
1221
1222    /// Start TCP logger
1223    pub fn start_tcp_logger(&mut self) -> Result<(), NanonisError> {
1224        self.client.tcplog_start()
1225    }
1226
1227    /// Stop TCP logger
1228    pub fn stop_tcp_logger(&mut self) -> Result<(), NanonisError> {
1229        self.client.tcplog_stop()
1230    }
1231
1232    /// Configure TCP logger channels
1233    pub fn set_tcp_logger_channels(&mut self, channels: Vec<i32>) -> Result<(), NanonisError> {
1234        self.client.tcplog_chs_set(channels)
1235    }
1236
1237    /// Set TCP logger oversampling
1238    pub fn set_tcp_logger_oversampling(&mut self, oversampling: i32) -> Result<(), NanonisError> {
1239        self.client.tcplog_oversampl_set(oversampling)
1240    }
1241
1242    /// Get TCP logger status
1243    pub fn get_tcp_logger_status(&mut self) -> Result<crate::types::TCPLogStatus, NanonisError> {
1244        self.client.tcplog_status_get()
1245    }
1246
1247    /// Execute a single action
1248    pub fn execute(&mut self, action: Action) -> Result<ActionResult, NanonisError> {
1249        let start_time = chrono::Utc::now();
1250        let start_instant = std::time::Instant::now();
1251
1252        let result = self.execute_internal(action.clone());
1253
1254        let duration = start_instant.elapsed();
1255
1256        // Log the action execution if logging is enabled
1257        if self.action_logging_enabled && self.action_logger.is_some() {
1258            let log_entry = match &result {
1259                Ok(action_result) => {
1260                    ActionLogEntry::new(&action, action_result, start_time, duration)
1261                }
1262                Err(error) => ActionLogEntry::new_error(&action, error, start_time, duration),
1263            };
1264
1265            if let Err(log_error) = self.action_logger.as_mut().unwrap().add(log_entry) {
1266                log::warn!("Failed to log action: {}", log_error);
1267            }
1268        }
1269
1270        result
1271    }
1272
1273    /// Execute action with optional data collection (unified interface)
1274    ///
1275    /// This provides a single interface for both normal execution and data collection.
1276    /// When data_collection is true, this method collects TCP signal data alongside action execution.
1277    ///
1278    /// # Arguments
1279    /// * `action` - The action to execute
1280    /// * `data_collection` - If true, collect TCP signal data (requires TCP reader to be active)
1281    /// * `pre_duration` - How much data to collect before action (only used if data_collection=true)
1282    /// * `post_duration` - How much data to collect after action (only used if data_collection=true)
1283    ///
1284    /// # Returns
1285    /// ActionResult for normal execution, or ActionResult::ExperimentData for data collection
1286    ///
1287    /// # Usage
1288    /// ```rust,ignore
1289    /// // Normal execution
1290    /// let result = driver.execute_with_options(action, false, Duration::ZERO, Duration::ZERO)?;
1291    ///
1292    /// // With data collection
1293    /// let result = driver.execute_with_options(action, true, Duration::from_millis(100), Duration::from_millis(200))?;
1294    /// ```
1295    pub fn execute_with_options(
1296        &mut self,
1297        action: Action,
1298        data_collection: bool,
1299        pre_duration: Duration,
1300        post_duration: Duration,
1301    ) -> Result<ActionResult, NanonisError> {
1302        if data_collection && self.tcp_reader.is_some() {
1303            // Use data collection execution
1304            let _experiment_data =
1305                self.execute_with_data_collection(action, pre_duration, post_duration)?;
1306            // Convert ExperimentData to ActionResult for unified return type
1307            Ok(ActionResult::Success) // For now, return Success - could extend ActionResult to include ExperimentData
1308        } else {
1309            // Use normal execution
1310            self.execute(action)
1311        }
1312    }
1313
1314    /// Execute chain with optional data collection (unified interface)
1315    ///
1316    /// # Arguments
1317    /// * `chain` - The action chain to execute
1318    /// * `data_collection` - If true, collect TCP signal data for the entire chain
1319    /// * `pre_duration` - How much data to collect before chain starts
1320    /// * `post_duration` - How much data to collect after chain ends
1321    ///
1322    /// # Returns
1323    /// Vector of ActionResults
1324    pub fn execute_chain_with_options(
1325        &mut self,
1326        chain: impl Into<ActionChain>,
1327        data_collection: bool,
1328        pre_duration: Duration,
1329        post_duration: Duration,
1330    ) -> Result<Vec<ActionResult>, NanonisError> {
1331        if data_collection && self.tcp_reader.is_some() {
1332            // Use data collection execution
1333            let chain_experiment_data = self.execute_chain_with_data_collection(
1334                chain.into().into_iter().collect(),
1335                pre_duration,
1336                post_duration,
1337            )?;
1338            // Return the action results from the chain
1339            Ok(chain_experiment_data.action_results)
1340        } else {
1341            // Use normal execution
1342            self.execute_chain(chain)
1343        }
1344    }
1345
1346    /// Internal execute method without logging (for performance-critical chains)
1347    fn execute_internal(&mut self, action: Action) -> Result<ActionResult, NanonisError> {
1348        match action {
1349            // === Signal Operations ===
1350            Action::ReadSignal {
1351                signal,
1352                wait_for_newest,
1353            } => {
1354                let value = self.client.signals_vals_get(
1355                    vec![SignalIndex::new(signal.index).into()],
1356                    wait_for_newest,
1357                )?;
1358                Ok(ActionResult::Value(value[0] as f64))
1359            }
1360
1361            Action::ReadSignals {
1362                signals,
1363                wait_for_newest,
1364            } => {
1365                let indices: Vec<i32> = signals
1366                    .iter()
1367                    .map(|s| SignalIndex::new(s.index).into())
1368                    .collect();
1369                let values = self.client.signals_vals_get(indices, wait_for_newest)?;
1370                Ok(ActionResult::Values(
1371                    values.into_iter().map(|v| v as f64).collect(),
1372                ))
1373            }
1374
1375            Action::ReadSignalNames => {
1376                let names = self.client.signal_names_get()?;
1377                Ok(ActionResult::Text(names))
1378            }
1379
1380            // === Bias Operations ===
1381            Action::ReadBias => {
1382                let bias = self.client.bias_get()?;
1383                Ok(ActionResult::Value(bias as f64))
1384            }
1385
1386            Action::SetBias { voltage } => {
1387                self.client.bias_set(voltage)?;
1388                Ok(ActionResult::Success)
1389            }
1390
1391            // === Oscilloscope Operations ===
1392            Action::ReadOsci {
1393                signal,
1394                trigger,
1395                data_to_get,
1396                is_stable,
1397            } => {
1398                self.client.osci1t_run()?;
1399
1400                self.client.osci1t_ch_set(signal.index as i32)?;
1401
1402                if let Some(trigger) = trigger {
1403                    self.client.osci1t_trig_set(
1404                        trigger.mode.into(),
1405                        trigger.slope.into(),
1406                        trigger.level,
1407                        trigger.hysteresis,
1408                    )?;
1409                }
1410
1411                match data_to_get {
1412                    crate::types::DataToGet::Stable { readings, timeout } => {
1413                        let osci_data = self.find_stable_oscilloscope_data_with_fallback(
1414                            data_to_get,
1415                            readings,
1416                            timeout,
1417                            0.01,
1418                            50e-15,
1419                            0.8,
1420                            is_stable,
1421                        )?;
1422                        Ok(ActionResult::OsciData(osci_data))
1423                    }
1424                    _ => {
1425                        // Use NextTrigger for actual data reading - Stable is just for our algorithm
1426                        let data_mode = match data_to_get {
1427                            DataToGet::Current => 0,
1428                            DataToGet::NextTrigger => 1,
1429                            DataToGet::Wait2Triggers => 2,
1430                            DataToGet::Stable { .. } => 1, // Use NextTrigger for stable
1431                        };
1432                        let (t0, dt, size, data) = self.client.osci1t_data_get(data_mode)?;
1433                        let osci_data = OsciData::new_stable(t0, dt, size, data);
1434                        Ok(ActionResult::OsciData(osci_data))
1435                    }
1436                }
1437            }
1438
1439            // === Fine Positioning Operations (Piezo) ===
1440            Action::ReadPiezoPosition {
1441                wait_for_newest_data,
1442            } => {
1443                let pos = self.client.folme_xy_pos_get(wait_for_newest_data)?;
1444                Ok(ActionResult::Position(pos))
1445            }
1446
1447            Action::SetPiezoPosition {
1448                position,
1449                wait_until_finished,
1450            } => {
1451                self.client
1452                    .folme_xy_pos_set(position, wait_until_finished)?;
1453                Ok(ActionResult::Success)
1454            }
1455
1456            Action::MovePiezoRelative { delta } => {
1457                // Get current position and add delta
1458                let current = self.client.folme_xy_pos_get(true)?;
1459                info!("Current position: {current:?}");
1460                let new_position = Position {
1461                    x: current.x + delta.x,
1462                    y: current.y + delta.y,
1463                };
1464                self.client.folme_xy_pos_set(new_position, true)?;
1465                Ok(ActionResult::Success)
1466            }
1467
1468            // === Coarse Positioning Operations (Motor) ===
1469            Action::MoveMotorAxis {
1470                direction,
1471                steps,
1472                blocking,
1473            } => {
1474                self.client
1475                    .motor_start_move(direction, steps, MotorGroup::Group1, blocking)?;
1476                Ok(ActionResult::Success)
1477            }
1478
1479            Action::MoveMotor3D {
1480                displacement,
1481                blocking,
1482            } => {
1483                // Convert 3D displacement to sequence of motor movements
1484                let movements = displacement_to_motor_movements(&displacement);
1485
1486                // Execute each movement in sequence
1487                for (direction, steps) in movements {
1488                    self.client
1489                        .motor_start_move(direction, steps, MotorGroup::Group1, blocking)?;
1490                }
1491                Ok(ActionResult::Success)
1492            }
1493
1494            Action::MoveMotorClosedLoop { target, mode } => {
1495                self.client.motor_start_closed_loop(
1496                    mode,
1497                    target,
1498                    true, // wait_until_finished
1499                    MotorGroup::Group1,
1500                )?;
1501                Ok(ActionResult::Success)
1502            }
1503
1504            Action::StopMotor => {
1505                self.client.motor_stop_move()?;
1506                Ok(ActionResult::Success)
1507            }
1508
1509            // === Control Operations ===
1510            Action::AutoApproach {
1511                wait_until_finished,
1512                timeout,
1513                center_freq_shift,
1514            } => {
1515                log::debug!(
1516                    "Starting auto-approach (wait: {}, timeout: {:?}, center_freq: {})",
1517                    wait_until_finished,
1518                    timeout,
1519                    center_freq_shift
1520                );
1521
1522                // Center frequency shift if requested
1523                if center_freq_shift {
1524                    // Approach to the surface
1525                    self.auto_approach(true, timeout)?;
1526
1527                    // Sleep for 0.2 secs
1528                    std::thread::sleep(Duration::from_millis(200));
1529
1530                    // Toggle on the safe tip
1531                    if let Ok(safetip_state) = self.client_mut().safe_tip_on_off_get() {
1532                        if !safetip_state {
1533                            self.client_mut().safe_tip_on_off_set(true)?;
1534                        }
1535                    } else {
1536                        log::warn!("Failed to read safe tip state, setting true");
1537                        self.client_mut().safe_tip_on_off_set(true)?;
1538                    }
1539
1540                    self.check_safetip_status("after enabling safe tip")?;
1541
1542                    // Home 50nm away from the surface
1543                    self.client_mut().z_ctrl_home()?;
1544
1545                    self.check_safetip_status("after z_ctrl_home")?;
1546
1547                    // Sleep for 0.5 secs
1548                    std::thread::sleep(Duration::from_millis(500));
1549
1550                    self.check_safetip_status("after 500ms settle")?;
1551
1552                    // Center the freq shift
1553                    if let Err(e) = self.center_freq_shift() {
1554                        log::warn!("Failed to center frequency shift: {}", e);
1555                        // Continue anyway, this is not critical
1556                    }
1557
1558                    self.check_safetip_status("after center_freq_shift")?;
1559
1560                    // Approach again
1561                    self.auto_approach(wait_until_finished, timeout)?;
1562
1563                    self.check_safetip_status("after final auto_approach")?;
1564
1565                    // Toggle of the safe tip
1566                    if let Ok(safetip_state) = self.client_mut().safe_tip_on_off_get() {
1567                        if safetip_state {
1568                            self.client_mut().safe_tip_on_off_set(false)?;
1569                        }
1570                    } else {
1571                        log::warn!("Failed to read safe tip state, setting false");
1572                        self.client_mut().safe_tip_on_off_set(false)?;
1573                    }
1574                } else {
1575                    self.auto_approach(wait_until_finished, timeout)?;
1576                }
1577
1578                Ok(ActionResult::Success)
1579            }
1580
1581            Action::Withdraw {
1582                wait_until_finished,
1583                timeout,
1584            } => {
1585                self.client.z_ctrl_withdraw(wait_until_finished, timeout)?;
1586                Ok(ActionResult::Success)
1587            }
1588
1589            Action::SafeReposition { x_steps, y_steps } => {
1590                // Safe repositioning with hardcoded defaults
1591                let displacement = crate::types::MotorDisplacement::new(x_steps, y_steps, -3);
1592                let withdraw_timeout = Duration::from_secs(5);
1593                let approach_timeout = Duration::from_secs(10);
1594                let stabilization_wait = Duration::from_millis(500);
1595
1596                // Execute the safe repositioning sequence
1597                // 1. Withdraw
1598                self.client.z_ctrl_withdraw(true, withdraw_timeout)?;
1599
1600                // 2. Move motor 3D (using the same logic as MoveMotor3D)
1601                let movements = displacement_to_motor_movements(&displacement);
1602                for (direction, steps) in movements {
1603                    self.client
1604                        .motor_start_move(direction, steps, MotorGroup::Group1, true)?;
1605                }
1606
1607                thread::sleep(Duration::from_millis(500));
1608
1609                // 3. Center frequency and auto approach
1610                self.run(Action::AutoApproach {
1611                    wait_until_finished: true,
1612                    timeout: approach_timeout,
1613                    center_freq_shift: true,
1614                })
1615                .go()?;
1616
1617                // 4. Wait for stabilization
1618                thread::sleep(stabilization_wait);
1619
1620                Ok(ActionResult::Success)
1621            }
1622
1623            Action::SetZSetpoint { setpoint } => {
1624                self.client.z_ctrl_setpoint_set(setpoint)?;
1625                Ok(ActionResult::Success)
1626            }
1627
1628            // === Scan Operations ===
1629            Action::ScanControl { action } => {
1630                self.client.scan_action(action, ScanDirection::Up)?;
1631                Ok(ActionResult::Success)
1632            }
1633
1634            Action::ReadScanStatus => {
1635                let is_scanning = self.client.scan_status_get()?;
1636                Ok(ActionResult::Status(is_scanning))
1637            }
1638
1639            // === Advanced Operations ===
1640            Action::BiasPulse {
1641                wait_until_done,
1642                pulse_width,
1643                bias_value_v,
1644                z_controller_hold,
1645                pulse_mode,
1646            } => {
1647                // Convert u16 parameters to enums (safe conversion with fallback)
1648                let hold_enum = match z_controller_hold {
1649                    0 => ZControllerHold::NoChange,
1650                    1 => ZControllerHold::Hold,
1651                    2 => ZControllerHold::Release,
1652                    _ => ZControllerHold::NoChange, // Safe fallback
1653                };
1654
1655                let mode_enum = match pulse_mode {
1656                    0 => PulseMode::Keep,
1657                    1 => PulseMode::Relative,
1658                    2 => PulseMode::Absolute,
1659                    _ => PulseMode::Keep, // Safe fallback
1660                };
1661
1662                self.client.bias_pulse(
1663                    wait_until_done,
1664                    pulse_width.as_secs_f32(),
1665                    bias_value_v,
1666                    hold_enum.into(),
1667                    mode_enum.into(),
1668                )?;
1669
1670                Ok(ActionResult::Success)
1671            }
1672
1673            Action::TipShaper {
1674                config,
1675                wait_until_finished,
1676                timeout,
1677            } => {
1678                // Set tip shaper configuration
1679                self.client.tip_shaper_props_set(config)?;
1680
1681                // Start tip shaper
1682                self.client.tip_shaper_start(wait_until_finished, timeout)?;
1683
1684                Ok(ActionResult::Success)
1685            }
1686
1687            Action::PulseRetract {
1688                pulse_width,
1689                pulse_height_v,
1690            } => {
1691                let current_bias = self.client_mut().bias_get().unwrap_or(500e-3);
1692
1693                let config = TipShaperConfig {
1694                    switch_off_delay: std::time::Duration::from_millis(10),
1695                    change_bias: true,
1696                    bias_v: pulse_height_v,
1697                    tip_lift_m: 0.0,
1698                    lift_time_1: pulse_width,
1699                    bias_lift_v: current_bias,
1700                    bias_settling_time: std::time::Duration::from_millis(50),
1701                    lift_height_m: 100e-9,
1702                    lift_time_2: std::time::Duration::from_millis(100),
1703                    end_wait_time: std::time::Duration::from_millis(50),
1704                    restore_feedback: false,
1705                };
1706
1707                // Set tip shaper configuration and start
1708                self.client_mut().tip_shaper_props_set(config)?;
1709                self.client_mut()
1710                    .tip_shaper_start(true, Duration::from_secs(5))?;
1711
1712                Ok(ActionResult::Success)
1713            }
1714
1715            Action::Wait { duration } => {
1716                thread::sleep(duration);
1717                Ok(ActionResult::None)
1718            }
1719
1720            // === Data Management ===
1721            Action::Store { key, action } => {
1722                let result = self.execute(*action)?;
1723                self.stored_values.insert(key, result.clone());
1724                Ok(result) // Return the original result directly
1725            }
1726
1727            Action::Retrieve { key } => match self.stored_values.get(&key) {
1728                Some(value) => Ok(value.clone()), // Return the stored result directly
1729                None => Err(NanonisError::Protocol(format!(
1730                    "No stored value found for key: {}",
1731                    key
1732                ))),
1733            },
1734
1735            // === TCP Logger Operations ===
1736            Action::StartTCPLogger => {
1737                self.start_tcp_logger()?;
1738                Ok(ActionResult::Success)
1739            }
1740
1741            Action::StopTCPLogger => {
1742                self.stop_tcp_logger()?;
1743                Ok(ActionResult::Success)
1744            }
1745
1746            Action::GetTCPLoggerStatus => {
1747                use crate::actions::TCPReaderStatus;
1748                let status = self.get_tcp_logger_status()?;
1749                let config = self.tcp_reader_config();
1750
1751                Ok(ActionResult::TCPReaderStatus(TCPReaderStatus {
1752                    status,
1753                    channels: config.map(|c| c.channels.clone()).unwrap_or_default(),
1754                    oversampling: config.map(|c| c.oversampling).unwrap_or(0),
1755                }))
1756            }
1757
1758            Action::ConfigureTCPLogger {
1759                channels,
1760                oversampling,
1761            } => {
1762                self.set_tcp_logger_channels(channels)?;
1763                self.set_tcp_logger_oversampling(oversampling)?;
1764                Ok(ActionResult::Success)
1765            }
1766
1767            Action::CheckTipState { method } => {
1768                use std::collections::HashMap;
1769
1770                use crate::{
1771                    actions::{TipCheckMethod, TipState},
1772                    types::TipShape,
1773                };
1774
1775                let (tip_shape, measured_signals, mut metadata) = match method {
1776                    TipCheckMethod::SignalBounds { signal, bounds } => {
1777                        // Debug TCP logger status before calling ReadStableSignal
1778                        if let Some(ref tcp_reader) = self.tcp_reader {
1779                            let (frame_count, _max_capacity, time_span) = tcp_reader.buffer_stats();
1780                            log::debug!("CheckTipState: TCP reader available with {} frames, timespan: {}ms", 
1781                                frame_count, time_span.as_millis());
1782                        } else {
1783                            log::warn!(
1784                                "CheckTipState: No TCP reader available for signal {}",
1785                                signal.index
1786                            );
1787                        }
1788
1789                        // Use ReadStableSignal instead of single instantaneous read
1790                        log::debug!(
1791                            "CheckTipState: Calling ReadStableSignal for signal {}",
1792                            signal.index
1793                        );
1794
1795                        // Calculate samples needed for configured data collection duration
1796                        let data_points = self
1797                            .calculate_samples_for_duration(
1798                                self.tip_state_config.data_collection_duration,
1799                            )
1800                            .unwrap_or(100); // Fallback to 100 if TCP not configured
1801
1802                        let stable_result = self
1803                            .run(Action::ReadStableSignal {
1804                                signal: signal.clone(),
1805                                data_points: Some(data_points),
1806                                use_new_data: true, // Get fresh data for tip state checking
1807                                stability_method: crate::actions::SignalStabilityMethod::Combined {
1808                                    max_std_dev: self.tip_state_config.max_std_dev,
1809                                    max_slope: self.tip_state_config.max_slope,
1810                                },
1811                                timeout: self.tip_state_config.read_timeout,
1812                                retry_count: Some(self.tip_state_config.read_retry_count),
1813                            })
1814                            .execute();
1815
1816                        let (value, raw_data, read_method) = match stable_result {
1817                            Ok(exec_result) => match exec_result {
1818                                ExecutionResult::Single(ActionResult::StableSignal(
1819                                    stable_signal,
1820                                )) => {
1821                                    // Use stable value from ReadStableSignal
1822                                    log::debug!("CheckTipState: ReadStableSignal succeeded with {} data points", stable_signal.raw_data.len());
1823                                    (
1824                                        stable_signal.stable_value,
1825                                        stable_signal.raw_data,
1826                                        "stable_signal",
1827                                    )
1828                                }
1829                                ExecutionResult::Single(ActionResult::Values(values)) => {
1830                                    // ReadStableSignal failed but returned raw data; use the
1831                                    // mean as an unbiased fallback (min skews the reading negative).
1832                                    log::warn!("CheckTipState: ReadStableSignal failed but returned {} raw values, using mean as fallback", values.len());
1833                                    let raw_data: Vec<f32> =
1834                                        values.iter().map(|&v| v as f32).collect();
1835                                    let mean_value = if raw_data.is_empty() {
1836                                        f32::NAN
1837                                    } else {
1838                                        raw_data.iter().sum::<f32>() / raw_data.len() as f32
1839                                    };
1840                                    (mean_value, raw_data, "fallback_mean")
1841                                }
1842                                _ => {
1843                                    // Unexpected result type, fallback to single read
1844                                    log::warn!("CheckTipState: ReadStableSignal returned unexpected result type, falling back to single read");
1845                                    let single_value =
1846                                        self.client.signal_val_get(signal.index, true)?;
1847                                    (single_value, vec![single_value], "single_read_fallback")
1848                                }
1849                            },
1850                            Err(e) => {
1851                                // Complete fallback to single read
1852                                log::warn!("CheckTipState: ReadStableSignal failed with error: {}, falling back to single read", e);
1853                                let single_value =
1854                                    self.client.signal_val_get(signal.index, true)?;
1855                                (single_value, vec![single_value], "single_read_fallback")
1856                            }
1857                        };
1858
1859                        let mut measured = HashMap::new();
1860                        measured.insert(SignalIndex::new(signal.index), value);
1861
1862                        let shape = if value >= bounds.0 && value <= bounds.1 {
1863                            TipShape::Sharp
1864                        } else {
1865                            TipShape::Blunt
1866                        };
1867
1868                        // Populate metadata with analysis context and dataset
1869                        let bounds_center = (bounds.0 + bounds.1) / 2.0;
1870                        let bounds_width = (bounds.1 - bounds.0).abs();
1871                        let distance_from_center = (value - bounds_center).abs();
1872                        let relative_distance = if bounds_width > 0.0 {
1873                            distance_from_center / (bounds_width / 2.0)
1874                        } else {
1875                            0.0
1876                        };
1877                        let mut metadata = HashMap::new();
1878                        metadata.insert("method".to_string(), "signal_bounds".to_string());
1879                        metadata.insert("signal_index".to_string(), signal.index.to_string());
1880                        metadata.insert("measured_value".to_string(), format!("{:.6e}", value));
1881                        metadata.insert("bounds_lower".to_string(), format!("{:.6e}", bounds.0));
1882                        metadata.insert("bounds_upper".to_string(), format!("{:.6e}", bounds.1));
1883                        metadata.insert(
1884                            "bounds_center".to_string(),
1885                            format!("{:.6e}", bounds_center),
1886                        );
1887                        metadata
1888                            .insert("bounds_width".to_string(), format!("{:.6e}", bounds_width));
1889                        metadata.insert(
1890                            "distance_from_center".to_string(),
1891                            format!("{:.6e}", distance_from_center),
1892                        );
1893                        metadata.insert(
1894                            "relative_distance".to_string(),
1895                            format!("{:.3}", relative_distance),
1896                        );
1897                        metadata.insert(
1898                            "within_bounds".to_string(),
1899                            (shape == TipShape::Sharp).to_string(),
1900                        );
1901                        metadata.insert("read_method".to_string(), read_method.to_string());
1902                        metadata.insert("dataset_size".to_string(), raw_data.len().to_string());
1903
1904                        // Store raw dataset for debugging stability measures
1905                        let raw_data_summary = if raw_data.len() <= 10 {
1906                            raw_data
1907                                .iter()
1908                                .map(|x| format!("{:.3e}", x))
1909                                .collect::<Vec<_>>()
1910                                .join(",")
1911                        } else {
1912                            let first_5: String = raw_data
1913                                .iter()
1914                                .take(5)
1915                                .map(|x| format!("{:.3e}", x))
1916                                .collect::<Vec<_>>()
1917                                .join(",");
1918                            let last_5: String = raw_data
1919                                .iter()
1920                                .rev()
1921                                .take(5)
1922                                .rev()
1923                                .map(|x| format!("{:.3e}", x))
1924                                .collect::<Vec<_>>()
1925                                .join(",");
1926                            format!("{},...,{}", first_5, last_5)
1927                        };
1928                        metadata.insert(
1929                            "raw_dataset_summary".to_string(),
1930                            format!("[{}]", raw_data_summary),
1931                        );
1932
1933                        if shape == TipShape::Blunt {
1934                            let margin_violation = if value < bounds.0 {
1935                                bounds.0 - value
1936                            } else {
1937                                value - bounds.1
1938                            };
1939                            metadata.insert(
1940                                "margin_violation".to_string(),
1941                                format!("{:.6e}", margin_violation),
1942                            );
1943                            metadata.insert(
1944                                "violation_direction".to_string(),
1945                                if value < bounds.0 {
1946                                    "below_lower_bound".to_string()
1947                                } else {
1948                                    "above_upper_bound".to_string()
1949                                },
1950                            );
1951                        }
1952
1953                        (shape, measured, metadata)
1954                    }
1955
1956                    TipCheckMethod::MultiSignalBounds { ref signals } => {
1957                        let mut measured = HashMap::new();
1958                        let mut violations = Vec::new();
1959                        let mut all_good = true;
1960                        let mut all_datasets = Vec::new();
1961                        let mut read_methods = Vec::new();
1962
1963                        // Calculate samples needed for configured data collection duration
1964                        let data_points = self
1965                            .calculate_samples_for_duration(
1966                                self.tip_state_config.data_collection_duration,
1967                            )
1968                            .unwrap_or(100); // Fallback to 100 if TCP not configured
1969
1970                        // Read each signal using ReadStableSignal
1971                        for (signal, bounds) in signals.iter() {
1972                            let stable_result = self
1973                                .run(Action::ReadStableSignal {
1974                                    signal: signal.clone(),
1975                                    data_points: Some(data_points),
1976                                    use_new_data: true, // Get fresh data for tip state checking
1977                                    stability_method:
1978                                        crate::actions::SignalStabilityMethod::Combined {
1979                                            max_std_dev: self.tip_state_config.max_std_dev,
1980                                            max_slope: self.tip_state_config.max_slope,
1981                                        },
1982                                    timeout: self.tip_state_config.read_timeout,
1983                                    retry_count: Some(self.tip_state_config.read_retry_count),
1984                                })
1985                                .execute();
1986
1987                            let (value, raw_data, read_method) = match stable_result {
1988                                Ok(exec_result) => match exec_result {
1989                                    ExecutionResult::Single(ActionResult::StableSignal(
1990                                        stable_signal,
1991                                    )) => (
1992                                        stable_signal.stable_value,
1993                                        stable_signal.raw_data,
1994                                        "stable_signal",
1995                                    ),
1996                                    ExecutionResult::Single(ActionResult::Values(values)) => {
1997                                        let raw_data: Vec<f32> =
1998                                            values.iter().map(|&v| v as f32).collect();
1999                                        let mean_value = if raw_data.is_empty() {
2000                                            f32::NAN
2001                                        } else {
2002                                            raw_data.iter().sum::<f32>() / raw_data.len() as f32
2003                                        };
2004                                        (mean_value, raw_data, "fallback_mean")
2005                                    }
2006                                    _ => {
2007                                        let single_value =
2008                                            self.client.signal_val_get(signal.index, true)?;
2009                                        (single_value, vec![single_value], "single_read_fallback")
2010                                    }
2011                                },
2012                                Err(_) => {
2013                                    let single_value =
2014                                        self.client.signal_val_get(signal.index, true)?;
2015                                    (single_value, vec![single_value], "single_read_fallback")
2016                                }
2017                            };
2018
2019                            measured.insert(SignalIndex::new(signal.index), value);
2020                            all_datasets.push(raw_data);
2021                            read_methods.push(read_method);
2022
2023                            let in_bounds = value >= bounds.0 && value <= bounds.1;
2024                            if !in_bounds {
2025                                violations.push((signal.clone(), value, *bounds));
2026                                all_good = false;
2027                            }
2028                        }
2029
2030                        let shape = if all_good {
2031                            TipShape::Sharp
2032                        } else {
2033                            TipShape::Blunt
2034                        };
2035
2036                        // Populate metadata with multi-signal analysis and datasets
2037                        let mut metadata = HashMap::new();
2038                        metadata.insert("method".to_string(), "multi_signal_bounds".to_string());
2039                        metadata.insert("signal_count".to_string(), signals.len().to_string());
2040                        metadata.insert(
2041                            "signals_in_bounds".to_string(),
2042                            (signals.len() - violations.len()).to_string(),
2043                        );
2044                        metadata
2045                            .insert("violation_count".to_string(), violations.len().to_string());
2046                        metadata.insert("overall_pass".to_string(), all_good.to_string());
2047
2048                        // Add individual signal details with datasets
2049                        for (i, ((signal, bounds), dataset)) in
2050                            signals.iter().zip(all_datasets.iter()).enumerate()
2051                        {
2052                            let prefix = format!("signal_{}", i);
2053                            let value = measured[&SignalIndex::new(signal.index)];
2054
2055                            metadata.insert(format!("{}_index", prefix), signal.index.to_string());
2056                            metadata.insert(format!("{}_value", prefix), format!("{:.6e}", value));
2057                            metadata.insert(
2058                                format!("{}_bounds", prefix),
2059                                format!("[{:.3e}, {:.3e}]", bounds.0, bounds.1),
2060                            );
2061                            metadata.insert(
2062                                format!("{}_in_bounds", prefix),
2063                                (value >= bounds.0 && value <= bounds.1).to_string(),
2064                            );
2065                            metadata.insert(
2066                                format!("{}_read_method", prefix),
2067                                read_methods[i].to_string(),
2068                            );
2069                            metadata.insert(
2070                                format!("{}_dataset_size", prefix),
2071                                dataset.len().to_string(),
2072                            );
2073
2074                            // Store dataset summary for debugging
2075                            let dataset_summary = if dataset.len() <= 10 {
2076                                dataset
2077                                    .iter()
2078                                    .map(|x| format!("{:.3e}", x))
2079                                    .collect::<Vec<_>>()
2080                                    .join(",")
2081                            } else {
2082                                let first_3: String = dataset
2083                                    .iter()
2084                                    .take(3)
2085                                    .map(|x| format!("{:.3e}", x))
2086                                    .collect::<Vec<_>>()
2087                                    .join(",");
2088                                let last_3: String = dataset
2089                                    .iter()
2090                                    .rev()
2091                                    .take(3)
2092                                    .rev()
2093                                    .map(|x| format!("{:.3e}", x))
2094                                    .collect::<Vec<_>>()
2095                                    .join(",");
2096                                format!("{},...,{}", first_3, last_3)
2097                            };
2098                            metadata.insert(
2099                                format!("{}_dataset_summary", prefix),
2100                                format!("[{}]", dataset_summary),
2101                            );
2102                        }
2103
2104                        (shape, measured, metadata)
2105                    }
2106                };
2107
2108                // Add TCP buffer context and recent signal trends if available
2109                if let Some(ref tcp_reader) = self.tcp_reader {
2110                    let (frame_count, _max_capacity, time_span) = tcp_reader.buffer_stats();
2111                    metadata.insert("tcp_buffer_frames".to_string(), frame_count.to_string());
2112                    metadata.insert(
2113                        "tcp_buffer_utilization".to_string(),
2114                        format!("{:.2}", tcp_reader.buffer_utilization()),
2115                    );
2116                    metadata.insert(
2117                        "tcp_data_timespan_ms".to_string(),
2118                        time_span.as_millis().to_string(),
2119                    );
2120                    metadata.insert(
2121                        "tcp_uptime_ms".to_string(),
2122                        tcp_reader.uptime().as_millis().to_string(),
2123                    );
2124
2125                    // Add recent signal trend analysis for correlation with stable signal data
2126                    for signal_idx in measured_signals.keys() {
2127                        if tcp_reader.frame_count() >= 20 {
2128                            // Need minimum data for trend analysis
2129                            let recent_frames = tcp_reader.get_recent_frames(50); // Last 50 data points
2130
2131                            // Extract signal values for this specific signal from recent TCP data
2132                            let signal_values: Vec<f32> = recent_frames
2133                                .iter()
2134                                .filter_map(|frame| {
2135                                    // Find the signal in the frame data (assuming signal index maps to data array position)
2136                                    let idx = signal_idx.get() as usize;
2137                                    if idx < frame.signal_frame.data.len() {
2138                                        Some(frame.signal_frame.data[idx])
2139                                    } else {
2140                                        None
2141                                    }
2142                                })
2143                                .collect();
2144
2145                            if signal_values.len() >= 10 {
2146                                // Minimum for meaningful statistics
2147                                let mean =
2148                                    signal_values.iter().sum::<f32>() / signal_values.len() as f32;
2149                                let variance = signal_values
2150                                    .iter()
2151                                    .map(|x| (x - mean).powi(2))
2152                                    .sum::<f32>()
2153                                    / signal_values.len() as f32;
2154                                let std_dev = variance.sqrt();
2155                                let relative_std = if mean.abs() > 1e-15 {
2156                                    (std_dev / mean.abs()) * 100.0
2157                                } else {
2158                                    0.0
2159                                };
2160
2161                                // Calculate trend (simple linear regression slope)
2162                                let x_values: Vec<f32> =
2163                                    (0..signal_values.len()).map(|i| i as f32).collect();
2164                                let x_mean = x_values.iter().sum::<f32>() / x_values.len() as f32;
2165                                let y_mean = mean;
2166
2167                                let numerator: f32 = x_values
2168                                    .iter()
2169                                    .zip(signal_values.iter())
2170                                    .map(|(x, y)| (x - x_mean) * (y - y_mean))
2171                                    .sum();
2172                                let denominator: f32 =
2173                                    x_values.iter().map(|x| (x - x_mean).powi(2)).sum();
2174
2175                                let trend_slope = if denominator.abs() > 1e-15 {
2176                                    numerator / denominator
2177                                } else {
2178                                    0.0
2179                                };
2180
2181                                let signal_prefix = format!("tcp_signal_{}", signal_idx.get());
2182                                metadata.insert(
2183                                    format!("{}_recent_samples", signal_prefix),
2184                                    signal_values.len().to_string(),
2185                                );
2186                                metadata.insert(
2187                                    format!("{}_recent_mean", signal_prefix),
2188                                    format!("{:.6e}", mean),
2189                                );
2190                                metadata.insert(
2191                                    format!("{}_recent_std", signal_prefix),
2192                                    format!("{:.6e}", std_dev),
2193                                );
2194                                metadata.insert(
2195                                    format!("{}_recent_relative_std_pct", signal_prefix),
2196                                    format!("{:.3}", relative_std),
2197                                );
2198                                metadata.insert(
2199                                    format!("{}_trend_slope", signal_prefix),
2200                                    format!("{:.6e}", trend_slope),
2201                                );
2202                                metadata.insert(
2203                                    format!("{}_current_vs_recent_mean", signal_prefix),
2204                                    format!("{:.6e}", measured_signals[signal_idx] - mean),
2205                                );
2206
2207                                // Classify signal stability based on recent data
2208                                let is_stable_signal =
2209                                    relative_std < 5.0 && trend_slope.abs() < (std_dev * 0.1);
2210                                metadata.insert(
2211                                    format!("{}_appears_stable", signal_prefix),
2212                                    is_stable_signal.to_string(),
2213                                );
2214
2215                                // Check if current measurement is within recent range
2216                                let min_recent =
2217                                    signal_values.iter().cloned().fold(f32::INFINITY, f32::min);
2218                                let max_recent = signal_values
2219                                    .iter()
2220                                    .cloned()
2221                                    .fold(f32::NEG_INFINITY, f32::max);
2222                                let current_in_recent_range = measured_signals[signal_idx]
2223                                    >= min_recent
2224                                    && measured_signals[signal_idx] <= max_recent;
2225                                metadata.insert(
2226                                    format!("{}_current_in_recent_range", signal_prefix),
2227                                    current_in_recent_range.to_string(),
2228                                );
2229                                metadata.insert(
2230                                    format!("{}_recent_range", signal_prefix),
2231                                    format!("[{:.6e}, {:.6e}]", min_recent, max_recent),
2232                                );
2233                            }
2234                        }
2235                    }
2236                }
2237
2238                // Add recent ReadStableSignal data for correlation and debugging
2239                let now = std::time::Instant::now();
2240                let recent_signals: Vec<_> = self
2241                    .recent_stable_signals
2242                    .iter()
2243                    .filter(|(_, timestamp)| {
2244                        now.duration_since(*timestamp) < std::time::Duration::from_secs(300)
2245                    }) // Last 5 minutes
2246                    .collect();
2247
2248                if !recent_signals.is_empty() {
2249                    metadata.insert(
2250                        "recent_stable_signals_count".to_string(),
2251                        recent_signals.len().to_string(),
2252                    );
2253
2254                    // Add details of the most recent ReadStableSignal for debugging
2255                    if let Some((most_recent_signal, timestamp)) = recent_signals.last() {
2256                        let age_ms = now.duration_since(*timestamp).as_millis();
2257                        metadata.insert(
2258                            "most_recent_stable_signal_age_ms".to_string(),
2259                            age_ms.to_string(),
2260                        );
2261                        metadata.insert(
2262                            "most_recent_stable_value".to_string(),
2263                            format!("{:.6e}", most_recent_signal.stable_value),
2264                        );
2265                        metadata.insert(
2266                            "most_recent_data_points".to_string(),
2267                            most_recent_signal.data_points_used.to_string(),
2268                        );
2269                        metadata.insert(
2270                            "most_recent_analysis_duration_ms".to_string(),
2271                            most_recent_signal.analysis_duration.as_millis().to_string(),
2272                        );
2273
2274                        // Include raw data summary for debugging (first 5, last 5 values to avoid huge logs)
2275                        let raw_data = &most_recent_signal.raw_data;
2276                        let raw_data_summary = if raw_data.len() <= 10 {
2277                            // Small dataset, include all
2278                            raw_data
2279                                .iter()
2280                                .map(|x| format!("{:.3e}", x))
2281                                .collect::<Vec<_>>()
2282                                .join(",")
2283                        } else {
2284                            // Large dataset, show first 5 and last 5
2285                            let first_5: String = raw_data
2286                                .iter()
2287                                .take(5)
2288                                .map(|x| format!("{:.3e}", x))
2289                                .collect::<Vec<_>>()
2290                                .join(",");
2291                            let last_5: String = raw_data
2292                                .iter()
2293                                .rev()
2294                                .take(5)
2295                                .rev()
2296                                .map(|x| format!("{:.3e}", x))
2297                                .collect::<Vec<_>>()
2298                                .join(",");
2299                            format!("{},...,{}", first_5, last_5)
2300                        };
2301                        metadata.insert(
2302                            "most_recent_raw_data_summary".to_string(),
2303                            format!("[{}]", raw_data_summary),
2304                        );
2305                        metadata.insert(
2306                            "most_recent_raw_data_full_count".to_string(),
2307                            raw_data.len().to_string(),
2308                        );
2309
2310                        // Include stability metrics
2311                        for (metric_name, metric_value) in &most_recent_signal.stability_metrics {
2312                            metadata.insert(
2313                                format!("most_recent_metric_{}", metric_name),
2314                                format!("{:.6e}", metric_value),
2315                            );
2316                        }
2317                    }
2318                }
2319
2320                // Add execution timestamp
2321                metadata.insert(
2322                    "execution_timestamp".to_string(),
2323                    chrono::Utc::now().to_rfc3339(),
2324                );
2325
2326                // Log a concise summary; full details at debug level
2327                let signal_values_str = measured_signals
2328                    .iter()
2329                    .map(|(signal_idx, value)| format!("signal_{}={:.3}", signal_idx.get(), value))
2330                    .collect::<Vec<_>>()
2331                    .join(", ");
2332
2333                log::info!(
2334                    "CheckTipState: shape={:?}, signals=[{}]",
2335                    tip_shape,
2336                    signal_values_str
2337                );
2338
2339                log::debug!(
2340                    "CheckTipState detail: read_method={}, dataset_size={}, recent_stable_count={}",
2341                    metadata
2342                        .get("read_method")
2343                        .map(|s| s.as_str())
2344                        .unwrap_or("unknown"),
2345                    metadata
2346                        .get("dataset_size")
2347                        .map(|s| s.as_str())
2348                        .unwrap_or("unknown"),
2349                    recent_signals.len()
2350                );
2351
2352                Ok(ActionResult::TipState(TipState {
2353                    shape: tip_shape,
2354                    measured_signals,
2355                    metadata,
2356                }))
2357            }
2358
2359            Action::CheckTipStability {
2360                method,
2361                max_duration: _,
2362            } => {
2363                use std::collections::HashMap;
2364
2365                use crate::actions::{StabilityResult, TipStabilityMethod};
2366
2367                let start_time = std::time::Instant::now();
2368                let mut metrics = HashMap::new();
2369                let mut recommendations = Vec::new();
2370
2371                let (is_stable, measured_values) = match method {
2372                    TipStabilityMethod::ExtendedMonitoring {
2373                        signal: _,
2374                        duration: _,
2375                        sampling_interval: _,
2376                        stability_threshold: _,
2377                    } => {
2378                        todo!("ExtendedMonitoring not yet implemented");
2379                    }
2380
2381                    TipStabilityMethod::BiasSweepResponse {
2382                        ref signal,
2383                        bias_range,
2384                        bias_steps,
2385                        step_duration,
2386                        allowed_signal_change,
2387                    } => {
2388                        log::info!(
2389                            "Performing simple bias sweep stability test: {:.2}V to {:.2}V",
2390                            bias_range.0,
2391                            bias_range.1
2392                        );
2393
2394                        // 1. Get signal channel index for TCP reader
2395                        let tcp_channel = signal.tcp_channel.ok_or_else(|| {
2396                            NanonisError::Protocol(format!(
2397                                "Signal {} (Nanonis index) has no TCP channel mapping",
2398                                signal.index
2399                            ))
2400                        })?;
2401
2402                        // 2. Save and configure scan properties
2403                        log::info!("Reading current scan properties...");
2404                        let original_props = self.client.scan_props_get()?;
2405                        log::info!(
2406                            "Original scan props: continuous={}, bouncy={}",
2407                            original_props.continuous_scan,
2408                            original_props.bouncy_scan
2409                        );
2410
2411                        // Configure scan for stability check
2412                        log::info!("Configuring scan: continuous=true, bouncy=true");
2413                        let scan_props = nanonis_rs::scan::ScanPropsBuilder::new()
2414                            .continuous_scan(true)
2415                            .bouncy_scan(true);
2416                        self.client.scan_props_set(scan_props)?;
2417                        log::info!("Scan properties configured");
2418
2419                        // 3. Get initial bias for restoration
2420                        let initial_bias = self.client.bias_get()?;
2421                        log::info!(
2422                            "Initial bias: {:.3} V (will restore after sweep)",
2423                            initial_bias
2424                        );
2425
2426                        // 4. Read baseline signal value once before starting
2427                        let baseline_value = {
2428                            let tcp_reader = self.tcp_reader_mut().ok_or_else(|| {
2429                                NanonisError::Protocol("TCP reader not available".to_string())
2430                            })?;
2431
2432                            let recent_frames = tcp_reader.get_recent_frames(1);
2433                            if recent_frames.is_empty() {
2434                                return Err(NanonisError::Protocol(
2435                                    "No frames available from TCP reader".to_string(),
2436                                ));
2437                            }
2438
2439                            recent_frames[0].signal_frame.data[tcp_channel as usize]
2440                        };
2441
2442                        log::info!(
2443                            "Baseline signal: {:.3}, threshold: {:.3}",
2444                            baseline_value,
2445                            allowed_signal_change
2446                        );
2447
2448                        // 4. Start scan
2449                        self.client
2450                            .scan_action(ScanAction::Start, ScanDirection::Down)?;
2451                        log::info!("Scan started");
2452
2453                        // Wait for scan to actually start (max 5 seconds)
2454                        let mut scan_started = false;
2455                        for _ in 0..50 {
2456                            // Check for shutdown request
2457                            if self.is_shutdown_requested() {
2458                                log::info!("Shutdown requested while waiting for scan to start");
2459                                let _ =
2460                                    self.client.scan_action(ScanAction::Stop, ScanDirection::Up);
2461                                let _ = self.client.bias_set(initial_bias);
2462                                return Err(NanonisError::Protocol(
2463                                    "Shutdown requested".to_string(),
2464                                ));
2465                            }
2466                            std::thread::sleep(Duration::from_millis(100));
2467                            let is_scanning = self.client.scan_status_get()?;
2468                            if is_scanning {
2469                                scan_started = true;
2470                                log::info!("Scan started successfully");
2471                                break;
2472                            }
2473                        }
2474
2475                        if !scan_started {
2476                            return Err(NanonisError::Protocol(
2477                                "Scan failed to start within 5 seconds".to_string(),
2478                            ));
2479                        }
2480
2481                        // 5. Sweep bias from upper to lower
2482                        let bias_step_size = (bias_range.1 - bias_range.0) / (bias_steps as f32);
2483                        let mut current_bias = bias_range.0;
2484
2485                        for step_num in 0..bias_steps {
2486                            // Check for shutdown request
2487                            if self.is_shutdown_requested() {
2488                                log::info!(
2489                                    "Shutdown requested during bias sweep at step {}/{}",
2490                                    step_num + 1,
2491                                    bias_steps
2492                                );
2493                                let _ =
2494                                    self.client.scan_action(ScanAction::Stop, ScanDirection::Up);
2495                                let _ = self.client.bias_set(initial_bias);
2496                                return Err(NanonisError::Protocol(
2497                                    "Shutdown requested".to_string(),
2498                                ));
2499                            }
2500                            self.client.bias_set(current_bias)?;
2501                            log::debug!(
2502                                "Step {}/{}: bias={:.2}V",
2503                                step_num + 1,
2504                                bias_steps,
2505                                current_bias
2506                            );
2507                            // Interruptible sleep: split into 10ms chunks for responsive shutdown
2508                            let sleep_chunks = (step_duration.as_millis() / 10).max(1) as u32;
2509                            let chunk_duration = step_duration / sleep_chunks;
2510                            for _ in 0..sleep_chunks {
2511                                if self.is_shutdown_requested() {
2512                                    log::info!("Shutdown requested during bias sweep step sleep");
2513                                    let _ = self
2514                                        .client
2515                                        .scan_action(ScanAction::Stop, ScanDirection::Up);
2516                                    let _ = self.client.bias_set(initial_bias);
2517                                    return Err(NanonisError::Protocol(
2518                                        "Shutdown requested".to_string(),
2519                                    ));
2520                                }
2521                                std::thread::sleep(chunk_duration);
2522                            }
2523                            current_bias += bias_step_size;
2524                        }
2525
2526                        log::info!("Bias sweep completed");
2527
2528                        // 6. Read final signal value once after finishing
2529                        let final_value = {
2530                            let tcp_reader = self.tcp_reader_mut().ok_or_else(|| {
2531                                NanonisError::Protocol("TCP reader not available".to_string())
2532                            })?;
2533
2534                            let recent_frames = tcp_reader.get_recent_frames(1);
2535                            if recent_frames.is_empty() {
2536                                return Err(NanonisError::Protocol(
2537                                    "No frames available from TCP reader".to_string(),
2538                                ));
2539                            }
2540
2541                            recent_frames[0].signal_frame.data[tcp_channel as usize]
2542                        };
2543
2544                        // 7. Stop scan, withdraw, then restore bias
2545                        let _ = self.client.scan_action(ScanAction::Stop, ScanDirection::Up);
2546
2547                        // Withdraw before changing bias
2548                        if let Err(e) = self.client.z_ctrl_withdraw(true, Duration::from_secs(5)) {
2549                            log::error!("Failed to withdraw before restoring bias: {}", e);
2550                        }
2551
2552                        // Delay before changing bias
2553                        std::thread::sleep(Duration::from_millis(200));
2554
2555                        if let Err(e) = self.client.bias_set(initial_bias) {
2556                            log::error!("Failed to restore initial bias: {}", e);
2557                        } else {
2558                            log::info!("Bias restored to {:.3} V", initial_bias);
2559                        }
2560
2561                        // 8. Check if change exceeded threshold
2562                        let signal_change = (final_value - baseline_value).abs();
2563                        let is_stable = signal_change <= allowed_signal_change;
2564
2565                        log::info!(
2566                            "Bias sweep result: baseline={:.3}, final={:.3}, change={:.3}, threshold={:.3}, stable={}",
2567                            baseline_value,
2568                            final_value,
2569                            signal_change,
2570                            allowed_signal_change,
2571                            is_stable
2572                        );
2573
2574                        // 9. Populate metrics
2575                        metrics.insert("baseline_value".to_string(), baseline_value);
2576                        metrics.insert("final_value".to_string(), final_value);
2577                        metrics.insert("signal_change".to_string(), signal_change);
2578                        metrics.insert("threshold".to_string(), allowed_signal_change);
2579
2580                        // 10. Add recommendations
2581                        if is_stable {
2582                            recommendations.push(format!(
2583                                "Tip is stable - signal change {:.3} within threshold {:.3}",
2584                                signal_change, allowed_signal_change
2585                            ));
2586                        } else {
2587                            recommendations.push(format!(
2588                                "Tip is blunt - signal change {:.3} exceeded threshold {:.3}. Tip shaping recommended.",
2589                                signal_change, allowed_signal_change
2590                            ));
2591                        }
2592
2593                        // Create measured values map
2594                        let mut measured_values = HashMap::new();
2595                        measured_values.insert(signal.clone(), vec![baseline_value, final_value]);
2596
2597                        (is_stable, measured_values)
2598                    }
2599                };
2600
2601                let analysis_duration = start_time.elapsed();
2602                let result = StabilityResult {
2603                    is_stable,
2604                    method_used: format!("{:?}", method.clone()),
2605                    measured_values,
2606                    analysis_duration,
2607                    metrics,
2608                    potential_damage_detected: !is_stable
2609                        && matches!(method, TipStabilityMethod::BiasSweepResponse { .. }),
2610                    recommendations,
2611                };
2612
2613                Ok(ActionResult::StabilityResult(result))
2614            }
2615
2616            Action::ReadStableSignal {
2617                signal,
2618                data_points,
2619                use_new_data,
2620                stability_method,
2621                timeout,
2622                retry_count,
2623            } => {
2624                use std::time::Instant;
2625
2626                let start_time = Instant::now();
2627                let data_points = data_points.unwrap_or(50);
2628                let max_retries = retry_count.unwrap_or(0);
2629
2630                // Validate TCP logger is configured and active
2631                let tcp_config = self.tcp_reader_config.as_ref().ok_or_else(|| {
2632                    NanonisError::Protocol("TCP logger not configured".to_string())
2633                })?;
2634
2635                // Convert Nanonis signal index to TCP channel using registry
2636                log::debug!(
2637                    "ReadStableSignal: Looking up signal {} in signal registry",
2638                    signal.index
2639                );
2640
2641                // Look up the signal from registry to get TCP channel
2642                let registry_signal =
2643                    self.signal_registry
2644                        .get_by_index(signal.index)
2645                        .ok_or_else(|| {
2646                            NanonisError::Protocol(format!(
2647                                "Signal {} not found in registry",
2648                                signal.index
2649                            ))
2650                        })?;
2651
2652                let tcp_channel = registry_signal.tcp_channel.ok_or_else(|| {
2653                    log::error!(
2654                        "ReadStableSignal: Signal {} (Nanonis index) has no TCP channel mapping",
2655                        signal.index
2656                    );
2657                    NanonisError::Protocol(format!(
2658                        "Signal {} (Nanonis index) has no TCP channel mapping",
2659                        signal.index
2660                    ))
2661                })?;
2662
2663                log::debug!(
2664                    "ReadStableSignal: Signal {} mapped to TCP channel {}",
2665                    signal.index,
2666                    tcp_channel
2667                );
2668
2669                // Find TCP channel in TCP config channels
2670                log::debug!(
2671                    "ReadStableSignal: Signal {} (Nanonis) maps to TCP channel {}",
2672                    signal.index,
2673                    tcp_channel
2674                );
2675                log::debug!(
2676                    "ReadStableSignal: Available TCP channels: {:?}",
2677                    tcp_config.channels
2678                );
2679                let signal_channel_idx = tcp_config
2680                    .channels
2681                    .iter()
2682                    .position(|&ch| ch == tcp_channel as i32)
2683                    .ok_or_else(|| {
2684                        log::error!("ReadStableSignal: TCP channel {} for signal {} (Nanonis) not found in TCP logger configuration. Available channels: {:?}",
2685                            tcp_channel, signal.index, tcp_config.channels);
2686                        NanonisError::Protocol(format!(
2687                            "TCP channel {} for signal {} (Nanonis) not found in TCP logger configuration. Available: {:?}",
2688                            tcp_channel, signal.index, tcp_config.channels
2689                        ))
2690                    })?;
2691
2692                log::debug!(
2693                    "ReadStableSignal: Signal {} (Nanonis) -> TCP channel {} -> Array position {}",
2694                    signal.index,
2695                    tcp_channel,
2696                    signal_channel_idx
2697                );
2698                log::debug!(
2699                    "ReadStableSignal: Full TCP channel list: {:?}",
2700                    tcp_config.channels
2701                );
2702
2703                // Retry loop for data collection and stability analysis
2704                let mut attempt = 0;
2705
2706                loop {
2707                    match self.attempt_stable_signal_read(
2708                        signal_channel_idx,
2709                        data_points,
2710                        use_new_data,
2711                        timeout,
2712                        &stability_method,
2713                    ) {
2714                        Ok((signal_data, is_stable, metrics)) => {
2715                            let analysis_duration = start_time.elapsed();
2716
2717                            if is_stable {
2718                                // Calculate stable value (mean of the data)
2719                                let stable_value =
2720                                    signal_data.iter().sum::<f32>() / signal_data.len() as f32;
2721
2722                                use crate::actions::StableSignal;
2723                                log::info!(
2724                                    "Stable signal acquired on attempt {} (retries: {})",
2725                                    attempt + 1,
2726                                    attempt
2727                                );
2728
2729                                let stable_signal = StableSignal {
2730                                    stable_value,
2731                                    data_points_used: signal_data.len(),
2732                                    analysis_duration,
2733                                    stability_metrics: metrics,
2734                                    // Only include full buffer when not stable (for debugging)
2735                                    // When stable, only keep the mean value to reduce log file size
2736                                    raw_data: if is_stable {
2737                                        vec![stable_value]
2738                                    } else {
2739                                        signal_data
2740                                    },
2741                                };
2742
2743                                // Store for correlation with future CheckTipState calls
2744                                self.recent_stable_signals
2745                                    .push_back((stable_signal.clone(), std::time::Instant::now()));
2746                                // Keep only last 10 stable signal results
2747                                while self.recent_stable_signals.len() > 10 {
2748                                    self.recent_stable_signals.pop_front();
2749                                }
2750
2751                                return Ok(ActionResult::StableSignal(stable_signal));
2752                            } else if attempt >= max_retries {
2753                                // No more retries, return raw data as fallback
2754                                let nan = f32::NAN;
2755                                log::warn!(
2756                                    "Signal not stable after {} attempts: std_dev={:.3} Hz (thresh {:.3}), drift={:.3} Hz/s (thresh {:.3}), n={} — returning raw data",
2757                                    attempt + 1,
2758                                    metrics.get("std_dev").copied().unwrap_or(nan),
2759                                    metrics.get("max_std_dev_threshold").copied().unwrap_or(nan),
2760                                    metrics.get("abs_slope").copied().unwrap_or(nan),
2761                                    metrics.get("max_slope_threshold").copied().unwrap_or(nan),
2762                                    metrics.get("data_points").copied().unwrap_or(nan) as usize,
2763                                );
2764                                let values: Vec<f64> =
2765                                    signal_data.iter().map(|&x| x as f64).collect();
2766                                return Ok(ActionResult::Values(values));
2767                            } else {
2768                                // Signal not stable, but we can retry
2769                                let nan = f32::NAN;
2770                                log::debug!(
2771                                    "Signal not stable on attempt {} (std_dev={:.3} Hz, drift={:.3} Hz/s), retrying...",
2772                                    attempt + 1,
2773                                    metrics.get("std_dev").copied().unwrap_or(nan),
2774                                    metrics.get("abs_slope").copied().unwrap_or(nan),
2775                                );
2776                            }
2777                        }
2778                        Err(e) => {
2779                            log::warn!("Data collection failed on attempt {}: {}", attempt + 1, e);
2780
2781                            if attempt >= max_retries {
2782                                return Err(e);
2783                            }
2784                        }
2785                    }
2786
2787                    attempt += 1;
2788
2789                    // Add delay between retries (exponential backoff)
2790                    if attempt <= max_retries {
2791                        let delay_ms = 100 * (1 << (attempt - 1).min(4)); // Cap at 1.6s delay
2792                        log::debug!(
2793                            "Waiting {}ms before retry attempt {}",
2794                            delay_ms,
2795                            attempt + 1
2796                        );
2797                        std::thread::sleep(Duration::from_millis(delay_ms));
2798                    }
2799                }
2800            }
2801            Action::ReachedTargedAmplitude => {
2802                let ampl_setpoint = self.client_mut().pll_amp_ctrl_setpnt_get(1)?;
2803
2804                let ampl_current = match self
2805                    .run(Action::ReadStableSignal {
2806                        signal: Signal::new("Amplitude".to_string(), 75, None).unwrap(),
2807                        data_points: Some(50),
2808                        use_new_data: false,
2809                        stability_method:
2810                            crate::actions::SignalStabilityMethod::RelativeStandardDeviation {
2811                                threshold_percent: 0.2,
2812                            },
2813                        timeout: Duration::from_millis(10),
2814                        retry_count: Some(3), // 3 retries for amplitude check
2815                    })
2816                    .go()? {
2817                        ActionResult::Values(values) => values.iter().map(|v| *v as f32).sum::<f32>() / values.len() as f32,
2818                        ActionResult::StableSignal(value) => value.stable_value,
2819                        other => {
2820                            return Err(NanonisError::Protocol(format!(
2821                                "CheckAmplitudeStability returned unexpected result type. Expected Values or StableSignal, got {:?}",
2822                                std::mem::discriminant(&other)
2823                            )))
2824                        }
2825                    };
2826
2827                let status = (ampl_setpoint - 5e-12..ampl_setpoint + 5e-12).contains(&ampl_current);
2828
2829                Ok(ActionResult::Status(status))
2830            }
2831        }
2832    }
2833
2834    fn check_safetip_status(&mut self, context: &str) -> Result<(), NanonisError> {
2835        if let Ok(status) = self.client_mut().z_ctrl_status_get() {
2836            if matches!(status, nanonis_rs::z_ctrl::ZControllerStatus::SafeTip) {
2837                return Err(NanonisError::Protocol(format!(
2838                    "SafeTip triggered ({}), abort!",
2839                    context
2840                )));
2841            }
2842        }
2843
2844        Ok(())
2845    }
2846
2847    /// Effective sample rate (Hz) of the TCP data stream.
2848    ///
2849    /// Used to convert per-sample regression slopes into a physically
2850    /// meaningful drift rate (Hz/s) that is independent of how many frames
2851    /// were buffered. Mirrors the rate used in `calculate_samples_for_duration`.
2852    fn effective_sample_rate_hz(&self) -> f32 {
2853        const BASE_RATE_HZ: f32 = 2000.0; // Typical Nanonis base rate
2854        match &self.tcp_reader_config {
2855            Some(config) => BASE_RATE_HZ / (config.oversampling as f32).max(1.0),
2856            None => BASE_RATE_HZ,
2857        }
2858    }
2859
2860    /// Attempt a single stable signal read (used by retry logic)
2861    fn attempt_stable_signal_read(
2862        &self,
2863        signal_channel_idx: usize,
2864        data_points: usize,
2865        use_new_data: bool,
2866        timeout: Duration,
2867        stability_method: &crate::actions::SignalStabilityMethod,
2868    ) -> Result<(Vec<f32>, bool, std::collections::HashMap<String, f32>), NanonisError> {
2869        // Collect signal data based on use_new_data flag
2870        let signal_data: Vec<f32> = if use_new_data {
2871            // Wait for new data with timeout
2872            self.collect_new_signal_data(signal_channel_idx, data_points, timeout)?
2873        } else {
2874            // Use buffered data
2875            self.extract_buffered_signal_data(signal_channel_idx, data_points)?
2876        };
2877
2878        if signal_data.is_empty() {
2879            return Err(NanonisError::Protocol(
2880                "No signal data available".to_string(),
2881            ));
2882        }
2883
2884        // Analyze stability using the specified method. The sample rate lets
2885        // slope-based methods report drift in Hz/s rather than Hz/sample.
2886        let (is_stable, metrics) = Self::analyze_signal_stability(
2887            &signal_data,
2888            stability_method,
2889            self.effective_sample_rate_hz(),
2890        );
2891
2892        Ok((signal_data, is_stable, metrics))
2893    }
2894
2895    /// Collect new signal data from TCP logger with timeout
2896    fn collect_new_signal_data(
2897        &self,
2898        signal_channel_idx: usize,
2899        data_points: usize,
2900        timeout: Duration,
2901    ) -> Result<Vec<f32>, NanonisError> {
2902        use std::time::Instant;
2903
2904        let tcp_reader = self
2905            .tcp_reader
2906            .as_ref()
2907            .ok_or_else(|| NanonisError::Protocol("TCP reader not available".to_string()))?;
2908
2909        let start_time = Instant::now();
2910        let mut collected_data = Vec::with_capacity(data_points);
2911
2912        log::debug!(
2913            "Collecting {} new data points for signal channel {} with timeout {:.1}s",
2914            data_points,
2915            signal_channel_idx,
2916            timeout.as_secs_f32()
2917        );
2918
2919        while collected_data.len() < data_points && start_time.elapsed() < timeout {
2920            // Get recent data in small chunks to avoid blocking too long
2921            let recent_frames = tcp_reader.get_recent_data(Duration::from_millis(100));
2922
2923            for frame in recent_frames {
2924                if collected_data.len() >= data_points {
2925                    break;
2926                }
2927
2928                if let Some(&value) = frame.signal_frame.data.get(signal_channel_idx) {
2929                    collected_data.push(value);
2930                }
2931            }
2932
2933            if collected_data.len() < data_points {
2934                std::thread::sleep(Duration::from_millis(50)); // Small delay before next check
2935            }
2936        }
2937
2938        if collected_data.is_empty() {
2939            log::warn!("No data collected within timeout");
2940        } else {
2941            log::debug!("Collected {} data points", collected_data.len());
2942        }
2943
2944        Ok(collected_data)
2945    }
2946
2947    /// Extract buffered signal data from TCP logger
2948    fn extract_buffered_signal_data(
2949        &self,
2950        signal_channel_idx: usize,
2951        data_points: usize,
2952    ) -> Result<Vec<f32>, NanonisError> {
2953        let tcp_reader = self
2954            .tcp_reader
2955            .as_ref()
2956            .ok_or_else(|| NanonisError::Protocol("TCP reader not available".to_string()))?;
2957
2958        // Get recent data based on how many points we need
2959        let recent_frames = tcp_reader.get_recent_frames(data_points);
2960
2961        let mut signal_data = Vec::new();
2962        for frame in recent_frames.iter().rev().take(data_points) {
2963            // Take most recent data points
2964            if let Some(&value) = frame.signal_frame.data.get(signal_channel_idx) {
2965                signal_data.push(value);
2966            }
2967        }
2968
2969        signal_data.reverse(); // Return in chronological order
2970
2971        log::info!("Extracted {} buffered data points", signal_data.len());
2972        Ok(signal_data)
2973    }
2974
2975    /// Analyze signal stability using the specified method
2976    fn analyze_signal_stability(
2977        data: &[f32],
2978        method: &crate::actions::SignalStabilityMethod,
2979        sample_rate_hz: f32,
2980    ) -> (bool, std::collections::HashMap<String, f32>) {
2981        use crate::actions::SignalStabilityMethod;
2982
2983        if data.len() < 2 {
2984            return (false, std::collections::HashMap::new());
2985        }
2986
2987        let mut metrics = std::collections::HashMap::new();
2988        let mean = data.iter().sum::<f32>() / data.len() as f32;
2989        let variance = data.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / data.len() as f32;
2990        let std_dev = variance.sqrt();
2991
2992        metrics.insert("mean".to_string(), mean);
2993        metrics.insert("std_dev".to_string(), std_dev);
2994        metrics.insert("variance".to_string(), variance);
2995
2996        let is_stable = match method {
2997            SignalStabilityMethod::StandardDeviation { threshold } => {
2998                metrics.insert("threshold".to_string(), *threshold);
2999                std_dev <= *threshold
3000            }
3001
3002            SignalStabilityMethod::RelativeStandardDeviation { threshold_percent } => {
3003                let relative_std = if mean.abs() > 1e-12 {
3004                    (std_dev / mean.abs()) * 100.0
3005                } else {
3006                    f32::INFINITY
3007                };
3008                metrics.insert("relative_std_percent".to_string(), relative_std);
3009                metrics.insert("threshold_percent".to_string(), *threshold_percent);
3010                relative_std <= *threshold_percent
3011            }
3012
3013            SignalStabilityMethod::MovingWindow {
3014                window_size,
3015                max_variation,
3016            } => {
3017                if data.len() < *window_size {
3018                    return (false, metrics);
3019                }
3020
3021                let mut max_window_variation = 0.0f32;
3022                for window in data.windows(*window_size) {
3023                    let window_min = window.iter().fold(f32::INFINITY, |a, &b| a.min(b));
3024                    let window_max = window.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
3025                    let variation = window_max - window_min;
3026                    max_window_variation = max_window_variation.max(variation);
3027                }
3028
3029                metrics.insert("max_window_variation".to_string(), max_window_variation);
3030                metrics.insert("window_size".to_string(), *window_size as f32);
3031                metrics.insert("max_variation_threshold".to_string(), *max_variation);
3032                max_window_variation <= *max_variation
3033            }
3034
3035            SignalStabilityMethod::TrendAnalysis { max_slope } => {
3036                // Simple linear regression to detect trend
3037                let n = data.len() as f32;
3038                let x_mean = (n - 1.0) / 2.0; // indices 0, 1, 2, ... n-1
3039                let y_mean = mean;
3040
3041                let mut numerator = 0.0;
3042                let mut denominator = 0.0;
3043                for (i, &y) in data.iter().enumerate() {
3044                    let x = i as f32;
3045                    numerator += (x - x_mean) * (y - y_mean);
3046                    denominator += (x - x_mean).powi(2);
3047                }
3048
3049                let slope_per_sample = if denominator > 1e-12 {
3050                    numerator / denominator
3051                } else {
3052                    0.0
3053                };
3054                // Convert to Hz/s so the threshold is independent of sample count.
3055                let slope = slope_per_sample * sample_rate_hz;
3056                let abs_slope = slope.abs();
3057
3058                metrics.insert("slope_per_sample".to_string(), slope_per_sample);
3059                metrics.insert("slope".to_string(), slope); // Hz/s
3060                metrics.insert("abs_slope".to_string(), abs_slope); // Hz/s
3061                metrics.insert("max_slope_threshold".to_string(), *max_slope);
3062                abs_slope <= *max_slope
3063            }
3064
3065            SignalStabilityMethod::Combined {
3066                max_std_dev,
3067                max_slope,
3068            } => {
3069                // Calculate slope via linear regression
3070                let n = data.len() as f32;
3071                let x_mean = (n - 1.0) / 2.0;
3072                let y_mean = mean;
3073
3074                let mut numerator = 0.0;
3075                let mut denominator = 0.0;
3076                for (i, &y) in data.iter().enumerate() {
3077                    let x = i as f32;
3078                    numerator += (x - x_mean) * (y - y_mean);
3079                    denominator += (x - x_mean).powi(2);
3080                }
3081
3082                let slope_per_sample = if denominator > 1e-12 {
3083                    numerator / denominator
3084                } else {
3085                    0.0
3086                };
3087                // Convert to Hz/s so the threshold is independent of sample count.
3088                let slope = slope_per_sample * sample_rate_hz;
3089                let abs_slope = slope.abs();
3090
3091                // Check both conditions: noise AND drift
3092                let noise_ok = std_dev <= *max_std_dev;
3093                let drift_ok = abs_slope <= *max_slope;
3094
3095                metrics.insert("slope_per_sample".to_string(), slope_per_sample);
3096                metrics.insert("slope".to_string(), slope); // Hz/s
3097                metrics.insert("abs_slope".to_string(), abs_slope); // Hz/s
3098                metrics.insert("max_slope_threshold".to_string(), *max_slope);
3099                metrics.insert("max_std_dev_threshold".to_string(), *max_std_dev);
3100                metrics.insert("noise_ok".to_string(), if noise_ok { 1.0 } else { 0.0 });
3101                metrics.insert("drift_ok".to_string(), if drift_ok { 1.0 } else { 0.0 });
3102                noise_ok && drift_ok
3103            }
3104        };
3105
3106        metrics.insert("data_points".to_string(), data.len() as f32);
3107
3108        (is_stable, metrics)
3109    }
3110
3111    /// Execute action and extract specific type with validation
3112    ///
3113    /// This is a convenience method that combines execute() with type extraction,
3114    /// providing better ergonomics while preserving type safety.
3115    ///
3116    /// # Example
3117    /// ```ignore
3118    /// use rusty_tip::{ActionDriver, Action, Signal};
3119    /// use rusty_tip::types::{DataToGet, OsciData};
3120    ///
3121    /// let mut driver = ActionDriver::new("127.0.0.1", 6501)?;
3122    /// let signal = Signal::new("Frequency Shift", 24, None).unwrap();
3123    /// let osci_data: OsciData = driver.execute_expecting(Action::ReadOsci {
3124    ///     signal,
3125    ///     trigger: None,
3126    ///     data_to_get: DataToGet::Current,
3127    ///     is_stable: None,
3128    /// })?;
3129    /// # Ok::<(), Box<dyn std::error::Error>>(())
3130    /// ```
3131    pub fn execute_expecting<T>(&mut self, action: Action) -> Result<T, NanonisError>
3132    where
3133        ActionResult: ExpectFromAction<T>,
3134    {
3135        let result = self.execute(action.clone())?;
3136        Ok(result.expect_from_action(&action))
3137    }
3138
3139    /// Find stable oscilloscope data with proper timeout handling
3140    ///
3141    /// This method implements stability detection logic with dual-threshold
3142    /// approach and timeout handling. It repeatedly reads oscilloscope data until
3143    /// stable values are found or timeout is reached.
3144    fn find_stable_oscilloscope_data(
3145        &mut self,
3146        _data_to_get: DataToGet,
3147        readings: u32,
3148        timeout: std::time::Duration,
3149        relative_threshold: f64,
3150        absolute_threshold: f64,
3151        min_window_percent: f64,
3152        stability_fn: Option<fn(&[f64]) -> bool>,
3153    ) -> Result<Option<OsciData>, NanonisError> {
3154        match poll_with_timeout(
3155            || {
3156                // Try to find stable data in a batch of readings
3157                for _attempt in 0..readings {
3158                    let (t0, dt, size, data) = self.client.osci1t_data_get(2)?; // Wait2Triggers = 2
3159
3160                    if let Some(stable_osci_data) = self.analyze_stability_window(
3161                        t0,
3162                        dt,
3163                        size,
3164                        data,
3165                        relative_threshold,
3166                        absolute_threshold,
3167                        min_window_percent,
3168                        stability_fn,
3169                    )? {
3170                        return Ok(Some(stable_osci_data));
3171                    }
3172
3173                    // Small delay between attempts to avoid overwhelming the system
3174                    std::thread::sleep(std::time::Duration::from_millis(100));
3175                }
3176
3177                // No stable data found in this batch, continue polling
3178                Ok(None)
3179            },
3180            timeout,
3181            std::time::Duration::from_millis(50), // Brief pause between reading cycles
3182        ) {
3183            Ok(Some(result)) => Ok(Some(result)),
3184            Ok(None) => Ok(None), // Timeout reached
3185            Err(PollError::ConditionError(e)) => Err(e),
3186            Err(PollError::Timeout) => unreachable!(), // poll_with_timeout returns Ok(None) on timeout
3187        }
3188    }
3189
3190    /// Analyze a single oscilloscope data window for stability
3191    fn analyze_stability_window(
3192        &self,
3193        t0: f64,
3194        dt: f64,
3195        size: i32,
3196        data: Vec<f64>,
3197        relative_threshold: f64,
3198        absolute_threshold: f64,
3199        min_window_percent: f64,
3200        stability_fn: Option<fn(&[f64]) -> bool>,
3201    ) -> Result<Option<OsciData>, NanonisError> {
3202        let min_window = (size as f64 * min_window_percent) as usize;
3203        let mut start = 0;
3204        let mut end = size as usize;
3205
3206        while (end - start) > min_window {
3207            let window = &data[start..end];
3208            let arr = Array1::from_vec(window.to_vec());
3209            let mean = arr.mean().expect(
3210                "There must be an non-empty array, osci1t_data_get would have returned early.",
3211            );
3212            let std_dev = arr.std(0.0);
3213            let relative_std = std_dev / mean.abs();
3214
3215            // Use custom stability function if provided, otherwise default dual-threshold
3216            let is_stable = if let Some(stability_fn) = stability_fn {
3217                stability_fn(window)
3218            } else {
3219                // Default dual-threshold approach: relative OR absolute
3220                let is_relative_stable = relative_std < relative_threshold;
3221                let is_absolute_stable = std_dev < absolute_threshold;
3222                is_relative_stable || is_absolute_stable
3223            };
3224
3225            if is_stable {
3226                let stable_data = window.to_vec();
3227                let stability_method = if stability_fn.is_some() {
3228                    "custom".to_string()
3229                } else {
3230                    // Default dual-threshold logic
3231                    let is_relative_stable = relative_std < relative_threshold;
3232                    let is_absolute_stable = std_dev < absolute_threshold;
3233                    match (is_relative_stable, is_absolute_stable) {
3234                        (true, true) => "both".to_string(),
3235                        (true, false) => "relative".to_string(),
3236                        (false, true) => "absolute".to_string(),
3237                        (false, false) => unreachable!(),
3238                    }
3239                };
3240
3241                let stats = SignalStats {
3242                    mean,
3243                    std_dev,
3244                    relative_std,
3245                    window_size: stable_data.len(),
3246                    stability_method,
3247                };
3248
3249                let mut osci_data =
3250                    OsciData::new_with_stats(t0, dt, stable_data.len() as i32, stable_data, stats);
3251                osci_data.is_stable = true; // Mark as stable since we found stable data
3252                return Ok(Some(osci_data));
3253            }
3254
3255            let shrink = ((end - start) / 10).max(1);
3256            start += shrink;
3257            end -= shrink;
3258        }
3259
3260        // No stable window found in this data
3261        Ok(None)
3262    }
3263
3264    /// Find stable oscilloscope data with fallback to single value
3265    ///
3266    /// This method attempts to find stable oscilloscope data. If successful,
3267    /// it returns OsciData with is_stable=true. If no stable data is found
3268    /// within the timeout, it returns OsciData with is_stable=false and
3269    /// a fallback single value reading.
3270    fn find_stable_oscilloscope_data_with_fallback(
3271        &mut self,
3272        data_to_get: DataToGet,
3273        readings: u32,
3274        timeout: std::time::Duration,
3275        relative_threshold: f64,
3276        absolute_threshold: f64,
3277        min_window_percent: f64,
3278        stability_fn: Option<fn(&[f64]) -> bool>,
3279    ) -> Result<OsciData, NanonisError> {
3280        // First try to find stable data
3281        if let Some(stable_osci_data) = self.find_stable_oscilloscope_data(
3282            data_to_get,
3283            readings,
3284            timeout,
3285            relative_threshold,
3286            absolute_threshold,
3287            min_window_percent,
3288            stability_fn,
3289        )? {
3290            return Ok(stable_osci_data);
3291        }
3292
3293        // If no stable data found, get a single reading as fallback
3294        let (t0, dt, size, data) = self.client.osci1t_data_get(1)?; // NextTrigger = 1
3295
3296        // Calculate fallback value (mean of the data)
3297        let fallback_value = if !data.is_empty() {
3298            data.iter().sum::<f64>() / data.len() as f64
3299        } else {
3300            0.0
3301        };
3302
3303        Ok(OsciData::new_unstable_with_fallback(
3304            t0,
3305            dt,
3306            size,
3307            data,
3308            fallback_value,
3309        ))
3310    }
3311
3312    /// Execute a chain of actions sequentially
3313    pub fn execute_chain(
3314        &mut self,
3315        chain: impl Into<ActionChain>,
3316    ) -> Result<Vec<ActionResult>, NanonisError> {
3317        let chain = chain.into();
3318        let mut results = Vec::with_capacity(chain.len());
3319
3320        for action in chain.into_iter() {
3321            let result = self.execute(action)?;
3322            results.push(result);
3323        }
3324
3325        Ok(results)
3326    }
3327
3328    /// Execute chain and return only the final result
3329    pub fn execute_chain_final(
3330        &mut self,
3331        chain: impl Into<ActionChain>,
3332    ) -> Result<ActionResult, NanonisError> {
3333        let results = self.execute_chain(chain)?;
3334        Ok(results.into_iter().last().unwrap_or(ActionResult::None))
3335    }
3336
3337    /// Execute chain with early termination on error, returning partial results
3338    pub fn execute_chain_partial(
3339        &mut self,
3340        chain: impl Into<ActionChain>,
3341    ) -> Result<Vec<ActionResult>, (Vec<ActionResult>, NanonisError)> {
3342        let chain = chain.into();
3343        let mut results = Vec::new();
3344
3345        for action in chain.into_iter() {
3346            match self.execute(action) {
3347                Ok(result) => results.push(result),
3348                Err(error) => return Err((results, error)),
3349            }
3350        }
3351
3352        Ok(results)
3353    }
3354
3355    /// Execute chain with deferred logging for timing-critical operations
3356    ///
3357    /// This method executes all actions using execute_internal() (no per-action logging)
3358    /// and then logs the entire chain as a single entry with total timing.
3359    /// Use this when you need precise timing without logging overhead between actions.
3360    ///
3361    /// # Arguments
3362    /// * `chain` - The action chain to execute
3363    ///
3364    /// # Returns
3365    /// Vector of all action results
3366    ///
3367    /// # Logging Behavior
3368    /// - Individual actions are NOT logged during execution
3369    /// - Single log entry created for the entire chain with total duration
3370    /// - Log entry includes chain summary and final result
3371    pub fn execute_chain_deferred(
3372        &mut self,
3373        chain: impl Into<ActionChain>,
3374    ) -> Result<Vec<ActionResult>, NanonisError> {
3375        let chain = chain.into();
3376        let start_time = chrono::Utc::now();
3377        let start_instant = std::time::Instant::now();
3378
3379        let mut results = Vec::with_capacity(chain.len());
3380
3381        // Execute all actions without per-action logging
3382        for action in chain.iter() {
3383            let result = self.execute_internal(action.clone())?;
3384            results.push(result);
3385        }
3386
3387        let duration = start_instant.elapsed();
3388
3389        // Log the entire chain as a single entry if logging is enabled
3390        if self.action_logging_enabled && self.action_logger.is_some() {
3391            let chain_summary = format!("Chain: {}", chain.summary());
3392            let final_result = results.last().unwrap_or(&ActionResult::None);
3393
3394            let log_entry = ActionLogEntry::new(
3395                &crate::actions::Action::Wait {
3396                    duration: Duration::from_millis(0),
3397                }, // Placeholder action
3398                final_result,
3399                start_time,
3400                duration,
3401            )
3402            .with_metadata("type", "chain_execution")
3403            .with_metadata("chain_summary", chain_summary)
3404            .with_metadata("action_count", results.len().to_string());
3405
3406            if let Err(log_error) = self.action_logger.as_mut().unwrap().add(log_entry) {
3407                log::warn!("Failed to log chain execution: {}", log_error);
3408            }
3409        }
3410
3411        Ok(results)
3412    }
3413
3414    /// Clear all stored values
3415    pub fn clear_storage(&mut self) {
3416        self.stored_values.clear();
3417    }
3418
3419    /// Get all stored value keys
3420    pub fn stored_keys(&self) -> Vec<&String> {
3421        self.stored_values.keys().collect()
3422    }
3423
3424    // ==================== Action Logging Control Methods ====================
3425
3426    /// Enable or disable action logging at runtime
3427    ///
3428    /// # Arguments
3429    /// * `enabled` - true to enable logging, false to disable
3430    ///
3431    /// # Returns
3432    /// Previous logging state
3433    ///
3434    /// # Usage
3435    /// ```rust,ignore
3436    /// let previous_state = driver.set_action_logging_enabled(false);
3437    /// // Execute timing-critical operations without logging overhead
3438    /// driver.execute(critical_action)?;
3439    /// driver.set_action_logging_enabled(previous_state); // Restore
3440    /// ```
3441    pub fn set_action_logging_enabled(&mut self, enabled: bool) -> bool {
3442        let previous = self.action_logging_enabled;
3443        self.action_logging_enabled = enabled;
3444        previous
3445    }
3446
3447    /// Check if action logging is currently enabled
3448    pub fn is_action_logging_enabled(&self) -> bool {
3449        self.action_logging_enabled && self.action_logger.is_some()
3450    }
3451
3452    /// Manually flush the action log buffer to file
3453    ///
3454    /// # Returns
3455    /// Result indicating if flush was successful
3456    ///
3457    /// # Usage
3458    /// Force immediate write of buffered actions to file, useful before
3459    /// critical operations or at experiment checkpoints
3460    pub fn flush_action_log(&mut self) -> Result<(), NanonisError> {
3461        if let Some(ref mut logger) = self.action_logger {
3462            logger.flush()?;
3463        }
3464        Ok(())
3465    }
3466
3467    /// Get action log buffer statistics
3468    ///
3469    /// # Returns
3470    /// Optional tuple of (current_buffer_count, is_logging_enabled) or None if no logger
3471    ///
3472    /// # Usage
3473    /// Monitor buffer utilization to understand logging overhead and frequency
3474    pub fn action_log_stats(&self) -> Option<(usize, bool)> {
3475        self.action_logger
3476            .as_ref()
3477            .map(|logger| (logger.len(), self.action_logging_enabled))
3478    }
3479
3480    /// Finalize action log as JSON array (if configured for JSON output)
3481    ///
3482    /// # Returns
3483    /// Result indicating if finalization was successful
3484    ///
3485    /// # Usage
3486    /// Call this at the end of your experiment to convert JSONL to JSON format
3487    /// for easier post-experiment analysis. This happens automatically on drop,
3488    /// but you can call it manually for explicit control.
3489    pub fn finalize_action_log(&mut self) -> Result<(), NanonisError> {
3490        if let Some(ref mut logger) = self.action_logger {
3491            logger.finalize_as_json()?;
3492        }
3493        Ok(())
3494    }
3495
3496    /// Convenience method to read oscilloscope data directly
3497    pub fn read_oscilloscope(
3498        &mut self,
3499        signal: Signal,
3500        trigger: Option<TriggerConfig>,
3501        data_to_get: DataToGet,
3502    ) -> Result<Option<OsciData>, NanonisError> {
3503        match self.execute(Action::ReadOsci {
3504            signal,
3505            trigger,
3506            data_to_get,
3507            is_stable: None,
3508        })? {
3509            ActionResult::OsciData(osci_data) => Ok(Some(osci_data)),
3510            ActionResult::None => Ok(None),
3511            _ => Err(NanonisError::Protocol("Expected oscilloscope data".into())),
3512        }
3513    }
3514
3515    /// Convenience method to read oscilloscope data with custom stability function
3516    pub fn read_oscilloscope_with_stability(
3517        &mut self,
3518        signal: Signal,
3519        trigger: Option<TriggerConfig>,
3520        data_to_get: DataToGet,
3521        is_stable: fn(&[f64]) -> bool,
3522    ) -> Result<Option<OsciData>, NanonisError> {
3523        match self.execute(Action::ReadOsci {
3524            signal,
3525            trigger,
3526            data_to_get,
3527            is_stable: Some(is_stable),
3528        })? {
3529            ActionResult::OsciData(osci_data) => Ok(Some(osci_data)),
3530            ActionResult::None => Ok(None),
3531            _ => Err(NanonisError::Protocol("Expected oscilloscope data".into())),
3532        }
3533    }
3534}
3535
3536/// Simple stability detection functions for oscilloscope windows
3537pub mod stability {
3538    /// Dual threshold stability (current default behavior)
3539    /// Uses relative (1%) OR absolute (50fA) thresholds
3540    pub fn dual_threshold_stability(window: &[f64]) -> bool {
3541        if window.len() < 3 {
3542            return false;
3543        }
3544
3545        let mean = window.iter().sum::<f64>() / window.len() as f64;
3546        let variance = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / window.len() as f64;
3547        let std_dev = variance.sqrt();
3548        let relative_std = std_dev / mean.abs();
3549
3550        // Stable if EITHER relative OR absolute threshold is met
3551        relative_std < 0.05 || std_dev < 50e-15
3552    }
3553
3554    /// Trend analysis stability detector
3555    /// Checks for low slope (no trend) and good signal-to-noise ratio
3556    pub fn trend_analysis_stability(window: &[f64]) -> bool {
3557        if window.len() < 5 {
3558            return false;
3559        }
3560
3561        // Calculate linear regression slope
3562        let n = window.len() as f64;
3563        let x_mean = (n - 1.0) / 2.0; // 0, 1, 2, ... n-1 mean
3564        let y_mean = window.iter().sum::<f64>() / n;
3565
3566        let mut numerator = 0.0;
3567        let mut denominator = 0.0;
3568
3569        for (i, &y) in window.iter().enumerate() {
3570            let x = i as f64;
3571            numerator += (x - x_mean) * (y - y_mean);
3572            denominator += (x - x_mean).powi(2);
3573        }
3574
3575        let slope = if denominator != 0.0 {
3576            numerator / denominator
3577        } else {
3578            0.0
3579        };
3580
3581        // Calculate signal-to-noise ratio
3582        let signal_level = y_mean.abs();
3583        let noise_level = {
3584            let variance = window.iter().map(|y| (y - y_mean).powi(2)).sum::<f64>() / n;
3585            variance.sqrt()
3586        };
3587
3588        let snr = if noise_level != 0.0 {
3589            signal_level / noise_level
3590        } else {
3591            f64::INFINITY
3592        };
3593
3594        // Thresholds: very low slope and decent SNR
3595        slope.abs() < 0.001 && snr > 10.0
3596    }
3597}
3598
3599/// Statistics about action execution
3600#[derive(Debug, Clone)]
3601pub struct ExecutionStats {
3602    pub total_actions: usize,
3603    pub successful_actions: usize,
3604    pub failed_actions: usize,
3605    pub total_duration: std::time::Duration,
3606}
3607
3608impl ExecutionStats {
3609    pub fn success_rate(&self) -> f64 {
3610        if self.total_actions == 0 {
3611            0.0
3612        } else {
3613            self.successful_actions as f64 / self.total_actions as f64
3614        }
3615    }
3616}
3617
3618/// Extension for ActionDriver with execution statistics
3619impl ActionDriver {
3620    /// Execute chain with detailed statistics
3621    pub fn execute_chain_with_stats(
3622        &mut self,
3623        chain: impl Into<ActionChain>,
3624    ) -> Result<(Vec<ActionResult>, ExecutionStats), NanonisError> {
3625        let chain = chain.into();
3626        let start_time = std::time::Instant::now();
3627        let mut results = Vec::with_capacity(chain.len());
3628        let mut successful = 0;
3629        let failed = 0;
3630
3631        for action in chain.into_iter() {
3632            match self.execute(action) {
3633                Ok(result) => {
3634                    results.push(result);
3635                    successful += 1;
3636                }
3637                Err(e) => {
3638                    // For stats purposes, we want to continue executing but track failures
3639                    // In a real application, you might want to decide whether to continue or stop
3640                    // For now, return the error to maintain proper error handling
3641                    return Err(e);
3642                }
3643            }
3644        }
3645
3646        let stats = ExecutionStats {
3647            total_actions: results.len(),
3648            successful_actions: successful,
3649            failed_actions: failed,
3650            total_duration: start_time.elapsed(),
3651        };
3652
3653        Ok((results, stats))
3654    }
3655}
3656
3657// ==================== Type-Safe Extraction Implementations ====================
3658
3659impl ExpectFromExecution<ActionResult> for ExecutionResult {
3660    fn expect_from_execution(self) -> Result<ActionResult, NanonisError> {
3661        self.into_single()
3662    }
3663}
3664
3665impl ExpectFromExecution<Vec<ActionResult>> for ExecutionResult {
3666    fn expect_from_execution(self) -> Result<Vec<ActionResult>, NanonisError> {
3667        self.into_chain()
3668    }
3669}
3670
3671impl ExpectFromExecution<crate::types::ExperimentData> for ExecutionResult {
3672    fn expect_from_execution(self) -> Result<crate::types::ExperimentData, NanonisError> {
3673        self.into_experiment_data()
3674    }
3675}
3676
3677impl ExpectFromExecution<crate::types::ChainExperimentData> for ExecutionResult {
3678    fn expect_from_execution(self) -> Result<crate::types::ChainExperimentData, NanonisError> {
3679        self.into_chain_experiment_data()
3680    }
3681}
3682
3683impl ExpectFromExecution<f64> for ExecutionResult {
3684    fn expect_from_execution(self) -> Result<f64, NanonisError> {
3685        match self {
3686            ExecutionResult::Single(ActionResult::Value(v)) => Ok(v),
3687            ExecutionResult::Single(ActionResult::Values(mut vs)) if vs.len() == 1 => {
3688                Ok(vs.pop().unwrap())
3689            }
3690            _ => Err(NanonisError::Protocol(
3691                "Expected single numeric value".to_string(),
3692            )),
3693        }
3694    }
3695}
3696
3697impl ExpectFromExecution<Vec<f64>> for ExecutionResult {
3698    fn expect_from_execution(self) -> Result<Vec<f64>, NanonisError> {
3699        match self {
3700            ExecutionResult::Single(ActionResult::Values(vs)) => Ok(vs),
3701            ExecutionResult::Single(ActionResult::Value(v)) => Ok(vec![v]),
3702            _ => Err(NanonisError::Protocol(
3703                "Expected numeric values".to_string(),
3704            )),
3705        }
3706    }
3707}
3708
3709impl ExpectFromExecution<bool> for ExecutionResult {
3710    fn expect_from_execution(self) -> Result<bool, NanonisError> {
3711        match self {
3712            ExecutionResult::Single(ActionResult::Status(b)) => Ok(b),
3713            _ => Err(NanonisError::Protocol(
3714                "Expected boolean status".to_string(),
3715            )),
3716        }
3717    }
3718}
3719
3720impl ExpectFromExecution<Position> for ExecutionResult {
3721    fn expect_from_execution(self) -> Result<Position, NanonisError> {
3722        match self {
3723            ExecutionResult::Single(ActionResult::Position(pos)) => Ok(pos),
3724            _ => Err(NanonisError::Protocol("Expected position data".to_string())),
3725        }
3726    }
3727}
3728
3729impl ExpectFromExecution<OsciData> for ExecutionResult {
3730    fn expect_from_execution(self) -> Result<OsciData, NanonisError> {
3731        match self {
3732            ExecutionResult::Single(ActionResult::OsciData(data)) => Ok(data),
3733            _ => Err(NanonisError::Protocol(
3734                "Expected oscilloscope data".to_string(),
3735            )),
3736        }
3737    }
3738}
3739
3740impl ExpectFromExecution<crate::types::TipShape> for ExecutionResult {
3741    fn expect_from_execution(self) -> Result<crate::types::TipShape, NanonisError> {
3742        match self {
3743            ExecutionResult::Single(ActionResult::TipState(tip_state)) => Ok(tip_state.shape),
3744            _ => Err(NanonisError::Protocol("Expected tip state".to_string())),
3745        }
3746    }
3747}
3748
3749impl ExpectFromExecution<crate::actions::TipState> for ExecutionResult {
3750    fn expect_from_execution(self) -> Result<crate::actions::TipState, NanonisError> {
3751        match self {
3752            ExecutionResult::Single(ActionResult::TipState(tip_state)) => Ok(tip_state),
3753            _ => Err(NanonisError::Protocol("Expected tip state".to_string())),
3754        }
3755    }
3756}
3757
3758impl ExpectFromExecution<crate::actions::StableSignal> for ExecutionResult {
3759    fn expect_from_execution(self) -> Result<crate::actions::StableSignal, NanonisError> {
3760        match self {
3761            ExecutionResult::Single(ActionResult::StableSignal(stable_signal)) => Ok(stable_signal),
3762            _ => Err(NanonisError::Protocol("Expected stable signal".to_string())),
3763        }
3764    }
3765}
3766
3767impl ExpectFromExecution<crate::actions::TCPReaderStatus> for ExecutionResult {
3768    fn expect_from_execution(self) -> Result<crate::actions::TCPReaderStatus, NanonisError> {
3769        match self {
3770            ExecutionResult::Single(ActionResult::TCPReaderStatus(tcp_status)) => Ok(tcp_status),
3771            _ => Err(NanonisError::Protocol(
3772                "Expected TCP reader status".to_string(),
3773            )),
3774        }
3775    }
3776}
3777
3778impl ExpectFromExecution<crate::actions::StabilityResult> for ExecutionResult {
3779    fn expect_from_execution(self) -> Result<crate::actions::StabilityResult, NanonisError> {
3780        match self {
3781            ExecutionResult::Single(ActionResult::StabilityResult(stability_result)) => {
3782                Ok(stability_result)
3783            }
3784            _ => Err(NanonisError::Protocol(
3785                "Expected stability result".to_string(),
3786            )),
3787        }
3788    }
3789}
3790
3791impl ExpectFromExecution<Vec<String>> for ExecutionResult {
3792    fn expect_from_execution(self) -> Result<Vec<String>, NanonisError> {
3793        match self {
3794            ExecutionResult::Single(ActionResult::Text(text)) => Ok(text),
3795            _ => Err(NanonisError::Protocol("Expected text data".to_string())),
3796        }
3797    }
3798}
3799
3800impl Drop for ActionDriver {
3801    fn drop(&mut self) {
3802        log::info!("ActionDriver cleanup starting...");
3803
3804        // Clean up TCP buffering first
3805        if let Some(mut reader) = self.tcp_reader.take() {
3806            let final_data = reader.get_all_data();
3807            let _ = reader.stop(); // Ignore errors during cleanup
3808            log::info!(
3809                "Stopped TCP buffering, collected {} frames",
3810                final_data.len()
3811            );
3812        }
3813
3814        // Disable safe tip protection before cleanup
3815        log::info!("Disabling safe tip protection...");
3816        if let Err(e) = self.client_mut().safe_tip_on_off_set(false) {
3817            log::warn!("Failed to disable safe tip: {}", e);
3818        }
3819
3820        // Perform safe shutdown sequence with blocking operations
3821        log::info!("Performing safe withdrawal...");
3822        let withdraw_result = self.execute_chain(vec![
3823            Action::Withdraw {
3824                wait_until_finished: true, // Make it blocking
3825                timeout: Duration::from_secs(5),
3826            },
3827            Action::MoveMotorAxis {
3828                direction: crate::MotorDirection::ZMinus,
3829                steps: 10,
3830                blocking: false,
3831            },
3832        ]);
3833
3834        if let Err(e) = withdraw_result {
3835            log::warn!("Cleanup withdrawal failed: {}", e);
3836        } else {
3837            log::info!("Safe withdrawal completed");
3838        }
3839
3840        log::info!("ActionDriver cleanup complete");
3841    }
3842}
3843
3844#[cfg(test)]
3845mod tests {
3846    use std::time::Duration;
3847
3848    use super::*;
3849    // Note: These tests will fail without actual Nanonis hardware
3850    // They're included to show the intended interface
3851
3852    #[test]
3853    fn test_action_translator_interface() {
3854        // This test shows how the translator would be used
3855        // It will fail without actual hardware, but demonstrates the API
3856
3857        let driver_result = ActionDriver::new("127.0.0.1", 6501);
3858        match driver_result {
3859            Ok(mut driver) => {
3860                // Test single action
3861                let action = Action::ReadBias;
3862                let _result = driver.execute(action);
3863
3864                // With real hardware, this would succeed
3865                // Without hardware, it will error, which is expected
3866
3867                // Test chain
3868                let chain = ActionChain::new(vec![
3869                    Action::ReadBias,
3870                    Action::Wait {
3871                        duration: Duration::from_millis(500),
3872                    },
3873                    Action::SetBias { voltage: 1.0 },
3874                ]);
3875
3876                let _chain_result = driver.execute_chain(chain);
3877            }
3878            Err(_) => {
3879                // Expected when signals can't be discovered
3880                println!("Signal discovery failed - this is expected without hardware");
3881            }
3882        }
3883    }
3884}
3885
3886/// Decompose a 3D motor displacement into an ordered sequence of single-axis
3887/// moves: retract (Z-) first for safety, then X, then Y, then approach (Z+).
3888///
3889/// nanonis-rs 0.4.0 dropped `MotorDisplacement::to_motor_movements`, so the
3890/// safe ordering lives here now.
3891fn displacement_to_motor_movements(
3892    d: &crate::types::MotorDisplacement,
3893) -> Vec<(crate::types::MotorDirection, u16)> {
3894    use crate::types::MotorDirection;
3895
3896    let mut movements = Vec::new();
3897    if d.z < 0 {
3898        movements.push((MotorDirection::ZMinus, (-d.z) as u16));
3899    }
3900    if d.x > 0 {
3901        movements.push((MotorDirection::XPlus, d.x as u16));
3902    } else if d.x < 0 {
3903        movements.push((MotorDirection::XMinus, (-d.x) as u16));
3904    }
3905    if d.y > 0 {
3906        movements.push((MotorDirection::YPlus, d.y as u16));
3907    } else if d.y < 0 {
3908        movements.push((MotorDirection::YMinus, (-d.y) as u16));
3909    }
3910    if d.z > 0 {
3911        movements.push((MotorDirection::ZPlus, d.z as u16));
3912    }
3913    movements
3914}