Files
ps4-lego-onebrain/README.md
T

121 lines
6.0 KiB
Markdown

# ps4-lego-onebrain
One ESP32. PS4 controller over Bluetooth Classic, two LEGO Powered Up hubs over
BLE, both on the same Bluetooth stack.
This is the harder of the two routes. If you want the boring one that works
first go, see `ps4-lego-bridge` — two boards and a UART between them.
## How it avoids the stack collision
The reason the obvious sketch fails is that Bluepad32 (BTstack) and Legoino
(NimBLE) are two Bluetooth host stacks fighting over one radio. The fix here is
to have only one stack: BTstack does the gamepad *and* the hubs.
Bluepad32 v4 already compiles BTstack with LE central and a GATT client, since
it supports BLE gamepads like the Xbox v5 firmware pads. That machinery is
sitting there unused for our purposes, so this sketch borrows it and speaks the
LEGO Wireless Protocol directly. No Legoino, no NimBLE.
What you give up: everything Legoino did for you. Sensor callbacks, port device
detection, tacho positioning, hub name reads. What you get back is one board and
about 300 lines.
## The two rules
**1. Threading.** BTstack is not thread-safe, and `loop()` does not run on the
BTstack task. From the Bluepad32 docs, the only BTstack call that is safe from
anywhere is `btstack_run_loop_execute_on_main_thread()`.
So the LEGO side lives entirely inside a repeating BTstack timer, which is
scheduled once at boot via that one safe call. `loop()` never touches BTstack —
it reads the sticks and writes three plain ints into `gDesired`, and the timer
picks them up. Any BTstack call added to `loop()` will crash at random places,
which is the worst kind of bug to chase.
**2. Boot order.** Gamepad discovery starts *disabled*. Both hubs connect first,
then `BP32.enableNewBluetoothConnections(true)` opens up for the pad. Bluepad32's
inquiry and LE scan otherwise run at the same time as our LE create-connection
and the hub connects unreliably or not at all.
## Setup
1. Preferences → Additional board manager URLs:
`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. Must be the original
ESP32 (WROOM/WROVER) — S3/C3/C6/H2 have no Bluetooth Classic.
4. Partition scheme: **Huge APP**. BTstack with both transports plus the GATT
client will not fit the default.
5. Put your hub addresses in `HUB_ADDR_STR`, flash, watch the serial monitor.
Do not install Legoino or NimBLE-Arduino. If they end up in the build you are
back to two host stacks.
## The protocol, in full
Everything the motors need is one 8-byte write, no response, to characteristic
`00001624-1212-EFDE-1623-785FEABCD123` inside service `...1623...`:
```
08 00 81 <port> 11 51 00 <power>
│ │ │ │ │ │ │ └── int8, -100..100 (127 = brake)
│ │ │ │ │ │ └───── mode 0x00 = POWER
│ │ │ │ │ └──────── subcommand 0x51 = WriteDirectModeData
│ │ │ │ └─────────── 0x11 = execute immediately + command feedback
│ │ │ └────────────────── port: A=0x00 B=0x01 C=0x02 D=0x03, LED=0x32
│ │ └───────────────────── message type 0x81 = Port Output Command
│ └──────────────────────── hub ID, always 0x00
└─────────────────────────── message length
```
That same shape sets the hub LED — port `0x32`, payload is a colour index
(`0x06` is green, which the sketch uses to show a hub came up).
Mode 0 is direct power, so it works on train motors and Technic motors alike.
There is no tacho-vs-basic distinction to get wrong here. The trade-off is that
you get no closed-loop speed control — power is power, and the motor slows under
load. `StartSpeed` (subcommand `0x07`) does regulate, if you want it later.
Full spec: https://lego.github.io/lego-ble-wireless-protocol-docs/
## What to expect on the bench
Verified from documentation: the Bluepad32 board-package install, the threading
contract and `btstack_run_loop_execute_on_main_thread()`, the BTstack GATT
client call signatures, and the LWP3 message encoding above.
Not verified, because it needs your hardware:
* **Address type.** LEGO's OUI is `90:84:2B`, a public range, so the sketch uses
`BD_ADDR_TYPE_LE_PUBLIC`. If the connect times out repeatedly with no LE
connection-complete event, switch `HUB_ADDR_TYPE` to `BD_ADDR_TYPE_LE_RANDOM`.
* **Whether BLE is enabled in your Bluepad32 build.** If `gap_connect` never
produces a connection-complete, this is the next thing to check.
* **The LE connection-complete event name.** BTstack moved this from
`HCI_EVENT_LE_META`/`HCI_SUBEVENT_LE_CONNECTION_COMPLETE` to
`HCI_EVENT_META_GAP`/`GAP_SUBEVENT_LE_CONNECTION_COMPLETE`. The sketch handles
both, with the newer one behind `#ifdef`, so whichever your version emits will
land. Guarded by connection state so a BLE gamepad connecting can't be
mistaken for a hub.
Turn on verbose BTstack logging before you start guessing. Nearly every failure
here shows up as a specific HCI event you can read.
## Known rough edges
* Commands are throttled to one per port per 60ms (`MOTOR_MIN_GAP_MS`). Push
that lower and the hub's BLE queue backs up and it drops the link. Stops are
exempt and always go out immediately.
* Write-without-response can fail silently when ACL buffers are full. The tick
retries 25ms later, so a dropped command is invisible in practice, but that is
why you should not treat a single write as a guarantee.
* There is no failsafe timer on the gamepad side beyond `isConnected()`. If the
pad goes out of range the stack can take 20 seconds to notice. The Bluepad32
FAQ has a `hasData()` plus timeout pattern worth adding if this ends up
driving something that can fall off a table.
---
Created by: Jess Rogerson (yelling commands at Claude.AI)