Files
hexapod-robot/2026-current/CLAUDE.md
T

174 lines
10 KiB
Markdown
Raw Normal View History

# Hexapod: project memory
Hexapod walking robot plus a custom handheld remote, both Teensy-based and
linked one-way over nRF24L01 (remote → robot, with hardware auto-ack only).
This file is the persistent context for working on either side. Read it
fully before changing anything.
## Layout and toolchain
PlatformIO, one `platformio.ini`, two environments:
| Env | Board | Sketch dir | Purpose |
|-----|-------|------------|---------|
| `robot` | `teensy40` | `robot/` | walking robot |
| `remote` | `teensy41` | `remote/` | handheld transmitter |
- `shared/` is PlatformIO's `lib_dir`. `shared/HexapodProtocol/` is a
header-only library both sketches `#include <HexapodProtocol.h>`.
- PlatformIO has a single global `src_dir` and only converts `.ino` files at
its root, so `scripts/select_src_dir.py` (a `pre:` extra script) swaps
`PROJECT_SRC_DIR` to each env's `custom_src_dir`. That keeps `robot/` and
`remote/` plain Arduino sketch folders: one `.ino` plus paired `.h/.cpp`
per concern. **Keep that structure**. No giant files, no `src/` folders.
- RF24 (nrf24/RF24, resolved 1.6.x) comes from `lib_deps`. Servo, Wire,
SPI and EEPROM ship with the Teensy framework.
- Build: `pio run` (both), `pio run -e robot`, `pio run -e remote`. On this
machine pio lives at `C:\Users\patri\.platformio\penv\Scripts\pio.exe`.
Both envs build clean. Build both after touching `shared/`.
## Communication contract (`shared/HexapodProtocol/HexapodProtocol.h`)
`CmdPacket`, 20 bytes (static_asserted), sent remote → robot at 100 Hz
(robot `CONTROL_HZ` is 60):
| Field | Range | Meaning |
|-------|-------|---------|
| `vx` | ±1000 | + forward |
| `vy` | ±1000 | + strafe **left** |
| `yaw_rate` | ±1000 | + turn left (CCW). Ignored by the robot in mode B |
| `rx, ry, rz` | ±1000 | right stick: roll/pitch/yaw, or X/Y/Z offset when `FLAG_RSTICK_XLATE` |
| `remote_roll, remote_pitch` | ±1000 | handset tilt feedforward, used only when `FLAG_REMOTE_GYRO` |
| `speed_scale` | 0–255 | multiplies vx/vy on the robot |
| `flags` | bits | `ARMED, STAND, WALK, MODE_B, RSTICK_XLATE, GAIT_WAVE, REMOTE_GYRO, ROBOT_GYRO` (bits 0–7) |
| `seq` | 0–255 | increments per packet (robot doesn't check it yet) |
- **Axis convention** (ROS REP-103 style): body +X forward, +Y left, +Z up.
+roll = right side down, +pitch = nose down, +yaw = CCW.
- **Normalized fields.** The remote never knows robot geometry. The robot's
`InputMapper` owns the scaling: `MAX_LINEAR_MM_S` 150,
`MAX_TURN_RATE_DEG_S` 90, `MAX_TILT_DEG` 20, `MAX_POSE_OFFSET_MM` 40.
- **Radio settings** live in the same header: address `"HXPD1"`, channel
96, 250 kbps, PA LOW, **static payload size = sizeof(CmdPacket)** on both
radios. That makes version mismatches fail closed: packets are not
received, the robot times out, and it disarms.
- **Changing the protocol means reflashing both boards.** Keep it ≤ 32
bytes and update the `static_assert`.
- **Failsafes:** the robot sets `linkOk()` false after `LINK_TIMEOUT_SEC`
(0.3 s) without packets and then uses a default `CmdPacket` (flags 0 =
disarmed, servos detached). The remote disarms itself after
`LINK_LOST_MS` (300 ms) without acks, so the robot can't re-arm itself
when the link returns.
- **Division of labor:** the remote only fills fields per this contract.
Mode A/B interpretation, heading-hold, pose scaling and all smoothing of
pose happen on the robot. Don't duplicate `InputMapper` logic in the
remote.
## Robot architecture (`robot/`, Teensy 4.0)
Loop at `CONTROL_HZ` (60): comms → InputMapper → IMU → heading integrate →
StateMachine → gait/sit-stand targets → BodyPose → Limits clamp → IK →
joint-rate clamp → servos.
| Module | Role |
|--------|------|
| `robot.ino` | setup/loop wiring, open-loop heading estimate, sit/stand blend, `TEST_SERIAL_INPUT` harness |
| `Config.h` | **all placeholder geometry/pins/rates** |
| `SmoothedValue.h` | rate-limited setpoint smoother used for every discrete command |
| `Kinematics` | per-leg 3-DOF analytic IK, `neutralStance()` |
| `GaitEngine` | tripod/wave patterns, per-leg phase, pattern swap only at touchdown |
| `BodyPose` | 6-axis body pose (smoothed command + IMU roll/pitch correction) applied to foot targets |
| `RobotIMU` | MPU6050 complementary filter (α 0.98), gated by `FLAG_ROBOT_GYRO` |
| `StateMachine` | DISARMED → SIT → STAND → WALKING, sit/stand blend at 1/s. Disarm is instant |
| `InputMapper` | CmdPacket → Twist + PoseCommand + mode flags. Mode B = heading-hold from stick angle. Adds remote tilt on top of stick pose when `FLAG_REMOTE_GYRO`, clamped to `MAX_TILT_DEG` |
| `Limits` | workspace clamp (70 mm horiz / 60 mm vert), joint rate clamp, ≥3-legs-in-stance check (not acted on yet) |
| `Comms` | nRF24 RX, link timeout, `injectPacket()` for the harness |
| `ServoDriver` | attach/detach/write, skips invalid IK solutions |
## Remote architecture (`remote/`, Teensy 4.1)
Loop: drain Nextion + USB serial input every pass. On each `TX_HZ` (100)
tick: sticks → IMU → radio link status → RemoteState (smoothers,
link-loss disarm) → PacketBuilder → `radio.write` → Nextion render.
| Module | Role |
|--------|------|
| `remote.ino` | loop wiring, status print, packet dump |
| `Config.h` | **all placeholder pins, calibration defaults, deadzones, rates** |
| `UiTypes.h` | `UiCmd`/`UiEvent` (operator requests), `UiView` (what the display shows) |
| `Sticks` | 6 analog axes: ADC averaging + IIR, piecewise calibration around center, invert, deadzone with rescale, expo → ±1000. Boot-time center capture, sweep calibration saved to EEPROM (magic/version/checksum) |
| `RemoteIMU` | MPU6050, same complementary filter as the robot, axis remap for mounting, boot gyro-bias capture, "level" zero |
| `RemoteState` | authoritative operator state and interlocks: arm needs centered sticks, disarm clears stand, stand needs armed, calibrate needs disarmed, link-loss auto-disarm. Smooths speed scale and fades remote tilt in/out |
| `PacketBuilder` | the inverse of InputMapper: stick frame → body frame per the contract, per-submode right-stick mapping, `FLAG_WALK` with 250 ms linger (twist ignored in mode B), tilt deadzone/scale × fade, zeros motion when disarmed |
| `RadioLink` | nRF24 TX, short retries (1, 3), ack-based link status and 32-packet quality % |
| `NextionUI` | library-free Nextion driver. HMI sends `'#' len cmd [val]` frames via `printh`. Remote pushes state by component name with diffing, a 2 s full refresh, and a redraw on the Nextion 0x88 "ready" frame |
| `SerialConsole` | USB keystrokes that mirror every UI command plus diagnostics |
| `SmoothedValue.h` | copy of the robot's (kept separate so robot/ stays untouched) |
| `NEXTION.md` | HMI build guide: component names, txt_maxl, touch event code |
Remote stick-frame convention: logical X + = right, Y + = forward, twist +
= CCW. The mapping into the packet is in `PacketBuilder.cpp` (e.g.
`vy = -L_X`, translate mode `ry = -R_X`).
## Testing without the other board
- **Robot without remote:** `#define TEST_SERIAL_INPUT` in `robot.ino` (on
by default). Serial 115200, single keys: `a` arm, `s` sit/stand, `w/x`
vx ±200, `q/e` vy ±200 (left/right), `z/c` yaw_rate ∓/±200, `g` gait,
`m` mode, space = zero velocity. Keys inject through
`Comms::injectPacket()`, the same path radio packets take. Order: arm →
stand → walk. The harness does not exercise the right stick or remote
tilt. Add keys if needed.
- **Remote without robot or display:** the USB serial console (see
`remote/README.md`). `p` shows raw/mapped sticks, IMU, link and state.
`d` dumps packets at 10 Hz. With no robot, the link shows DOWN and
arming still works; the link-loss disarm only fires after the link was
up once.
## Placeholders and bench-check items (not yet verified on hardware)
Robot (`robot/README.md` has detail):
- All of `Config.h`: leg mount angles/positions, link lengths, joint
limits, stance radius/height, servo pins, µs/deg calibration,
`MAX_JOINT_DEG_S` (in robot.ino), Limits envelope, InputMapper maxima.
- **Pin conflict:** `SERVO_PINS` leg 3 uses pins 11 and 12, which are
Teensy 4.0 SPI MOSI/MISO needed by the nRF24. Must be moved. Some servo
pins (26, 27, 30…) may also not be PWM-capable on the 4.0. Verify.
- IK knee direction (`tibia_rad` sign), BodyPose roll/pitch sign, IMU
register map, and the sit pose assuming feet stay grounded.
- **Suspected gait direction inversion:** in `GaitEngine::update` the
stance phase moves the foot from `neutral - half` to `neutral + half`,
i.e. with the commanded direction. That pushes the body *backward* for
+vx, and the same happens for yaw. Fix = swap start/end. Flagged to the
owner, **not changed**. The harness `z`/`c` yaw keys currently rely on
this behavior and need rechecking after the fix.
- Heading for mode B is open-loop (integrated commanded ω). Fusing IMU yaw
is a planned upgrade.
- `Limits::stancePolygonOk()` is computed but not acted on.
Remote (`remote/README.md` has detail):
- All pins in `remote/Config.h` (radio CE 9 / CSN 10, sticks A0–A3/A6/A7,
MPU6050 on 18/19, Nextion on Serial2 7/8).
- `STICK_INVERT[]`, calibration defaults, deadzones, expo.
- `IMU_AXIS_FROM_CHIP` / `IMU_AXIS_SIGN` depend on how the MPU6050 is
mounted in the handset.
- The Nextion must have `bauds=115200` in Program.s, and the HMI must match
`NEXTION.md`. No `.HMI` file is in the repo yet. The compiled `.tft` is
gitignored.
- Radio retry timing (`RADIO_RETRY_*`) is untested under real link loss.
## Working rules for this repo
- The **robot side is owner-verified code**. Don't edit `robot/` unless
something is factually wrong relative to the protocol, and ask the owner
first even then. Flag suspected robot bugs instead of fixing them.
- Protocol changes go in `shared/HexapodProtocol/` only. Update the size
`static_assert`, this file's contract table, and both READMEs.
- Match the existing style: config constants at the top of `Config.h`
with BENCH comments for anything hardware-dependent, `SmoothedValue`
rate limiting on any setpoint that can jump, and paired `.h/.cpp` per
concern.
- Planned or possible next steps: an ack-payload telemetry channel from
robot → remote (state, battery, link) to show real robot state on the
Nextion, battery monitoring on both sides, a `.HMI` file checked into
`remote/`.