m0601 0.1.1

Driver for the DFRobot M0601 (FIT1042) direct-drive hub motor over RS485
Documentation

m0601 — a Rust library + CLI for the DFRobot M0601 hub motor

CI crates.io docs.rs MSRV License: MIT

A reusable driver crate (m0601/) and a CLI (m0601-cli/, binary m0601) for the DFRobot M0601 direct-drive hub motor over half-duplex RS485.

M0601 is the motor model — a rebadged Direct Drive Tech M0601C-111; FIT1042 (left) and FIT1038 (right) are DFRobot's SKUs for its mirror-image builds. They are electrically identical and speak the same protocol, so this one library covers both — see the mirror flag below.

The M0601 is not Modbus: fixed 10-byte frames at 115200 8N1, and a polling protocol — the motor keeps moving only while drive frames keep arriving at ~50 Hz (official docs state only a 500 Hz maximum; the 50 Hz floor is the community consensus and matches observation). If the host stops sending, the wheel coasts to a stop; that is the protocol's built-in fail-safe.

Host frames carry a CRC-8/MAXIM in byte 9, with two exceptions: the mode-switch frame puts the mode there instead, and the set-ID frame has no checksum at all. Replies carry the same CRC (verified on real hardware, though some reference implementations dispute it) — by default the driver does not reject telemetry on it, but the opt-in strict mode (Bus::with_strict_crc / M0601::with_strict_crc) turns a bad checksum into Ok(None).

Telemetry replies come in two layouts: a 0x74 query reply carries the winding temperature and a coarse 8-bit position, while replies to drive frames carry a fine 16-bit position and no temperature. The library decodes each reply by the command that elicited it.

Documentation map — the full docs live in docs/content/docs/ (a Hugo site; cd docs && hugo server to read it rendered):

  • Getting started and the first-spin tutorial — build, wire, permissions, and a wheel actually turning.
  • Safety — what brakes, what coasts, and what will hurt you. Short; read it before the wheel is on the ground.
  • CLI guide — a page per subcommand: output samples, exit codes, footguns.
  • Library guide — drive loops, modes, telemetry, mirroring, bus budgeting, odometry, testing with mocks.
  • Sample code — the runnable code in this repo (see below).
  • Concepts — why the driver behaves the way it does: the fail-safe, the bus, echoes, stopping, adapter latency.
  • Protocol reference — spec tables, wiring, every frame byte-by-byte, both reply layouts, and the known contradictions between sources.
  • docs.rs/m0601 (or cargo doc --open -p m0601) — the library API contract.
  • CHANGELOG.md — what changed, per release.

One rule worth carrying into any code you write against this: a zero setpoint does not mean "stop". It only does in velocity mode — the same zero-valued frame commands a move to 0° in position mode and zero torque (a coast) in current mode.

Build & install

Needs Rust 1.88 or newer (edition 2024 plus let-chains). Linux is the tested platform; the serial layer is portable, but the /dev/ttyUSB0 paths and the dialout group below are Linux-specific.

git clone https://github.com/dougcalobrisi/m0601-rs.git
cd m0601-rs
cargo build --release              # binary at target/release/m0601
cargo install --path m0601-cli     # or install `m0601` into ~/.cargo/bin

If opening /dev/ttyUSB0 fails with a permission error, add yourself to the dialout group: sudo usermod -aG dialout $USER (log out and back in).

Before you spin it

control and drive start driving the motor immediately, with no confirmation prompt. A direct-drive hub motor has no gearbox to slow it down, and the 15 presets reach 250 RPM.

  • Clear the wheel, and secure the chassis so it cannot drive itself off the bench.
  • Remember that a zero setpoint does not mean "stop" outside velocity mode (see above).
  • Ctrl-C brakes. So does every other exit path — but only while the process is alive to do it.

Usage

Global flags, valid before or after the subcommand: --port /dev/ttyUSB0, --id 0x01 (hex or decimal), --timeout 0.15 (seconds); accepted ranges are in the CLI overview page. Data goes to stdout and diagnostics to stderr, so m0601 info > readout.txt captures only the readout.

