1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
//! Driver for the DFRobot **M0601** direct-drive hub motor over half-duplex
//! RS485. Covers both SKUs — **FIT1042** (left) and **FIT1038** (right) are
//! mirror-image builds of the same motor and speak the identical protocol;
//! see [`M0601::mirrored`] for making "forward" mean the same thing on both
//! sides of a chassis.
//!
//! The M0601 is **not Modbus**. It speaks a fixed 10-byte frame protocol at
//! 115200 8N1 with a CRC-8/MAXIM checksum, and it is a *polling* device:
//! motion is sustained only while the host keeps resending drive frames.
//! RS485 is multi-drop: several motors share one A/B pair (IDs
//! `0x01..=0xFE`) — a [`Bus`] owns the port and mints per-motor [`M0601`]
//! handles.
//!
//! # Safety and the polling protocol
//!
//! **A single drive command will not keep the wheel spinning.** The motor
//! moves only while drive frames arrive at
//! ≥[`DRIVE_HZ_MIN`](protocol::DRIVE_HZ_MIN) (50) Hz, up to
//! [`CMD_HZ_MAX`](protocol::CMD_HZ_MAX) (500) Hz. If the host stops —
//! crash, unplugged adapter, power loss — the motor **coasts to a stop**.
//! That is the protocol's built-in fail-safe; [`M0601::safe_stop`] upgrades
//! a coast to an active braked stop for orderly shutdowns and should be
//! called on every exit path of a control loop.
//!
//! One consequence deserves stating outright: **a zero setpoint does not
//! mean "stop"** except in velocity mode. The same zero-valued `0x64` frame
//! commands a move to 0° in position mode and zero torque in current mode,
//! which is why [`M0601::safe_stop`] establishes velocity mode before it
//! sends anything else.
//!
//! The motor also protects itself in hardware (each auto-resets after ~5 s):
//!
//! | Protection | Trip | Fault bit |
//! |-------------------|-------------------------|-----------|
//! | Sensor error | hall/encoder fault | `0x01` |
//! | Bus overcurrent | 3 A | `0x02` |
//! | Phase overcurrent | 4.6 A | `0x04` |
//! | Stall | locked > 5 s | `0x08` |
//! | Over-temperature | 80 °C (releases 75 °C) | `0x10` |
//!
//! # Wire format
//!
//! Host → motor frames (see [`protocol`]):
//!
//! | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
//! |------|----|-----|--------|--------|---|---|-------|-------|---|-----|
//! | | ID | CMD | VAL_HI | VAL_LO | 0 | 0 | ACCEL | BRAKE | 0 | CRC |
//!
//! - `CMD` is `0x64` (drive), `0x74` (feedback query) or `0xA0` (mode
//! switch). **For `0xA0` the last byte is the mode (`01`/`02`/`03`), not
//! a CRC.**
//! - `ACCEL` sets ramp steepness: larger is gentler, and `0` selects the
//! motor default — which measures identical to `1`, the *fastest* ramp.
//! No vendor source states that direction; see
//! [`protocol::frame_velocity`] for the measurement.
//! - `BRAKE` = `0xFF` engages the electric brake (velocity mode only).
//! - Two special unaddressed frames exist: the broadcast ID query
//! (`C8 64 00×7 DE`) and set-ID (`AA 55 53 <id> 00×6`, no CRC, must be
//! sent 5×, one motor on the bus).
//!
//! Motor → host telemetry replies come in **two layouts**, selected by the
//! command that elicited them ([`ReplyKind`]):
//!
//! Reply to a `0x74` feedback query ([`ReplyKind::Query`]):
//!
//! | Byte | 0 | 1 | 2–3 | 4–5 | 6 | 7 | 8 | 9 |
//! |------|----|------|--------------------|-----------------|---------|--------------|--------|-----|
//! | | ID | mode | current (i16 BE) | speed (i16 BE) | temp °C | position u8 | faults | chk |
//!
//! Reply to a `0x64` drive frame or the broadcast ID query
//! ([`ReplyKind::Drive`]) — no temperature, but a 16-bit position:
//!
//! | Byte | 0 | 1 | 2–3 | 4–5 | 6–7 | 8 | 9 |
//! |------|----|------|--------------------|-----------------|----------------------|--------|-----|
//! | | ID | mode | current (i16 BE) | speed (i16 BE) | position (u16 BE) | faults | chk |
//!
//! Current scales ×8/32767 to amps; the 8-bit position ×360/255 and the
//! 16-bit position ×360/32767 to degrees. Replies carry a CRC-8/MAXIM in
//! byte 9 (verified on hardware). **By default** telemetry is not rejected
//! on it — [`Feedback::crc_ok`] is informational — but the opt-in strict
//! mode ([`Bus::with_strict_crc`] / [`M0601::with_strict_crc`]) turns a bad
//! checksum into `Ok(None)`. See `PROTOCOL.md`.
//!
//! # Multiple motors on one bus
//!
//! [`Bus`] enforces a minimum idle gap between frames
//! ([`Bus::with_min_gap`]) so no two frames — or a frame and the reply an
//! earlier drive frame elicited — can overlap on the half-duplex pair;
//! [`Bus::set_mode_all`] and [`Bus::safe_stop_all`] switch or stop every
//! wheel round-major, so a vehicle stops in the same ~300 ms as one motor.
//! Budget the wire: each motor needs its drive frame at ≥50 Hz, so N
//! motors put ≥N×50 frames/s (plus replies, plus gaps) through one bus.
//! [`bus_period`] computes that occupancy from [`frame_time`] and the gap;
//! a loop's cycle must exceed it yet stay within [`drive_floor`]. See
//! [Budgeting the wire] for the worked arithmetic.
//!
//! [Budgeting the wire]: https://github.com/dougcalobrisi/m0601-rs/blob/main/docs/content/docs/library/budgeting.md
//!
//! Coming from another fieldbus or motor-control ecosystem, the concepts
//! map directly:
//!
//! | Here | Elsewhere |
//! |------|-----------|
//! | enforced inter-frame gap ([`Bus::with_min_gap`]) | Modbus RTU's 3.5-character silence; CANopen's PDO inhibit time |
//! | coast when drive frames stop (the 50 Hz floor) | a command watchdog / failsafe timeout, permanently enabled |
//! | [`Bus::set_mode_all`] / [`Bus::safe_stop_all`] (reply-less batching) | Dynamixel's broadcast Sync Write |
//! | automatic low-latency request ([`SerialTransport::low_latency`](transport::SerialTransport::low_latency)) | pyserial's `set_low_latency_mode(True)` |
//!
//! # Control modes
//!
//! | [`Mode`] | Wire | Value range | Meaning |
//! |-----------------------|------|--------------------|----------------|
//! | [`Mode::Current`] | 0x01 | −32767..=32767 | ≈ −8 A..+8 A |
//! | [`Mode::Velocity`] | 0x02 | −330..=330 | RPM |
//! | [`Mode::Position`] | 0x03 | 0..=32767 | 0°..360° |
//!
//! Setpoints outside these ranges are clamped, never wrapped. Mode switches
//! must be sent five times ([`M0601::set_mode`] does). Switching to position
//! mode requires the wheel to be under 10 RPM.
//!
//! # Example
//!
//! Real hardware:
//!
//! ```no_run
//! use std::time::Duration;
//! use m0601::M0601;
//!
//! # fn main() -> m0601::Result<()> {
//! let mut motor = M0601::open("/dev/ttyUSB0", 0x01, Duration::from_millis(150))?;
//! match motor.query()? {
//! // `query()` replies always carry the winding temperature.
//! Some(fb) if fb.temp_c.is_some_and(|t| t < 70) => {
//! println!("{:+} RPM, faults: {}", fb.speed_rpm, fb.faults);
//! }
//! Some(fb) => println!("running hot: {:?} °C", fb.temp_c),
//! None => println!("no reply — check 18 V power, wiring (brown → GND), A/B polarity"),
//! }
//! # Ok(())
//! # }
//! ```
//!
//! No hardware needed — every driver behavior runs against
//! [`MockTransport`]:
//!
//! ```
//! use std::time::Duration;
//! use m0601::{M0601, MockTransport};
//!
//! # fn main() -> m0601::Result<()> {
//! let mock = MockTransport::with_replies([
//! vec![0x01, 0x02, 0x00, 0x00, 0x00, 0x64, 0x28, 0x00, 0x00, 0x00],
//! ]);
//! let mut motor = M0601::with_transport(mock, 0x01, Duration::from_millis(150))?;
//! let fb = motor.query()?.unwrap();
//! assert_eq!(fb.speed_rpm, 100);
//! # Ok(())
//! # }
//! ```
//!
//! # References
//!
//! The repository's [protocol reference] is the full protocol and hardware
//! reference, with per-claim sourcing and the known contradictions between
//! sources (every `PROTOCOL.md` mention in these docs points there — the
//! root `PROTOCOL.md` is now a pointer to that page). Primary materials:
//!
//! [protocol reference]: https://github.com/dougcalobrisi/m0601-rs/blob/main/docs/content/docs/protocol.md
//!
//! - [DDT M0601C_111 manual (PDF)](https://d2air1d4eqhwg2.cloudfront.net/media/files/a48110eb-432c-4083-a159-9e0f35913b23.pdf)
//! — the manufacturer's 16-page datasheet (the M0601 is a rebadged DDT
//! M0601C-111)
//! - [DDTRobot/motor-driver-examples](https://github.com/DDTRobot/motor-driver-examples)
//! — the manufacturer's own sample code
//! - [DFRobot FIT1042 protocol wiki](https://wiki.dfrobot.com/fit1042/docs/23322)
//! - [DDT_M0601C_111, third-party samples](https://github.com/tech-life-hacking/DDT_M0601C_111)
//! - [navigation_robot, independent C driver](https://github.com/Il1yasviel/navigation_robot)
//! - [MotorLink, independent implementation](https://github.com/MukeshSankhla/MotorLink)
// `deny`, not `forbid`: the single place unsafe exists is the pair of Linux
// TIOCGSERIAL/TIOCSSERIAL ioctls in `low_latency` (scoped allow there, with
// the safety argument). Everything else remains unsafe-free.
pub use ;
pub use ;
pub use ;
pub use SlewLimiter;
pub use ;
pub use ;