# 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 `. - 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/`.