Skip to main content
One-time setup for the Almond Axol CAN buses. Installs the gs_usb kernel driver if the kernel lacks it (see can.driver — needed on NVIDIA Jetson / ZED Box), writes persistent udev rules, assigns fixed interface names, registers a startup script in the root crontab, installs a hotplug bring-up service, and brings up the interfaces immediately. Driver selection is host-safe: NVIDIA L4T/Jetson kernels that omit gs_usb build the vendored backport; an older or unverifiable native Linux driver is conservatively replaced; a Linux 6.13+ in-tree driver (including a resolved weak-updates symlink) is kept when it advertises every ID needed by the adapters currently attached. Maintained 6.2+ distro kernels with a backport are also kept when kmod metadata proves endpoint discovery, including signed Secure Boot builds. On a Secure Boot/signature-enforcing host that genuinely needs an unsigned backport, setup stops before changing the active module and tells you to sign/enroll it or use a newer native kernel. A trusted, signed legacy Almond backport is recognized only when its selected/loaded source identity, canonical path, and USB-ID fingerprint match. Installation verifies the selected and loaded module identities and restores the previous module/config on failure. Up to four buses, each optional and independent:
  • Left arm → can_alm_axol_l (Axol Hub channel 0)
  • Right arm → can_alm_axol_r (Axol Hub channel 1)
  • Jelly’s wheel bus (if present) → can_alm_axol_b
  • Chest bus / lift controller (if present) → can_alm_axol_c
Jelly’s lift controller can be wired either way: on its own chest bus (a second single-channel adapter, can_alm_axol_c), or sharing the wheel bus with the four Damiao motors (one adapter, can_alm_axol_b). The jelly_legs IDs (0x420–0x422) are clear of every Damiao range, so both protocols coexist on one 1 Mbit/s bus. can.setup detects which topology is attached: an adapter on which both the wheel motors and the lift controller answer is pinned as the shared wheel + lift bus (can_alm_axol_b only, no chest interface), and the lift driver follows automatically — axol teleop, lift.home, lift.goto and diag.lift-cycle all use can_alm_axol_c when that interface exists and can_alm_axol_b otherwise. Moving the lift from its own adapter onto the wheel bus needs nothing more than re-running axol can.setup: the now-redundant chest pin is dropped once the lift is found on the wheel bus. The Axol Hub and Mantis Hub are dual-channel USB CAN adapters; the wheel and chest buses use single-channel adapters. can.setup identifies all of them by probing the devices on their CAN lines: an Axol arm has a shoulder motor at ID 0x001, a Mantis publishes trigger frames on ID 0x009 (both the 6-byte and current 7-byte firmware frame are accepted), the wheel bus has Damiao motors at IDs 0x01–0x04 (their register replies must arrive on their own feedback IDs 0x11–0x14), and the jelly_legs lift controller answers a status request on 0x421. The attached devices must be powered for automatic identification. Already configured adapters are probed again when attached, so a live response corrects stale Axol/Mantis or wheel/chest assignments from an earlier setup. If an apparently-up gs_usb adapter is not receiving, setup resets the relevant bus and retries before deciding it is silent. A silent or unplugged configured single-channel adapter keeps its previous assignment as an explicitly unverified fallback; a new silent adapter is offered to the operator and can replace that fallback. An unresolved adapter pinned to two roles is rejected before rules are written. For a silent dual-channel hub, the normal axol can.setup prompt lets the operator keep the previous role or choose Axol/Mantis; no separate reassignment command is needed. Each identified adapter is pinned by USB serial, and the startup scripts bring every configured interface up while skipping anything unplugged. A Jetson host’s built-in system CAN controller is ignored entirely. Running axol can.setup is deliberately a disruptive link reset for every attached adapter in its supported scope: the Axol arm pair, Jelly wheel bus, chest/lift bus, and Mantis pair are taken down, restored to 1 Mbit/s with the expected queue length, and brought back up before they are trusted. Both channels of a dual hub go down before either comes back up; wheel and lift adapters reset independently. This does not reboot/zero/calibrate an actuator, clear a healthy saved assignment, reload a healthy USB driver, or touch the Jetson’s built-in mttcan interfaces. As a one-time migration on older Jetson installs, setup automatically rebuilds an Almond-managed gs_usb override that predates reliable build identity; overrides outside Almond’s managed path and signature-enforcing hosts still fail closed. Do not run setup during an active robot operation. The Mantis handheld data-collection rig uses the same dual-channel CAN board as the arm hub, with one gripper bus per channel — each bus carries that side’s Damiao gripper motor (ID 0x008) and trigger node (ID 0x009):
  • Channel 0 → can_mantis_l (left gripper)
  • Channel 1 → can_mantis_r (right gripper)
