Drive loops#
drive_velocity, drive_current, and drive_position each send one frame and
return. That’s the whole method. Motion is sustained only if you keep calling them at
50 Hz or faster; stop, and the wheel coasts within a couple of cycles. This isn’t a
limitation to paper over — it’s the protocol’s fail-safe, and the API is honest about
it rather than hiding a background thread you can’t see.
use std::time::{Duration, Instant};
use m0601::M0601;
fn main() -> m0601::Result<()> {
let mut motor = M0601::open("/dev/ttyUSB0", 0x01, Duration::from_millis(150))?;
// Spin at 100 RPM for three seconds: one frame every 20 ms.
let deadline = Instant::now() + Duration::from_secs(3);
while Instant::now() < deadline {
motor.drive_velocity(100)?;
std::thread::sleep(Duration::from_millis(20));
}
motor.safe_stop(); // force velocity, zero, brake. Never errors.
Ok(())
}20 ms per cycle is 50 Hz, the floor. Faster is fine (up to 500 Hz); slower and the motor starts coasting between frames, which reads as a wheel that stutters or won’t hold speed.
Stopping on exit#
safe_stop() is the counterpart to the loop, and you should call it on every
exit path — the normal end, an error, a panic, a signal handler. It’s built to run
from those places: it returns nothing and swallows I/O errors, so it can’t itself
panic or fail partway and leave the wheel driven.
What it actually does is worth knowing, because it explains a design choice you’ll
see echoed in the CLI. It forces velocity mode first, then sends zero, then brakes.
The mode switch is not incidental: a zero setpoint only means “stop” in velocity
mode. In position mode those same zero bytes mean “rotate to 0°” — a stop command
that could spin the wheel up to half a turn — and in current mode they mean zero
torque, a coast. Since safe_stop runs from panic and signal paths where the active
mode isn’t knowable, it establishes velocity mode itself rather than assuming.
Stopping safely covers this in full.
And if your process dies before safe_stop can run? The wheel coasts, because frames
stopped arriving. Worst case, the fail-safe still catches it.
Acceleration#
drive_velocity uses acceleration 1 by default — which is the motor’s fastest
ramp, not a gentle one, and so is 0, which selects the motor default and
measures the same.
On a loaded wheel a large velocity step at that ramp can draw a current spike big
enough to trip the 3 A bus-overcurrent protection, which drops the wheel until it
auto-resets ~5 s later. If you see that, ramp softer — larger is gentler, but keep it
small: 120 RPM takes ~0.45 s at 1, ~2 s at 5, and over 3 s at 20. Per call:
motor.drive_velocity_accel(200, 5)?; // larger = gentler; 0 and 1 are the fastest
Or change the default drive_velocity uses, once, so ordinary calls ramp gently — on
the whole bus or one handle:
let bus = Bus::open("/dev/ttyUSB0", timeout)?.with_default_accel(10); // every motor
let mut motor = bus.motor(0x01)?.with_default_accel(20); // just this one
motor.drive_velocity(200)?; // now uses accel 20; drive_velocity_accel still overrides
(The vendor docs give this byte a unit that reads like a rate, which contradicted the “1 is fastest” direction everyone agrees on. Hardware capture settled it: the byte scales the ramp at roughly 3.6 ms per RPM per unit — see contradiction 6 in the protocol notes.)
Setpoint ramping: SlewLimiter#
accel ramps the motor toward whatever setpoint it was last given; it does not
bound how fast you move that setpoint. A keystroke, a joystick snap, or a mixer
output that jumps between cycles is still a step change on the wire.
SlewLimiter bounds the setpoint’s rate of change. It holds no clock — you pass the
elapsed time per step, so the scheduler stays yours. The worked example, the
constructor’s error contract, the two safety rules (stop paths must bypass it; a held
brake must not let it wind up), why it’s for RPM/amps but not position, and why this
is the driver’s job at all, all live in one place:
Setpoint shaping.
Telemetry inside the loop#
You don’t have to choose between driving and reading — every drive frame’s reply
carries telemetry. transact sends a frame and returns the parsed reply in one
exchange, which is the right shape for a loop that both commands and monitors. That’s
the subject of the Telemetry page.