use std::sync::Arc;
use std::time::{Duration, Instant};
use can_transport::CanBus;
mod common;
use hex_motor::cia402::{
Cia402Event, Cia402Manager, Cia402ManagerOptions, EventStreamItem, LiveState, MotorLifecycle,
};
use hex_motor::types::{MotorMode, MotorTarget};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let iface = std::env::args()
.nth(1)
.unwrap_or_else(|| "vcan0".to_string());
let our_nid: u8 = arg_u8(2)?.unwrap_or(0x10);
let target: u8 =
arg_u8(3)?.ok_or_else(|| anyhow::anyhow!("3rd arg required: target motor nid"))?;
let modes = parse_modes(&std::env::args().nth(4).unwrap_or_else(|| "pv".into()))?;
let max_torque_permille: u16 = std::env::args()
.nth(5)
.map(|s| s.parse::<u16>())
.transpose()?
.unwrap_or(200);
let secs_per_mode: u64 = std::env::args()
.nth(6)
.map(|s| s.parse::<u64>())
.transpose()?
.unwrap_or(4);
println!(
"Opening {iface}\n our HB nid = 0x{our_nid:02X}\n target motor nid = 0x{target:02X}\n \
modes = {modes:?}\n max torque = {max_torque_permille}‰ of peak\n \
secs per mode = {secs_per_mode}\n ⚠️ motor should be UNLOADED / free to spin."
);
let bus: Arc<dyn CanBus> = common::open_bus(&iface).await?;
let mgr = Cia402Manager::new(
bus.clone(),
Cia402ManagerOptions {
heartbeat_node_id: our_nid,
..Default::default()
},
)?;
println!("\n=== Waiting for nid 0x{target:02X} to be Identified ===");
wait_for_identified(&mgr, target, Duration::from_secs(10)).await?;
println!("=== Initialize ===");
mgr.initialize(target).await?;
println!(" -> Initialized");
let peak_nm = mgr
.list()
.into_iter()
.find(|m| m.node_id == target)
.and_then(|m| m.peak_torque_nm);
match peak_nm {
Some(p) => println!(" peak torque (0x6076) = {p:.3} Nm"),
None => println!(" peak torque (0x6076) = <not read> (Torque mode will be skipped)"),
}
println!("\n=== set_max_torque({max_torque_permille}‰) ===");
mgr.set_max_torque(target, max_torque_permille).await?;
if let Some(p) = peak_nm {
println!(
" -> 0x6072 = {max_torque_permille}‰ (≈ {:.3} Nm)",
p * max_torque_permille as f32 / 1000.0
);
}
let mut events = mgr.subscribe_events();
for (i, mode) in modes.iter().enumerate() {
if i > 0 {
println!("\n--- switching mode WITHOUT power cycle ---");
}
let r = run_one_mode(
&mgr,
target,
*mode,
peak_nm,
Duration::from_secs(secs_per_mode),
&mut events,
)
.await;
if let Err(e) = r {
println!(" !! mode {mode:?} aborted: {e}");
let _ = mgr.disable(target).await;
}
}
println!("\n=== all modes done, disabling ===");
match mgr.disable(target).await {
Ok(()) => println!(" disable() ok"),
Err(e) => println!(" disable() failed: {e}"),
}
Ok(())
}
async fn run_one_mode(
mgr: &Cia402Manager,
nid: u8,
mode: MotorMode,
peak_nm: Option<f32>,
dwell: Duration,
events: &mut hex_motor::cia402::EventStream,
) -> anyhow::Result<()> {
println!("\n=== set_mode({mode:?}) ===");
let t0 = Instant::now();
mgr.set_mode(nid, mode).await?;
println!(" -> Enabled({mode:?}) confirmed in {:?}", t0.elapsed());
let cur_pos = mgr.status(nid).measurements.position_rev.unwrap_or(0.0);
let target = match mode {
MotorMode::ProfilePosition => Some(MotorTarget::Position { rev: 0.1 }),
MotorMode::ProfileVelocity => Some(MotorTarget::Velocity { rev_per_s: 0.3 }),
MotorMode::Torque => match peak_nm {
Some(p) => Some(MotorTarget::Torque { nm: p * 0.05 }),
None => {
println!(" (skip target: peak torque unknown, can't build a safe Nm target)");
None
}
},
MotorMode::Mit => Some(MotorTarget::Mit {
pos: cur_pos,
vel: 3.0,
tor: 0.0,
kp: 0.0,
kd: 1.0,
}),
};
if let Some(t) = target {
println!(" set_target({t:?})");
mgr.set_target(nid, t).await?;
}
let deadline = Instant::now() + dwell;
let mut last_print = Instant::now() - Duration::from_secs(1);
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!("\n Ctrl+C → disable + exit");
let _ = mgr.disable(nid).await;
std::process::exit(0);
}
ev = events.recv() => {
if let Some(EventStreamItem::Event(Cia402Event::EnteredError { nid: n, kind, raw })) = ev {
if n == nid {
anyhow::bail!("motor entered Error: kind={kind:?} raw=0x{raw:04X}");
}
}
}
_ = tokio::time::sleep(Duration::from_millis(50)) => {}
}
if last_print.elapsed() >= Duration::from_millis(500) {
print_live(nid, &mgr.status(nid));
last_print = Instant::now();
}
if Instant::now() >= deadline {
break;
}
}
println!(" disable()");
mgr.disable(nid).await?;
Ok(())
}
fn print_live(nid: u8, s: &LiveState) {
let m = &s.measurements;
let f = |o: Option<f32>| {
o.map(|v| format!("{v:+.4}"))
.unwrap_or_else(|| " -- ".into())
};
println!(
" 0x{nid:02X} pos={} rev vel={} rev/s tau={} Nm sw={} ts={}us logic={:?}",
f(m.position_rev),
f(m.velocity_rev_per_s),
f(m.torque_nm),
m.status_word
.map(|w| format!("0x{w:04X}"))
.unwrap_or_else(|| "----".into()),
m.timestamp_us
.map(|t| t.to_string())
.unwrap_or_else(|| "--".into()),
s.logic,
);
}
fn parse_modes(s: &str) -> anyhow::Result<Vec<MotorMode>> {
s.split(',')
.map(|tok| match tok.trim().to_lowercase().as_str() {
"pp" | "profileposition" | "position" => Ok(MotorMode::ProfilePosition),
"pv" | "profilevelocity" | "velocity" => Ok(MotorMode::ProfileVelocity),
"torque" | "pt" | "t" => Ok(MotorMode::Torque),
"mit" => Ok(MotorMode::Mit),
other => Err(anyhow::anyhow!(
"unknown mode '{other}' (use pp/pv/torque/mit)"
)),
})
.collect()
}
async fn wait_for_identified(
mgr: &Cia402Manager,
nid: u8,
timeout: Duration,
) -> anyhow::Result<()> {
let deadline = Instant::now() + timeout;
let mut events = mgr.subscribe_events();
if mgr.list().iter().any(|m| {
m.node_id == nid
&& matches!(
m.lifecycle,
MotorLifecycle::Identified | MotorLifecycle::NeedsReinit { .. }
)
}) {
return Ok(());
}
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
anyhow::bail!("timeout waiting for nid 0x{nid:02X} to be Identified");
}
match tokio::time::timeout(remaining, events.recv()).await {
Ok(Some(EventStreamItem::Event(Cia402Event::Identified { nid: n, .. })))
if n == nid =>
{
return Ok(())
}
Ok(Some(_)) => continue,
Ok(None) => anyhow::bail!("event stream closed"),
Err(_) => anyhow::bail!("timeout waiting for nid 0x{nid:02X} to be Identified"),
}
}
}
fn arg_u8(idx: usize) -> anyhow::Result<Option<u8>> {
std::env::args()
.nth(idx)
.map(|s| {
let s = s.trim_start_matches("0x");
u8::from_str_radix(s, 16)
.or_else(|_| s.parse())
.map_err(|e| anyhow::anyhow!("invalid u8 '{s}': {e}"))
})
.transpose()
}