use std::fmt;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
pub const STEPS_PER_MM: f64 = 400.0;
pub const DEFAULT_ORIGIN: i32 = 400;
pub const DEFAULT_OUTER_LIMIT: i32 = 4400;
pub const MOVING_POLL_INTERVAL: Duration = Duration::from_millis(250);
pub const IDLE_POLL_INTERVAL: Duration = Duration::from_secs(5);
pub const MOVE_TIMEOUT: Duration = Duration::from_secs(300);
pub const MOVE_HESITATION: Duration = Duration::from_millis(100);
pub const RESPONSE_TIMEOUT: Duration = Duration::from_millis(250);
pub const ERROR_RECONNECT_INTERVAL: Duration = Duration::from_secs(600);
pub const NO_ERROR: i32 = 0;
pub const ERROR_SOFT_LIMITS: i32 = 15;
pub const ERROR_UNKNOWN: i32 = 16;
pub const ERROR_BAD_ID: i32 = 17;
pub const ERROR_COMM_ERROR: i32 = 18;
pub const HSC_ERROR_MESSAGES: [&str; 14] = [
"Missing Command",
"Unrecognized Command",
"Input Buffer Overflow",
"No new Alias Given",
"Alias too long",
"Invalid Field Parameter",
"Value Out of Range",
"Parameter is read-only",
"Invalid/Missing Argument",
"No Movement Required",
"Uncalibrated: no motion allowed",
"Motion out of range",
"Invalid/missing direction character",
"Invalid Motor Specified",
];
pub const CSW_PWRLVL: i32 = 0x03;
pub const CSW_LIMITS: i32 = 0x04;
pub const CSW_BANNER: i32 = 0x08;
pub const CSW_ECHO: i32 = 0x10;
pub const CSW_LOCK: i32 = 0x20;
pub const CSW_ALIAS: i32 = 0x40;
pub const CSW_TEXT: i32 = 0x80;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HOrient {
#[default]
LeftRight,
RightLeft,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VOrient {
#[default]
TopBottom,
BottomTop,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HscCommand {
KillAll,
Kill(String),
PositionInquiry(String),
ModuleInquiry(String),
Move(String, i32, i32),
CalibrateImmediate,
ReadRegister(String, u8),
WriteRegister(String, u8, i32),
}
pub mod register {
pub const OUTER_MOTION_LIMIT: u8 = 1;
pub const ORIGIN_POSITION: u8 = 2;
pub const MOTOR_A_POSITION: u8 = 3;
pub const MOTOR_B_POSITION: u8 = 4;
pub const MOTOR_STEP_DELAY: u8 = 5;
pub const GEAR_BACKLASH: u8 = 6;
pub const CONTROL_WORD: u8 = 7;
}
impl HscCommand {
pub fn to_serial(&self) -> String {
match self {
HscCommand::KillAll => "!ALL K".to_string(),
HscCommand::Kill(id) => format!("!{id} K"),
HscCommand::PositionInquiry(id) => format!("!{id} P"),
HscCommand::ModuleInquiry(id) => format!("!{id} I"),
HscCommand::Move(id, a, b) => format!("!{id} M {a} {b}"),
HscCommand::CalibrateImmediate => "!ALL 0 I".to_string(),
HscCommand::ReadRegister(id, reg) => format!("!{id} R {reg}"),
HscCommand::WriteRegister(id, reg, val) => format!("!{id} W {reg} {val}"),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut s = self.to_serial();
s.push('\r');
s.into_bytes()
}
}
impl fmt::Display for HscCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_serial())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HscResponse {
Ok(String),
Busy(String),
Position { id: String, pos_a: i32, pos_b: i32 },
PositionOk { id: String, pos_a: i32, pos_b: i32 },
Error { id: String, code: Option<i32> },
RegisterValue { id: String, value: i32 },
Identity { id: String, info: String },
Unknown(String),
}
impl HscResponse {
pub fn id(&self) -> Option<&str> {
match self {
HscResponse::Ok(id)
| HscResponse::Busy(id)
| HscResponse::Position { id, .. }
| HscResponse::PositionOk { id, .. }
| HscResponse::Error { id, .. }
| HscResponse::RegisterValue { id, .. }
| HscResponse::Identity { id, .. } => Some(id),
HscResponse::Unknown(_) => None,
}
}
}
pub fn parse_response(line: &str) -> HscResponse {
let line = line.trim();
if line.is_empty() {
return HscResponse::Unknown(String::new());
}
let words: Vec<&str> = line.split_whitespace().collect();
if words.is_empty() {
return HscResponse::Unknown(line.to_string());
}
let id = if let Some(stripped) = words[0].strip_prefix('%') {
stripped.to_string()
} else {
return HscResponse::Unknown(line.to_string());
};
match words.len() {
1 => HscResponse::Unknown(line.to_string()),
2 => match words[1] {
"OK;" => HscResponse::Ok(id),
"BUSY;" => HscResponse::Busy(id),
s if s.starts_with("ERROR;") => HscResponse::Error { id, code: None },
_ => HscResponse::Unknown(line.to_string()),
},
3 => {
if words[1] == "ERROR;" {
let code = words[2].parse::<i32>().ok();
HscResponse::Error { id, code }
} else {
if let Ok(value) = words[2].parse::<i32>() {
HscResponse::RegisterValue { id, value }
} else {
HscResponse::Unknown(line.to_string())
}
}
}
4 => {
if words[3] == "DONE;" {
if let (Ok(a), Ok(b)) = (words[1].parse::<i32>(), words[2].parse::<i32>()) {
HscResponse::Position {
id,
pos_a: a,
pos_b: b,
}
} else {
HscResponse::Unknown(line.to_string())
}
} else {
HscResponse::Unknown(line.to_string())
}
}
5 => {
if words[1] == "OK" {
if let (Ok(a), Ok(b)) = (words[2].parse::<i32>(), words[3].parse::<i32>()) {
HscResponse::PositionOk {
id,
pos_a: a,
pos_b: b,
}
} else {
HscResponse::Unknown(line.to_string())
}
} else {
HscResponse::Unknown(line.to_string())
}
}
_ => HscResponse::Unknown(line.to_string()),
}
}
pub fn validate_response(command: &str, response: &str) -> bool {
let response = response.trim();
if response.is_empty() {
return false;
}
let cmd_id = command
.strip_prefix('!')
.unwrap_or(command)
.split_whitespace()
.next()
.unwrap_or("");
let resp_id = response
.strip_prefix('%')
.unwrap_or(response)
.split_whitespace()
.next()
.unwrap_or("");
cmd_id == resp_id
}
pub fn raw_to_dial(raw: i32, origin: i32) -> f64 {
(raw as f64 - origin as f64) / STEPS_PER_MM
}
pub fn dial_to_raw(dial: f64, origin: i32) -> i32 {
(dial * STEPS_PER_MM + 0.5 + origin as f64) as i32
}
pub fn validate_hsc_id(id: &str) -> bool {
if id.is_empty() {
return false;
}
if let Some(rest) = id.strip_prefix("XIAHSC-") {
return parse_id_suffix(rest);
}
parse_id_suffix(id)
}
fn parse_id_suffix(s: &str) -> bool {
let mut chars = s.chars();
let first = match chars.next() {
Some(c) if c.is_ascii_alphabetic() => c,
_ => return false,
};
let rest: String = chars.collect();
if let Some(num_str) = rest.strip_prefix('-') {
return num_str.parse::<i32>().is_ok() && !num_str.is_empty();
}
let _ = first; rest.parse::<i32>().is_ok() && !rest.is_empty()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ControlStatusWord {
pub power_level: i32,
pub limits: bool,
pub banner: bool,
pub echo: bool,
pub lock: bool,
pub alias: bool,
pub text: bool,
}
impl ControlStatusWord {
pub fn from_raw(csw: i32) -> Self {
Self {
power_level: csw & CSW_PWRLVL,
limits: (csw & CSW_LIMITS) != 0,
banner: (csw & CSW_BANNER) != 0,
echo: (csw & CSW_ECHO) != 0,
lock: (csw & CSW_LOCK) != 0,
alias: (csw & CSW_ALIAS) != 0,
text: (csw & CSW_TEXT) != 0,
}
}
pub fn to_raw(&self) -> i32 {
let mut v = self.power_level & CSW_PWRLVL;
if self.limits {
v |= CSW_LIMITS;
}
if self.banner {
v |= CSW_BANNER;
}
if self.echo {
v |= CSW_ECHO;
}
if self.lock {
v |= CSW_LOCK;
}
if self.alias {
v |= CSW_ALIAS;
}
if self.text {
v |= CSW_TEXT;
}
v
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BladePositions {
pub left: f64,
pub right: f64,
pub top: f64,
pub bottom: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SlitGeometry {
pub width: f64,
pub height: f64,
pub h_center: f64,
pub v_center: f64,
}
pub fn width_from_blades(left: f64, right: f64) -> f64 {
left + right
}
pub fn h_center_from_blades(left: f64, right: f64) -> f64 {
(right - left) / 2.0
}
pub fn height_from_blades(top: f64, bottom: f64) -> f64 {
top + bottom
}
pub fn v_center_from_blades(top: f64, bottom: f64) -> f64 {
(top - bottom) / 2.0
}
pub fn blades_from_width_center(width: f64, h_center: f64) -> (f64, f64) {
let left = width / 2.0 - h_center;
let right = width / 2.0 + h_center;
(left, right)
}
pub fn blades_from_height_center(height: f64, v_center: f64) -> (f64, f64) {
let top = height / 2.0 + v_center;
let bottom = height / 2.0 - v_center;
(top, bottom)
}
pub fn geometry_from_blades(blades: &BladePositions) -> SlitGeometry {
SlitGeometry {
width: width_from_blades(blades.left, blades.right),
height: height_from_blades(blades.top, blades.bottom),
h_center: h_center_from_blades(blades.left, blades.right),
v_center: v_center_from_blades(blades.top, blades.bottom),
}
}
pub fn compute_axis_limits(origin: i32, outer_limit: i32) -> (f64, f64) {
let lo = raw_to_dial(0, origin);
let hi = raw_to_dial(outer_limit, origin);
(lo, hi)
}
pub fn compute_width_limits(blade_lo: f64, blade_hi: f64) -> (f64, f64) {
let width_lo = blade_lo.max(0.0);
let width_hi = blade_hi * 2.0;
(width_lo, width_hi)
}
pub fn compute_center_limits(blade_lo: f64, blade_hi: f64) -> (f64, f64) {
let center_lo = (blade_lo - blade_hi) / 2.0;
let center_hi = (blade_hi - blade_lo) / 2.0;
(center_lo, center_hi)
}
pub fn limit_test(lo: f64, val: f64, hi: f64) -> bool {
lo <= val && val <= hi
}
#[derive(Debug, Clone, Copy)]
pub struct AxisLimits {
pub blade_a_lo: f64,
pub blade_a_hi: f64,
pub blade_b_lo: f64,
pub blade_b_hi: f64,
pub gap_lo: f64,
pub gap_hi: f64,
pub center_lo: f64,
pub center_hi: f64,
}
impl AxisLimits {
pub fn from_hsc_params(origin: i32, outer_limit: i32) -> Self {
let (lo, hi) = compute_axis_limits(origin, outer_limit);
let (gap_lo, gap_hi) = compute_width_limits(lo, hi);
let (center_lo, center_hi) = compute_center_limits(lo, hi);
Self {
blade_a_lo: lo,
blade_a_hi: hi,
blade_b_lo: lo,
blade_b_hi: hi,
gap_lo,
gap_hi,
center_lo,
center_hi,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HscState {
Startup,
Disable,
CommError,
Init,
InitLimits,
Idle,
PreMove,
GetReadback,
}
impl fmt::Display for HscState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HscState::Startup => write!(f, "startup"),
HscState::Disable => write!(f, "disable"),
HscState::CommError => write!(f, "comm_error"),
HscState::Init => write!(f, "init"),
HscState::InitLimits => write!(f, "init_limits"),
HscState::Idle => write!(f, "idle"),
HscState::PreMove => write!(f, "premove"),
HscState::GetReadback => write!(f, "get_readback"),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct AxisReadback {
pub blade_a: f64,
pub blade_b: f64,
pub gap: f64,
pub center: f64,
}
pub struct HscController {
pub state: HscState,
pub h_id: String,
pub v_id: String,
pub h_orient: HOrient,
pub v_orient: VOrient,
pub h_origin: i32,
pub v_origin: i32,
pub h_outer_limit: i32,
pub v_outer_limit: i32,
pub h_limits: AxisLimits,
pub v_limits: AxisLimits,
pub h_target: (f64, f64),
pub v_target: (f64, f64),
pub h_readback: AxisReadback,
pub v_readback: AxisReadback,
pub h_busy: bool,
pub v_busy: bool,
pub error: i32,
pub error_msg: String,
pub enabled: bool,
pub init_requested: bool,
pub calibrate_requested: bool,
pub locate_requested: bool,
pub stop_requested: bool,
pub h_move_pending: bool,
pub v_move_pending: bool,
h_target_old: (f64, f64),
v_target_old: (f64, f64),
h_gap_old: f64,
v_gap_old: f64,
h_center_old: f64,
v_center_old: f64,
}
impl Default for HscController {
fn default() -> Self {
let h_limits = AxisLimits::from_hsc_params(DEFAULT_ORIGIN, DEFAULT_OUTER_LIMIT);
let v_limits = AxisLimits::from_hsc_params(DEFAULT_ORIGIN, DEFAULT_OUTER_LIMIT);
Self {
state: HscState::Startup,
h_id: String::new(),
v_id: String::new(),
h_orient: HOrient::default(),
v_orient: VOrient::default(),
h_origin: DEFAULT_ORIGIN,
v_origin: DEFAULT_ORIGIN,
h_outer_limit: DEFAULT_OUTER_LIMIT,
v_outer_limit: DEFAULT_OUTER_LIMIT,
h_limits,
v_limits,
h_target: (0.0, 0.0),
v_target: (0.0, 0.0),
h_readback: AxisReadback::default(),
v_readback: AxisReadback::default(),
h_busy: false,
v_busy: false,
error: NO_ERROR,
error_msg: "no error".to_string(),
enabled: true,
init_requested: true,
calibrate_requested: false,
locate_requested: false,
stop_requested: false,
h_move_pending: false,
v_move_pending: false,
h_target_old: (0.0, 0.0),
v_target_old: (0.0, 0.0),
h_gap_old: 0.0,
v_gap_old: 0.0,
h_center_old: 0.0,
v_center_old: 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HscAction {
None,
SendCommand(HscCommand),
SendCommandReadResponse(HscCommand),
Wait(Duration),
UpdateHReadback(AxisReadback),
UpdateVReadback(AxisReadback),
ReportError(i32, String),
HMotorIdle,
VMotorIdle,
HMotorBusy,
VMotorBusy,
}
impl HscController {
pub fn new(h_id: String, v_id: String) -> Self {
Self {
h_id,
v_id,
..Default::default()
}
}
pub fn set_h_target_blades(&mut self, left: f64, right: f64) -> bool {
let width = width_from_blades(left, right);
let center = h_center_from_blades(left, right);
if !limit_test(self.h_limits.blade_a_lo, left, self.h_limits.blade_a_hi)
|| !limit_test(self.h_limits.blade_b_lo, right, self.h_limits.blade_b_hi)
|| !limit_test(self.h_limits.gap_lo, width, self.h_limits.gap_hi)
|| !limit_test(self.h_limits.center_lo, center, self.h_limits.center_hi)
{
self.error = ERROR_SOFT_LIMITS;
self.error_msg = "H soft limits exceeded".to_string();
return false;
}
self.error = NO_ERROR;
self.error_msg = "no error".to_string();
self.h_target = (left, right);
self.h_move_pending = true;
true
}
pub fn set_h_target_gap_center(&mut self, width: f64, center: f64) -> bool {
let (left, right) = blades_from_width_center(width, center);
self.set_h_target_blades(left, right)
}
pub fn set_v_target_blades(&mut self, top: f64, bottom: f64) -> bool {
let height = height_from_blades(top, bottom);
let center = v_center_from_blades(top, bottom);
if !limit_test(self.v_limits.blade_a_lo, top, self.v_limits.blade_a_hi)
|| !limit_test(self.v_limits.blade_b_lo, bottom, self.v_limits.blade_b_hi)
|| !limit_test(self.v_limits.gap_lo, height, self.v_limits.gap_hi)
|| !limit_test(self.v_limits.center_lo, center, self.v_limits.center_hi)
{
self.error = ERROR_SOFT_LIMITS;
self.error_msg = "V soft limits exceeded".to_string();
return false;
}
self.error = NO_ERROR;
self.error_msg = "no error".to_string();
self.v_target = (top, bottom);
self.v_move_pending = true;
true
}
pub fn set_v_target_gap_center(&mut self, height: f64, center: f64) -> bool {
let (top, bottom) = blades_from_height_center(height, center);
self.set_v_target_blades(top, bottom)
}
pub fn h_raw_positions(&self) -> (i32, i32) {
let (left, right) = self.h_target;
match self.h_orient {
HOrient::LeftRight => (
dial_to_raw(left, self.h_origin),
dial_to_raw(right, self.h_origin),
),
HOrient::RightLeft => (
dial_to_raw(right, self.h_origin),
dial_to_raw(left, self.h_origin),
),
}
}
pub fn v_raw_positions(&self) -> (i32, i32) {
let (top, bottom) = self.v_target;
match self.v_orient {
VOrient::TopBottom => (
dial_to_raw(top, self.v_origin),
dial_to_raw(bottom, self.v_origin),
),
VOrient::BottomTop => (
dial_to_raw(bottom, self.v_origin),
dial_to_raw(top, self.v_origin),
),
}
}
pub fn process_h_position(&mut self, pos_a: i32, pos_b: i32) {
let a_dial = raw_to_dial(pos_a, self.h_origin);
let b_dial = raw_to_dial(pos_b, self.h_origin);
let (left, right) = match self.h_orient {
HOrient::LeftRight => (a_dial, b_dial),
HOrient::RightLeft => (b_dial, a_dial),
};
self.h_readback = AxisReadback {
blade_a: left,
blade_b: right,
gap: width_from_blades(left, right),
center: h_center_from_blades(left, right),
};
self.h_busy = false;
self.h_target_old = (left, right);
self.h_gap_old = self.h_readback.gap;
self.h_center_old = self.h_readback.center;
}
pub fn process_v_position(&mut self, pos_a: i32, pos_b: i32) {
let a_dial = raw_to_dial(pos_a, self.v_origin);
let b_dial = raw_to_dial(pos_b, self.v_origin);
let (top, bottom) = match self.v_orient {
VOrient::TopBottom => (a_dial, b_dial),
VOrient::BottomTop => (b_dial, a_dial),
};
self.v_readback = AxisReadback {
blade_a: top,
blade_b: bottom,
gap: height_from_blades(top, bottom),
center: v_center_from_blades(top, bottom),
};
self.v_busy = false;
self.v_target_old = (top, bottom);
self.v_gap_old = self.v_readback.gap;
self.v_center_old = self.v_readback.center;
}
pub fn update_h_limits(&mut self) {
self.h_limits = AxisLimits::from_hsc_params(self.h_origin, self.h_outer_limit);
}
pub fn update_v_limits(&mut self) {
self.v_limits = AxisLimits::from_hsc_params(self.v_origin, self.v_outer_limit);
}
pub fn process_response(&mut self, response: &HscResponse) {
match response {
HscResponse::Ok(_) => {
}
HscResponse::Busy(id) => {
if id == &self.h_id {
self.h_busy = true;
} else if id == &self.v_id {
self.v_busy = true;
}
}
HscResponse::Position { id, pos_a, pos_b } => {
if id == &self.h_id {
self.process_h_position(*pos_a, *pos_b);
} else if id == &self.v_id {
self.process_v_position(*pos_a, *pos_b);
}
}
HscResponse::PositionOk { id, pos_a, pos_b } => {
if id == &self.h_id {
self.process_h_position(*pos_a, *pos_b);
} else if id == &self.v_id {
self.process_v_position(*pos_a, *pos_b);
}
}
HscResponse::Error { id, code } => {
let code_val = code.unwrap_or(0);
let msg = if (0..14).contains(&code_val) {
HSC_ERROR_MESSAGES[code_val as usize].to_string()
} else {
format!("{}: unknown error", id)
};
self.error = code.unwrap_or(ERROR_UNKNOWN);
self.error_msg = msg;
if id == &self.h_id {
self.h_busy = false;
} else if id == &self.v_id {
self.v_busy = false;
}
}
HscResponse::RegisterValue { .. } => {
}
HscResponse::Identity { .. } => {
}
HscResponse::Unknown(_) => {
}
}
}
}
pub struct HscActorConfig {
pub h_id: String,
pub v_id: String,
pub h_orient: HOrient,
pub v_orient: VOrient,
}
#[derive(Debug, Clone)]
pub enum HscActorCommand {
Init,
SetEnabled(bool),
Stop,
SetHBlades(f64, f64),
SetHGapCenter(f64, f64),
SetVBlades(f64, f64),
SetVGapCenter(f64, f64),
Locate,
Calibrate,
Shutdown,
}
#[derive(Debug, Clone)]
pub struct HscActorStatus {
pub state: HscState,
pub h_readback: AxisReadback,
pub v_readback: AxisReadback,
pub h_busy: bool,
pub v_busy: bool,
pub error: i32,
pub error_msg: String,
pub enabled: bool,
}
pub async fn run<R, W>(
config: HscActorConfig,
reader: R,
writer: W,
mut cmd_rx: tokio::sync::mpsc::Receiver<HscActorCommand>,
status_tx: watch::Sender<HscActorStatus>,
) where
R: tokio::io::AsyncRead + Unpin + Send,
W: tokio::io::AsyncWrite + Unpin + Send,
{
let mut ctrl = HscController::new(config.h_id.clone(), config.v_id.clone());
ctrl.h_orient = config.h_orient;
ctrl.v_orient = config.v_orient;
let mut buf_reader = BufReader::new(reader);
let mut writer = writer;
let mut line_buf = String::new();
async fn send_cmd<W2: tokio::io::AsyncWrite + Unpin>(
writer: &mut W2,
cmd: &HscCommand,
) -> Result<(), std::io::Error> {
let bytes = cmd.to_bytes();
writer.write_all(&bytes).await?;
writer.flush().await?;
Ok(())
}
async fn read_response<R2: tokio::io::AsyncBufRead + Unpin>(
reader: &mut R2,
buf: &mut String,
) -> Result<HscResponse, std::io::Error> {
buf.clear();
let n = tokio::time::timeout(RESPONSE_TIMEOUT, reader.read_line(buf)).await;
match n {
Ok(Ok(0)) => Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"serial port closed",
)),
Ok(Ok(_)) => Ok(parse_response(buf)),
Ok(Err(e)) => Err(e),
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"response timeout",
)),
}
}
async fn send_and_read<
R2: tokio::io::AsyncBufRead + Unpin,
W2: tokio::io::AsyncWrite + Unpin,
>(
writer: &mut W2,
reader: &mut R2,
buf: &mut String,
cmd: &HscCommand,
) -> Result<HscResponse, std::io::Error> {
send_cmd(writer, cmd).await?;
read_response(reader, buf).await
}
fn publish_status(ctrl: &HscController, tx: &watch::Sender<HscActorStatus>) {
let _ = tx.send(HscActorStatus {
state: ctrl.state,
h_readback: ctrl.h_readback,
v_readback: ctrl.v_readback,
h_busy: ctrl.h_busy,
v_busy: ctrl.v_busy,
error: ctrl.error,
error_msg: ctrl.error_msg.clone(),
enabled: ctrl.enabled,
});
}
info!("HSC actor starting: h_id={}, v_id={}", ctrl.h_id, ctrl.v_id);
ctrl.state = HscState::Init;
publish_status(&ctrl, &status_tx);
loop {
match ctrl.state {
HscState::Startup | HscState::Init => {
if ctrl.h_id == ctrl.v_id {
ctrl.error = ERROR_BAD_ID;
ctrl.error_msg = "H & V IDs must be different".to_string();
publish_status(&ctrl, &status_tx);
tokio::time::sleep(Duration::from_secs(30)).await;
continue;
}
if !validate_hsc_id(&ctrl.h_id) {
ctrl.error = ERROR_BAD_ID;
ctrl.error_msg = "H ID not a valid HSC ID".to_string();
publish_status(&ctrl, &status_tx);
tokio::time::sleep(Duration::from_secs(30)).await;
continue;
}
if !validate_hsc_id(&ctrl.v_id) {
ctrl.error = ERROR_BAD_ID;
ctrl.error_msg = "V ID not a valid HSC ID".to_string();
publish_status(&ctrl, &status_tx);
tokio::time::sleep(Duration::from_secs(30)).await;
continue;
}
ctrl.h_busy = false;
ctrl.v_busy = false;
ctrl.error = NO_ERROR;
ctrl.error_msg = "no error".to_string();
if let Err(e) = send_cmd(&mut writer, &HscCommand::KillAll).await {
error!("Failed to send kill command: {e}");
ctrl.state = HscState::CommError;
publish_status(&ctrl, &status_tx);
continue;
}
tokio::time::sleep(Duration::from_millis(100)).await;
ctrl.state = HscState::InitLimits;
publish_status(&ctrl, &status_tx);
}
HscState::InitLimits => {
let mut read_error = false;
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::ReadRegister(ctrl.h_id.clone(), register::OUTER_MOTION_LIMIT),
)
.await
{
Ok(HscResponse::RegisterValue { value, .. }) => {
ctrl.h_outer_limit = value;
}
Ok(resp) => {
debug!("Unexpected response reading H outer limit: {:?}", resp);
read_error = true;
}
Err(e) => {
warn!("Error reading H outer limit: {e}");
read_error = true;
}
}
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::ReadRegister(ctrl.h_id.clone(), register::ORIGIN_POSITION),
)
.await
{
Ok(HscResponse::RegisterValue { value, .. }) => {
ctrl.h_origin = value;
}
Ok(resp) => {
debug!("Unexpected response reading H origin: {:?}", resp);
read_error = true;
}
Err(e) => {
warn!("Error reading H origin: {e}");
read_error = true;
}
}
if !read_error {
ctrl.update_h_limits();
}
read_error = false;
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::ReadRegister(ctrl.v_id.clone(), register::OUTER_MOTION_LIMIT),
)
.await
{
Ok(HscResponse::RegisterValue { value, .. }) => {
ctrl.v_outer_limit = value;
}
Ok(resp) => {
debug!("Unexpected response reading V outer limit: {:?}", resp);
read_error = true;
}
Err(e) => {
warn!("Error reading V outer limit: {e}");
read_error = true;
}
}
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::ReadRegister(ctrl.v_id.clone(), register::ORIGIN_POSITION),
)
.await
{
Ok(HscResponse::RegisterValue { value, .. }) => {
ctrl.v_origin = value;
}
Ok(resp) => {
debug!("Unexpected response reading V origin: {:?}", resp);
read_error = true;
}
Err(e) => {
warn!("Error reading V origin: {e}");
read_error = true;
}
}
if !read_error {
ctrl.update_v_limits();
}
ctrl.locate_requested = true;
ctrl.state = HscState::Idle;
publish_status(&ctrl, &status_tx);
}
HscState::Disable => {
publish_status(&ctrl, &status_tx);
loop {
match cmd_rx.recv().await {
Some(HscActorCommand::SetEnabled(true)) => {
ctrl.enabled = true;
ctrl.init_requested = true;
ctrl.state = HscState::Init;
break;
}
Some(HscActorCommand::Shutdown) => {
info!("HSC actor shutting down");
return;
}
None => {
info!("HSC actor command channel closed");
return;
}
_ => {} }
}
publish_status(&ctrl, &status_tx);
}
HscState::CommError => {
ctrl.error = ERROR_COMM_ERROR;
ctrl.error_msg = "communications error".to_string();
publish_status(&ctrl, &status_tx);
tokio::select! {
_ = tokio::time::sleep(ERROR_RECONNECT_INTERVAL) => {
ctrl.state = HscState::Init;
}
cmd = cmd_rx.recv() => {
match cmd {
Some(HscActorCommand::Init) => {
ctrl.state = HscState::Init;
}
Some(HscActorCommand::Shutdown) => {
info!("HSC actor shutting down");
return;
}
None => {
info!("HSC actor command channel closed");
return;
}
_ => {}
}
}
}
publish_status(&ctrl, &status_tx);
}
HscState::Idle => {
if !ctrl.enabled {
ctrl.state = HscState::Disable;
publish_status(&ctrl, &status_tx);
continue;
}
if ctrl.init_requested {
ctrl.init_requested = false;
ctrl.state = HscState::Init;
publish_status(&ctrl, &status_tx);
continue;
}
if ctrl.stop_requested {
ctrl.stop_requested = false;
if let Err(e) = send_cmd(&mut writer, &HscCommand::KillAll).await {
error!("Failed to send kill command: {e}");
ctrl.state = HscState::CommError;
publish_status(&ctrl, &status_tx);
continue;
}
tokio::time::sleep(Duration::from_millis(100)).await;
ctrl.locate_requested = true;
}
if ctrl.calibrate_requested {
ctrl.calibrate_requested = false;
if let Err(e) = send_cmd(&mut writer, &HscCommand::CalibrateImmediate).await {
error!("Failed to send calibrate command: {e}");
ctrl.state = HscState::CommError;
publish_status(&ctrl, &status_tx);
continue;
}
tokio::time::sleep(Duration::from_millis(100)).await;
ctrl.locate_requested = true;
}
if ctrl.h_move_pending {
ctrl.h_move_pending = false;
if ctrl.h_busy {
let _ = send_cmd(&mut writer, &HscCommand::Kill(ctrl.h_id.clone())).await;
tokio::time::sleep(Duration::from_millis(100)).await;
}
ctrl.h_busy = true;
let (pos_a, pos_b) = ctrl.h_raw_positions();
let cmd = HscCommand::Move(ctrl.h_id.clone(), pos_a, pos_b);
if let Err(e) = send_cmd(&mut writer, &cmd).await {
error!("Failed to send H move: {e}");
ctrl.state = HscState::CommError;
publish_status(&ctrl, &status_tx);
continue;
}
tokio::time::sleep(Duration::from_millis(100)).await;
ctrl.locate_requested = true;
publish_status(&ctrl, &status_tx);
}
if ctrl.v_move_pending {
ctrl.v_move_pending = false;
if ctrl.v_busy {
let _ = send_cmd(&mut writer, &HscCommand::Kill(ctrl.v_id.clone())).await;
tokio::time::sleep(Duration::from_millis(100)).await;
}
ctrl.v_busy = true;
let (pos_a, pos_b) = ctrl.v_raw_positions();
let cmd = HscCommand::Move(ctrl.v_id.clone(), pos_a, pos_b);
if let Err(e) = send_cmd(&mut writer, &cmd).await {
error!("Failed to send V move: {e}");
ctrl.state = HscState::CommError;
publish_status(&ctrl, &status_tx);
continue;
}
tokio::time::sleep(Duration::from_millis(100)).await;
ctrl.locate_requested = true;
publish_status(&ctrl, &status_tx);
}
if ctrl.locate_requested {
ctrl.locate_requested = false;
ctrl.state = HscState::GetReadback;
publish_status(&ctrl, &status_tx);
continue;
}
let poll = if ctrl.h_busy || ctrl.v_busy {
MOVING_POLL_INTERVAL
} else {
IDLE_POLL_INTERVAL
};
tokio::select! {
_ = tokio::time::sleep(poll) => {
ctrl.locate_requested = true;
}
cmd = cmd_rx.recv() => {
match cmd {
Some(HscActorCommand::Init) => {
ctrl.init_requested = true;
}
Some(HscActorCommand::SetEnabled(en)) => {
ctrl.enabled = en;
}
Some(HscActorCommand::Stop) => {
ctrl.stop_requested = true;
}
Some(HscActorCommand::SetHBlades(l, r)) => {
ctrl.set_h_target_blades(l, r);
}
Some(HscActorCommand::SetHGapCenter(w, c)) => {
ctrl.set_h_target_gap_center(w, c);
}
Some(HscActorCommand::SetVBlades(t, b)) => {
ctrl.set_v_target_blades(t, b);
}
Some(HscActorCommand::SetVGapCenter(h, c)) => {
ctrl.set_v_target_gap_center(h, c);
}
Some(HscActorCommand::Locate) => {
ctrl.locate_requested = true;
}
Some(HscActorCommand::Calibrate) => {
ctrl.calibrate_requested = true;
}
Some(HscActorCommand::Shutdown) => {
info!("HSC actor shutting down");
return;
}
None => {
info!("HSC actor command channel closed");
return;
}
}
}
}
publish_status(&ctrl, &status_tx);
}
HscState::GetReadback => {
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::PositionInquiry(ctrl.h_id.clone()),
)
.await
{
Ok(ref resp) => {
let cmd_str = HscCommand::PositionInquiry(ctrl.h_id.clone()).to_serial();
if validate_response(&cmd_str, &line_buf) {
ctrl.process_response(resp);
} else {
debug!("H position response ID mismatch");
}
}
Err(e) => {
warn!("Error reading H position: {e}");
}
}
match send_and_read(
&mut writer,
&mut buf_reader,
&mut line_buf,
&HscCommand::PositionInquiry(ctrl.v_id.clone()),
)
.await
{
Ok(ref resp) => {
let cmd_str = HscCommand::PositionInquiry(ctrl.v_id.clone()).to_serial();
if validate_response(&cmd_str, &line_buf) {
ctrl.process_response(resp);
} else {
debug!("V position response ID mismatch");
}
}
Err(e) => {
warn!("Error reading V position: {e}");
}
}
ctrl.state = HscState::Idle;
publish_status(&ctrl, &status_tx);
}
HscState::PreMove => {
ctrl.state = HscState::Idle;
publish_status(&ctrl, &status_tx);
}
}
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn raw_to_dial_at_origin() {
let origin = 400;
assert!((raw_to_dial(400, origin) - 0.0).abs() < 1e-9);
}
#[test]
fn raw_to_dial_positive() {
let origin = 400;
assert!((raw_to_dial(800, origin) - 1.0).abs() < 1e-9);
}
#[test]
fn raw_to_dial_negative() {
let origin = 400;
assert!((raw_to_dial(0, origin) - (-1.0)).abs() < 1e-9);
}
#[test]
fn dial_to_raw_roundtrip() {
let origin = 400;
for raw in [0, 100, 400, 800, 1200, 4400] {
let dial = raw_to_dial(raw, origin);
let back = dial_to_raw(dial, origin);
assert_eq!(back, raw, "roundtrip failed for raw={raw}");
}
}
#[test]
fn dial_to_raw_at_origin() {
assert_eq!(dial_to_raw(0.0, 400), 400);
}
#[test]
fn width_from_blades_symmetric() {
assert!((width_from_blades(2.5, 2.5) - 5.0).abs() < 1e-9);
}
#[test]
fn h_center_from_blades_centered() {
assert!((h_center_from_blades(2.5, 2.5) - 0.0).abs() < 1e-9);
}
#[test]
fn h_center_from_blades_offset() {
assert!((h_center_from_blades(1.0, 3.0) - 1.0).abs() < 1e-9);
}
#[test]
fn blades_from_width_center_roundtrip() {
let left = 1.5;
let right = 3.5;
let w = width_from_blades(left, right);
let c = h_center_from_blades(left, right);
let (l2, r2) = blades_from_width_center(w, c);
assert!((l2 - left).abs() < 1e-9);
assert!((r2 - right).abs() < 1e-9);
}
#[test]
fn height_and_v_center_roundtrip() {
let top = 4.0;
let bottom = 2.0;
let h = height_from_blades(top, bottom);
let c = v_center_from_blades(top, bottom);
let (t2, b2) = blades_from_height_center(h, c);
assert!((t2 - top).abs() < 1e-9);
assert!((b2 - bottom).abs() < 1e-9);
}
#[test]
fn geometry_from_blades_all() {
let bp = BladePositions {
left: 1.0,
right: 3.0,
top: 4.0,
bottom: 2.0,
};
let g = geometry_from_blades(&bp);
assert!((g.width - 4.0).abs() < 1e-9);
assert!((g.height - 6.0).abs() < 1e-9);
assert!((g.h_center - 1.0).abs() < 1e-9);
assert!((g.v_center - 1.0).abs() < 1e-9);
}
#[test]
fn axis_limits_default() {
let lim = AxisLimits::from_hsc_params(DEFAULT_ORIGIN, DEFAULT_OUTER_LIMIT);
let (lo, hi) = compute_axis_limits(DEFAULT_ORIGIN, DEFAULT_OUTER_LIMIT);
assert!((lim.blade_a_lo - lo).abs() < 1e-9);
assert!((lim.blade_a_hi - hi).abs() < 1e-9);
assert!((lo - (-1.0)).abs() < 1e-9);
assert!((hi - 10.0).abs() < 1e-9);
}
#[test]
fn limit_test_passes() {
assert!(limit_test(0.0, 5.0, 10.0));
assert!(limit_test(0.0, 0.0, 10.0));
assert!(limit_test(0.0, 10.0, 10.0));
}
#[test]
fn limit_test_fails() {
assert!(!limit_test(0.0, -0.1, 10.0));
assert!(!limit_test(0.0, 10.1, 10.0));
}
#[test]
fn command_kill_all() {
assert_eq!(HscCommand::KillAll.to_serial(), "!ALL K");
}
#[test]
fn command_position_inquiry() {
assert_eq!(
HscCommand::PositionInquiry("H-1234".into()).to_serial(),
"!H-1234 P"
);
}
#[test]
fn command_move() {
assert_eq!(
HscCommand::Move("V-5678".into(), 100, 200).to_serial(),
"!V-5678 M 100 200"
);
}
#[test]
fn command_read_register() {
assert_eq!(
HscCommand::ReadRegister("H-1".into(), 1).to_serial(),
"!H-1 R 1"
);
}
#[test]
fn command_write_register() {
assert_eq!(
HscCommand::WriteRegister("H-1".into(), 7, 255).to_serial(),
"!H-1 W 7 255"
);
}
#[test]
fn command_calibrate() {
assert_eq!(HscCommand::CalibrateImmediate.to_serial(), "!ALL 0 I");
}
#[test]
fn command_to_bytes_has_cr() {
let bytes = HscCommand::KillAll.to_bytes();
assert_eq!(bytes, b"!ALL K\r");
}
#[test]
fn parse_ok() {
assert_eq!(
parse_response("%H-1234 OK;"),
HscResponse::Ok("H-1234".into())
);
}
#[test]
fn parse_busy() {
assert_eq!(
parse_response("%V-5678 BUSY;"),
HscResponse::Busy("V-5678".into())
);
}
#[test]
fn parse_position_done() {
assert_eq!(
parse_response("%H-1234 500 600 DONE;"),
HscResponse::Position {
id: "H-1234".into(),
pos_a: 500,
pos_b: 600,
}
);
}
#[test]
fn parse_position_ok() {
assert_eq!(
parse_response("%H-1234 OK 500 600 DONE;"),
HscResponse::PositionOk {
id: "H-1234".into(),
pos_a: 500,
pos_b: 600,
}
);
}
#[test]
fn parse_error_no_code() {
assert_eq!(
parse_response("%H-1234 ERROR;"),
HscResponse::Error {
id: "H-1234".into(),
code: None,
}
);
}
#[test]
fn parse_error_with_code() {
assert_eq!(
parse_response("%H-1234 ERROR; 5"),
HscResponse::Error {
id: "H-1234".into(),
code: Some(5),
}
);
}
#[test]
fn parse_register_value() {
assert_eq!(
parse_response("%H-1234 R 4400"),
HscResponse::RegisterValue {
id: "H-1234".into(),
value: 4400,
}
);
}
#[test]
fn parse_empty() {
assert_eq!(parse_response(""), HscResponse::Unknown(String::new()));
}
#[test]
fn parse_no_prefix() {
match parse_response("no prefix here") {
HscResponse::Unknown(_) => {}
other => panic!("Expected Unknown, got {:?}", other),
}
}
#[test]
fn parse_whitespace_trimmed() {
assert_eq!(
parse_response(" %H-1 OK; \n"),
HscResponse::Ok("H-1".into())
);
}
#[test]
fn validate_response_matching() {
assert!(validate_response("!H-1234 P", "%H-1234 500 600 DONE;"));
}
#[test]
fn validate_response_mismatch() {
assert!(!validate_response("!H-1234 P", "%V-5678 500 600 DONE;"));
}
#[test]
fn validate_response_empty() {
assert!(!validate_response("!H-1234 P", ""));
}
#[test]
fn valid_hsc_ids() {
assert!(validate_hsc_id("XIAHSC-H-1234"));
assert!(validate_hsc_id("H-1234"));
assert!(validate_hsc_id("V-5678"));
assert!(validate_hsc_id("A1234"));
}
#[test]
fn invalid_hsc_ids() {
assert!(!validate_hsc_id(""));
assert!(!validate_hsc_id("1234"));
assert!(!validate_hsc_id("-1234"));
assert!(!validate_hsc_id("H-"));
}
#[test]
fn csw_decode_zero() {
let csw = ControlStatusWord::from_raw(0);
assert_eq!(csw.power_level, 0);
assert!(!csw.limits);
assert!(!csw.banner);
assert!(!csw.echo);
assert!(!csw.lock);
assert!(!csw.alias);
assert!(!csw.text);
}
#[test]
fn csw_decode_all_set() {
let csw = ControlStatusWord::from_raw(0xFF);
assert_eq!(csw.power_level, 3);
assert!(csw.limits);
assert!(csw.banner);
assert!(csw.echo);
assert!(csw.lock);
assert!(csw.alias);
assert!(csw.text);
}
#[test]
fn csw_roundtrip() {
for raw in [0, 1, 2, 3, 0x04, 0x44, 0x7F, 0xFF] {
let csw = ControlStatusWord::from_raw(raw);
assert_eq!(csw.to_raw(), raw & 0xFF);
}
}
#[test]
fn controller_set_h_target_within_limits() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.v_id = "V-1".to_string();
ctrl.update_h_limits();
assert!(ctrl.set_h_target_blades(1.0, 2.0));
assert_eq!(ctrl.h_target, (1.0, 2.0));
assert!(ctrl.h_move_pending);
}
#[test]
fn controller_set_h_target_exceeds_limits() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.v_id = "V-1".to_string();
ctrl.update_h_limits();
assert!(!ctrl.set_h_target_blades(-2.0, 2.0)); assert_eq!(ctrl.error, ERROR_SOFT_LIMITS);
}
#[test]
fn controller_process_h_position_left_right() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.h_orient = HOrient::LeftRight;
ctrl.h_origin = 400;
ctrl.process_h_position(800, 1200);
assert!((ctrl.h_readback.blade_a - 1.0).abs() < 1e-9);
assert!((ctrl.h_readback.blade_b - 2.0).abs() < 1e-9);
assert!((ctrl.h_readback.gap - 3.0).abs() < 1e-9);
assert!((ctrl.h_readback.center - 0.5).abs() < 1e-9);
}
#[test]
fn controller_process_h_position_right_left() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.h_orient = HOrient::RightLeft;
ctrl.h_origin = 400;
ctrl.process_h_position(800, 1200);
assert!((ctrl.h_readback.blade_a - 2.0).abs() < 1e-9); assert!((ctrl.h_readback.blade_b - 1.0).abs() < 1e-9); }
#[test]
fn controller_process_response_busy() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.v_id = "V-1".to_string();
ctrl.process_response(&HscResponse::Busy("H-1".into()));
assert!(ctrl.h_busy);
assert!(!ctrl.v_busy);
}
#[test]
fn controller_process_response_error() {
let mut ctrl = HscController::default();
ctrl.h_id = "H-1".to_string();
ctrl.v_id = "V-1".to_string();
ctrl.h_busy = true;
ctrl.process_response(&HscResponse::Error {
id: "H-1".into(),
code: Some(6),
});
assert!(!ctrl.h_busy);
assert_eq!(ctrl.error, 6);
assert_eq!(ctrl.error_msg, "Value Out of Range");
}
#[test]
fn controller_h_raw_positions_left_right() {
let mut ctrl = HscController::default();
ctrl.h_orient = HOrient::LeftRight;
ctrl.h_origin = 400;
ctrl.h_target = (1.0, 2.0);
let (a, b) = ctrl.h_raw_positions();
assert_eq!(a, dial_to_raw(1.0, 400));
assert_eq!(b, dial_to_raw(2.0, 400));
}
#[test]
fn controller_h_raw_positions_right_left() {
let mut ctrl = HscController::default();
ctrl.h_orient = HOrient::RightLeft;
ctrl.h_origin = 400;
ctrl.h_target = (1.0, 2.0); let (a, b) = ctrl.h_raw_positions();
assert_eq!(a, dial_to_raw(2.0, 400));
assert_eq!(b, dial_to_raw(1.0, 400));
}
}