Skip to content

Add tool board firmware that speaks the MKS drive frames - #32

Merged
yassinsolim merged 57 commits into
mainfrom
feature/tool-board-firmware
Oct 10, 2026
Merged

yassinsolim merged 57 commits into
mainfrom
feature/tool-board-firmware

Conversation

@yassinsolim

Copy link
Copy Markdown
Member

Covers item 6 from the Oct 3 list: firmware for the 7th board, the biomedical tool board. Stacked on #31, which adds the set-zero frame.

What changes

  • firmware/tool_board/tool_board.ino: the four NEMA 8 steppers (8HS11-0204S on A4988 drivers) answer on CAN IDs 7 to 10 like MKS SERVO drives. The board handles:

    • read encoder (31h), work mode (82h), replies (8Ch), set zero (92h), heartbeat (98h), enable (F3h), and absolute moves (F5h, where speed 0 stops).

    The host can therefore drive the tool with the code it already uses for the arm.

  • Positions: reported in MKS counts (16384 per turn), converted from the step count, since the motors have no encoders.

  • Speed limits: requests are capped at what a NEMA 8 can follow (60 rpm, 20000 steps/s²).

  • Travel limits: targets past a limit stop at the limit and report status 3 (stopped at end limit).

  • mks_frames.h: the frame code as plain C++. test_tool_board_frames.py compiles it with g++ and checks it against mks_can.py both ways: commands built by mks_can decode correctly, and the firmware's replies parse with mks_can.

  • CI: the new Tool Board Firmware workflow compiles the sketch with arduino/compile-sketches for the UNO R4 WiFi when firmware/ changes.

  • firmware/tool_board/README.md: pin table, command table, build and flash steps, and a cansend bench check with the expected replies (generated with mks_can).

Needs checking before use

  • Hardware assumptions: UNO R4 WiFi, CAN transceiver on D10/D13, and CNC Shield V3 step and direction pins. The fourth motor's direction pin moves to D11, because D13 is the CAN receive pin.
    • All of these are constants at the top of the sketch.
    • The sketch refuses to compile for the UNO R4 Minima, whose CAN pins (D4/D5) collide with the step pins.
  • Travel limits: placeholders (±1 turn). Copy the real limits from the Biomedical team's nema8_2pair_LimitedRotationV3.ino before driving the tool, because the cables limit how far each motor may turn.
  • Enable pin: the four A4988s share one enable pin, so enabling any motor powers all four.
  • Clamp model: the tool's joints come from Karis's clamp model. Once it exists, the motors go into arm_drives.yaml as drives 7 to 10. Ten drives at 120 Hz use about half the 1 Mbit/s bus.

How I tested it

  • arduino-cli 1.5.2 with arduino:renesas_uno 1.6.0 and AccelStepper 1.64:
    • The UNO R4 WiFi build compiles with no warnings from the sketch (60 KB, 22% of flash).
    • The Minima build stops at the pin guard, as intended.
  • docker compose --progress plain build test: all 781 tests pass, 14 of them new, none skipped.
  • Not run on hardware.

Replace the placeholder with the five-joint arm exported from
full-arm-smaller.SLDASM. The tools in scripts/cad regenerate the URDF
and meshes after CAD changes. demo_mode:=true sweeps each joint in turn
and reports a pass/fail row per joint on /diagnostics.

Supersedes #7.
Drive the five joints with an Xbox controller through placeholder MKS
SERVO42D/57D CAN drives, simulated until the real drives are connected.
On Windows, a Python-only XInput bridge sends the controller state to
the new wslg-teleop Compose service over UDP. The RViz camera now
follows the tool so the end of the arm stays in view.
# Conflicts:
#	waybionic_bringup/launch/ground_station.launch.py
# Conflicts:
#	waybionic_bringup/CMakeLists.txt
demo_mode:=True or 1 started joint_state_publisher_gui next to the demo,
because the launch compared the raw text with 'true'. And/Not substitutions
parse booleans the same way IfCondition does. The joint demo test now uses
True and fails without this change.

