use crate::types::{MotorErrorKind, MotorMode};
use super::types::Logic;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tpdo1Frame {
pub position_rev: f32,
pub timestamp_us: u32,
pub torque_permille: i16,
pub error_code: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tpdo2Frame {
pub status_word: u16,
pub driver_temp_x10: i16,
pub motor_temp_x10: i16,
pub control_word_readback: u16,
pub error_code: u16,
}
impl Tpdo2Frame {
pub fn driver_temp_c(&self) -> f32 {
self.driver_temp_x10 as f32 * 0.1
}
pub fn motor_temp_c(&self) -> f32 {
self.motor_temp_x10 as f32 * 0.1
}
}
pub fn decode_tpdo1(data: &[u8]) -> Option<Tpdo1Frame> {
if data.len() < 12 {
return None;
}
Some(Tpdo1Frame {
position_rev: f32::from_le_bytes([data[0], data[1], data[2], data[3]]),
timestamp_us: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
torque_permille: i16::from_le_bytes([data[8], data[9]]),
error_code: u16::from_le_bytes([data[10], data[11]]),
})
}
pub fn decode_tpdo2(data: &[u8]) -> Option<Tpdo2Frame> {
if data.len() < 10 {
return None;
}
Some(Tpdo2Frame {
status_word: u16::from_le_bytes([data[0], data[1]]),
driver_temp_x10: i16::from_le_bytes([data[2], data[3]]),
motor_temp_x10: i16::from_le_bytes([data[4], data[5]]),
control_word_readback: u16::from_le_bytes([data[6], data[7]]),
error_code: u16::from_le_bytes([data[8], data[9]]),
})
}
pub fn status_word_has_fault(sw: u16) -> bool {
(sw & 0x0008) != 0
}
pub fn status_word_is_operation_enabled(sw: u16) -> bool {
(sw & 0x0007) == 0x0007
}
pub fn status_word_to_logic(
sw: u16,
current_target_mode: Option<MotorMode>,
error_code: u16,
) -> Logic {
if status_word_has_fault(sw) {
return Logic::Error {
kind: error_code_to_kind(error_code),
raw_code: error_code,
};
}
if status_word_is_operation_enabled(sw) {
if let Some(m) = current_target_mode {
return Logic::Enabled(m);
}
}
Logic::Disabled
}
pub fn error_code_to_kind(code: u16) -> MotorErrorKind {
match code {
0x2310 => MotorErrorKind::OverCurrent,
0x3210 => MotorErrorKind::OverVoltage,
0x3220 => MotorErrorKind::UnderVoltage,
0x4210 => MotorErrorKind::DriverOverTemp,
0x4310 => MotorErrorKind::MotorOverTemp,
0x7305 => MotorErrorKind::EncoderError,
0x8130 => MotorErrorKind::HeartbeatLost,
_ => MotorErrorKind::Other,
}
}
pub fn mode_to_cia402_code(mode: MotorMode) -> i8 {
match mode {
MotorMode::ProfilePosition => 1,
MotorMode::ProfileVelocity => 3,
MotorMode::Torque => 4,
MotorMode::Mit => 5,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_tpdo1_too_short_returns_none() {
assert!(decode_tpdo1(&[]).is_none());
assert!(decode_tpdo1(&[0; 11]).is_none());
}
#[test]
fn decode_tpdo1_roundtrip() {
let mut buf = [0u8; 12];
buf[..4].copy_from_slice(&0.25f32.to_le_bytes());
buf[4..8].copy_from_slice(&12345u32.to_le_bytes());
buf[8..10].copy_from_slice(&(-500i16).to_le_bytes());
buf[10..12].copy_from_slice(&0x2310u16.to_le_bytes());
let f = decode_tpdo1(&buf).unwrap();
assert!((f.position_rev - 0.25).abs() < f32::EPSILON);
assert_eq!(f.timestamp_us, 12345);
assert_eq!(f.torque_permille, -500);
assert_eq!(f.error_code, 0x2310);
}
#[test]
fn decode_tpdo2_too_short_returns_none() {
assert!(decode_tpdo2(&[0; 9]).is_none());
}
#[test]
fn decode_tpdo2_roundtrip() {
let mut buf = [0u8; 10];
buf[..2].copy_from_slice(&0x0237u16.to_le_bytes()); buf[2..4].copy_from_slice(&425i16.to_le_bytes()); buf[4..6].copy_from_slice(&510i16.to_le_bytes()); buf[6..8].copy_from_slice(&0x000Fu16.to_le_bytes());
buf[8..10].copy_from_slice(&0u16.to_le_bytes());
let f = decode_tpdo2(&buf).unwrap();
assert_eq!(f.status_word, 0x0237);
assert!((f.driver_temp_c() - 42.5).abs() < 0.01);
assert!((f.motor_temp_c() - 51.0).abs() < 0.01);
assert_eq!(f.control_word_readback, 0x000F);
assert_eq!(f.error_code, 0);
}
#[test]
fn status_fault_detection() {
assert!(!status_word_has_fault(0x0237));
assert!(status_word_has_fault(0x0008));
assert!(status_word_has_fault(0x00FF));
}
#[test]
fn status_op_enabled_detection() {
assert!(status_word_is_operation_enabled(0x0237));
assert!(status_word_is_operation_enabled(0x0007));
assert!(!status_word_is_operation_enabled(0x0003)); assert!(!status_word_is_operation_enabled(0x0040)); }
#[test]
fn logic_from_sw_fault_overrides_enabled_bits() {
let l = status_word_to_logic(0x000F, Some(MotorMode::ProfileVelocity), 0x2310);
assert!(matches!(
l,
Logic::Error {
kind: MotorErrorKind::OverCurrent,
raw_code: 0x2310,
}
));
}
#[test]
fn logic_from_sw_enabled_uses_target_mode() {
let l = status_word_to_logic(0x0237, Some(MotorMode::ProfileVelocity), 0);
assert_eq!(l, Logic::Enabled(MotorMode::ProfileVelocity));
}
#[test]
fn logic_from_sw_enabled_without_target_mode_is_disabled() {
let l = status_word_to_logic(0x0237, None, 0);
assert_eq!(l, Logic::Disabled);
}
#[test]
fn logic_from_sw_not_enabled_is_disabled() {
let l = status_word_to_logic(0x0040, Some(MotorMode::ProfileVelocity), 0);
assert_eq!(l, Logic::Disabled);
}
#[test]
fn error_code_known_codes_map_correctly() {
assert_eq!(error_code_to_kind(0x2310), MotorErrorKind::OverCurrent);
assert_eq!(error_code_to_kind(0x3210), MotorErrorKind::OverVoltage);
assert_eq!(error_code_to_kind(0x3220), MotorErrorKind::UnderVoltage);
assert_eq!(error_code_to_kind(0x4210), MotorErrorKind::DriverOverTemp);
assert_eq!(error_code_to_kind(0x8130), MotorErrorKind::HeartbeatLost);
assert_eq!(error_code_to_kind(0x9999), MotorErrorKind::Other);
}
#[test]
fn mode_codes_match_cia402_spec() {
assert_eq!(mode_to_cia402_code(MotorMode::ProfilePosition), 1);
assert_eq!(mode_to_cia402_code(MotorMode::ProfileVelocity), 3);
assert_eq!(mode_to_cia402_code(MotorMode::Torque), 4);
assert_eq!(mode_to_cia402_code(MotorMode::Mit), 5);
}
}