m0601 scan                     # discover motor IDs (broadcast)
m0601 scan --full              # poll every ID 0x01..0xFE (~40 s)
m0601 info                     # config + one-shot live readout
m0601 monitor --hz 5           # live line dashboard, Ctrl+C to stop
m0601 monitor --csv log.csv    # ... also log rows to CSV
m0601 control --rpm 100        # full-screen keyboard control (see below)
m0601 control --accel 1        # ... with the motor's fastest ramp (default 3)
m0601 drive velocity --rpm 100 --secs 3  # spin at 100 RPM for 3 s, then brake
m0601 drive current --amps 1.0           # hold ~1 A of torque (until Ctrl-C)
m0601 drive position --deg 180           # rotate to 180° and hold (needs <10 RPM)
m0601 set-id --new 0x02        # change the motor's persistent RS485 ID
m0601 set-id --new 0x02 --yes  # ... skipping the confirmation prompt
m0601 raw "01 74 00 00 00 00 00 00 00"   # arbitrary frame, CRC auto-added
m0601 raw --yes "01 64 00 64 00 00 03 00 00"  # a motion frame needs --yes

raw refuses the two command bytes that can move the wheel — 0x64 (drive) and 0xA0 (mode switch) — unless you pass --yes, and brakes the motor the frame addressed (byte 0) on exit when it sends one — except a broadcast C8 drive frame, which commands every motor while a unicast brake covers only one. It still has none of drive's other rails: no loop, and no position-mode pre-flight check.

drive is the scriptable counterpart to control: it holds one setpoint in one mode — velocity (RPM), current (amps), or position (degrees) — resending at 50 Hz until --secs elapses or you Ctrl-C, then it brakes. Every exit path runs safe_stop (forces velocity, zeroes, brakes), so the wheel is stopped on a clean exit, a signal, or a panic. Position mode is refused at 10 RPM or above, per protocol, and also when no telemetry has arrived — without a reading the speed is unknown, not zero.

--secs accepts 0–3600; omit it to drive until Ctrl-C.

