From 11d47c27e2e2310a89d149a9e69147e1bb4f88db Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 28 Aug 2026 10:11:31 +1000 Subject: [PATCH] Document threading rules, boot order, LWP3 encoding and open questions --- README.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2671231..efb9f45 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +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 11 51 00 +│ │ │ │ │ │ │ └── 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)