The Mantis profile gets its own udev rule file, startup script, and hotplug bring-up unit, so the robot and rig can be discovered and configured together by one axol can.setup run. Its managed names are isolated from the Axol profile, so configuring or correcting one profile does not evict the other. A replug of the handheld brings its interfaces back up without operator action. If a trigger and arm motor are both silent, setup asks whether that adapter is Axol, Mantis, or should be skipped; no hardware-profile flag is needed.
sudo will be invoked automatically where required.
At the end of setup, when an arm hub was identified, can.setup also fetches this robot’s factory calibration (friction + gravity, keyed by the hub serial) from the public calibration bucket into ~/.almond/factory_calibration.json — see tune.factory. Keyless and best-effort: an offline setup just keeps whatever cache exists.
After setup, the interfaces come up automatically on every reboot (via a @reboot cron hook) and whenever the adapter re-enumerates mid-session: a udev rule pulls in the axol-can-up.service bring-up whenever the hub (re-)appears, so a replug — or a USB drop caused by motor EMI — heals itself within a second or two without operator action. You don’t need can.setup just to use Jelly — axol teleop brings Jelly’s own interface up on its own — but pinning it gives the adapter a stable name across reboots. The control panel pins a powered Jelly’s wheel and chest/lift adapters automatically when it identifies attached CAN hardware.
The same bring-up script also recovers the bus after an emergency stop. Cutting motor power mid-motion leaves the in-flight CAN commands unacknowledged, so the kernel parks up to txqueuelen of them on the interface instead of dropping them; without intervention they replay the instant the arm is powered back on and enabled. The arm then jerks to the pose it was commanded as power died — not the pose it sagged to once torque was gone, which is why the position it snaps to often looks like nowhere the robot has been. The CAN layer detects the stalled bus, flaps the interfaces to drop the stale frames, and holds every send until a probe frame is acknowledged on the wire again, so the arm stays put until the bus is genuinely back online.On the arm hub a flap is not enough. The hub’s firmware keeps the frames it had already accepted from the host — up to 10 per channel, the driver’s in-flight limit — through the interface reset and transmits them on the next open, possibly minutes later: a new session, or a can.setup recovery cycle (the kernel logs Unexpected unused echo id for each one). The purge therefore USB-resets the hub instead (/etc/almond-axol/can/reset_adapter.sh, written by can.setup: usbreset, or a sysfs deauthorize/reauthorize where that is missing, then the bring-up script), and can.setup’s own recovery cycles of a silent arm hub reset it the same way. The wheel and chest adapters keep the plain flap. Until a robot has re-run can.setup and axol provision for the script and its grant, the purge falls back to the flap — unplug and replug the hub after any TX queue stalled report before running anything that talks to the arms.Flapping an interface needs root. The hosted install’s axol serve is root and has it; a manual axol serve / axol teleop from the operator’s shell gets it from the sudoers.d drop-in axol provision installs for exactly the reset and bring-up scripts (almond_axol.utils.can_purge) — without that grant the purge silently does nothing and the stale frames survive to the next session. As a backstop, every arm bring-up checks each interface’s queue before enabling a motor and flaps it if anything is still parked there, refusing to enable rather than replaying it.A momentary TX-queue overflow is handled more gently. Both arm channels share one dual-channel USB adapter, so a brief stall of its USB pipe — e.g. camera traffic during data collection — can back the queue up while the bus itself is healthy and draining. Told apart from a dead bus by how long it persists, a brief overflow just drops the frame (a rate-limited warning) and the next command proceeds, so heavy USB traffic no longer crashes teleop or data collection mid-session; only an overflow that keeps persisting (nothing draining) escalates to the interface flap and probe-gated hold above.
On Raspberry Pi 5 hosts, can.setup additionally applies an RP1 USB quirk (axol-rp1-usb-quirk.service): the Pi 5’s USB controllers ship with a hair-trigger babble/loss-of-activity detector that EMI from the arms can trip, kicking the hub off the bus (disabled by hub (EMI?) in dmesg). The quirk enables the controller’s noise filter — the workaround recommended by Raspberry Pi engineers. If disconnects persist, add a ferrite choke to (or shield) the hub’s USB cable, or put a small powered USB hub between the Pi and the Axol Hub.