#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use m0601::{M0601, Mode};
const TIMEOUT: Duration = Duration::from_millis(150);
static PORT: Mutex<()> = Mutex::new(());
fn port_guard() -> MutexGuard<'static, ()> {
PORT.lock().unwrap_or_else(PoisonError::into_inner)
}
fn port() -> String {
std::env::var("M0601_PORT").expect("set M0601_PORT (e.g. /dev/ttyUSB0) to run hardware tests")
}
fn motor_id() -> u8 {
match std::env::var("M0601_ID") {
Ok(s) => {
let t = s.trim();
let parsed = t
.strip_prefix("0x")
.or_else(|| t.strip_prefix("0X"))
.map_or_else(|| t.parse::<u8>().ok(), |h| u8::from_str_radix(h, 16).ok());
parsed.expect("M0601_ID must be a byte, e.g. 0x01 or 1")
}
Err(_) => 0x01,
}
}
fn open() -> M0601 {
M0601::open(&port(), motor_id(), TIMEOUT).expect("open serial port")
}
#[test]
#[ignore = "needs hardware: set M0601_PORT"]
fn scan_finds_motor() {
let _guard = port_guard();
let bus = m0601::Bus::open(&port(), TIMEOUT).expect("open serial port");
let report = bus.scan(std::iter::empty(), |_| {}).expect("scan I/O");
assert!(
!report.ids.is_empty() || report.garbled,
"no motor answered the broadcast ID query"
);
}
#[test]
#[ignore = "needs hardware: set M0601_PORT"]
fn query_returns_telemetry() {
let _guard = port_guard();
let id = motor_id();
let mut m = open();
let fb = m.query().expect("query I/O").unwrap_or_else(|| {
panic!("motor 0x{id:02X} did not reply — check power/wiring, or set M0601_ID")
});
assert_eq!(fb.id, id);
let temp = fb.temp_c.expect("0x74 reply carries winding temperature");
assert!(temp < 80, "implausible temperature {temp}");
assert!(fb.mode.is_some(), "unknown mode byte 0x{:02X}", fb.mode_raw);
}
#[test]
#[ignore = "needs hardware: set M0601_PORT"]
fn reply_checksum_capture() {
let _guard = port_guard();
let id = motor_id();
let mut m = open();
let report = |label: &str, tx: &[u8], rx: &[u8]| {
let rx = rx.strip_prefix(tx).unwrap_or(rx);
let Some(frame) = rx.get(..10) else {
eprintln!("{label}: no reply captured ({} bytes)", rx.len());
return;
};
let crc = m0601::protocol::crc8_maxim(&frame[..9]);
let hex: Vec<String> = frame.iter().map(|b| format!("{b:02X}")).collect();
eprintln!(
"{label}: {} — byte 9 = 0x{:02X}, CRC-8/MAXIM(bytes 0..9) = 0x{crc:02X} → {}",
hex.join(" "),
frame[9],
if frame[9] == crc {
"MATCHES"
} else {
"DIFFERS"
},
);
};
let query = m0601::protocol::frame_feedback(id);
let rx = m
.send_raw(&query, Duration::from_millis(50))
.expect("query I/O");
report("0x74 query reply", &query, &rx);
let drive = m0601::protocol::frame_velocity(id, 0, 1);
let rx = m
.send_raw(&drive, Duration::from_millis(50))
.expect("drive I/O");
report("0x64 drive reply", &drive, &rx);
}
struct StopOnDrop(Option<M0601>);
impl Drop for StopOnDrop {
fn drop(&mut self) {
if let Some(mut m) = self.0.take() {
m.safe_stop();
}
}
}
#[test]
#[ignore = "needs hardware AND spins the wheel: set M0601_PORT and M0601_ALLOW_MOTION=1"]
fn spin_and_stop() {
let _guard = port_guard();
assert_eq!(
std::env::var("M0601_ALLOW_MOTION").as_deref(),
Ok("1"),
"spin_and_stop moves the wheel; set M0601_ALLOW_MOTION=1 to allow it \
(make sure the wheel is off the ground), or deselect this test with \
`--skip spin_and_stop`"
);
let mut guard = StopOnDrop(Some(open()));
let m = guard.0.as_mut().expect("just constructed");
m.set_mode(Mode::Velocity).expect("set velocity mode");
let deadline = Instant::now() + Duration::from_secs(2);
let mut fastest: i16 = 0;
while Instant::now() < deadline {
let frame = m0601::protocol::frame_velocity(m.id(), 60, 1);
if let Ok(Some(fb)) = m.transact(&frame, Duration::from_millis(6)) {
fastest = fastest.max(fb.speed_rpm);
assert!(fb.temp_c.is_none(), "drive reply carried a temperature");
assert!(
(0.0..=360.0).contains(&fb.position_deg),
"drive-reply position out of range: {}",
fb.position_deg
);
}
std::thread::sleep(Duration::from_millis(14));
}
m.safe_stop();
assert!(fastest > 20, "wheel never spun up (peak {fastest} RPM)");
std::thread::sleep(Duration::from_millis(500));
let fb = m.query().expect("query I/O").expect("telemetry after stop");
assert!(
fb.speed_rpm.abs() < 10,
"still moving: {} RPM",
fb.speed_rpm
);
}