joint_demo rejects a speed_deg_s of zero or less at startup instead of never
finishing a move, and receives use_sim_time like the other nodes. The CAD
script's help now says which export --check-moves compares.
# Conflicts:
#	waybionic_bringup/launch/ground_station.launch.py
follow_camera:=false stopped the camera follower, so RViz lost the
view_focus frame it orbits. The follower now always runs and holds its
default focus when follow is false, and a launch test checks the frame.

teleop and joy_source use And/Or/Equals substitutions, so teleop:=True
works like IfCondition; the teleop test now uses True. The new nodes
receive use_sim_time, and the UDP receiver reads at most 100 packets per
poll so a flood cannot starve its other callbacks.
The arm is a base yaw, three parallel pitch joints and a roll through the
tool tip, so kinematics.py solves any tip position and tool tilt in closed
form from the URDF. The new Cartesian group (Y) moves the tip along straight
lines in base_link with the tilt fixed; LB keeps only the strongest axis.
At a joint speed limit or the workspace edge the whole step shrinks, so the
tip stops on the line, and the roll turns against the yaw to keep the tool's
heading.

Each drive now gets the next setpoint one period ahead and the speed that
reaches it in exactly one period, measured from its encoder, so all drives
arrive together. acc is 255 because a gentle drive ramp adds the same rpm/s
to every drive and the one with the largest change falls behind. In
simulation a sideways cut at 30:1 gearing stays within 7-17 um of the line,
compared with up to 186 um with the old 1.5x speed rule. At the 1:1
placeholder gearing it stays within about 0.13 mm, one encoder count at the
tip.
D-pad left/right changes the tool pitch while the tip stays in place, so
the shoulder, elbow and wrist move around a fixed point. The tilt uses the
same closed-form step as straight moves, so a joint limit stops it with the
tip still in place. It runs at up to 30 deg/s, scaled by the speed level.
- Start is refused while a tilt button or A is held, like the sticks.
- Homing clears the Cartesian rates, so releasing A can't resume the last move.
- Cartesian roll ramps with the acceleration limit and scales with the accepted step.
- The drives' setpoint one period ahead stays within the URDF joint limits, which the
  drive node now reads from robot_description.
- If one drive would pass max_rpm, every drive slows by the same factor, so they still
  arrive together.
- The roll compensation is documented as stopping the tool spinning about its own axis;
  that keeps a blade's heading only when the tool points straight down.
- The Cartesian launch test is registered where it won't conflict with #22.
Reviewed in #24, which was merged into its stacked base after #23 had
already been squash-merged into main, so it never reached main. This is
the same change, applied to main.

Drive the five joints with an Xbox controller through placeholder MKS
SERVO42D/57D CAN drives, simulated until the real drives are connected.
On Windows, a Python-only XInput bridge sends the controller state to
the new wslg-teleop Compose service over UDP. The RViz camera follows
the tool so the end of the arm stays in view; follow_camera:=false keeps
a fixed view.

teleop and joy_source use And/Or/Equals substitutions, so teleop:=True
works like IfCondition. The new nodes receive use_sim_time, and the UDP
receiver reads at most 100 packets per poll so a flood cannot starve its
other callbacks.
The drive node can now send its frames to real MKS SERVO42D/57D drives
through any python-can interface, chosen with the drive_interface and
drive_channel launch arguments. The simulation stays the default.

Because the encoders count from power-on, nothing is published or
driven until the arm is zeroed (92h) through ~/zero. Simulated drives
are zeroed at start-up. A drive that stops answering stops every
drive and must be zeroed again. Stale commands hold the last target,
and the drives are stopped when the node exits.

mks_drive_sim answers on a CAN interface like the drives do. A new CI
job runs the end-to-end test over vcan0. UDP multicast hands every
frame back to its sender, and MKS replies reuse the command's CAN ID,
so the bus drops its own echoes.
The biomedical tool's four NEMA 8 steppers (A4988 drivers) answer on
CAN IDs 7 to 10 like MKS SERVO drives: read encoder, mode, replies,
zero, heartbeat, enable and absolute moves. The ground station can drive
them with the same host code as the arm. The sketch targets the UNO R4
WiFi with a CNC Shield V3 layout. Its travel limits are placeholders
until the Biomedical team's limits are copied in.

