use std::time::Duration;
use crate::canopen::tpdo_config::SdoWrite;
use crate::error::{Error, Result};
use crate::types::{MotorMode, MotorTarget};
use super::codec::mode_to_cia402_code;
use super::types::Logic;
const OD_CONTROL_WORD: u16 = 0x6040;
const OD_MODE_OF_OPERATION: u16 = 0x6060;
const OD_TARGET_POSITION: u16 = 0x607A;
const OD_TARGET_TORQUE: u16 = 0x6071;
const OD_TARGET_VELOCITY: u16 = 0x60FF;
const OD_MIT_CONTROL_PARAM: u16 = 0x2003;
mod mit {
pub const SUB_POSITION: u8 = 0x01; pub const SUB_VELOCITY: u8 = 0x02; pub const SUB_TORQUE: u8 = 0x03; pub const SUB_KP: u8 = 0x04; pub const SUB_KD: u8 = 0x05; }
mod cw {
pub const SHUTDOWN: u16 = 0x0006;
pub const SWITCH_ON: u16 = 0x0007;
pub const ENABLE_OPERATION: u16 = 0x000F;
pub const FAULT_RESET: u16 = 0x0080;
pub const ENABLE_PP_NEW_SP_CLEARED: u16 = 0x002F;
pub const ENABLE_PP_NEW_SP_LATCHED: u16 = 0x003F;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SetTargetContext {
pub current_mode: Option<MotorMode>,
pub peak_torque_nm: Option<f32>,
pub mit_kp_kd_factor: Option<f32>,
}
pub const INTER_WRITE_DELAY: Duration = Duration::from_millis(10);
pub fn build_set_mode_writes(target: MotorMode, current_logic: Option<&Logic>) -> Vec<SdoWrite> {
let mut out = Vec::with_capacity(7);
if matches!(current_logic, Some(Logic::Error { .. })) {
out.push(SdoWrite::u16(OD_CONTROL_WORD, 0, cw::FAULT_RESET));
}
out.push(SdoWrite::u16(OD_CONTROL_WORD, 0, cw::SHUTDOWN));
out.push(SdoWrite::i8(
OD_MODE_OF_OPERATION,
0,
mode_to_cia402_code(target),
));
out.push(SdoWrite::u16(OD_CONTROL_WORD, 0, cw::SHUTDOWN));
out.push(SdoWrite::u16(OD_CONTROL_WORD, 0, cw::SWITCH_ON));
out.push(SdoWrite::u16(OD_CONTROL_WORD, 0, cw::ENABLE_OPERATION));
out
}
pub fn build_disable_writes() -> Vec<SdoWrite> {
vec![SdoWrite::u16(OD_CONTROL_WORD, 0, cw::SHUTDOWN)]
}
pub fn build_clear_error_writes() -> Vec<SdoWrite> {
vec![SdoWrite::u16(OD_CONTROL_WORD, 0, cw::FAULT_RESET)]
}
pub fn build_set_target_writes(
target: &MotorTarget,
ctx: SetTargetContext,
) -> Result<Vec<SdoWrite>> {
if matches!(target, MotorTarget::Disable) {
return Ok(build_disable_writes());
}
let Some(mode) = ctx.current_mode else {
return Err(Error::Internal(
"set_target: motor mode unknown (call set_mode first)".into(),
));
};
if !target.matches_mode(mode) {
return Err(Error::TargetModeMismatch {
expected: format!("{:?}", mode),
given: target.variant_name(),
});
}
match (target, mode) {
(MotorTarget::Velocity { rev_per_s }, MotorMode::ProfileVelocity) => {
Ok(vec![SdoWrite::f32(OD_TARGET_VELOCITY, 0, *rev_per_s)])
}
(MotorTarget::Position { rev }, MotorMode::ProfilePosition) => {
Ok(build_pp_position_writes(*rev))
}
(MotorTarget::Torque { nm }, MotorMode::Torque) => build_torque_writes(*nm, &ctx),
(MotorTarget::Mit { pos, vel, tor, kp, kd }, MotorMode::Mit) => {
build_mit_writes(*pos, *vel, *tor, *kp, *kd, &ctx)
}
_ => Err(Error::TargetModeMismatch {
expected: format!("{:?}", mode),
given: target.variant_name(),
}),
}
}
fn build_pp_position_writes(rev: f32) -> Vec<SdoWrite> {
vec![
SdoWrite::u16(OD_CONTROL_WORD, 0, cw::ENABLE_PP_NEW_SP_CLEARED),
SdoWrite::f32(OD_TARGET_POSITION, 0, rev),
SdoWrite::u16(OD_CONTROL_WORD, 0, cw::ENABLE_PP_NEW_SP_LATCHED),
]
}
fn build_torque_writes(nm: f32, ctx: &SetTargetContext) -> Result<Vec<SdoWrite>> {
let peak = ctx.peak_torque_nm.ok_or_else(|| {
Error::Internal(
"set_target(Torque): peak_torque not cached. \
initialize() must read 0x6076 first; this motor may not expose it."
.into(),
)
})?;
if !peak.is_finite() || peak.abs() < f32::EPSILON {
return Err(Error::Internal(format!(
"set_target(Torque): cached peak_torque is {peak} Nm; cannot convert"
)));
}
let permille = (nm / peak * 1000.0).round();
let clamped = permille.clamp(-1000.0, 1000.0) as i16;
Ok(vec![SdoWrite::i16(OD_TARGET_TORQUE, 0, clamped)])
}
fn build_mit_writes(
pos: f32,
vel: f32,
tor: f32,
kp: f32,
kd: f32,
ctx: &SetTargetContext,
) -> Result<Vec<SdoWrite>> {
let factor = ctx.mit_kp_kd_factor.ok_or_else(|| {
Error::Internal(
"set_target(Mit): mit_kp_kd_factor not cached. \
initialize() must read 0x2003:07 first; this motor may not expose it."
.into(),
)
})?;
if !factor.is_finite() || factor.abs() < f32::EPSILON {
return Err(Error::Internal(format!(
"set_target(Mit): cached mit_kp_kd_factor is {factor}; cannot convert"
)));
}
let kp_int = (kp / factor).round().clamp(0.0, u16::MAX as f32) as u16;
let kd_int = (kd / factor).round().clamp(0.0, u16::MAX as f32) as u16;
let kp_int = kp_int.min(10_000);
let kd_int = kd_int.min(10_000);
Ok(vec![
SdoWrite::f32(OD_MIT_CONTROL_PARAM, mit::SUB_POSITION, pos),
SdoWrite::f32(OD_MIT_CONTROL_PARAM, mit::SUB_VELOCITY, vel),
SdoWrite::f32(OD_MIT_CONTROL_PARAM, mit::SUB_TORQUE, tor),
SdoWrite::u16(OD_MIT_CONTROL_PARAM, mit::SUB_KP, kp_int),
SdoWrite::u16(OD_MIT_CONTROL_PARAM, mit::SUB_KD, kd_int),
])
}
#[cfg(test)]
mod tests {
use super::*;
fn cw_writes_only(writes: &[SdoWrite]) -> Vec<u16> {
writes
.iter()
.filter(|w| w.index == OD_CONTROL_WORD)
.map(|w| u16::from_le_bytes([w.data[0], w.data[1]]))
.collect()
}
#[test]
fn set_mode_default_ramp_no_fault_reset() {
let w = build_set_mode_writes(MotorMode::ProfileVelocity, None);
assert_eq!(w.len(), 5);
assert_eq!(
cw_writes_only(&w),
vec![cw::SHUTDOWN, cw::SHUTDOWN, cw::SWITCH_ON, cw::ENABLE_OPERATION]
);
let mode_w = &w[1];
assert_eq!(mode_w.index, OD_MODE_OF_OPERATION);
assert_eq!(mode_w.data, vec![3u8]);
}
#[test]
fn set_mode_prepends_fault_reset_when_in_error() {
let logic = Logic::Error {
kind: crate::types::MotorErrorKind::OverCurrent,
raw_code: 0x2310,
};
let w = build_set_mode_writes(MotorMode::Mit, Some(&logic));
assert_eq!(w.len(), 6);
assert_eq!(
u16::from_le_bytes([w[0].data[0], w[0].data[1]]),
cw::FAULT_RESET
);
assert_eq!(w[2].index, OD_MODE_OF_OPERATION);
assert_eq!(w[2].data, vec![5u8]);
}
#[test]
fn set_mode_for_each_mode_writes_correct_code() {
for (m, code) in [
(MotorMode::ProfilePosition, 1u8),
(MotorMode::ProfileVelocity, 3),
(MotorMode::Torque, 4),
(MotorMode::Mit, 5),
] {
let w = build_set_mode_writes(m, None);
let mode_w = w.iter().find(|w| w.index == OD_MODE_OF_OPERATION).unwrap();
assert_eq!(mode_w.data[0], code, "mode {m:?}");
}
}
#[test]
fn disable_is_single_shutdown() {
let w = build_disable_writes();
assert_eq!(w.len(), 1);
assert_eq!(w[0].index, OD_CONTROL_WORD);
assert_eq!(u16::from_le_bytes([w[0].data[0], w[0].data[1]]), cw::SHUTDOWN);
}
#[test]
fn clear_error_is_single_fault_reset() {
let w = build_clear_error_writes();
assert_eq!(w.len(), 1);
assert_eq!(
u16::from_le_bytes([w[0].data[0], w[0].data[1]]),
cw::FAULT_RESET
);
}
fn ctx_pv() -> SetTargetContext {
SetTargetContext {
current_mode: Some(MotorMode::ProfileVelocity),
..Default::default()
}
}
#[test]
fn target_disable_works_in_any_mode() {
assert!(build_set_target_writes(&MotorTarget::Disable, SetTargetContext::default()).is_ok());
assert!(build_set_target_writes(&MotorTarget::Disable, ctx_pv()).is_ok());
}
#[test]
fn target_velocity_in_pv_mode_writes_60ff_f32() {
let w = build_set_target_writes(&MotorTarget::Velocity { rev_per_s: 1.5 }, ctx_pv()).unwrap();
assert_eq!(w.len(), 1);
assert_eq!(w[0].index, OD_TARGET_VELOCITY);
assert_eq!(w[0].subindex, 0);
assert_eq!(w[0].data.len(), 4);
let v = f32::from_le_bytes([w[0].data[0], w[0].data[1], w[0].data[2], w[0].data[3]]);
assert!((v - 1.5).abs() < f32::EPSILON);
}
#[test]
fn target_velocity_without_known_mode_errs() {
let r = build_set_target_writes(
&MotorTarget::Velocity { rev_per_s: 1.0 },
SetTargetContext::default(),
);
assert!(matches!(r, Err(Error::Internal(_))));
}
#[test]
fn target_velocity_in_wrong_mode_errs() {
let r = build_set_target_writes(
&MotorTarget::Velocity { rev_per_s: 1.0 },
SetTargetContext {
current_mode: Some(MotorMode::Torque),
..Default::default()
},
);
assert!(matches!(r, Err(Error::TargetModeMismatch { .. })));
}
#[test]
fn target_position_in_pp_mode_emits_csi_handshake() {
let w = build_set_target_writes(
&MotorTarget::Position { rev: 0.25 },
SetTargetContext {
current_mode: Some(MotorMode::ProfilePosition),
..Default::default()
},
)
.unwrap();
assert_eq!(w.len(), 3);
assert_eq!(w[0].index, OD_CONTROL_WORD);
assert_eq!(u16::from_le_bytes([w[0].data[0], w[0].data[1]]), 0x002F);
assert_eq!(w[1].index, OD_TARGET_POSITION);
let pos = f32::from_le_bytes([w[1].data[0], w[1].data[1], w[1].data[2], w[1].data[3]]);
assert!((pos - 0.25).abs() < f32::EPSILON);
assert_eq!(w[2].index, OD_CONTROL_WORD);
assert_eq!(u16::from_le_bytes([w[2].data[0], w[2].data[1]]), 0x003F);
}
#[test]
fn target_position_in_wrong_mode_errs() {
let r = build_set_target_writes(
&MotorTarget::Position { rev: 0.5 },
SetTargetContext {
current_mode: Some(MotorMode::ProfileVelocity),
..Default::default()
},
);
assert!(matches!(r, Err(Error::TargetModeMismatch { .. })));
}
#[test]
fn target_torque_converts_nm_to_permille_of_peak() {
let w = build_set_target_writes(
&MotorTarget::Torque { nm: 1.0 },
SetTargetContext {
current_mode: Some(MotorMode::Torque),
peak_torque_nm: Some(4.0),
..Default::default()
},
)
.unwrap();
assert_eq!(w.len(), 1);
assert_eq!(w[0].index, OD_TARGET_TORQUE);
let v = i16::from_le_bytes([w[0].data[0], w[0].data[1]]);
assert_eq!(v, 250);
}
#[test]
fn target_torque_clamps_to_plus_minus_1000_permille() {
let w = build_set_target_writes(
&MotorTarget::Torque { nm: 99.0 },
SetTargetContext {
current_mode: Some(MotorMode::Torque),
peak_torque_nm: Some(4.0),
..Default::default()
},
)
.unwrap();
let v = i16::from_le_bytes([w[0].data[0], w[0].data[1]]);
assert_eq!(v, 1000);
let w = build_set_target_writes(
&MotorTarget::Torque { nm: -99.0 },
SetTargetContext {
current_mode: Some(MotorMode::Torque),
peak_torque_nm: Some(4.0),
..Default::default()
},
)
.unwrap();
let v = i16::from_le_bytes([w[0].data[0], w[0].data[1]]);
assert_eq!(v, -1000);
}
#[test]
fn target_torque_without_peak_cached_errs() {
let r = build_set_target_writes(
&MotorTarget::Torque { nm: 1.0 },
SetTargetContext {
current_mode: Some(MotorMode::Torque),
peak_torque_nm: None,
..Default::default()
},
);
assert!(matches!(r, Err(Error::Internal(_))));
}
#[test]
fn target_mit_emits_five_writes_with_kp_kd_converted() {
let w = build_set_target_writes(
&MotorTarget::Mit {
pos: 0.1,
vel: 0.2,
tor: 0.3,
kp: 5.0, kd: 0.5, },
SetTargetContext {
current_mode: Some(MotorMode::Mit),
mit_kp_kd_factor: Some(0.01),
..Default::default()
},
)
.unwrap();
assert_eq!(w.len(), 5);
assert_eq!(w[0].index, 0x2003);
assert_eq!(w[0].subindex, 0x01);
let pos = f32::from_le_bytes([w[0].data[0], w[0].data[1], w[0].data[2], w[0].data[3]]);
assert!((pos - 0.1).abs() < 1e-6);
assert_eq!(w[3].subindex, 0x04);
let kp = u16::from_le_bytes([w[3].data[0], w[3].data[1]]);
assert_eq!(kp, 500);
assert_eq!(w[4].subindex, 0x05);
let kd = u16::from_le_bytes([w[4].data[0], w[4].data[1]]);
assert_eq!(kd, 50);
}
#[test]
fn target_mit_clamps_kp_to_10000() {
let w = build_set_target_writes(
&MotorTarget::Mit {
pos: 0.0,
vel: 0.0,
tor: 0.0,
kp: 1e6,
kd: 0.0,
},
SetTargetContext {
current_mode: Some(MotorMode::Mit),
mit_kp_kd_factor: Some(0.01),
..Default::default()
},
)
.unwrap();
let kp = u16::from_le_bytes([w[3].data[0], w[3].data[1]]);
assert_eq!(kp, 10_000);
}
#[test]
fn target_mit_without_factor_cached_errs() {
let r = build_set_target_writes(
&MotorTarget::Mit {
pos: 0.0, vel: 0.0, tor: 0.0, kp: 1.0, kd: 0.0,
},
SetTargetContext {
current_mode: Some(MotorMode::Mit),
mit_kp_kd_factor: None,
..Default::default()
},
);
assert!(matches!(r, Err(Error::Internal(_))));
}
}