use crate::{
types::{
DataToGet, MovementMode, OsciData, Position, Position3D, ScanAction, SignalIndex,
TriggerConfig,
},
MotorDirection, TipShaperConfig,
};
use std::time::Duration;
#[derive(Debug, Clone)]
pub enum Action {
ReadSignal {
signal: SignalIndex,
wait_for_newest: bool,
},
ReadSignals {
signals: Vec<SignalIndex>,
wait_for_newest: bool,
},
ReadSignalNames,
ReadBias,
SetBias { voltage: f32 },
ReadOsci {
signal: SignalIndex,
trigger: Option<TriggerConfig>,
data_to_get: DataToGet,
is_stable: Option<fn(&[f64]) -> bool>,
},
ReadPiezoPosition { wait_for_newest_data: bool },
SetPiezoPosition {
position: Position,
wait_until_finished: bool,
},
MovePiezoRelative { delta: Position },
MoveMotor {
direction: MotorDirection,
steps: u16,
},
MoveMotorClosedLoop {
target: Position3D,
mode: MovementMode,
},
StopMotor,
AutoApproach {
wait_until_finished: bool,
timeout: Duration,
},
Withdraw {
wait_until_finished: bool,
timeout: Duration,
},
SetZSetpoint { setpoint: f32 },
ScanControl { action: ScanAction },
ReadScanStatus,
BiasPulse {
wait_until_done: bool,
pulse_width: Duration,
bias_value_v: f32,
z_controller_hold: u16,
pulse_mode: u16,
},
TipShaper {
config: TipShaperConfig,
wait_until_finished: bool,
timeout: Duration,
},
PulseRetract {
pulse_width: Duration,
pulse_height_v: f32,
},
Wait { duration: Duration },
Store { key: String, action: Box<Action> },
Retrieve { key: String },
}
#[derive(Debug, Clone)]
pub enum ActionResult {
Value(f64),
Values(Vec<f64>),
Text(Vec<String>),
Status(bool),
Position(Position),
OsciData(OsciData),
Success,
None,
}
impl ActionResult {
pub fn as_f64(&self) -> Option<f64> {
match self {
ActionResult::Value(v) => Some(*v),
ActionResult::Values(values) => {
if values.len() == 1 {
Some(values[0])
} else {
None
}
}
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
ActionResult::Status(b) => Some(*b),
_ => None,
}
}
pub fn as_position(&self) -> Option<Position> {
match self {
ActionResult::Position(pos) => Some(*pos),
_ => None,
}
}
pub fn as_osci_data(&self) -> Option<&OsciData> {
match self {
ActionResult::OsciData(data) => Some(data),
_ => None,
}
}
pub fn expect_osci_data(self, action: &Action) -> OsciData {
match (action, self) {
(Action::ReadOsci { .. }, ActionResult::OsciData(data)) => data,
(action, result) => panic!(
"Expected OsciData from action {:?}, got {:?}",
action, result
),
}
}
pub fn expect_signal_value(self, action: &Action) -> f64 {
match (action, self) {
(Action::ReadSignal { .. }, ActionResult::Value(v)) => v,
(Action::ReadSignal { .. }, ActionResult::Values(mut vs)) if vs.len() == 1 => {
vs.pop().unwrap()
}
(Action::ReadBias, ActionResult::Value(v)) => v,
(action, result) => panic!(
"Expected signal value from action {:?}, got {:?}",
action, result
),
}
}
pub fn expect_values(self, action: &Action) -> Vec<f64> {
match (action, self) {
(Action::ReadSignals { .. }, ActionResult::Values(values)) => values,
(Action::ReadSignal { .. }, ActionResult::Value(v)) => vec![v],
(action, result) => {
panic!("Expected values from action {:?}, got {:?}", action, result)
}
}
}
pub fn expect_position(self, action: &Action) -> Position {
match (action, self) {
(Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => pos,
(action, result) => panic!(
"Expected position from action {:?}, got {:?}",
action, result
),
}
}
pub fn expect_bias_voltage(self, action: &Action) -> f32 {
match (action, self) {
(Action::ReadBias, ActionResult::Value(v)) => v as f32,
(action, result) => panic!(
"Expected bias voltage from action {:?}, got {:?}",
action, result
),
}
}
pub fn expect_signal_names(self, action: &Action) -> Vec<String> {
match (action, self) {
(Action::ReadSignalNames, ActionResult::Text(names)) => names,
(action, result) => panic!(
"Expected signal names from action {:?}, got {:?}",
action, result
),
}
}
pub fn expect_status(self, action: &Action) -> bool {
match (action, self) {
(Action::ReadScanStatus, ActionResult::Status(status)) => status,
(action, result) => {
panic!("Expected status from action {:?}, got {:?}", action, result)
}
}
}
pub fn try_into_osci_data(self, action: &Action) -> Result<OsciData, String> {
match (action, self) {
(Action::ReadOsci { .. }, ActionResult::OsciData(data)) => Ok(data),
(action, result) => Err(format!(
"Expected OsciData from action {:?}, got {:?}",
action, result
)),
}
}
pub fn try_into_signal_value(self, action: &Action) -> Result<f64, String> {
match (action, self) {
(Action::ReadSignal { .. }, ActionResult::Value(v)) => Ok(v),
(Action::ReadSignal { .. }, ActionResult::Values(mut vs)) if vs.len() == 1 => {
Ok(vs.pop().unwrap())
}
(Action::ReadBias, ActionResult::Value(v)) => Ok(v),
(action, result) => Err(format!(
"Expected signal value from action {:?}, got {:?}",
action, result
)),
}
}
pub fn try_into_position(self, action: &Action) -> Result<Position, String> {
match (action, self) {
(Action::ReadPiezoPosition { .. }, ActionResult::Position(pos)) => Ok(pos),
(action, result) => Err(format!(
"Expected position from action {:?}, got {:?}",
action, result
)),
}
}
pub fn try_into_status(self, action: &Action) -> Result<bool, String> {
match (action, self) {
(Action::ReadScanStatus, ActionResult::Status(status)) => Ok(status),
(action, result) => Err(format!(
"Expected status from action {:?}, got {:?}",
action, result
)),
}
}
}
pub trait ExpectFromAction<T> {
fn expect_from_action(self, action: &Action) -> T;
}
impl ExpectFromAction<OsciData> for ActionResult {
fn expect_from_action(self, action: &Action) -> OsciData {
self.expect_osci_data(action)
}
}
impl ExpectFromAction<f64> for ActionResult {
fn expect_from_action(self, action: &Action) -> f64 {
self.expect_signal_value(action)
}
}
impl ExpectFromAction<Vec<f64>> for ActionResult {
fn expect_from_action(self, action: &Action) -> Vec<f64> {
self.expect_values(action)
}
}
impl ExpectFromAction<Position> for ActionResult {
fn expect_from_action(self, action: &Action) -> Position {
self.expect_position(action)
}
}
impl ExpectFromAction<f32> for ActionResult {
fn expect_from_action(self, action: &Action) -> f32 {
self.expect_bias_voltage(action)
}
}
impl ExpectFromAction<Vec<String>> for ActionResult {
fn expect_from_action(self, action: &Action) -> Vec<String> {
self.expect_signal_names(action)
}
}
impl ExpectFromAction<bool> for ActionResult {
fn expect_from_action(self, action: &Action) -> bool {
self.expect_status(action)
}
}
impl Action {
pub fn is_positioning_action(&self) -> bool {
matches!(
self,
Action::SetPiezoPosition { .. }
| Action::MovePiezoRelative { .. }
| Action::MoveMotor { .. }
| Action::MoveMotorClosedLoop { .. }
)
}
pub fn is_read_action(&self) -> bool {
matches!(
self,
Action::ReadSignal { .. }
| Action::ReadSignals { .. }
| Action::ReadSignalNames
| Action::ReadBias
| Action::ReadPiezoPosition { .. }
| Action::ReadScanStatus
| Action::Retrieve { .. }
)
}
pub fn is_control_action(&self) -> bool {
matches!(
self,
Action::AutoApproach { .. }
| Action::Withdraw { .. }
| Action::ScanControl { .. }
| Action::StopMotor
)
}
pub fn modifies_bias(&self) -> bool {
matches!(self, Action::SetBias { .. } | Action::BiasPulse { .. })
}
pub fn involves_motor(&self) -> bool {
matches!(
self,
Action::MoveMotor { .. } | Action::MoveMotorClosedLoop { .. } | Action::StopMotor
)
}
pub fn involves_piezo(&self) -> bool {
matches!(
self,
Action::SetPiezoPosition { .. }
| Action::MovePiezoRelative { .. }
| Action::ReadPiezoPosition { .. }
)
}
pub fn description(&self) -> String {
match self {
Action::ReadSignal { signal, .. } => {
format!("Read signal {}", signal.0)
}
Action::ReadSignals { signals, .. } => {
let indices: Vec<i32> = signals.iter().map(|s| s.0).collect();
format!("Read signals: {:?}", indices)
}
Action::SetBias { voltage } => {
format!("Set bias to {:.3}V", voltage)
}
Action::SetPiezoPosition { position, .. } => {
format!(
"Set piezo position to ({:.3e}, {:.3e})",
position.x, position.y
)
}
Action::MoveMotor { direction, steps } => {
format!("Move motor {:?} {} steps", direction, steps)
}
Action::AutoApproach {
wait_until_finished,
timeout,
} => format!(
"Auto approach blocking: {wait_until_finished}, timeout: {:?}",
timeout
),
Action::Withdraw { timeout, .. } => {
format!("Withdraw tip (timeout: {}ms)", timeout.as_micros())
}
Action::SetZSetpoint { setpoint } => {
format!("Set Z setpoint: {:.3e}", setpoint)
}
Action::Wait { duration } => {
format!("Wait {:.1}s", duration.as_secs_f64())
}
Action::BiasPulse {
wait_until_done: _,
pulse_width,
bias_value_v,
z_controller_hold: _,
pulse_mode: _,
} => {
format!("Bias pulse {:.3}V for {:?}ms", bias_value_v, pulse_width)
}
Action::TipShaper {
config,
wait_until_finished,
timeout,
} => {
format!(
"Tip shaper: bias {:.1}V, lift {:.0}nm, times {:.1?}s/{:.1?}s (wait: {}, timeout: {:?}ms)",
config.bias_v,
config.tip_lift_m * 1e9,
config.lift_time_1.as_secs_f32(),
config.lift_time_2.as_secs_f32(),
wait_until_finished,
timeout
)
}
Action::PulseRetract {
pulse_width,
pulse_height_v,
} => {
format!(
"Pulse retract {:.1}V for {:.0?}ms",
pulse_height_v, pulse_width
)
}
Action::ReadOsci {
signal,
trigger,
data_to_get,
is_stable,
} => {
let trigger_desc = match trigger {
Some(config) => format!("trigger: {:?}", config.mode),
None => "no trigger config".to_string(),
};
let stability_desc = match is_stable {
Some(_) => " with custom stability",
None => "",
};
format!(
"Read oscilloscope signal {} with {} (mode: {:?}){}",
signal.0, trigger_desc, data_to_get, stability_desc
)
}
_ => format!("{:?}", self),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_result_extraction() {
let bias_result = ActionResult::Value(2.5);
assert_eq!(bias_result.as_f64(), Some(2.5));
let position_result = ActionResult::Position(Position { x: 1e-9, y: 2e-9 });
assert_eq!(
position_result.as_position(),
Some(Position { x: 1e-9, y: 2e-9 })
);
}
}
#[derive(Debug, Clone)]
pub struct ActionChain {
actions: Vec<Action>,
name: Option<String>,
}
impl ActionChain {
pub fn new(actions: Vec<Action>) -> Self {
Self {
actions,
name: None,
}
}
pub fn from_actions(actions: impl IntoIterator<Item = Action>) -> Self {
Self::new(actions.into_iter().collect())
}
pub fn named(actions: Vec<Action>, name: impl Into<String>) -> Self {
Self {
actions,
name: Some(name.into()),
}
}
pub fn empty() -> Self {
Self::new(vec![])
}
pub fn actions(&self) -> &[Action] {
&self.actions
}
pub fn actions_mut(&mut self) -> &mut Vec<Action> {
&mut self.actions
}
pub fn push(&mut self, action: Action) {
self.actions.push(action);
}
pub fn extend(&mut self, actions: impl IntoIterator<Item = Action>) {
self.actions.extend(actions);
}
pub fn insert(&mut self, index: usize, action: Action) {
self.actions.insert(index, action);
}
pub fn remove(&mut self, index: usize) -> Action {
self.actions.remove(index)
}
pub fn pop(&mut self) -> Option<Action> {
self.actions.pop()
}
pub fn clear(&mut self) {
self.actions.clear();
}
pub fn chain_with(mut self, other: ActionChain) -> Self {
self.actions.extend(other.actions);
self
}
pub fn iter(&self) -> std::slice::Iter<'_, Action> {
self.actions.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Action> {
self.actions.iter_mut()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn set_name(&mut self, name: impl Into<String>) {
self.name = Some(name.into());
}
pub fn len(&self) -> usize {
self.actions.len()
}
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn positioning_actions(&self) -> Vec<&Action> {
self.actions
.iter()
.filter(|a| a.is_positioning_action())
.collect()
}
pub fn read_actions(&self) -> Vec<&Action> {
self.actions.iter().filter(|a| a.is_read_action()).collect()
}
pub fn control_actions(&self) -> Vec<&Action> {
self.actions
.iter()
.filter(|a| a.is_control_action())
.collect()
}
pub fn involves_motor(&self) -> bool {
self.actions.iter().any(|a| a.involves_motor())
}
pub fn involves_piezo(&self) -> bool {
self.actions.iter().any(|a| a.involves_piezo())
}
pub fn modifies_bias(&self) -> bool {
self.actions.iter().any(|a| a.modifies_bias())
}
pub fn summary(&self) -> String {
if let Some(name) = &self.name {
format!("{} ({} actions)", name, self.len())
} else {
format!("Action chain with {} actions", self.len())
}
}
pub fn analysis(&self) -> ChainAnalysis {
ChainAnalysis {
total_actions: self.len(),
positioning_actions: self.positioning_actions().len(),
read_actions: self.read_actions().len(),
control_actions: self.control_actions().len(),
involves_motor: self.involves_motor(),
involves_piezo: self.involves_piezo(),
modifies_bias: self.modifies_bias(),
}
}
}
#[derive(Debug, Clone)]
pub struct ChainAnalysis {
pub total_actions: usize,
pub positioning_actions: usize,
pub read_actions: usize,
pub control_actions: usize,
pub involves_motor: bool,
pub involves_piezo: bool,
pub modifies_bias: bool,
}
impl IntoIterator for ActionChain {
type Item = Action;
type IntoIter = std::vec::IntoIter<Action>;
fn into_iter(self) -> Self::IntoIter {
self.actions.into_iter()
}
}
impl<'a> IntoIterator for &'a ActionChain {
type Item = &'a Action;
type IntoIter = std::slice::Iter<'a, Action>;
fn into_iter(self) -> Self::IntoIter {
self.actions.iter()
}
}
impl FromIterator<Action> for ActionChain {
fn from_iter<T: IntoIterator<Item = Action>>(iter: T) -> Self {
Self::from_actions(iter)
}
}
impl From<Vec<Action>> for ActionChain {
fn from(actions: Vec<Action>) -> Self {
Self::new(actions)
}
}
impl ActionChain {
pub fn system_status_check() -> Self {
ActionChain::named(
vec![
Action::ReadSignalNames,
Action::ReadBias,
Action::ReadPiezoPosition {
wait_for_newest_data: true,
},
],
"System status check",
)
}
pub fn safe_tip_approach() -> Self {
ActionChain::named(
vec![
Action::ReadPiezoPosition {
wait_for_newest_data: true,
},
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
},
Action::Wait {
duration: Duration::from_millis(500),
},
Action::ReadSignal {
signal: SignalIndex(24),
wait_for_newest: true,
}, Action::ReadSignal {
signal: SignalIndex(0),
wait_for_newest: true,
}, ],
"Safe tip approach",
)
}
pub fn move_and_approach(target: Position) -> Self {
ActionChain::named(
vec![
Action::SetPiezoPosition {
position: target,
wait_until_finished: true,
},
Action::Wait {
duration: Duration::from_millis(100),
},
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
},
Action::ReadSignal {
signal: SignalIndex(24),
wait_for_newest: true,
},
],
format!("Move to ({:.1e}, {:.1e}) and approach", target.x, target.y),
)
}
pub fn bias_pulse_sequence(voltage: f32, duration_ms: u32) -> Self {
ActionChain::named(
vec![
Action::ReadBias,
Action::SetBias { voltage },
Action::Wait {
duration: Duration::from_millis(50),
},
Action::Wait {
duration: Duration::from_millis(duration_ms as u64),
},
Action::SetBias { voltage: 0.0 },
],
format!("Bias pulse {:.3}V for {}ms", voltage, duration_ms),
)
}
pub fn position_survey(positions: Vec<Position>) -> Self {
let position_count = positions.len(); let mut actions = Vec::new();
for pos in positions {
actions.extend([
Action::SetPiezoPosition {
position: pos,
wait_until_finished: true,
},
Action::Wait {
duration: Duration::from_millis(100),
},
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
},
Action::ReadSignal {
signal: SignalIndex(24),
wait_for_newest: true,
}, Action::ReadSignal {
signal: SignalIndex(0),
wait_for_newest: true,
}, Action::Withdraw {
wait_until_finished: true,
timeout: Duration::from_secs(5),
},
]);
}
ActionChain::named(
actions,
format!("Position survey ({} points)", position_count),
)
}
pub fn tip_recovery_sequence() -> Self {
ActionChain::named(
vec![
Action::Withdraw {
wait_until_finished: true,
timeout: Duration::from_secs(5),
},
Action::MovePiezoRelative {
delta: Position { x: 3e-9, y: 3e-9 },
},
Action::Wait {
duration: Duration::from_millis(200),
},
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
},
Action::ReadSignal {
signal: SignalIndex(24),
wait_for_newest: true,
},
],
"Tip recovery sequence",
)
}
}
#[cfg(test)]
mod chain_tests {
use super::*;
use crate::types::MotorDirection;
#[test]
fn test_vec_foundation() {
let mut chain = ActionChain::new(vec![Action::ReadBias, Action::SetBias { voltage: 1.0 }]);
assert_eq!(chain.len(), 2);
chain.push(Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
});
assert_eq!(chain.len(), 3);
let action = chain.pop().unwrap();
assert!(matches!(
action,
Action::AutoApproach {
wait_until_finished: true,
timeout: _
}
));
assert_eq!(chain.len(), 2);
chain.extend([
Action::Wait {
duration: Duration::from_millis(100),
},
Action::ReadBias,
]);
assert_eq!(chain.len(), 4);
}
#[test]
fn test_simple_construction() {
let chain = ActionChain::named(
vec![
Action::ReadBias,
Action::SetBias { voltage: 1.0 },
Action::Wait {
duration: Duration::from_millis(100),
},
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(300),
},
],
"Test chain",
);
assert_eq!(chain.name(), Some("Test chain"));
assert_eq!(chain.len(), 4);
let analysis = chain.analysis();
assert_eq!(analysis.total_actions, 4);
assert_eq!(analysis.read_actions, 1);
assert_eq!(analysis.control_actions, 1);
assert!(analysis.modifies_bias);
}
#[test]
fn test_programmatic_generation() {
let mut chain = ActionChain::empty();
for _ in 0..3 {
chain.push(Action::MoveMotor {
direction: MotorDirection::XPlus,
steps: 10,
});
chain.push(Action::Wait {
duration: Duration::from_millis(100),
});
}
assert_eq!(chain.len(), 6);
assert!(chain.involves_motor());
let actions: Vec<Action> = (0..5).map(|_| Action::ReadBias).collect();
let iter_chain: ActionChain = actions.into_iter().collect();
assert_eq!(iter_chain.len(), 5);
}
#[test]
fn test_pre_built_patterns() {
let status_check = ActionChain::system_status_check();
assert!(status_check.name().is_some());
assert!(!status_check.is_empty());
let approach = ActionChain::safe_tip_approach();
assert!(!approach.control_actions().is_empty());
let positions = vec![Position { x: 1e-9, y: 1e-9 }, Position { x: 2e-9, y: 2e-9 }];
let survey = ActionChain::position_survey(positions);
assert_eq!(survey.len(), 12); }
#[test]
fn test_chain_analysis() {
let chain = ActionChain::new(vec![
Action::MoveMotor {
direction: MotorDirection::XPlus,
steps: 100,
},
Action::SetPiezoPosition {
position: Position { x: 1e-9, y: 1e-9 },
wait_until_finished: true,
},
Action::ReadBias,
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(1),
},
Action::SetBias { voltage: 1.5 },
]);
let analysis = chain.analysis();
assert_eq!(analysis.total_actions, 5);
assert_eq!(analysis.positioning_actions, 2);
assert_eq!(analysis.read_actions, 1);
assert_eq!(analysis.control_actions, 1);
assert!(analysis.involves_motor);
assert!(analysis.involves_piezo);
assert!(analysis.modifies_bias);
}
#[test]
fn test_iteration() {
let chain = ActionChain::new(vec![
Action::ReadBias,
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(1),
},
Action::Wait {
duration: Duration::from_millis(100),
},
]);
let mut count = 0;
for _ in &chain {
count += 1;
}
assert_eq!(count, 3);
let actions: Vec<Action> = chain.into_iter().collect();
assert_eq!(actions.len(), 3);
}
#[test]
fn test_from_vec_action() {
let actions = vec![
Action::ReadBias,
Action::SetBias { voltage: 1.5 },
Action::AutoApproach {
wait_until_finished: true,
timeout: Duration::from_secs(1),
},
];
let chain: ActionChain = actions.into();
assert_eq!(chain.len(), 3);
assert!(chain.name().is_none());
let vec_actions = vec![
Action::ReadBias,
Action::Wait {
duration: Duration::from_millis(50),
},
];
fn accepts_into_action_chain(_chain: impl Into<ActionChain>) {
}
accepts_into_action_chain(vec_actions);
}
}