Files
hexapod-robot/2026-current/CLAUDE.md
T
mangavalkandClaude Opus 5.5 91dac16fca Draft: shared protocol, remote firmware, project docs
Not yet verified on hardware.

- shared/HexapodProtocol: CmdPacket (now 20 bytes, adds remote_roll /
  remote_pitch), FLAG_* bits and radio settings as the single protocol
  definition; static payload size = sizeof(CmdPacket) on both radios.
- robot: use the shared protocol; vx = forward (harness w/x <-> q/e);
  InputMapper adds remote tilt on top of the right stick when
  FLAG_REMOTE_GYRO is set, and robot.ino no longer zeroes stick
  roll/pitch when it's off.
- remote (Teensy 4.1): Sticks (calibration, EEPROM), RemoteIMU,
  RemoteState (interlocks, link-loss disarm, smoothing), PacketBuilder,
  RadioLink, NextionUI, SerialConsole.
- CLAUDE.md, README.md, remote/README.md, remote/NEXTION.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:14:00 +02:00

175 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`.