121 lines
5.9 KiB
Markdown
121 lines
5.9 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.
|
|
|
|
**Build and deploy instructions: [docs/BUILD.md](docs/BUILD.md).** This will not
|
|
compile out of the box — the board package ships the BTstack headers but does
|
|
not put them on the include path, and that has to be fixed first.
|
|
|
|
## 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.
|
|
|
|
## Layout
|
|
|
|
```
|
|
ps4_lego_onebrain/ps4_lego_onebrain.ino the sketch
|
|
docs/BUILD.md install, include-path fix, first run
|
|
docs/platform.local.txt the include-path override, for restoring
|
|
after a Bluepad32 package update
|
|
```
|
|
|
|
## 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: the Bluepad32 board-package install, the threading contract and
|
|
`btstack_run_loop_execute_on_main_thread()`, the BTstack GATT client call
|
|
signatures, the LWP3 message encoding above, and — on Jess's machine — that
|
|
package 4.1.0 ships every header this needs under `tools\sdk\esp32\include\`.
|
|
|
|
Not verified, because it needs the hardware powered up:
|
|
|
|
* **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)
|