174 lines
10 KiB
Markdown
174 lines
10 KiB
Markdown
# 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/`.
|