- Pin the toolchain versions, with the Legoino/core 3.x error text. - Note DIR_* direction constants and the corrected control table. - Add the swapped-hub-address story to step 4, and the Arduino prototype-ordering trap to the gotchas list.
258 lines
11 KiB
Markdown
258 lines
11 KiB
Markdown
# ps4-lego-bridge
|
|
|
|
PS4 controller -> two LEGO Technic hubs, using two ESP32 boards.
|
|
|
|
Driving a Johnny 5 (Short Circuit) MOC — seven motors across two hubs.
|
|
|
|
**Status: working on hardware.** The configuration in this repo is the one that
|
|
runs, not the one that ought to.
|
|
|
|
## Why two boards
|
|
|
|
The obvious sketch — Bluepad32 for the gamepad, Legoino for the hubs, one ESP32 —
|
|
does not work, and it is not a library-version problem.
|
|
|
|
* **Bluepad32** needs Bluetooth Classic (BR/EDR), because a DualShock 4 is a
|
|
BR/EDR device. It gets that from **BTstack**, and it ships as a whole
|
|
replacement ESP32 board package rather than a normal library.
|
|
* **Legoino** talks to the hubs over BLE using **NimBLE-Arduino**.
|
|
|
|
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.
|
|
|
|
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.)
|
|
|
|
## Toolchain versions — pin these
|
|
|
|
| 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 |
|
|
|
|
Board B is the fussy one. **Core 3.x will not build Legoino.** ESP-IDF 5 changed
|
|
`uint32_t` from `unsigned int` to `unsigned long`, so Legoino's own header and
|
|
.cpp no longer agree, and it never included `<string>` itself. The symptoms are:
|
|
|
|
```
|
|
'string' in namespace 'std' does not name a type
|
|
no declaration matches 'uint32_t LegoinoCommon::ReadUInt32LE(uint8_t*, int)'
|
|
```
|
|
|
|
Downgrade the core rather than patching the library — patching works, but it is
|
|
the first of several layers and any Library Manager update silently reverts it.
|
|
Board A is unaffected; it uses a separate package.
|
|
|
|
## Hardware
|
|
|
|
* 2x ESP32 dev boards. Both must be the **original ESP32** (WROOM/WROVER).
|
|
S3, C3, C6 and H2 have no Bluetooth Classic, so a PS4 pad will not pair.
|
|
* 3 jumper wires, or 4 if you daisy-chain power.
|
|
|
|
```
|
|
Board A (Bluepad32) Board B (Legoino)
|
|
GPIO17 TX -------------> GPIO16 RX
|
|
GPIO16 RX <------------- GPIO17 TX
|
|
GND -------------- GND <- do not skip this one
|
|
5V -------------- 5V <- only in the model setup, see below
|
|
```
|
|
|
|
TX goes to RX, not TX to TX. If nothing arrives, that swap is the first thing
|
|
to check.
|
|
|
|
Without a common ground the UART has no shared voltage reference and you get
|
|
garbage or silence, so that wire is not optional even when both boards have
|
|
their own USB.
|
|
|
|
## Power
|
|
|
|
Two arrangements, and the choice matters more than it looks.
|
|
|
|
**Bench setup — separate USB, no 5V wire.** Both boards on their own USB cables.
|
|
You get both serial monitors and can reflash either board without unplugging
|
|
anything. Use this for everything up to step 5.
|
|
|
|
**Model setup — one supply, 5V jumper.** USB into board A, then a fourth wire
|
|
from **A's 5V pin to B's 5V pin** (labelled VIN on some boards). Ground is
|
|
already joined by the wire you have.
|
|
|
|
Use 5V to 5V, never 3.3V to 3.3V. Each board's onboard AMS1117 regulator makes
|
|
its own 3.3V, and tying the outputs together back-feeds one regulator from the
|
|
other.
|
|
|
|
**Never have both USB cables plugged in while the 5V wire is connected.** That
|
|
ties two host supplies together through your jumper. Some devkits have a Schottky
|
|
diode on VBUS that prevents it, plenty do not, and you cannot tell by looking.
|
|
Pull the 5V jumper before plugging in a USB cable to reflash.
|
|
|
|
**Current budget.** An ESP32 idles around 80-120mA and peaks at a few hundred mA
|
|
on Bluetooth transmit. Two of them off one USB 2.0 port sits right at the 500mA
|
|
that port promises. It usually works, but a marginal supply shows up as random
|
|
reboots that look exactly like a software fault. A 1A+ charger or power bank
|
|
removes the doubt.
|
|
|
|
**Diode drops.** Many boards put a diode between VBUS and the 5V pin, so board
|
|
A's 5V pin sits nearer 4.7V. Through board B's own diode you are at maybe 4.4V
|
|
into a regulator wanting roughly 1.1V of headroom. It works, but the margin is
|
|
thin. If you are running off a power bank anyway, prefer separate leads to each
|
|
board's 5V pin over chaining B off A — same wire count, no stacked drops.
|
|
|
|
## Test the power-loss case deliberately
|
|
|
|
The 400ms failsafe in `receiver.ino` only fires if the receiver is *running*. If
|
|
board B loses power mid-command — and on a shared supply that now happens
|
|
whenever board A does — the failsafe cannot fire, and whether the motors stop is
|
|
left to the hub's own behaviour on BLE disconnect.
|
|
|
|
Find that out on purpose rather than by accident. Set a track running slowly,
|
|
pull power from board B, and watch what the motor does. If it keeps running you
|
|
want a physical switch on the hubs within arm's reach before driving this
|
|
anywhere interesting.
|
|
|
|
Worth doing the same test by pulling the UART wire instead — that path *does*
|
|
hit the failsafe, and confirming it works takes ten seconds.
|
|
|
|
## Step by step
|
|
|
|
**1. Board A — the gamepad board.**
|
|
|
|
Preferences -> Additional board manager URLs, add:
|
|
|
|
```
|
|
https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
|
|
```
|
|
|
|
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`.
|
|
|
|
Flash `transmitter/transmitter.ino`. Nothing else needs installing — Bluepad32
|
|
lives inside the board package. Do not install Legoino or NimBLE on this board.
|
|
|
|
**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, version 2.0.17.
|
|
Library Manager -> **NimBLE-Arduino** pinned to **1.4.x**, then **Legoino**. See
|
|
the toolchain table above for why the versions matter.
|
|
|
|
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`.
|
|
|
|
Nothing else may be holding the hubs. A hub accepts one connection at a time and
|
|
stops advertising once taken, so power down any other ESP32 and close the LEGO
|
|
Powered Up app before scanning.
|
|
|
|
**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 step is not optional busywork. On this build the two hub addresses were the
|
|
wrong way round, and the symptom — an arm moving when the track stick moved —
|
|
looks exactly like a software bug. It is not one, and no amount of reading the
|
|
code will find it.
|
|
|
|
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) |
|
|
| Cross / Triangle | 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.
|
|
|
|
**Motor directions** are the `DIR_*` constants at the top of `receiver.ino`. Flip
|
|
one to `-1` if an axis runs backwards — mirrored mountings are normal on a
|
|
symmetric model. `DIR_RIGHT_TRACK` is `-1` on this build.
|
|
|
|
**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,<lx>,<ly>,<rx>,<ry>,<buttons>,<dpad>,<l2>,<r2>*<XX>\n
|
|
```
|
|
|
|
* axes are Bluepad32 raw values, -512..511
|
|
* `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 `*`
|
|
|
|
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
|
|
|
|
* **`'Frame' has not been declared`, or any type "not declared" at its own
|
|
definition.** The Arduino IDE injects generated prototypes immediately before
|
|
the first function definition in the file. Any type used in a signature must be
|
|
declared above that point — which is why `HubLink`, `PortState` and `Frame` all
|
|
sit in one block near the top. Do not move a function above them.
|
|
* **Legoino will not compile.** Core 3.x. See the toolchain table.
|
|
* **Nothing arrives at board B.** TX/RX swapped, or no common ground. Both are
|
|
silent failures.
|
|
* **Random reboots under load.** Supply, not software. See the current budget
|
|
above.
|
|
* **Hubs never connect.** Something else is holding them, or they went to sleep.
|
|
Power down other controllers, close the LEGO app, press the green buttons.
|
|
* **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 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`.
|
|
* **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.
|
|
|
|
---
|
|
|
|
Created by: Jess Rogerson (yelling commands at Claude.AI)
|