--timeout governs scan, info, monitor and set-id. It does not apply to control or drive, whose 50 Hz loops use a fixed 6 ms reply wait (only drive's pre-flight speed check, before entering position mode, waits the full --timeout), and raw raises it to a 200 ms floor so a slow reply is not missed.

set-id polls all 254 IDs before writing, because the set-ID frame is unaddressed and would rename every motor that hears it — a broadcast scan cannot prove only one is connected. Expect it to take ~40 s.

control keys

Key Action
F / B forward / backward at the --rpm preset (switches to velocity mode)
15 50–250 RPM (switches to velocity mode)
/ nudge ±10 RPM (velocity mode only)
S 0 RPM in velocity mode; hold the current angle in position mode; zero torque — a coast, not a stop — in current mode
K electric brake (velocity mode only; ignored in current and position mode)
V/C/P switch mode: velocity / current / position
Q / Esc / Ctrl-C quit — forces velocity mode, zeroes, then brakes

control latches: releasing a key does not stop the wheel. F, B, and 15 set a sustained setpoint that holds until S, K, Q, or a signal. Do not walk away from a spinning wheel expecting it to stop on its own.

--accel sets the ramp used for active driving (default 3, gentler than the motor's fastest 1) — a keystroke commands a large step, and the sharpest ramp can trip the 3 A overcurrent protection on a loaded wheel. Larger is gentler; 0 is not a middle setting, it selects the motor's default, which measures identical to 1. Keep it small — 120 RPM takes ~2 s at 5 and over 3 s at 20. See the protocol notes. The stop ramp is separate; see below.

P is refused at 10 RPM or above, and also when no telemetry has arrived — without a reading the speed is unknown, not zero. Entering position mode seeds the target with the wheel's present angle, so the switch itself never commands a move.

The status line shows the mode the motor reports, in red alongside the requested one if the two ever disagree.

Safety: the wheel is stopped on every exit path — quit keys, panics, SIGINT/SIGTERM/SIGHUP (e.g. a dropped SSH session). safe_stop sends 5× mode-switch-to-velocity, then 5× velocity-0, then 5× brake, ~300 ms in all; the velocity-0 rounds shed nearly half the speed (measured: 120 → ~64 RPM on an unloaded wheel, against 119 RPM for coasting) and the brake rounds finish the job. The stop_accel byte is tunable via Bus::with_stop_accel / BusTiming but measures inert — 0 and 255 decelerate identically. On SIGKILL or power loss the polling simply stops and the motor coasts, per protocol. Keep the wheel clear before spinning it.

Library: multi-motor bus + left/right mirroring

RS485 is multi-drop: several motors share one A/B pair, each with a unique ID (0x01..=0xFE — assign them one at a time with m0601 set-id). A Bus owns the port and mints cheap, cloneable, thread-safe per-motor handles. The bus enforces a minimum idle gap between frames so no two can overlap on the half-duplex wire (with_min_gap tunes it — drive frames elicit replies even when unread, so unspaced sends corrupt), stops or mode-switches a whole vehicle at once (safe_stop_all / set_mode_all, round-major so N motors stop in the same ~300 ms as one), and requests low-latency delivery from the kernel to defeat the FTDI 16 ms latency timer. mirrored(true) makes "positive = robot forward" hold on a mirrored wheel by negating velocity/current setpoints and flipping reported speed/current signs (reported position passes through by default — angle mirroring depends on your mechanical convention, so it's opt-in via position_mirror):

use std::time::Duration;
use m0601::Bus;

fn main() -> m0601::Result<()> {
    let bus = Bus::open("/dev/ttyUSB0", Duration::from_millis(150))?;
    let mut left = bus.motor(0x01)?.mirrored(true); // FIT1042 (left)
    let mut right = bus.motor(0x02)?;               // FIT1038 (right)
    // drive both "forward" — remember: resend at >=50 Hz to sustain motion
    left.drive_velocity(100)?;
    right.drive_velocity(100)?;
    Ok(())
}

Four-wheel example → full app

Start with the one-screen example — open a bus, mint four mirrored handles, arm a stop guard, run a drive→poll→stop cycle, no TUI or scheduler in the way:

cargo run --example four_wheel_minimal -- /dev/ttyUSB0

m0601-quad is that same wiring grown into a real application. It drives four wheels as one skid-steer rover and doubles as the reference implementation for multi-motor use of the library: a TOML wheel map (wheels.toml) validated fail-closed, a single pilot thread owning the bus at 55.6 Hz, latched fault handling with manual re-arm, a 2×2 terminal dashboard, CSV logging, and a --dry-run mode that opens no port. Bring-up order: check --probemonitorjog/calibratedrive. It is not published to crates.io — clone the repo and run it from the workspace:

cargo run -p m0601-quad -- --config m0601-quad/wheels.toml check
cargo run -p m0601-quad -- --config m0601-quad/wheels.toml drive --dry-run

Both are documented in full under Sample code.

Wiring checklist (no motors found?)

  • 18 V power on?
  • Brown wire → GND?
  • A/B swapped? (try orange ↔ white)
  • Right --id? Run m0601 scan.

Tests

cargo test --workspace                          # vectors + mock-bus + doctests
# hardware-in-loop (--test-threads=1 required: the serial port is exclusive)
M0601_PORT=/dev/ttyUSB0 cargo test -p m0601 --test hardware -- --ignored --test-threads=1

The protocol layer is validated byte-for-byte against golden vectors whose expected bytes are written out as literals, derived from the DFRobot frame layout and the CRC-8/MAXIM specification — the CRC implementation is anchored to that algorithm's published check value (crc8("123456789") == 0xA1), so no assertion recomputes its own expectation with the code under test. A further set of known-answer frames is cross-checked against two independent implementations that have driven real hardware — every byte of those frames is mechanically determined by the frame layout and the CRC, so they are reproducible facts about the wire rather than copied source (see the protocol reference and NOTICE). Driver behavior (echo stripping, wrong-ID reply rejection, 5× frame repeats, safe-stop sequencing) runs against an in-memory mock transport. The spin_and_stop hardware test additionally requires M0601_ALLOW_MOTION=1 — it briefly spins the wheel.

References

See the protocol reference for the full spec with per-claim sourcing.

License

MIT — see LICENSE.

This project is not affiliated with DFRobot or Direct Drive Tech. It drives physical hardware that can cause injury or damage; it comes with no warranty of any kind, as set out in the license.