use std::time::Instant;
use crate::canopen::nmt::NmtState;
use crate::types::{MotorErrorKind, MotorIdentity, MotorMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotorLifecycle {
Unknown,
Identified,
Initializing,
Initialized,
NeedsReinit { reason: ReinitReason },
}
impl MotorLifecycle {
pub fn is_ready(&self) -> bool {
matches!(self, MotorLifecycle::Initialized)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReinitReason {
LeftOperational,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Logic {
Disabled,
Enabled(MotorMode),
Error { kind: MotorErrorKind, raw_code: u16 },
}
#[derive(Debug, Clone)]
pub struct MotorInfo {
pub node_id: u8,
pub identity: Option<MotorIdentity>,
pub lifecycle: MotorLifecycle,
pub online: bool,
pub logic: Option<Logic>,
pub nmt_state: Option<NmtState>,
pub peak_torque_nm: Option<f32>,
}
impl MotorInfo {
pub fn is_ready(&self) -> bool {
self.lifecycle.is_ready() && self.online
}
pub fn friendly_name(&self) -> String {
match &self.identity {
Some(id) => crate::cia402::known_devices::human_friendly_name(id),
None => format!("Node 0x{:02X}", self.node_id),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Measurements {
pub position_rev: Option<f32>,
pub velocity_rev_per_s: Option<f32>,
pub torque_nm: Option<f32>,
pub driver_temp_c: Option<f32>,
pub motor_temp_c: Option<f32>,
pub status_word: Option<u16>,
pub mode_display: Option<u8>,
pub error_register: Option<u8>,
pub timestamp_us: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct Connection {
pub last_heartbeat: Option<Instant>,
pub last_tpdo: Option<Instant>,
pub online: bool,
pub nmt_state: Option<NmtState>,
}
#[derive(Debug, Clone)]
pub struct LiveState {
pub connection: Connection,
pub logic: Option<Logic>,
pub measurements: Measurements,
pub timestamp: Instant,
}
impl LiveState {
pub fn empty(now: Instant) -> Self {
Self {
connection: Connection::default(),
logic: None,
measurements: Measurements::default(),
timestamp: now,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lifecycle_is_ready_only_initialized() {
assert!(MotorLifecycle::Initialized.is_ready());
assert!(!MotorLifecycle::Unknown.is_ready());
assert!(!MotorLifecycle::Identified.is_ready());
assert!(!MotorLifecycle::Initializing.is_ready());
assert!(!MotorLifecycle::NeedsReinit {
reason: ReinitReason::LeftOperational
}
.is_ready());
}
fn motor_info(
nid: u8,
identity: Option<MotorIdentity>,
lifecycle: MotorLifecycle,
online: bool,
) -> MotorInfo {
MotorInfo {
node_id: nid,
identity,
lifecycle,
online,
logic: None,
nmt_state: None,
peak_torque_nm: None,
}
}
#[test]
fn motor_info_is_ready_requires_both() {
let id = Some(MotorIdentity {
node_id: 0x10,
vendor_id: 0,
product_code: 0,
revision_number: 0,
serial_number: 0,
product_name: None,
});
assert!(motor_info(0x10, id.clone(), MotorLifecycle::Initialized, true).is_ready());
assert!(!motor_info(0x10, id.clone(), MotorLifecycle::Initialized, false).is_ready());
assert!(!motor_info(0x10, id.clone(), MotorLifecycle::Identified, true).is_ready());
assert!(!motor_info(0x10, None, MotorLifecycle::Unknown, true).is_ready());
}
#[test]
fn friendly_name_falls_back_when_no_identity() {
let m = motor_info(0x42, None, MotorLifecycle::Unknown, true);
assert_eq!(m.friendly_name(), "Node 0x42");
}
#[test]
fn friendly_name_uses_known_devices_table() {
let id = MotorIdentity {
node_id: 0x10,
vendor_id: 0x0068_6578,
product_code: 0xAAAA_0002,
revision_number: 0,
serial_number: 0,
product_name: None,
};
let m = motor_info(0x10, Some(id), MotorLifecycle::Identified, true);
assert_eq!(m.friendly_name(), "HexMeow Motor");
}
}