From 8824f807eb27d748de75148e5cd6ed0f51082117 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 23 Sep 2026 01:39:20 +1000 Subject: [PATCH] Add session notes: Johnny 5 PS4 control build --- .../2026-09-23-johnny5-ps4-control.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 .claude-context/sessions/2026-09-23-johnny5-ps4-control.md diff --git a/.claude-context/sessions/2026-09-23-johnny5-ps4-control.md b/.claude-context/sessions/2026-09-23-johnny5-ps4-control.md new file mode 100644 index 0000000..ca45ce2 --- /dev/null +++ b/.claude-context/sessions/2026-09-23-johnny5-ps4-control.md @@ -0,0 +1,151 @@ +# PS4 Controller to Two LEGO Hubs — Johnny 5 Build + +**Date:** 2026-09-23 +**Project:** ps4-lego-bridge (also ps4-lego-onebrain) +**Status:** ongoing +**Tags:** esp32, bluepad32, legoino, nimble, btstack, lwp3, lego-powered-up, ps4, arduino + +## Context +Goal was to drive a Johnny 5 (Short Circuit) LEGO MOC — seven motors across two +Technic (Control+) hubs — from a PS4 DualShock 4 via ESP32. The starting sketch +combined Bluepad32 and Legoino on one ESP32 and failed to compile +(`Bluepad32.h: No such file or directory`). The session went through a +single-board attempt, then settled on two boards, then iterated the control +scheme on a separate branch. + +## Key Decisions +- **Two ESP32 boards, not one.** Bluepad32 uses BTstack (needed for BR/EDR — + the DS4 is Bluetooth Classic); Legoino uses NimBLE. Two host stacks cannot + share one radio/VHCI. Board A runs Bluepad32 only, board B runs Legoino only, + linked by UART. + - *Rejected:* single-board via hand-rolled LWP3 GATT client on BTstack + (`ps4-lego-onebrain`). It compiled and connected, but was far more code, an + unresolved routing fault appeared, and the pivot made that moot. Kept as a + separate repo, not maintained. + - *Rejected:* PS4-esp32 (Bluedroid) + BLE on one board — PS4-esp32 depends + on internal Bluedroid symbols and is broken on modern cores. +- **ASCII UART protocol with XOR checksum** over binary — debuggable with any + USB-serial adapter. Receiver drops bad frames and stops all motors after + 400ms of silence. +- **Tank drive, one input per motor.** Arcade drive was tried first and + rejected: mixing put one stick on both track motors. +- **Direction via `DIR_*` constants** rather than editing signs inline — + mirrored mountings are normal and flips should be one-line changes. +- **Board B pinned to arduino-esp32 core 2.0.17.** Core 3.x breaks Legoino. + Downgrading chosen over patching the library (patch is the first of several + layers and gets reverted by Library Manager updates). +- **Arms moved to position control** on the evolved branch: trigger position = + arm angle, via `setAbsoluteMotorPosition` with `BrakingStyle::HOLD`. + *Rejected:* power-based arm control with separate lower buttons — works, but + the user wanted angle-proportional behaviour. +- **Arm encoders zeroed once per boot, not per reconnect.** The hub retains its + encoder preset; re-zeroing mid-session would redefine zero wherever the arms + happened to be. +- **Separate branch `johnny5-evolved`** for the new control scheme so `main` + keeps the proven configuration. + +## Technical Details +**Toolchain** +| Board | Package / library | Version | +| --- | --- | --- | +| A | esp32_bluepad32 | 4.1.0 | +| B | esp32 (Espressif) | 2.0.17 | +| B | NimBLE-Arduino | 1.4.x | +| B | Legoino | latest | + +- Bluepad32 board manager URL: + `https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json` +- Selecting the `esp32_bluepad32` package starts BTstack **before `setup()`**. + Any NimBLE sketch compiled under it aborts with `ESP_ERR_INVALID_STATE` at + `esp_bt_controller_init`. Board B must use the plain `esp32` package. +- Core 3.x Legoino errors: `'string' in namespace 'std' does not name a type` + and `no declaration matches ... ReadUInt32LE`. Cause: ESP-IDF 5 made + `uint32_t` `unsigned long`. + +**Wiring:** A GPIO17 TX → B GPIO16 RX; A GPIO16 RX ← B GPIO17 TX; common GND +(mandatory). Optional 5V→5V daisy-chain for single supply — never with both USB +cables connected. LEDs on board B: GPIO25 and GPIO26, each via resistor to +board B GND (not tapped off the UART ground wire). + +**Link frame:** `G,,,,,,,,*\n`, +115200 8N1. Axes −512..511, triggers 0..1023, XX = XOR of payload. + +**Port map (verified on hardware)** +- hub0 `90:84:2b:61:f2:d7` — A right track, B left track, C free, D body lift +- hub1 `90:84:2b:61:e6:8c` — A left arm, B head tilt, C head turn, D right arm +- Note: the two addresses were originally the wrong way round, and hub1 A/C and + B/C were both mislabelled at points. All found by observation, not code. + +**Arm calibration:** travel ≈ +280 (left) / −275 (right) motor degrees — under +one revolution, so the single-turn absolute encoder is unambiguous. Spans set +to ±250 to keep backlash clear of the end stop. + +**Rate limits:** `MOTOR_MIN_GAP_MS` 100 (per port), `HUB_MIN_GAP_MS` 25 (per +hub, ~40 cmd/s), `ARM_MIN_GAP_MS` 60. Hub1 carries four motors, hence the +per-hub cap. + +**Arduino gotchas hit this session** +- The IDE injects generated prototypes before the *first function definition*. + Types used in signatures (`HubLink`, `PortState`, `ArmState`, `Frame`) must + be declared above it — broke the build twice. +- `Lpf2Hub::init()` starts an **asynchronous** scan. Guarding it on + `!isConnected() && !isConnecting()` re-enters `NimBLEDevice::init()` + continuously and crashes. Use a one-shot `initialised` flag. +- A scan that finds nothing ends silently; without a timeout the hub sits dead + until power cycle. Fixed with `scanExpiresAt` / `SCAN_TIMEOUT_MS` (12s). +- Bluepad32 keeps pairing keys in NVS; reflashing doesn't clear them. + `BP32.forgetBluetoothKeys()` must be commented out after first pairing. + +**Startup order:** wake both hubs (green button, blinking) → arms physically +down → power board B. hub1 is only serviced after hub0 connects. + +## Code Changes +- `transmitter/transmitter.ino` — frame extended with d-pad and analog + triggers; `DEBUG_FRAMES` flag. +- `receiver/receiver.ino` (main) — seven motors, tank drive, `DIR_*` + constants, swapped hub addresses, right-arm button swap, types hoisted, + core 2.0.17 noted in header. +- `receiver/receiver.ino` (johnny5-evolved) — evolved scheme, position- + controlled arms, GPIO LED toggles, head tilt/turn port swap, once-per-boot + arm zeroing, scan timeout. +- `tools/arm_calibrate/arm_calibrate.ino` — measures arm span in motor degrees + (delivered as a file; Gitea push timed out — verify it's on the branch). +- `README.md` (main) — build guide, toolchain table, power section, + power-loss test, gotchas. + +**Evolved control scheme (johnny5-evolved)** +| Input | Action | +| --- | --- | +| Left / right stick Y | Left / right track | +| L2 / R2 analog | Left / right arm angle | +| L1 / R1 | Head turn left / right | +| D-pad ↑/↓ | Body lift | +| D-pad ←/→ | Head tilt | +| Square | All stop (arms go limp) | +| Cross | Toggle 50/100% track speed | +| Circle / Triangle | Toggle LED pair 1 / 2 | + +## Open Questions / Next Steps +- **Head direction untested.** `DIR_HEAD_TURN` and `DIR_HEAD_TILT` are +1 by + default; flip if L1/R1 or d-pad ←/→ run backwards. +- **Scan timeout tuning.** 12s is a guess; don't go below ~6s. +- **Power-loss test not yet run.** The 400ms failsafe only fires if board B is + alive. Cut board B's power with a track running and confirm whether the hub + stops on BLE disconnect. +- **Heavy-load test.** Both arms held mid-travel plus head swinging is the peak + hub1 command load — check it doesn't drop. +- **Arm calibrate sketch** — confirm it landed on `johnny5-evolved`. +- **Merge decision** — `johnny5-evolved` into `main` once verified. +- **Boot track speed** defaults to 50%; user may want 100%. + +## Artifacts +- Repo: https://gitea.hideawaygaming.com.au/jessikitty/ps4-lego-bridge + (branches `main`, `johnny5-evolved`) +- Single-board attempt: https://gitea.hideawaygaming.com.au/jessikitty/ps4-lego-onebrain +- Bluepad32 docs: https://bluepad32.readthedocs.io/ +- LEGO Wireless Protocol 3.0: https://lego.github.io/lego-ble-wireless-protocol-docs/ +- Device type IDs: https://github.com/pybricks/technical-info/blob/master/assigned-numbers.md +- Legoino: https://github.com/corneliusmunz/legoino + +--- +Created by: Jess Rogerson (yelling commands at Claude.AI)