Skip to content

The servos report velocity and load every tick, and the wire dropped both - #260

Open
xaqmusic wants to merge 1 commit into
pollen-robotics:mainfrom
xaqmusic:state-velocities-currents
Open

xaqmusic wants to merge 1 commit into
pollen-robotics:mainfrom
xaqmusic:state-velocities-currents

Conversation

@xaqmusic

Copy link
Copy Markdown

bus.rs reads one contiguous twelve-byte block per tick at register 124 — present_pwm, present_current, present_velocity, present_position — and unpacks all four into Sensors. Only position has ever reached robot.state. The other two are measured, paid for, and discarded at the socket.

Neither is recoverable from outside the daemon. Differencing joints between frames is not velocity: subscribers are decimated per connection, so a client at 10 Hz differences across five ticks, and any dropped frame becomes a spike. Load has no substitute at all — present current is the robot's only measure of external force: a foot taking weight, a hand pressing the beak, a servo on its way to an overload shutdown.

The change

RobotState gains velocities (rad/s) and currents_ma, indexed as JOINT_NAMES, filled from the Sensors the loop already holds. Both ride as #[serde(default, skip_serializing_if = "Vec::is_empty")], on the rule odom set: a frame from an older robotd still parses, and absent stays distinguishable from zero — a robot at rest and a robot that cannot say what it is doing must not render alike. API_VERSION goes 27 → 28 with an entry in the history, as v24 and v25 did for additive fields on the state stream.

FakeIo gains set_velocities / set_currents_ma beside set_imu. Positions are echoed from what was written, and deriving these the same way would make any test of the new path pass vacuously. robotctl's monitor fixture names every field of the frame, so it gains the two, empty: the monitor draws neither.

Cost

Measured on a frame assembled the way the loop assembles it, with the IMU, frames and skeleton present: 3042 B → 3346 B standing (+304 B, +10 %) and 3422 B walking (+380 B, +12 %); the spread is the digits a velocity carries once it is counts × 0.229 rpm. That is +15 to +19 KB/s at 50 Hz and +3 to +4 KB/s at a 10 Hz dashboard. The loop already skips frame assembly when nobody is subscribed, so a robot with no client pays nothing.

Validation

Three tests: the blocks land under their documented names and survive a round trip; an older frame reports empty rather than zeros; and the control loop, driven by FakeIo with values distinguishable from each other and from the joint angles, puts both on the stream. duck-ipc-proto 50 → 52, robotd 150 → 151, the workspace 1363 → 1366, 0 failed. cargo fmt --all --check and cargo clippy --workspace --all-targets under -D warnings are clean, and cargo llvm-cov reports 72.17 % of lines on this branch against 72.11 % on main and the floor of 72.

What it does not show: everything here runs against FakeIo, so it covers the plumbing from Sensors to the wire and nothing below it. The unpack in bus.rs is untouched, and whether an XL330 reports a sensible current under load is a bench question — I have no hardware.

Out of scope

robotctl monitor still displays neither field. And serde_json round-trips some f64 on this wire inexactly (1.4000000000000001 comes back 1.4), which affects joints, targets and gravity today and is about 1e-16 against a sensor resolution near 1e-3; the round-trip test compares approximately, on the rule SafetyState states when it drops Eq. Both belong in their own change, if you want them at all.

…both

`bus.rs` reads one contiguous twelve-byte block per tick at register 124 --
present_pwm, present_current, present_velocity, present_position -- and unpacks
all four into `Sensors`. Only position has ever reached `robot.state`. The other
two are measured, paid for, and discarded at the socket.

Neither is recoverable from outside the daemon:

  * Differencing `joints` between frames is not velocity. Subscribers are
    decimated per connection, so a client asking for 10 Hz differences across
    five ticks, and any dropped frame becomes a spike.
  * Load has no substitute at all. Present current is the robot's only measure
    of external force -- a foot taking weight, a hand pressing the beak, a servo
    on its way to an overload shutdown.

Both ride as `#[serde(default, skip_serializing_if = "Vec::is_empty")]`, on the
rule `odom` set: a frame from an older `robotd` still parses, and *absent* stays
distinguishable from *zero*. A robot at rest and a robot that cannot say what it
is doing must not render alike. `API_VERSION` goes 27 -> 28 with an entry in the
history, as v24 and v25 did for additive fields on the state stream.

Cost, measured on a frame assembled the way the loop assembles it, with the IMU,
frames and skeleton present: 3042 B -> 3346 B standing (+304 B, +10 %) and
3422 B walking (+380 B, +12 %); the spread is the digits a velocity carries once
it is counts times 0.229 rpm. That is +15 to +19 KB/s at 50 Hz and +3 to +4 KB/s
at a 10 Hz dashboard. The loop already skips frame assembly entirely when nobody
is subscribed, so a robot with no client pays nothing.

`FakeIo` gains `set_velocities` / `set_currents_ma`, mirroring `set_imu`.
Positions are echoed from what was written, and deriving these the same way
would make any test of the new path pass vacuously. `robotctl`'s monitor
fixture names every field of the frame, so it gains the two, empty: the monitor
draws neither.

Tests: the blocks land under their documented names and survive a round trip;
an older frame reports empty rather than zeros; and the control loop, driven by
`FakeIo` with values distinguishable from each other and from the joint angles,
puts both on the stream.

  duck-ipc-proto 50 -> 52, robotd 150 -> 151, the workspace 1363 -> 1366,
  0 failed. cargo fmt --all --check clean; cargo clippy --workspace
  --all-targets clean under -D warnings.

What this does not show. Every test here runs against `FakeIo`, so it covers the
plumbing from `Sensors` to the wire and nothing below it. The unpack in `bus.rs`
is untouched and unmeasured by this change; whether a real XL330 reports a
sensible current under load is a bench question, and I have no hardware.

Deliberately not included. `robotctl monitor` still displays neither field, and
`serde_json` round-trips some f64 on this wire inexactly (1.4000000000000001
comes back 1.4) -- which affects `joints`, `targets` and `gravity` today and is
about 1e-16 against a sensor resolution near 1e-3. Both belong in their own
change, if you want them at all.

Assisted-by: Claude:claude-fable-5-1
xaqmusic added a commit to xaqmusic/xaq-ai that referenced this pull request Sep 10, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiVPtCnrXZi5jbREK8ohSG
Signed-off-by: xaqmusic <xaqmusic@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant