From 9dd74d9d607cf1d595e88f66a2ef4998886809b9 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 9 Sep 2026 19:30:18 +1000 Subject: [PATCH] Rewrite as a step-by-step build guide for the Johnny 5 setup Adds bring-up order (pair the pad before wiring, verify the port map with DEBUG_MOTORS before trusting it), the tank drive control table, the extended link protocol, and the wiring failure modes. --- README.md | 158 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 2da9f71..0c4ed2d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # ps4-lego-bridge -PS4 controller -> two LEGO Powered Up / Technic hubs, using two ESP32 boards. +PS4 controller -> two LEGO Technic hubs, using two ESP32 boards. + +Driving a Johnny 5 (Short Circuit) MOC — seven motors across two hubs. ## Why two boards @@ -14,13 +16,15 @@ does not work, and it is not a library-version problem. BTstack and NimBLE are both Bluetooth *host* stacks. There is one radio and one VHCI interface on the chip, and whichever stack registers second wins. You -cannot run both in one firmware image. Every mature project in this space -(Anton's Mindstorms LMS-ESP32, the Pybricks bridges) solves it the same way: -one chip owns the gamepad, and it hands the data to something else over a wire. +cannot run both in one firmware image. So: board A runs Bluepad32 and nothing else. Board B runs Legoino and nothing else. Three jumper wires between them. +(The single-board version, which speaks the LEGO protocol directly against +BTstack, lives in `ps4-lego-onebrain`. It works, but it is a lot more code and +a lot more ways to be wrong.) + ## Hardware * 2x ESP32 dev boards. Both must be the **original ESP32** (WROOM/WROVER). @@ -31,85 +35,127 @@ else. Three jumper wires between them. Board A (Bluepad32) Board B (Legoino) GPIO17 TX -------------> GPIO16 RX GPIO16 RX <------------- GPIO17 TX - GND ------------- GND <- do not skip this one + GND -------------- GND <- do not skip this one ``` -Power both from the same supply if you can. Separate USB bricks are fine as -long as the grounds are tied together. +TX goes to RX, not TX to TX. If nothing arrives, that swap is the first thing +to check. -## Software setup +Power both from the same supply if you can. Separate USB bricks are fine as long +as the grounds are tied together — without a common ground the UART has no +shared voltage reference and you get garbage or silence. -### Board A — transmitter +## Step by step -1. Arduino IDE -> Preferences -> Additional board manager URLs, add: - `https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json` -2. Boards Manager -> install **esp32_bluepad32**. -3. Tools -> Board -> **esp32_bluepad32** -> ESP32 Dev Module. - Selecting the board from the normal `esp32` package is what produces - `fatal error: Bluepad32.h: No such file or directory`. Bluepad32 is not in - the Library Manager and installing it as a ZIP library will not help. -4. Flash `transmitter/transmitter.ino`. +**1. Board A — the gamepad board.** -### Board B — receiver +Preferences -> Additional board manager URLs, add: -1. Boards Manager -> the standard **esp32** package by Espressif. -2. Library Manager -> **NimBLE-Arduino**. Pin this to **1.4.x**. Legoino has - not moved to the NimBLE 2.x API, so 2.x gives a wall of compile errors. -3. Library Manager -> **Legoino**. -4. Flash `tools/hub_scanner/hub_scanner.ino` first, note both hub addresses, - paste them into `receiver.ino`, then flash `receiver/receiver.ino`. +``` +https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json +``` -## Controls as shipped +Boards Manager -> install **esp32_bluepad32**. Tools -> Board -> pick ESP32 Dev +Module from under **esp32_bluepad32**, not the plain `esp32` group. Getting that +wrong is what produces `fatal error: Bluepad32.h: No such file or directory`. -| Input | Action | -| ----------------- | ------------------------------- | -| Left stick Y | Left track, hub 1 port A | -| Right stick Y | Right track, hub 1 port B | -| Left stick X | Head rotation, hub 2 port A | +Flash `transmitter/transmitter.ino`. Nothing else needs installing — Bluepad32 +lives inside the board package. Do not install Legoino or NimBLE on this board. -Change the mapping in the frame-handling block of `receiver.ino`. +**2. Pair the pad, before wiring anything.** + +Open Serial Monitor at 115200. Hold SHARE + PS on the controller until the light +bar flashes. You want `Controller connected in slot 0`. Once it pairs reliably, +comment out `BP32.forgetBluetoothKeys()` in `setup()` — it is in there to clear +stale pairings, and leaving it means re-pairing on every boot. + +Set `DEBUG_FRAMES` to 1 temporarily and confirm frames stream past as you move +the sticks. If they do, board A is finished. + +**3. Board B — the hub board.** + +Boards Manager -> the standard **esp32** package by Espressif. Library Manager +-> **NimBLE-Arduino**, pinned to **1.4.x** (Legoino has not moved to the 2.x +API, and 2.x gives a wall of compile errors), then **Legoino**. + +Flash `tools/hub_scanner/hub_scanner.ino` first. Press each hub's green button +and note the addresses it reports, then paste them into `receiver.ino` as +`HUB0_ADDR` and `HUB1_ADDR`. + +**4. Verify the port map before you trust it.** + +Flash `receiver/receiver.ino` with `DEBUG_MOTORS` set to 1. Every motor command +logs which hub and port it lands on. Move one control at a time and check the +log matches what physically moves. This is worth doing properly — a swapped hub +address or a motor in the wrong port looks exactly like a software bug and will +waste an afternoon. + +Set `DEBUG_MOTORS` back to 0 once it checks out. + +**5. Wire the boards together** per the diagram above, power both, and drive it. + +Onboard LED on board A is solid when the pad is connected. On board B it is +solid when both hubs are connected. + +## Controls + +Tank drive — every input drives exactly one motor. + +| Input | Function | +| --- | --- | +| Left stick Y | Left track (hub 0 port B) | +| Right stick Y | Right track (hub 0 port A) | +| D-pad up / down | Head tilt (hub 1 port A) | +| D-pad left / right | Head turn (hub 1 port B) | +| R2 / L2 | Body lift up / down, proportional (hub 0 port D) | +| Square / Circle | Left arm up / down (hub 1 port C) | +| Triangle / Cross | Right arm up / down (hub 1 port D) | +| L1 held | Precision, 40% track speed | +| R1 held | Full, 100% track speed | +| L1 + R1 | All stop | + +Default track scale is 75%. Stick X axes are unused. + +**Everything except the tracks runs into a mechanical end stop**, and there is no +position feedback, so holding a direction at a stop stalls the motor. That is +what `HEAD_MAX`, `LIFT_MAX` and `ARM_MAX` are for. Lower them if an axis feels +forceful, and do not hold a direction once an axis has stopped moving. ## Link protocol ASCII, newline terminated, 115200 8N1: ``` -G,,,,,*\n +G,,,,,,,,*\n ``` * axes are Bluepad32 raw values, -512..511 -* `buttons` is the 16-bit mask as a decimal number +* `buttons` is the 16-bit mask, `dpad` the 8-bit mask, both decimal +* `l2`/`r2` are the analog triggers, 0..1023 * `XX` is a two-digit hex XOR checksum of everything before the `*` -Being plain text means you can watch the link with a USB-serial adapter when -something misbehaves. +Plain text means you can watch the link with any USB-serial adapter when +something misbehaves. The receiver drops any frame that fails the checksum, and +stops all motors if nothing valid arrives for 400ms. ## Things that bite +* **Nothing arrives at board B.** TX/RX swapped, or no common ground. Both are + silent failures. * **One hub connects, the other does not.** Legoino shares a single NimBLE - scanner. `receiver.ino` connects them strictly one at a time for this reason. - Do not "optimise" that into two parallel `init()` calls. -* **Motors stutter or the hub drops out.** You are sending commands faster than - the hub can chew. `MOTOR_MIN_INTERVAL_MS` throttles per port; raise it before - blaming the radio. -* **More than 3 hubs later on.** Edit `CONFIG_BT_NIMBLE_MAX_CONNECTIONS` in - `NimBLE-Arduino/src/nimconfig.h`, then restart the IDE to force a rebuild. + scanner. `receiver.ino` connects them strictly one at a time for this reason — + do not "optimise" that into two parallel `init()` calls. +* **Motors stutter or a hub drops out.** Commands are outrunning the hub. Raise + `MOTOR_MIN_GAP_MS` (per port) or `HUB_MIN_GAP_MS` (per hub). Hub 1 carries four + motors, which is why the per-hub limit exists at all. * **Wrong motor command.** Technic/Control+ motors want `setTachoMotorSpeed`; train and simple PU motors want `setBasicMotorSpeed`. Toggle - `USE_TACHO_MOTORS` in `receiver.ino`. -* **Bluepad32 API drift.** v4 renamed `GamepadPtr` to `ControllerPtr` and - `BP32_MAX_GAMEPADS` to `BP32_MAX_CONTROLLERS`. This code uses the v4 names. - -## If you really want one board - -Two options, both more work than a second ESP32: - -* Write the LEGO Wireless Protocol GATT client directly against BTstack inside - the Bluepad32 sketch. Bluepad32 v4 does support BLE, so the radio can do it — - you would just be reimplementing the parts of Legoino you need. The motor - command is only eight bytes. -* Put Pybricks on the hubs and drive them from an LMS-ESP32 running the - prebuilt Bluepad32 LPF2 firmware over the LEGO wire. + `USE_TACHO_MOTORS`. +* **Button masks.** The values in `receiver.ino` are Bluepad32's standard + layout. If a face button does the wrong thing, set `DEBUG_BUTTONS` in the + transmitter, press each one, and correct the constants. +* **More than 3 hubs later on.** Edit `CONFIG_BT_NIMBLE_MAX_CONNECTIONS` in + `NimBLE-Arduino/src/nimconfig.h`, then restart the IDE to force a rebuild. ---