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.
This commit is contained in:
2026-09-09 19:30:18 +10:00
parent 0b32384578
commit 9dd74d9d60
+102 -56
View File
@@ -1,6 +1,8 @@
# ps4-lego-bridge # 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 ## 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 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 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 cannot run both in one firmware image.
(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.
So: board A runs Bluepad32 and nothing else. Board B runs Legoino and nothing So: board A runs Bluepad32 and nothing else. Board B runs Legoino and nothing
else. Three jumper wires between them. 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 ## Hardware
* 2x ESP32 dev boards. Both must be the **original ESP32** (WROOM/WROVER). * 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) Board A (Bluepad32) Board B (Legoino)
GPIO17 TX -------------> GPIO16 RX GPIO17 TX -------------> GPIO16 RX
GPIO16 RX <------------- GPIO17 TX 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 TX goes to RX, not TX to TX. If nothing arrives, that swap is the first thing
long as the grounds are tied together. 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: **1. Board A — the gamepad board.**
`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`.
### 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 https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
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`.
## 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 | Flash `transmitter/transmitter.ino`. Nothing else needs installing — Bluepad32
| ----------------- | ------------------------------- | lives inside the board package. Do not install Legoino or NimBLE on this board.
| 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 |
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 ## Link protocol
ASCII, newline terminated, 115200 8N1: ASCII, newline terminated, 115200 8N1:
``` ```
G,<lx>,<ly>,<rx>,<ry>,<buttons>*<XX>\n G,<lx>,<ly>,<rx>,<ry>,<buttons>,<dpad>,<l2>,<r2>*<XX>\n
``` ```
* axes are Bluepad32 raw values, -512..511 * 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 `*` * `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 Plain text means you can watch the link with any USB-serial adapter when
something misbehaves. something misbehaves. The receiver drops any frame that fails the checksum, and
stops all motors if nothing valid arrives for 400ms.
## Things that bite ## 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 * **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. scanner. `receiver.ino` connects them strictly one at a time for this reason
Do not "optimise" that into two parallel `init()` calls. do not "optimise" that into two parallel `init()` calls.
* **Motors stutter or the hub drops out.** You are sending commands faster than * **Motors stutter or a hub drops out.** Commands are outrunning the hub. Raise
the hub can chew. `MOTOR_MIN_INTERVAL_MS` throttles per port; raise it before `MOTOR_MIN_GAP_MS` (per port) or `HUB_MIN_GAP_MS` (per hub). Hub 1 carries four
blaming the radio. motors, which is why the per-hub limit exists at all.
* **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.
* **Wrong motor command.** Technic/Control+ motors want `setTachoMotorSpeed`; * **Wrong motor command.** Technic/Control+ motors want `setTachoMotorSpeed`;
train and simple PU motors want `setBasicMotorSpeed`. Toggle train and simple PU motors want `setBasicMotorSpeed`. Toggle
`USE_TACHO_MOTORS` in `receiver.ino`. `USE_TACHO_MOTORS`.
* **Bluepad32 API drift.** v4 renamed `GamepadPtr` to `ControllerPtr` and * **Button masks.** The values in `receiver.ino` are Bluepad32's standard
`BP32_MAX_GAMEPADS` to `BP32_MAX_CONTROLLERS`. This code uses the v4 names. layout. If a face button does the wrong thing, set `DEBUG_BUTTONS` in the
transmitter, press each one, and correct the constants.
## If you really want one board * **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.
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.
--- ---