<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>m0601 on m0601 — RS485 hub motor driver</title><link>https://doug.sh/docs/m0601-rs/</link><description>Recent content in m0601 on m0601 — RS485 hub motor driver</description><generator>Hugo</generator><language>en</language><atom:link href="https://doug.sh/docs/m0601-rs/index.xml" rel="self" type="application/rss+xml"/><item><title>Crate examples</title><link>https://doug.sh/docs/m0601-rs/docs/samples/examples/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/samples/examples/</guid><description>&lt;h1 id="crate-examples"&gt;Crate examples&lt;a class="anchor" href="#crate-examples"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Two files, with very different jobs. One is meant to be read and run; the other
exists only to be compiled.&lt;/p&gt;
&lt;h2 id="four_wheel_minimalrs--the-driver-on-one-screen"&gt;&lt;code&gt;four_wheel_minimal.rs&lt;/code&gt; — the driver on one screen&lt;a class="anchor" href="#four_wheel_minimalrs--the-driver-on-one-screen"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo run -p m0601 --example four_wheel_minimal -- /dev/ttyUSB0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four motors at IDs 1–4 on one RS485 adapter. About 50 lines of actual code, and it
touches every part of the multi-motor API in the order you&amp;rsquo;d meet them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;One shared bus.&lt;/strong&gt; &lt;code&gt;Bus::open(&amp;amp;port, Duration::from_millis(150))?.with_min_gap(…)&lt;/code&gt;
— the generous timeout is a backstop for the open; the loop passes its own short
reply wait per poll. The idle gap is tightened to 2 ms so the cycle in step 5
fits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One handle per wheel, sign convention applied once.&lt;/strong&gt; A &lt;code&gt;const WHEELS: [(u8, bool); 4]&lt;/code&gt; table pairs each RS485 id with whether that corner is a mirror-image
build, and &lt;code&gt;bus.motor(id)?.mirrored(mirror)&lt;/code&gt; bakes it in at construction. After
that line the application never touches a sign again — &lt;code&gt;+60&lt;/code&gt; means forward on all
four. (&lt;a href="https://doug.sh/docs/m0601-rs/docs/library/multi-motor/"&gt;Mirroring&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A stop guard armed &lt;em&gt;before&lt;/em&gt; the first frame.&lt;/strong&gt; A tiny &lt;code&gt;struct StopGuard { bus, ids }&lt;/code&gt; whose &lt;code&gt;Drop&lt;/code&gt; calls &lt;code&gt;bus.safe_stop_all(&amp;amp;self.ids)&lt;/code&gt;. Because &lt;code&gt;Bus&lt;/code&gt; is &lt;code&gt;Clone&lt;/code&gt;
it holds its own handle to the same port, and because it&amp;rsquo;s armed before anything
moves, a &lt;code&gt;?&lt;/code&gt; early-return or a panic anywhere below still lands on a bus-wide
stop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A group mode switch.&lt;/strong&gt; &lt;code&gt;bus.set_mode_all(&amp;amp;ids, Mode::Velocity)?&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The loop, budgeted before it moves.&lt;/strong&gt; A startup &lt;code&gt;assert!&lt;/code&gt; on
&lt;code&gt;bus_period(4, 1, bus.min_gap(), REPLY_WAIT)&lt;/code&gt; — ~17.2 ms at the 2 ms gap and 2 ms
reply wait — checks the traffic fits inside the 20 ms cycle and that the cycle
stays at or under &lt;code&gt;drive_floor()&lt;/code&gt;. Then: drive all four at 60 RPM, resend every
~20 ms (the &lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/polling-and-failsafe/"&gt;50 Hz floor&lt;/a&gt;), and
poll &lt;strong&gt;one&lt;/strong&gt; wheel per cycle round-robin — never substituting a query for a drive
frame, and never polling all four in one cycle. That is the
&lt;a href="https://doug.sh/docs/m0601-rs/docs/library/budgeting/"&gt;bus budget&lt;/a&gt; advice as working code, right
down to the assertion that page recommends.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the distilled essence of &lt;a href="https://doug.sh/docs/m0601-rs/docs/samples/quad/"&gt;&lt;code&gt;m0601-quad&lt;/code&gt;&lt;/a&gt; with the TUI,
the logger, and the safety state machine removed, so the driver API is the only thing
on screen.&lt;/p&gt;</description></item><item><title>Polling and the fail-safe</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/polling-and-failsafe/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/polling-and-failsafe/</guid><description>&lt;h1 id="polling-and-the-fail-safe"&gt;Polling and the fail-safe&lt;a class="anchor" href="#polling-and-the-fail-safe"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;A drive command to an M0601 does not latch. Send &amp;ldquo;100 RPM&amp;rdquo; once and the wheel gives
you a brief twitch and then coasts. To make it &lt;em&gt;hold&lt;/em&gt; 100 RPM you have to keep saying
so — resend the drive frame at least every 20 ms, which is the 50 Hz floor for
sustained motion. The motor accepts commands up to 500 Hz; below ~50 Hz it decides
you&amp;rsquo;ve gone away and spins down.&lt;/p&gt;</description></item><item><title>Quickstart</title><link>https://doug.sh/docs/m0601-rs/docs/library/quickstart/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/quickstart/</guid><description>&lt;h1 id="quickstart"&gt;Quickstart&lt;a class="anchor" href="#quickstart"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The smallest useful program: open the port, ask the motor how it&amp;rsquo;s doing, print it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;M0601&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;m0601&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;M0601&lt;/span&gt;::&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/dev/ttyUSB0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{:+}&lt;/span&gt;&lt;span class="s"&gt; RPM, &lt;/span&gt;&lt;span class="si"&gt;{:.1}&lt;/span&gt;&lt;span class="s"&gt;°, &lt;/span&gt;&lt;span class="si"&gt;{:?}&lt;/span&gt;&lt;span class="s"&gt; °C, faults: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speed_rpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position_deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;faults&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;no reply — check 18 V power, wiring (brown → GND), A/B, --id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;query()&lt;/code&gt; sends a &lt;code&gt;0x74&lt;/code&gt; feedback frame and parses the reply. It&amp;rsquo;s the only reply
that carries winding temperature, which is why &lt;code&gt;fb.temp_c&lt;/code&gt; is populated here and
comes back &lt;code&gt;None&lt;/code&gt; from a reply to a drive frame (&lt;a href="https://doug.sh/docs/m0601-rs/docs/library/telemetry/"&gt;Telemetry&lt;/a&gt; has the details).&lt;/p&gt;</description></item><item><title>scan</title><link>https://doug.sh/docs/m0601-rs/docs/cli/scan/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/scan/</guid><description>&lt;h1 id="scan--find-motors-on-the-bus"&gt;&lt;code&gt;scan&lt;/code&gt; — find motors on the bus&lt;a class="anchor" href="#scan--find-motors-on-the-bus"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 scan &lt;span class="c1"&gt;# broadcast, then poll 0x01..0x0F (~3 s)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 scan --full &lt;span class="c1"&gt;# poll every address 0x01..0xFE (~40 s)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reach for &lt;code&gt;scan&lt;/code&gt; first whenever the bus misbehaves: it&amp;rsquo;s how you confirm a motor is
present, discover an address you&amp;rsquo;ve forgotten, or check that you don&amp;rsquo;t have two
motors fighting over one ID.&lt;/p&gt;
&lt;h2 id="the-two-stage-scan"&gt;The two-stage scan&lt;a class="anchor" href="#the-two-stage-scan"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A default scan does two things. It sends one broadcast query that every motor
answers at once, then it polls addresses &lt;code&gt;0x01&lt;/code&gt; through &lt;code&gt;0x0F&lt;/code&gt; individually. That
range is a deliberate bet: motors ship at &lt;code&gt;0x01&lt;/code&gt; and small fleets stay in single
digits, so polling sixteen addresses catches the common case in a couple of seconds
instead of the ~40 it takes to walk all 254. If your motors live at higher
addresses, you need &lt;code&gt;--full&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Drive loops</title><link>https://doug.sh/docs/m0601-rs/docs/library/drive-loops/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/drive-loops/</guid><description>&lt;h1 id="drive-loops"&gt;Drive loops&lt;a class="anchor" href="#drive-loops"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;drive_velocity&lt;/code&gt;, &lt;code&gt;drive_current&lt;/code&gt;, and &lt;code&gt;drive_position&lt;/code&gt; each send &lt;strong&gt;one frame&lt;/strong&gt; and
return. That&amp;rsquo;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&amp;rsquo;t a
limitation to paper over — it&amp;rsquo;s the protocol&amp;rsquo;s fail-safe, and the API is honest about
it rather than hiding a background thread you can&amp;rsquo;t see.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;M0601&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;m0601&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;M0601&lt;/span&gt;::&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/dev/ttyUSB0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Spin at 100 RPM for three seconds: one frame every 20 ms.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt;::&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt;::&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drive_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;thread&lt;/span&gt;::&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// force velocity, zero, brake. Never errors.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;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&amp;rsquo;t
hold speed.&lt;/p&gt;</description></item><item><title>info</title><link>https://doug.sh/docs/m0601-rs/docs/cli/info/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/info/</guid><description>&lt;h1 id="info--config-plus-a-live-snapshot"&gt;&lt;code&gt;info&lt;/code&gt; — config plus a live snapshot&lt;a class="anchor" href="#info--config-plus-a-live-snapshot"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 info
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 --id 0x02 info&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;info&lt;/code&gt; answers &amp;ldquo;is this motor there, and what does it say about itself right now?&amp;rdquo;
It prints the connection configuration and the fixed ranges for each control mode,
sends a single query, and shows one decoded reply. Nothing moves. It&amp;rsquo;s the natural
follow-up to &lt;code&gt;scan&lt;/code&gt; and a handy scriptable presence check, because it exits non-zero
when the motor doesn&amp;rsquo;t answer.&lt;/p&gt;</description></item><item><title>m0601-quad</title><link>https://doug.sh/docs/m0601-rs/docs/samples/quad/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/samples/quad/</guid><description>&lt;h1 id="m0601-quad--the-four-wheel-reference-app"&gt;&lt;code&gt;m0601-quad&lt;/code&gt; — the four-wheel reference app&lt;a class="anchor" href="#m0601-quad--the-four-wheel-reference-app"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;m0601-quad&lt;/code&gt; drives four M0601 wheels as one skid-steer rover, and it is this repo&amp;rsquo;s
&lt;strong&gt;reference implementation for multi-motor use of the library&lt;/strong&gt;. Where
&lt;a href="https://doug.sh/docs/m0601-rs/docs/samples/examples/"&gt;&lt;code&gt;four_wheel_minimal&lt;/code&gt;&lt;/a&gt; shows you the API, &lt;code&gt;m0601-quad&lt;/code&gt;
shows what an application built on that API has to add: a durable wheel map, a
dedicated scheduler thread, a safety state machine, a dashboard, and logging.&lt;/p&gt;
&lt;p&gt;It is &lt;strong&gt;not published to crates.io&lt;/strong&gt; (&lt;code&gt;publish = false&lt;/code&gt;) — it&amp;rsquo;s meant to be read and
adapted, not depended on. Clone the repo and run it from the workspace.&lt;/p&gt;</description></item><item><title>The bus</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/the-bus/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/the-bus/</guid><description>&lt;h1 id="the-bus"&gt;The bus&lt;a class="anchor" href="#the-bus"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;RS485 is a two-wire, half-duplex, multi-drop bus. One differential pair (A/B) carries
traffic in both directions but only one direction at a time, and any number of motors
can hang off it, each with a unique address from &lt;code&gt;0x01&lt;/code&gt; to &lt;code&gt;0xFE&lt;/code&gt;. The host talks, a
motor answers, and everyone shares the same two wires.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;One direction at a time&amp;rdquo; is the whole story on this page. It&amp;rsquo;s why the driver is
careful about &lt;em&gt;when&lt;/em&gt; frames go out, not just what&amp;rsquo;s in them.&lt;/p&gt;</description></item><item><title>Modes</title><link>https://doug.sh/docs/m0601-rs/docs/library/modes/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/modes/</guid><description>&lt;h1 id="modes"&gt;Modes&lt;a class="anchor" href="#modes"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The motor runs one control loop at a time, and the active mode decides how it reads
the 16-bit value in a drive frame. Switch with &lt;code&gt;set_mode&lt;/code&gt;, then drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt;::&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// sends the 0xA0 switch frame 5×, ~100 ms
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drive_current&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ≈ 1 A of torque; resend at 50 Hz to hold
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt;::&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// only legal below 10 RPM
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drive_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16384&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ≈ 180°; resend at 50 Hz to hold
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Mode&lt;/th&gt;
 &lt;th&gt;Setpoint&lt;/th&gt;
 &lt;th&gt;Physical meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Velocity&lt;/code&gt; (default)&lt;/td&gt;
 &lt;td&gt;−330..330 (i16)&lt;/td&gt;
 &lt;td&gt;RPM&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Current&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;−32767..32767 (i16)&lt;/td&gt;
 &lt;td&gt;≈ −8..+8 A (&lt;code&gt;A = raw × 8 / 32767&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Position&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;0..32767 (u16)&lt;/td&gt;
 &lt;td&gt;0°..360° (&lt;code&gt;deg = raw × 360 / 32767&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A few things the table doesn&amp;rsquo;t tell you:&lt;/p&gt;</description></item><item><title>monitor</title><link>https://doug.sh/docs/m0601-rs/docs/cli/monitor/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/monitor/</guid><description>&lt;h1 id="monitor--continuous-telemetry"&gt;&lt;code&gt;monitor&lt;/code&gt; — continuous telemetry&lt;a class="anchor" href="#monitor--continuous-telemetry"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 monitor --hz &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="c1"&gt;# live one-line dashboard, Ctrl+C to stop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 monitor --hz &lt;span class="m"&gt;20&lt;/span&gt; --csv log.csv &lt;span class="c1"&gt;# ...and log every reading to a CSV (see the truncation note)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;monitor&lt;/code&gt; is &lt;code&gt;info&lt;/code&gt; on repeat: it polls at a rate you choose and keeps a single line
updated in place, optionally logging every reading to CSV. It only ever &lt;em&gt;queries&lt;/em&gt; —
it never sends a drive frame — so the wheel keeps doing whatever it was doing (idle,
or driven by something else on the bus). Use it to watch temperature climb under
load, capture a run for later analysis, or just confirm a controller elsewhere is
doing what you expect.&lt;/p&gt;</description></item><item><title>Telemetry and echo</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/telemetry-and-echo/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/telemetry-and-echo/</guid><description>&lt;h1 id="telemetry-and-echoes"&gt;Telemetry and echoes&lt;a class="anchor" href="#telemetry-and-echoes"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Two things make decoding a reply less obvious than reading ten bytes off the wire:
the motor answers in two different layouts, and many USB adapters prepend a copy of
what you just sent. Get either wrong and you don&amp;rsquo;t get an error — you get a plausible,
confidently wrong number. On a motor with real torque, one of those wrong numbers is
dangerous.&lt;/p&gt;
&lt;h2 id="the-two-reply-layouts"&gt;The two reply layouts&lt;a class="anchor" href="#the-two-reply-layouts"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Bytes 0–5, 8, and 9 of a reply are the same in both layouts: address, mode, current,
speed, faults, checksum. Only bytes 6–7 differ, and which meaning they carry depends
on the command that asked:&lt;/p&gt;</description></item><item><title>control</title><link>https://doug.sh/docs/m0601-rs/docs/cli/control/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/control/</guid><description>&lt;h1 id="control--drive-from-the-keyboard"&gt;&lt;code&gt;control&lt;/code&gt; — drive from the keyboard&lt;a class="anchor" href="#control--drive-from-the-keyboard"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 control --rpm &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="c1"&gt;# full-screen dashboard; F/B drive at ±100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;control&lt;/code&gt; is the interactive cockpit: a full-screen dashboard that runs the 50 Hz
drive loop for you and takes live keystrokes. It&amp;rsquo;s what you want when you&amp;rsquo;re bringing
up a motor by hand and reacting to what you see. Its non-interactive twin is
&lt;a href="https://doug.sh/docs/m0601-rs/docs/cli/drive/"&gt;&lt;code&gt;drive&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote class='book-hint caution'&gt;
&lt;p&gt;This starts a live control session with no confirmation. Presets reach 250 RPM on a
gearless wheel. Clear it first. → &lt;a href="https://doug.sh/docs/m0601-rs/docs/safety/"&gt;Safety&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Stopping safely</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/stopping-safely/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/stopping-safely/</guid><description>&lt;h1 id="stopping-safely"&gt;Stopping safely&lt;a class="anchor" href="#stopping-safely"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Stopping a motor sounds like it should be the easy part — send zero. It isn&amp;rsquo;t,
because on this motor zero doesn&amp;rsquo;t mean what you&amp;rsquo;d assume, and because a stop often
has to run from the worst possible moment: a panic, a signal, a dropped connection.&lt;/p&gt;
&lt;h2 id="the-mode-switch-before-the-stop"&gt;The mode switch before the stop&lt;a class="anchor" href="#the-mode-switch-before-the-stop"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A zero setpoint means &amp;ldquo;stop&amp;rdquo; only in velocity mode. The identical zero-valued drive
frame means &amp;ldquo;rotate to 0°&amp;rdquo; in position mode — a stop command that could spin the wheel
up to half a turn on its way to &amp;ldquo;stopping&amp;rdquo; — and in current mode it means zero torque,
a coast, with the brake byte ignored entirely.&lt;/p&gt;</description></item><item><title>Telemetry</title><link>https://doug.sh/docs/m0601-rs/docs/library/telemetry/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/telemetry/</guid><description>&lt;h1 id="telemetry"&gt;Telemetry&lt;a class="anchor" href="#telemetry"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Every frame the motor replies to carries telemetry, including drive frames — so a
50 Hz loop can read while it commands. &lt;code&gt;transact&lt;/code&gt; is the method for that: send a
frame, get the parsed reply back in one exchange.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;protocol&lt;/span&gt;::&lt;span class="n"&gt;frame_velocity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// A drive reply: fine 16-bit angle, but NO temperature.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;assert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temp_c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_none&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{:+}&lt;/span&gt;&lt;span class="s"&gt; RPM at &lt;/span&gt;&lt;span class="si"&gt;{:.2}&lt;/span&gt;&lt;span class="s"&gt;°&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speed_rpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position_deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-two-layouts"&gt;The two layouts&lt;a class="anchor" href="#the-two-layouts"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The catch is that the motor answers in &lt;strong&gt;two different frame layouts&lt;/strong&gt;, and which one
you get depends on the command that asked:&lt;/p&gt;</description></item><item><title>drive</title><link>https://doug.sh/docs/m0601-rs/docs/cli/drive/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/drive/</guid><description>&lt;h1 id="drive--hold-a-setpoint-scriptably"&gt;&lt;code&gt;drive&lt;/code&gt; — hold a setpoint, scriptably&lt;a class="anchor" href="#drive--hold-a-setpoint-scriptably"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 drive velocity --rpm &lt;span class="m"&gt;100&lt;/span&gt; --secs &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 drive current --amps 1.5 --secs &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 drive position --deg &lt;span class="m"&gt;180&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;drive&lt;/code&gt; is the batch counterpart to &lt;a href="https://doug.sh/docs/m0601-rs/docs/cli/control/"&gt;&lt;code&gt;control&lt;/code&gt;&lt;/a&gt;: it holds
a single setpoint in one mode, resending it at 50 Hz until a timer elapses or you
interrupt, then it brakes. It&amp;rsquo;s what you use in scripts and test rigs, where you
want one command that does one thing and stops itself cleanly.&lt;/p&gt;</description></item><item><title>Latency</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/latency/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/latency/</guid><description>&lt;h1 id="adapter-latency"&gt;Adapter latency&lt;a class="anchor" href="#adapter-latency"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Here&amp;rsquo;s a failure that looks like a protocol problem but is really a USB one. You send
a query, wait a reasonable 6 ms for the reply, read the port — and get nothing. Do it
again, same result. The motor is fine, your framing is fine, and the reply &lt;em&gt;did&lt;/em&gt;
arrive. It&amp;rsquo;s just still sitting inside the adapter.&lt;/p&gt;
&lt;p&gt;FTDI-based adapters — the usual RS485 dongle — don&amp;rsquo;t hand each byte to the host as it
lands. They batch received bytes until a USB packet fills or their &lt;strong&gt;latency timer&lt;/strong&gt;
fires, and from the factory that timer is &lt;strong&gt;16 ms&lt;/strong&gt;. A 10-byte reply that physically
arrived in under a millisecond can therefore sit in the adapter for up to 16 ms before
your program sees it. That&amp;rsquo;s longer than this protocol&amp;rsquo;s entire reply window, and it&amp;rsquo;s
far longer than the 20 ms cycle a 50 Hz loop lives in. Short reply waits read nothing,
every time, and the bus looks dead.&lt;/p&gt;</description></item><item><title>Odometry</title><link>https://doug.sh/docs/m0601-rs/docs/library/odometry/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/odometry/</guid><description>&lt;h1 id="odometry"&gt;Odometry&lt;a class="anchor" href="#odometry"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The motor&amp;rsquo;s position reading is &lt;strong&gt;single-turn absolute&lt;/strong&gt;: it tells you where in a
revolution the wheel is, and it wraps from 359.99° back to 0° every turn. That&amp;rsquo;s
exactly wrong for distance travelled — naïvely differencing samples gives you a −360°
jump each revolution.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PositionAccumulator&lt;/code&gt; unwraps it into a continuous angle:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;PositionAccumulator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;odo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PositionAccumulator&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// in the poll loop:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply_wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;travelled_deg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;odo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position_deg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{travelled_deg:.1}&lt;/span&gt;&lt;span class="s"&gt;° (&lt;/span&gt;&lt;span class="si"&gt;{:.2}&lt;/span&gt;&lt;span class="s"&gt; rev)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;odo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revolutions&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Method&lt;/th&gt;
 &lt;th&gt;What it does&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;update(deg)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;feed one absolute sample; returns the updated continuous angle&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;update_raw(u16)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;same, from a raw drive-reply reading (&lt;code&gt;0..=32767&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;cumulative_deg()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the running total without feeding a sample&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;revolutions()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the same value as whole and fractional turns&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;reset()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;forget the reference and zero the total&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three properties worth knowing:&lt;/p&gt;</description></item><item><title>Multi-motor bus</title><link>https://doug.sh/docs/m0601-rs/docs/library/multi-motor/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/multi-motor/</guid><description>&lt;h1 id="multi-motor-robots"&gt;Multi-motor robots&lt;a class="anchor" href="#multi-motor-robots"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;RS485 is multi-drop: every wheel shares one A/B pair, each answering to a unique
address. A &lt;code&gt;Bus&lt;/code&gt; owns that shared port and mints per-motor handles that are cheap to
clone and safe to move between threads — they all funnel through one physical port,
with the coordination handled for you.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="n"&gt;Bus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;m0601&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bus&lt;/span&gt;::&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/dev/ttyUSB0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mirrored&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// FIT1042, left side
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// FIT1038, right side
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Mirrored, &amp;#34;+100&amp;#34; drives the robot forward on both sides.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drive_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drive_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...and resend both at ≥50 Hz
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="mirroring"&gt;Mirroring&lt;a class="anchor" href="#mirroring"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;On a two-sided robot the left and right wheels face opposite ways, so &amp;ldquo;forward&amp;rdquo; is
+RPM on one and −RPM on the other. &lt;code&gt;mirrored(true)&lt;/code&gt; hides that: it negates
velocity and current &lt;em&gt;setpoints&lt;/em&gt; on the way out and flips the &lt;em&gt;signs&lt;/em&gt; of reported
speed and current on the way in, so your code speaks in robot-forward terms and both
sides agree.&lt;/p&gt;</description></item><item><title>set-id</title><link>https://doug.sh/docs/m0601-rs/docs/cli/set-id/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/set-id/</guid><description>&lt;h1 id="set-id--change-a-motors-address"&gt;&lt;code&gt;set-id&lt;/code&gt; — change a motor&amp;rsquo;s address&lt;a class="anchor" href="#set-id--change-a-motors-address"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 set-id --new 0x02 &lt;span class="c1"&gt;# prompts for confirmation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 set-id --new 0x02 --yes &lt;span class="c1"&gt;# skips the prompt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every motor on an RS485 bus needs a unique address. Fresh motors all ship at &lt;code&gt;0x01&lt;/code&gt;,
so before you can put two on one bus you have to renumber them — one at a time, with
this command. The new address is written to flash and survives power cycles.&lt;/p&gt;
&lt;h2 id="options"&gt;Options&lt;a class="anchor" href="#options"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Flag&lt;/th&gt;
 &lt;th&gt;Default&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--new &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;em&gt;(required)&lt;/em&gt;&lt;/td&gt;
 &lt;td&gt;the new address, &lt;code&gt;0x01..0xFE&lt;/code&gt; (hex or decimal)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;--yes&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;off&lt;/td&gt;
 &lt;td&gt;skip the confirmation prompt&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-one-motor-guard"&gt;The one-motor guard&lt;a class="anchor" href="#the-one-motor-guard"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The set-ID frame is &lt;strong&gt;unaddressed&lt;/strong&gt;. It carries no target — every motor that hears
it takes the new address. Put two motors on the bus and run &lt;code&gt;set-id&lt;/code&gt; once, and now
you have two motors at the same address, which you can only untangle by
disconnecting them one at a time and renumbering again. It&amp;rsquo;s persistent state that&amp;rsquo;s
tedious to undo, so the command works hard to stop you doing it by accident.&lt;/p&gt;</description></item><item><title>The driver boundary</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/driver-boundary/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/driver-boundary/</guid><description>&lt;h1 id="the-driver-boundary"&gt;The driver boundary&lt;a class="anchor" href="#the-driver-boundary"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;This crate is a &lt;strong&gt;motor driver&lt;/strong&gt;, not a robot framework. Knowing exactly where it
stops saves you from two opposite mistakes: reaching around it to re-encode things it
already owns, and waiting for it to grow features that are yours to write. This page
draws the line and explains why it sits where it does.&lt;/p&gt;
&lt;h2 id="the-boundary-rule"&gt;The boundary rule&lt;a class="anchor" href="#the-boundary-rule"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The driver owns three layers, and nothing above them:&lt;/p&gt;</description></item><item><title>Budgeting the wire</title><link>https://doug.sh/docs/m0601-rs/docs/library/budgeting/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/budgeting/</guid><description>&lt;h1 id="budgeting-the-wire"&gt;Budgeting the wire&lt;a class="anchor" href="#budgeting-the-wire"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Every multi-motor loop eventually asks the same question: &lt;em&gt;will N wheels plus their
polls fit in my cycle?&lt;/em&gt; The crate answers it with arithmetic rather than advice, so
you size a loop instead of guessing at one and discovering the answer as a wheel that
stutters.&lt;/p&gt;
&lt;p&gt;Three functions, all pure and I/O-free:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bus_period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drive_floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame_time&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;frame_time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ~868 µs — one 10-byte frame at 115200 8N1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;floor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drive_floor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 20 ms — the longest gap before a wheel coasts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bus_period&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// four drives + one poll ≈ 17.2 ms
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-two-cycle-bounds"&gt;The two cycle bounds&lt;a class="anchor" href="#the-two-cycle-bounds"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A periodic control loop has to satisfy both of these at once, and they squeeze from
opposite sides:&lt;/p&gt;</description></item><item><title>raw</title><link>https://doug.sh/docs/m0601-rs/docs/cli/raw/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/cli/raw/</guid><description>&lt;h1 id="raw--send-an-arbitrary-frame"&gt;&lt;code&gt;raw&lt;/code&gt; — send an arbitrary frame&lt;a class="anchor" href="#raw--send-an-arbitrary-frame"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 raw &lt;span class="s2"&gt;&amp;#34;01 74 00 00 00 00 00 00 00&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 9 bytes: CRC appended for you&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 raw &lt;span class="s2"&gt;&amp;#34;01 74 00 00 00 00 00 00 00 FF&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# 10 bytes: sent exactly as typed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;m0601 raw --yes &lt;span class="s2"&gt;&amp;#34;01 64 00 64 00 00 03 00 00&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# a drive frame — needs --yes&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;raw&lt;/code&gt; is the protocol workbench. It sends bytes you specify and prints what comes
back, decoding the reply when the command you sent is one that elicits telemetry.
Use it to poke at the protocol, reproduce a frame from the &lt;a href="https://doug.sh/docs/m0601-rs/docs/protocol/"&gt;reference&lt;/a&gt;, or test how the motor reacts to something deliberately malformed.&lt;/p&gt;</description></item><item><title>Testing without hardware</title><link>https://doug.sh/docs/m0601-rs/docs/library/testing/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/library/testing/</guid><description>&lt;h1 id="testing-without-hardware"&gt;Testing without hardware&lt;a class="anchor" href="#testing-without-hardware"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;M0601&lt;/code&gt; and &lt;code&gt;Bus&lt;/code&gt; are generic over a &lt;code&gt;Transport&lt;/code&gt;, and one of the transports is a
mock. That means the same code you ship on hardware runs unchanged against a
scripted in-memory bus — no &lt;code&gt;cfg&lt;/code&gt; flags, no seams to maintain. It&amp;rsquo;s how the crate&amp;rsquo;s
own driver tests work, and it&amp;rsquo;s public API you can use for yours.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;time&lt;/span&gt;::&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m0601&lt;/span&gt;::&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;M0601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MockTransport&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MockTransport&lt;/span&gt;::&lt;span class="n"&gt;with_replies&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// one query reply: 100 RPM, 40 °C, position byte 0x00
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;M0601&lt;/span&gt;::&lt;span class="n"&gt;with_transport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;::&lt;span class="n"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="fm"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speed_rpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Then inspect exactly what your code put on the wire:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;motor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;into_transport&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="fm"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two halves make it useful: you script what the &amp;ldquo;motor&amp;rdquo; says with &lt;code&gt;with_replies&lt;/code&gt;, and
afterward you recover the transport with &lt;code&gt;into_transport&lt;/code&gt; to assert on &lt;code&gt;sent&lt;/code&gt; — every
frame your code transmitted, in order. So you can test both directions: did the motor
report what I expected me to do with it, and did I send the right bytes?&lt;/p&gt;</description></item><item><title>Getting started</title><link>https://doug.sh/docs/m0601-rs/docs/getting-started/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/getting-started/</guid><description>&lt;h1 id="getting-started"&gt;Getting started&lt;a class="anchor" href="#getting-started"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;This page gets you built, wired, and permitted. If you&amp;rsquo;d rather learn by doing,
the &lt;a href="https://doug.sh/docs/m0601-rs/docs/tutorial/"&gt;first-spin tutorial&lt;/a&gt; covers the same ground with a
motor actually turning.&lt;/p&gt;
&lt;h2 id="build--install"&gt;Build &amp;amp; install&lt;a class="anchor" href="#build--install"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You need Rust &lt;strong&gt;1.88&lt;/strong&gt; or newer — the crate uses edition 2024 and let-chains, so
older toolchains won&amp;rsquo;t compile it. Linux is the tested platform; the serial layer
is portable, but the device paths and the &lt;code&gt;dialout&lt;/code&gt; group below are Linux-isms.&lt;/p&gt;</description></item><item><title>First spin</title><link>https://doug.sh/docs/m0601-rs/docs/tutorial/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/tutorial/</guid><description>&lt;h1 id="first-spin"&gt;First spin&lt;a class="anchor" href="#first-spin"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;This is the motor-on-the-bench walkthrough: from bare wires to a spinning wheel and
back to a controlled stop, with what you should see at each step and what to do when
you don&amp;rsquo;t. It assumes you&amp;rsquo;ve built the CLI (&lt;a href="https://doug.sh/docs/m0601-rs/docs/getting-started/"&gt;Getting started&lt;/a&gt;).&lt;/p&gt;
&lt;blockquote class='book-hint caution'&gt;
&lt;p&gt;A direct-drive hub motor has real torque (2 N·m stall) and no gearbox to slow it
down. Before anything spins: get the wheel off the ground or clear of fingers,
cables, and the edge of the bench. &lt;code&gt;Ctrl-C&lt;/code&gt; brakes, but only while the process is
alive to do it. → &lt;a href="https://doug.sh/docs/m0601-rs/docs/safety/"&gt;Safety&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Safety</title><link>https://doug.sh/docs/m0601-rs/docs/safety/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/safety/</guid><description>&lt;h1 id="safety"&gt;Safety&lt;a class="anchor" href="#safety"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;A direct-drive hub motor is not a hobby servo. There is &lt;strong&gt;no gearbox&lt;/strong&gt; between the
winding and the tyre, 2 N·m of stall torque, and the CLI&amp;rsquo;s presets reach 250 RPM. It
will drive a chassis off a bench, and it will not notice your hand.&lt;/p&gt;
&lt;p&gt;This page is the one place all of that is stated plainly. The warnings scattered
through the rest of the docs point here.&lt;/p&gt;</description></item><item><title>Setpoint shaping</title><link>https://doug.sh/docs/m0601-rs/docs/concepts/setpoint-shaping/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/concepts/setpoint-shaping/</guid><description>&lt;h1 id="setpoint-shaping"&gt;Setpoint shaping&lt;a class="anchor" href="#setpoint-shaping"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;There are four different things in this system that can be called &amp;ldquo;ramping,&amp;rdquo; and
they act at different places. Getting them straight is the difference between a
machine that starts smoothly and one that trips its own overcurrent protection.&lt;/p&gt;
&lt;h2 id="firmware-side-control-loops"&gt;Firmware-side control loops&lt;a class="anchor" href="#firmware-side-control-loops"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The M0601 closes velocity, current, and position &lt;strong&gt;in its own firmware&lt;/strong&gt;. Selecting a
&lt;a href="https://doug.sh/docs/m0601-rs/docs/library/modes/"&gt;&lt;code&gt;Mode&lt;/code&gt;&lt;/a&gt; chooses &lt;em&gt;which of the motor&amp;rsquo;s PIDs&lt;/em&gt; your
drive frame feeds. It does not hand you an open-loop actuator to close a loop around.&lt;/p&gt;</description></item><item><title>Protocol reference</title><link>https://doug.sh/docs/m0601-rs/docs/protocol/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/protocol/</guid><description>&lt;h1 id="m0601-protocol--hardware-reference"&gt;M0601 protocol &amp;amp; hardware reference&lt;a class="anchor" href="#m0601-protocol--hardware-reference"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Everything this repo knows about the DFRobot &lt;strong&gt;M0601&lt;/strong&gt; direct-drive hub motor,
compiled from the official documentation and three independent implementations,
cross-checked byte-for-byte against this crate&amp;rsquo;s test vectors. Where sources
disagree, the disagreement is recorded (see &lt;a href="#known-contradictions-between-sources"&gt;Known
contradictions&lt;/a&gt;) rather than silently resolved.&lt;/p&gt;
&lt;p&gt;This is the byte-level reference. For what these frames &lt;em&gt;mean&lt;/em&gt; and why the driver
handles them the way it does, see &lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/"&gt;Concepts&lt;/a&gt; — especially
&lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/the-bus/"&gt;The bus&lt;/a&gt; and &lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/telemetry-and-echo/"&gt;Telemetry and echo&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>FAQ &amp; gotchas</title><link>https://doug.sh/docs/m0601-rs/docs/faq/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/faq/</guid><description>&lt;h1 id="faq--gotchas"&gt;FAQ &amp;amp; gotchas&lt;a class="anchor" href="#faq--gotchas"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;The questions that come up once you&amp;rsquo;re past &amp;ldquo;does it turn,&amp;rdquo; most of them the kind you
only ask after something surprising happened. &lt;a href="https://doug.sh/docs/m0601-rs/docs/troubleshooting/"&gt;Troubleshooting&lt;/a&gt; is the symptom-to-fix table; this is the why-does-it-do-that.&lt;/p&gt;
&lt;h2 id="spins-then-stops"&gt;Wheel spins briefly, then coasts&lt;a class="anchor" href="#spins-then-stops"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Your control loop is running below 50 Hz. A drive command doesn&amp;rsquo;t latch — the motor
holds a setpoint only while frames keep arriving at least every 20 ms, and coasts
otherwise. Either your loop&amp;rsquo;s sleep is too long, or you replaced a drive frame with a
telemetry query on some cycles (that leaves a hole in the drive cadence). Read
telemetry from the drive reply instead, and keep the loop at 20 ms.
See &lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/polling-and-failsafe/"&gt;Polling and the fail-safe&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Troubleshooting</title><link>https://doug.sh/docs/m0601-rs/docs/troubleshooting/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/troubleshooting/</guid><description>&lt;h1 id="troubleshooting"&gt;Troubleshooting&lt;a class="anchor" href="#troubleshooting"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;Symptom on the left, the thing to check on the right. This page is deliberately just
the table — where a symptom has a &lt;strong&gt;Why&lt;/strong&gt; link, that&amp;rsquo;s the &lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/"&gt;FAQ&lt;/a&gt;
entry which owns the full explanation, so nothing is explained twice in two places.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Symptom&lt;/th&gt;
 &lt;th&gt;Check&lt;/th&gt;
 &lt;th&gt;Why&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;scan&lt;/code&gt; finds nothing&lt;/td&gt;
 &lt;td&gt;18 V power on? A/B swapped (try orange ↔ white)? Brown wire → GND? A silent bus is almost always wiring.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#empty-scan"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Permission denied&lt;/code&gt; on the port&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;sudo usermod -aG dialout $USER&lt;/code&gt;, then log out and back in.&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Found the motor, but &lt;code&gt;info&lt;/code&gt; won&amp;rsquo;t read it&lt;/td&gt;
 &lt;td&gt;Wrong &lt;code&gt;--id&lt;/code&gt;, or the RX half of the wiring. &lt;code&gt;scan&lt;/code&gt; shows the real address.&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Wheel spins briefly, then coasts&lt;/td&gt;
 &lt;td&gt;Your loop is under 50 Hz, or a query is replacing a drive frame on some cycles. Keep the drive cadence at 20 ms.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#spins-then-stops"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Sent zero, but it moved or didn&amp;rsquo;t brake&lt;/td&gt;
 &lt;td&gt;You weren&amp;rsquo;t in velocity mode. Use &lt;code&gt;safe_stop&lt;/code&gt; / the &lt;code&gt;S&lt;/code&gt; key, which force velocity first.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#zero-didnt-stop"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;control&lt;/code&gt;&amp;rsquo;s &lt;code&gt;P&lt;/code&gt; (or &lt;code&gt;drive position&lt;/code&gt;) refused&lt;/td&gt;
 &lt;td&gt;Wheel at ≥10 RPM, or no telemetry yet (it fails closed on unknown speed).&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#position-refused"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Faults the instant a drive starts&lt;/td&gt;
 &lt;td&gt;Ramp too steep — accel &lt;code&gt;1&lt;/code&gt; (and &lt;code&gt;0&lt;/code&gt;, the same ramp) spikes current past the 3 A protection. Soften with a larger accel byte; &lt;code&gt;3&lt;/code&gt;–&lt;code&gt;5&lt;/code&gt;, not 40. Auto-resets in ~5 s.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#fault-on-start"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Motor ignores drive frames, a fault bit is set&lt;/td&gt;
 &lt;td&gt;A protection is active (3 A bus / 4.6 A phase / 80 °C / stall). The motor stops responding to drive commands until it clears — ~5 s, or on cooling to 75 °C for overheat.&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;raw&lt;/code&gt; refuses to send my frame&lt;/td&gt;
 &lt;td&gt;It&amp;rsquo;s a motion command (byte 1 = &lt;code&gt;0x64&lt;/code&gt; or &lt;code&gt;0xA0&lt;/code&gt;). Pass &lt;code&gt;--yes&lt;/code&gt;.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#raw-by-design"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Intermittent garbage / dropouts&lt;/td&gt;
 &lt;td&gt;Brown wire floating, or missing 120 Ω termination on a cable over ~1 m.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#intermittent"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Chaos after a &lt;code&gt;set-id&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The unaddressed frame renamed every motor that heard it. Reconnect one at a time and renumber.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#set-id-renamed-all"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;A rejected &lt;code&gt;--rpm&lt;/code&gt; / &lt;code&gt;--amps&lt;/code&gt; / &lt;code&gt;--deg&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Out of range. The CLI rejects rather than clamps; the library clamps.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#out-of-range"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;kill -9&lt;/code&gt; didn&amp;rsquo;t stop the motor&lt;/td&gt;
 &lt;td&gt;Correct — nothing runs, so it coasts. Not an emergency stop; cut power.&lt;/td&gt;
 &lt;td&gt;&lt;a href="https://doug.sh/docs/m0601-rs/docs/faq/#kill-9-coasts"&gt;why&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Short reply waits read nothing&lt;/td&gt;
 &lt;td&gt;FTDI 16 ms latency timer. See &lt;a href="https://doug.sh/docs/m0601-rs/docs/concepts/latency/"&gt;Latency&lt;/a&gt; — the udev rule fixes it.&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="end-to-end-checklist"&gt;End-to-end checklist&lt;a class="anchor" href="#end-to-end-checklist"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Work outward from the physical layer, because that&amp;rsquo;s where the failures cluster:&lt;/p&gt;</description></item><item><title>Internals</title><link>https://doug.sh/docs/m0601-rs/docs/internals/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://doug.sh/docs/m0601-rs/docs/internals/</guid><description>&lt;h1 id="internals"&gt;Internals&lt;a class="anchor" href="#internals"&gt;#&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;For contributors and the curious: how the crate is put together, and why it&amp;rsquo;s shaped
that way. None of this is needed to &lt;em&gt;use&lt;/em&gt; the driver — it&amp;rsquo;s the map you&amp;rsquo;d want before
changing it.&lt;/p&gt;
&lt;h2 id="the-transport-seam"&gt;The transport seam&lt;a class="anchor" href="#the-transport-seam"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;All I/O goes through one small trait, &lt;code&gt;Transport&lt;/code&gt;, with two implementations:
&lt;code&gt;SerialTransport&lt;/code&gt; for real hardware and &lt;code&gt;MockTransport&lt;/code&gt; for tests. &lt;code&gt;M0601&lt;/code&gt; and &lt;code&gt;Bus&lt;/code&gt;
are generic over it, so the exact code that ships runs unchanged against a scripted
in-memory bus — there are no &lt;code&gt;cfg(test)&lt;/code&gt; branches threading through the driver logic.&lt;/p&gt;</description></item></channel></rss>