Robot stack for SpotMicro/Spot running ROS 2 Kilted on k3s (Raspberry Pi), deployed via Rancher Fleet (GitOps).
This repo is intentionally minimal.
deployment.yaml deploys two DaemonSets (one pod per robot node):
ros2-smoke(core):node-label-config: reads Kubernetes Node labels of its own node (RBAC-enabled), converts them into a PCA9685 servo mapping, and publishes it to/spot/config/servo_map(reacts to label changes via watch; no pod restart).servo-driver: subscribes to/spot/config/servo_map, drives PCA9685 over I2C (/dev/i2c-1), accepts JSON commands on/spot/cmd/servo, and publishes status to/spot/state/servo(runs privileged for I2C access).
spot-champ(optional):champ-controller: runs CHAMP gait controller (joint_states,/cmd_vel, etc).champ-bridge: bridges CHAMPjoint_statestoservo-drivercommands on/spot/cmd/servo.
Safety defaults:
- starts disarmed (
START_ARMED=0) - clamps each joint to
min/center/maxfrom node labels - hard safety clamp via
SAFE_MIN_US/SAFE_MAX_US(defaults:500..2500) - slew-rate limiting (
MAX_SLEW_US_PER_S)
Pods are scheduled only on nodes matching:
kubernetes.io/arch=arm64gorizond.io/robot=true
- Power the RPi4 from a dedicated
5.1–5.2Vregulator (>=3A), ideally from the 2S battery directly. - Keep servo power (
PCA9685 V+/ servos) on a separate6VBEC; tie grounds at a star point. - Brownouts often show up as boot-loops; measure 5V at the GPIO 5V/GND pins under load.
Create a Fleet GitRepo in your Fleet workspace (e.g. workspace-negashev) pointing to this repository and target your robot cluster.
spec:
repo: https://github.com/gorizond/spot
branch: main
targets:
- clusterName: spotsPer-robot mapping is stored on the Kubernetes Node object as labels.
Label prefix: gorizond.io/spot-pca9685-
...i2c-bus(default:1)...address(default:0x40)- Per channel
0..15:...chN-joint(unset/empty => unused)...chN-us(min/center/max, default:1000-1500-2000)...chN-invert(0/1, default:0)
...chN-us value must be a valid Kubernetes label value (no commas). Use e.g. 1450-1500-1550.
(Backward-compatible) ...chN-min-us, ...chN-center-us, ...chN-max-us are still supported if ...chN-us is not set.
For the current SpotMicro wiring, CH6–CH9 are empty (leave ch6-joint..ch9-joint unset).
-
Put the robot in a safe position (lifted / legs can move freely).
-
Exec into the DaemonSet pod and use the
servo-drivercontainer:
kubectl -n spot-system exec -it ds/ros2-smoke -c servo-driver -- bash- Source ROS 2 environment (required for
rclpy/ros2commands):
source /opt/ros/kilted/setup.bash- (Optional) Reset targets to home (0.0):
python3 /opt/spot/spot_cli.py --repeat 1 home- Arm (does not move anything until you
seta joint):
python3 /opt/spot/spot_cli.py --repeat 1 arm- Enable + move one joint with small steps (calibration-friendly):
python3 /opt/spot/spot_cli.py --repeat 1 set-us rf_hip=1500
python3 /opt/spot/spot_cli.py --repeat 1 set-us rf_hip=1510
python3 /opt/spot/spot_cli.py --repeat 1 set-us rf_hip=1500- Disarm when done (note: may drop torque if
DISARM_FULL_OFF=1):
python3 /opt/spot/spot_cli.py disarmIf something goes wrong:
python3 /opt/spot/spot_cli.py estopAfter calibration, a conservative stand pose is:
python3 /opt/spot/spot_cli.py --repeat 1 standDefaults (override via flags or env STAND_HIP/STAND_UPPER/STAND_LOWER):
hip=0.02upper=0.05lower=0.05
One-leg micro-step (lift/return one leg by moving *_lower):
python3 /opt/spot/spot_cli.py --repeat 1 step lhRepeat micro-steps (in-place crawl):
python3 /opt/spot/spot_cli.py --repeat 1 walk --steps 3Tune lift amplitude and timing (start small):
python3 /opt/spot/spot_cli.py --repeat 1 walk --steps 1 --lift 0.05 --lift-hold 0.2 --down-hold 0.2(Experimental) add a small hip swing while the leg is lifted:
python3 /opt/spot/spot_cli.py --repeat 1 walk --steps 1 --hip-swing 0.03This uses CHAMP (model-based gait controller) and bridges its joint_states to the low-level servo-driver.
spot-champ is gated by a node label so it doesn't override manual spot_cli commands.
To enable CHAMP on a robot node:
- Add label
gorizond.io/spot-champ=trueto the node (e.g.spot-1).
By default, spot-champ uses ghcr.io/gorizond/spot-champ:main (built and pushed by GitHub Actions in this repo).
- Tags:
main(default branch) andsha-<short>(per-commit, immutable). - If your package is private, configure an
imagePullSecretfor GHCR.
To build locally instead, edit deployment.yaml back to spot-champ:local and run:
docker build -t spot-champ:local -f docker/champ/Dockerfile .Then, drive the gait by publishing /cmd_vel (start small):
kubectl -n spot-system exec -it ds/spot-champ -c champ-controller -- bash
source /opt/ros/kilted/setup.bash
[ -f /ws/install/setup.bash ] && source /ws/install/setup.bash
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.05}}' -r 2Notes:
champ-bridgepublishes servo targets only whenservo-driverisarmed=true.- Tune bridge scaling via env on
champ-bridge:CHAMP_GAIN,CHAMP_*_RANGE_RAD,STAND_*.
- DaemonSets are running in namespace
spot-system(ros2-smoke,spot-champ) - Logs:
node-label-configprints which channels are mappedservo-driverprints PCA9685 connect + updates
- ROS topics:
ros2 topic echo /spot/config/servo_map --once
ros2 topic echo /spot/state/servo --once/spot/cmd/servo expects std_msgs/String JSON:
- arm/disarm:
{"cmd":"arm","value":true}/{"cmd":"arm","value":false}(arming gates output; joints move only after they are enabled viacmd=set) - estop:
{"cmd":"estop","value":true}/{"cmd":"estop","value":false} - home:
{"cmd":"home"} - set targets (also enables those joints):
- normalized:
{"cmd":"set","mode":"norm","targets":{"rf_hip":0.1}}(range-1..1) - microseconds:
{"cmd":"set","mode":"us","targets":{"rf_hip":1500}}
- normalized:
For convenience inside the pod, use python3 /opt/spot/spot_cli.py ....