Document threading rules, boot order, LWP3 encoding and open questions
This commit is contained in:
@@ -1,3 +1,120 @@
|
||||
# ps4-lego-onebrain
|
||||
|
||||
Single-ESP32 PS4 controller to two LEGO Powered Up hubs - Bluepad32 plus a hand-rolled LWP3 GATT client on BTstack
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user