The frame code is plain C++, and a teleop test compiles it to check it
against mks_can.py. A CI job compiles the sketch with arduino-cli.
Copilot AI balanced review requested due to automatic review settings October 3, 2026 18:55
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1a33da02-6f9f-415b-bb45-64277442fee6

📥 Commits

Reviewing files that changed from the base of the PR and between 8723ac8 and cb59ea6.


📒 Files selected for processing (9)
  • .github/workflows/firmware.yml
  • firmware/tool_board/README.md
  • firmware/tool_board/mks_frames.h
  • firmware/tool_board/reply_queue.h
  • firmware/tool_board/tool_board.ino
  • firmware/tool_board/tool_motor.h
  • waybionic_teleop/test/test_tool_board_frames.py
  • waybionic_teleop/test/test_tool_board_replies.py
  • waybionic_teleop/test/test_tool_motor.py

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Stop commands ignore their requested acceleration, and heartbeat safety behavior lacks automated coverage.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds UNO R4 firmware allowing four biomedical tool-board steppers to emulate MKS CAN drives.

Changes:

  • Implements MKS-compatible motion, limits, heartbeat, and status handling.
  • Adds frame interoperability tests and firmware documentation.
  • Adds UNO R4 WiFi compilation CI.
File Description
.github/​workflows/​firmware.yml Compiles the firmware in CI.
firmware/​tool_board/​README.md Documents hardware, commands, and flashing.
firmware/​tool_board/​mks_frames.h Implements MKS frame encoding and decoding.
firmware/​tool_board/​tool_board.ino Controls four steppers over CAN.
waybionic_teleop/​test/​test_tool_board_frames.py Verifies frame compatibility with the host.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread firmware/tool_board/tool_board.ino Outdated
Comment thread firmware/tool_board/tool_board.ino Outdated
yassinsolim and others added 11 commits October 3, 2026 13:46
Each drive must now confirm its mode, reply, enable and heartbeat
settings before the arm moves. Unconfirmed settings are sent again
every 0.5 s, so a lost frame can't leave a drive running without its
heartbeat stop. Both buses report whether a frame went out, and a
target the interface refused is not recorded as sent, so the next tick
sends it again.
The per-motor command handling now lives in tool_motor.h, templated on
the stepper driver. The sketch uses it with AccelStepper, and a teleop
test compiles it with a fake stepper to check moves, completion and
limit reports, the heartbeat stop, stops, enable and zero. A stop
frame now uses its own acceleration, and acceleration 0 stops at
once, as the manual says. The heartbeat also stops at once.
actionlint (shellcheck SC2046) flagged the unquoted uname expansion.
On a busy CI runner the teleop node could still be loading the robot description when the test pressed Start, so the press was lost and the arm never moved. The test now sends idle packets until teleop reports its group and fresh controller input.
The tests could press Start before the teleop node had loaded the robot description or received controller input, so the press was lost on a busy machine. They now send idle packets until teleop reports its group and fresh input. Same fix as on #28.
- Joint commands that stop mid-move, say because teleop exited, now stop every drive where it is instead of letting it run on to the last target.
- The simulated drives integrate the full time since the last tick in 10 ms steps, so a pause longer than the heartbeat stops a moving drive, as on hardware.
- Axis coordinates take the whole int24 range, down to -0x800000.
A second robot_description replaced an enabled teleop with a disabled one without sending a hold, so the drives kept their last target.
A position-only JointState leaves every velocity at zero, so the stale-command
check never fired for an external publisher: the drives ran on to the last
target after the publisher died. The check now looks only at how long it has
been since the last command, latched to one stop and one warning so start-up
and idle holds stay quiet.
absolute_axis rejected -0x800000, which the manual's 3-byte signed coordinate
encodes. The node also capped the elapsed time it handed the drives at 100 ms,
so a scheduler stall longer than the heartbeat never stopped a moving drive the
way hardware would. The drive splits a long step into 10 ms motion steps and
counts all of it toward the heartbeat.
@yassinsolim
yassinsolim changed the base branch from feature/mks-can-host to main October 10, 2026 21:21
@yassinsolim
yassinsolim merged commit 55ab8b3 into main Oct 10, 2026
8 checks passed
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.

2 participants