use std::sync::Arc;
use std::time::{Duration, Instant};
use can_transport::CanBus;
use tokio::sync::broadcast;
use crate::canopen::{
heartbeat::encode_consumer_heartbeat_entry,
nmt::{self, NmtCommand, NmtState},
sdo,
tpdo_config::{build_tpdo_config_writes, TpdoCommParams, TpdoEntry, TpdoRecipe},
};
use crate::error::{Error, Result};
use super::events::Cia402Event;
use super::manager::Cia402ManagerOptions;
use super::motor_entry::MotorEntry;
use super::types::MotorLifecycle;
pub const DEFAULT_TPDO1_ENTRIES: &[TpdoEntry] = &[
TpdoEntry {
index: 0x6064,
subindex: 0,
bit_len: 32,
}, TpdoEntry {
index: 0x1013,
subindex: 0,
bit_len: 32,
}, TpdoEntry {
index: 0x6077,
subindex: 0,
bit_len: 16,
}, TpdoEntry {
index: 0x603F,
subindex: 0,
bit_len: 16,
}, ];
pub const DEFAULT_TPDO2_ENTRIES: &[TpdoEntry] = &[
TpdoEntry {
index: 0x6041,
subindex: 0,
bit_len: 16,
}, TpdoEntry {
index: 0x2204,
subindex: 1,
bit_len: 16,
}, TpdoEntry {
index: 0x2204,
subindex: 2,
bit_len: 16,
}, TpdoEntry {
index: 0x6040,
subindex: 0,
bit_len: 16,
}, TpdoEntry {
index: 0x603F,
subindex: 0,
bit_len: 16,
}, ];
pub const DEFAULT_TPDO1_COMM: TpdoCommParams = TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 5, event_timer_ms: 1, };
pub const DEFAULT_TPDO2_COMM: TpdoCommParams = TpdoCommParams {
transmission_type: 255,
inhibit_time_x100us: 190, event_timer_ms: 20, };
pub fn default_tpdo1_recipe(nid: u8) -> TpdoRecipe {
TpdoRecipe {
tpdo_index: 0,
cob_id: 0x180 + nid as u16,
entries: DEFAULT_TPDO1_ENTRIES.to_vec(),
comm: DEFAULT_TPDO1_COMM,
}
}
pub fn default_tpdo2_recipe(nid: u8) -> TpdoRecipe {
TpdoRecipe {
tpdo_index: 1,
cob_id: 0x280 + nid as u16,
entries: DEFAULT_TPDO2_ENTRIES.to_vec(),
comm: DEFAULT_TPDO2_COMM,
}
}
const FAULT_RESET_SETTLE: Duration = Duration::from_millis(50);
async fn clear_fault_edge(
bus: &dyn CanBus,
nid: u8,
sdo_timeout: Option<Duration>,
) -> Result<()> {
sdo::download_u16(bus, nid, 0x6040, 0, 0x0000, sdo_timeout).await?;
tokio::time::sleep(FAULT_RESET_SETTLE).await;
sdo::download_u16(bus, nid, 0x6040, 0, 0x0080, sdo_timeout).await?;
tokio::time::sleep(FAULT_RESET_SETTLE).await;
sdo::download_u16(bus, nid, 0x6040, 0, 0x0000, sdo_timeout).await?;
tokio::time::sleep(FAULT_RESET_SETTLE).await;
Ok(())
}
pub(crate) async fn run_initialize(
bus: &dyn CanBus,
entry: Arc<MotorEntry>,
events_tx: &broadcast::Sender<Cia402Event>,
opts: &Cia402ManagerOptions,
) -> Result<()> {
let nid = entry.node_id;
let sdo_timeout = Some(opts.sdo_timeout);
{
let mut inner = entry.inner.lock().unwrap();
if matches!(inner.lifecycle, MotorLifecycle::Initializing) {
return Err(Error::Internal(format!(
"nid 0x{nid:02X}: initialize already running"
)));
}
inner.lifecycle = MotorLifecycle::Initializing;
inner.target_mode = None;
inner.logic = None;
inner.peak_torque_nm = None;
inner.mit_kp_kd_factor = None;
inner.measurements = Default::default();
inner.vel_filter = Default::default();
}
let _ = events_tx.send(Cia402Event::Initializing { nid });
let mut rollback = LifecycleRollback::new(entry.clone());
let preop_cmd = nmt::build_nmt_command(NmtCommand::EnterPreOperational, nid)?;
bus.send(preop_cmd).await?;
wait_for_nmt_state(
&entry,
NmtState::PreOperational,
opts.motor_heartbeat_period * 2,
)
.await?;
log::info!("nid 0x{nid:02X}: NMT = PreOperational");
let _sw = sdo::upload_u16(bus, nid, 0x6041, 0, sdo_timeout).await?;
apply_tpdo_recipe(bus, nid, &default_tpdo1_recipe(nid), sdo_timeout).await?;
apply_tpdo_recipe(bus, nid, &default_tpdo2_recipe(nid), sdo_timeout).await?;
read_runtime_constants(bus, &entry, sdo_timeout).await;
let _ = sdo::download_u16(bus, nid, 0x2003, 0x06, 1000, sdo_timeout)
.await
.map_err(|e| {
log::debug!(
"nid 0x{nid:02X}: 0x2003:06 (MIT PD limit) not writable ({e}); \
Mit mode will rely on motor default"
);
});
let op_cmd = nmt::build_nmt_command(NmtCommand::StartRemoteNode, nid)?;
bus.send(op_cmd).await?;
wait_for_nmt_state(&entry, NmtState::Operational, opts.motor_heartbeat_period * 2).await?;
log::info!("nid 0x{nid:02X}: NMT = Operational");
let timeout_ms = opts
.consumer_heartbeat_timeout
.as_millis()
.min(u16::MAX as u128) as u16;
let consumer = encode_consumer_heartbeat_entry(opts.heartbeat_node_id, timeout_ms);
let verify_wait = opts.consumer_heartbeat_timeout + Duration::from_millis(100);
let attempts = opts.init_fault_clear_attempts.max(1);
let mut cleared = false;
for attempt in 1..=attempts {
sdo::download_u32(bus, nid, 0x1016, 1, 0, sdo_timeout).await?;
clear_fault_edge(bus, nid, sdo_timeout).await?;
sdo::download_u32(bus, nid, 0x1016, 1, consumer, sdo_timeout).await?;
tokio::time::sleep(verify_wait).await;
match sdo::upload_u16(bus, nid, 0x6041, 0, sdo_timeout).await {
Ok(sw) if (sw & 0x0008) == 0 => {
log::info!(
"nid 0x{nid:02X}: heartbeat/CiA402 fault cleared & armed \
(sw=0x{sw:04X}, 0x1016=0x{consumer:08X}) on attempt {attempt}/{attempts}"
);
cleared = true;
break;
}
Ok(sw) => log::warn!(
"nid 0x{nid:02X}: still faulted (sw=0x{sw:04X}) after attempt \
{attempt}/{attempts}; re-toggling heartbeat monitor to flip phase"
),
Err(e) => log::warn!(
"nid 0x{nid:02X}: read 0x6041 failed on attempt {attempt}/{attempts}: {e}"
),
}
}
if !cleared {
return Err(Error::Internal(format!(
"nid 0x{nid:02X}: could not clear heartbeat/CiA402 fault after {attempts} \
attempts; motor may need a power cycle"
)));
}
{
let mut inner = entry.inner.lock().unwrap();
inner.lifecycle = MotorLifecycle::Initialized;
}
rollback.disarm();
let _ = events_tx.send(Cia402Event::Initialized { nid });
Ok(())
}
async fn read_runtime_constants(
bus: &dyn CanBus,
entry: &Arc<MotorEntry>,
sdo_timeout: Option<Duration>,
) {
let nid = entry.node_id;
match sdo::upload_f32(bus, nid, 0x6076, 0, sdo_timeout).await {
Ok(nm) => {
log::info!("nid 0x{nid:02X}: 0x6076 (Motor Peak Torque) = {nm} Nm");
entry.inner.lock().unwrap().peak_torque_nm = Some(nm);
}
Err(e) => {
log::warn!(
"nid 0x{nid:02X}: 0x6076 (Motor Peak Torque) not readable ({e}); \
Torque-mode target writes will be unavailable"
);
}
}
match sdo::upload_f32(bus, nid, 0x2003, 0x07, sdo_timeout).await {
Ok(factor) => {
log::info!("nid 0x{nid:02X}: 0x2003:07 (MIT KP/KD Factor) = {factor}");
entry.inner.lock().unwrap().mit_kp_kd_factor = Some(factor);
}
Err(e) => {
log::warn!(
"nid 0x{nid:02X}: 0x2003:07 (MIT KP/KD Factor) not readable ({e}); \
Mit-mode target writes will be unavailable"
);
}
}
}
async fn apply_tpdo_recipe(
bus: &dyn CanBus,
nid: u8,
recipe: &TpdoRecipe,
sdo_timeout: Option<Duration>,
) -> Result<()> {
let writes = build_tpdo_config_writes(recipe)?;
log::debug!(
"nid 0x{nid:02X}: TPDO{} cob_id=0x{:03X}: {} SDO ops ({} bytes/frame)",
recipe.tpdo_index + 1,
recipe.cob_id,
writes.len(),
recipe.total_bytes(),
);
for w in &writes {
sdo::download(bus, nid, w.index, w.subindex, &w.data, sdo_timeout).await?;
}
Ok(())
}
async fn wait_for_nmt_state(
entry: &Arc<MotorEntry>,
target: NmtState,
timeout: Duration,
) -> Result<()> {
{
let inner = entry.inner.lock().unwrap();
if inner.nmt_state == Some(target) {
return Ok(());
}
}
let deadline = Instant::now() + timeout;
let poll_period = Duration::from_millis(20);
loop {
tokio::time::sleep(poll_period).await;
{
let inner = entry.inner.lock().unwrap();
if inner.nmt_state == Some(target) {
return Ok(());
}
}
if Instant::now() >= deadline {
let observed = entry.inner.lock().unwrap().nmt_state;
return Err(Error::Internal(format!(
"nid 0x{:02X}: timeout waiting NMT {:?} (last observed {:?})",
entry.node_id, target, observed,
)));
}
}
}
struct LifecycleRollback {
entry: Arc<MotorEntry>,
armed: bool,
}
impl LifecycleRollback {
fn new(entry: Arc<MotorEntry>) -> Self {
Self { entry, armed: true }
}
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for LifecycleRollback {
fn drop(&mut self) {
if !self.armed {
return;
}
let mut inner = self.entry.inner.lock().unwrap();
if matches!(inner.lifecycle, MotorLifecycle::Initializing) {
inner.lifecycle = if inner.identity.is_some() {
MotorLifecycle::Identified
} else {
MotorLifecycle::Unknown
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tpdo1_is_12_bytes_4_entries() {
let r = default_tpdo1_recipe(0x10);
assert_eq!(r.total_bytes(), 12);
assert_eq!(r.entries.len(), 4);
assert_eq!(r.cob_id, 0x190);
assert_eq!(r.tpdo_index, 0);
assert!(r.validate().is_ok());
}
#[test]
fn default_tpdo2_is_10_bytes_5_entries() {
let r = default_tpdo2_recipe(0x10);
assert_eq!(r.total_bytes(), 10);
assert_eq!(r.entries.len(), 5);
assert_eq!(r.cob_id, 0x290);
assert_eq!(r.tpdo_index, 1);
assert!(r.validate().is_ok());
}
#[test]
fn default_tpdo1_timing_is_high_speed() {
assert_eq!(DEFAULT_TPDO1_COMM.transmission_type, 255);
assert_eq!(DEFAULT_TPDO1_COMM.inhibit_time_x100us, 5);
assert_eq!(DEFAULT_TPDO1_COMM.event_timer_ms, 1);
}
#[test]
fn default_tpdo2_timing_is_low_speed() {
assert_eq!(DEFAULT_TPDO2_COMM.transmission_type, 255);
assert_eq!(DEFAULT_TPDO2_COMM.inhibit_time_x100us, 190);
assert_eq!(DEFAULT_TPDO2_COMM.event_timer_ms, 20);
}
#[test]
fn tpdo1_and_tpdo2_use_different_cob_and_index() {
let r1 = default_tpdo1_recipe(0x21);
let r2 = default_tpdo2_recipe(0x21);
assert_eq!(r1.cob_id, 0x1A1);
assert_eq!(r2.cob_id, 0x2A1);
assert_ne!(r1.tpdo_index, r2.tpdo_index);
}
}