Files

5.9 KiB

Build and deploy

Target: Windows, Arduino IDE 2.x, board package esp32-bluepad32 4.1.0, original ESP32 (WROOM/WROVER). S3/C3/C6/H2 have no Bluetooth Classic, so a PS4 pad will not pair with them.

1. Install the board package

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. Then Tools -> Board -> esp32_bluepad32 -> ESP32 Dev Module.

Picking the board out of the normal esp32 package is what gives you fatal error: Bluepad32.h: No such file or directory. Bluepad32 is a replacement board package, not a Library Manager entry, and adding it as a ZIP library will not work.

Do not install Legoino or NimBLE-Arduino. If either ends up in the build you are back to two Bluetooth host stacks fighting over one radio, which is the whole problem this project exists to avoid.

2. Put the BTstack headers on the include path

Package 4.1.0 ships the BTstack and Bluepad32 headers under tools\sdk\<mcu>\include\, but does not add them to the compiler's include path. Without this step you get:

fatal error: btstack.h: No such file or directory

4.1.0 is built on the arduino-esp32 2.x layout, so there is no flags\includes file to edit (that only exists on core 3.x). The include paths live in compiler.cpreprocessor.flags in platform.txt. Rather than edit that file, drop a platform.local.txt beside it — Arduino reads it as an override, the original stays untouched, and rolling back is deleting one file.

In PowerShell:

$plat = "$env:LOCALAPPDATA\Arduino15\packages\esp32-bluepad32\hardware\esp32\4.1.0"

$flags = 'compiler.cpreprocessor.flags={compiler.cpreprocessor.flags.{build.mcu}} ' +
         '"-I{compiler.sdk.path}/{build.memory_type}/include" ' +
         '"-I{compiler.sdk.path}/include/btstack/src" ' +
         '"-I{compiler.sdk.path}/include/btstack/include" ' +
         '"-I{compiler.sdk.path}/include/btstack/platform/freertos" ' +
         '"-I{compiler.sdk.path}/include/bluepad32/include"'

$flags | Out-File "$plat\platform.local.txt" -Encoding ascii
Get-Content "$plat\platform.local.txt"

It has to be a single line. The Get-Content at the end is there to confirm it did not wrap.

The first two tokens restate what platform.txt already had — the override replaces the whole value, so they have to be repeated. {compiler.sdk.path} resolves to tools\sdk\<mcu>, so these entries work for every MCU in the package, not just esp32.

What each added path is for:

Path Provides
btstack/src btstack.h, gatt_client.h, btstack_run_loop.h
btstack/include btstack_config.h — separate directory, and BTstack includes it from btstack.h, so missing it gives you a confusing second error right after fixing the first
btstack/platform/freertos the ESP32 run loop implementation
bluepad32/include uni.h, if you later want the uni_bt_allowlist API

Duplicate -I flags are harmless, so it does not matter if bluepad32/include was already covered.

Two things to know about this edit:

  • A Bluepad32 package update wipes it. Keep a copy of platform.local.txt somewhere outside Arduino15 — there is one in docs/ here.
  • A malformed value breaks compilation for every ESP32 sketch on the machine, not just this one. If things go strange, delete the file.

3. Board settings

  • Board: ESP32 Dev Module (under esp32_bluepad32)
  • Partition Scheme: Huge APP (3MB No OTA/1MB SPIFFS). BTstack with both transports plus the GATT client does not fit the default partition table.
  • Upload speed 921600 is fine; drop to 115200 if uploads fail.

4. Before flashing

After creating platform.local.txt, fully quit the Arduino IDE — closing the sketch window is not enough, it has to re-read the platform — and clear the build cache:

Remove-Item "$env:LOCALAPPDATA\Temp\arduino\sketches\*" -Recurse -Force -ErrorAction SilentlyContinue

Then set your two hub addresses in HUB_ADDR_STR at the top of the sketch and compile.

5. First run

  1. Power both hubs and press their green buttons so they are advertising.
  2. Flash, open Serial Monitor at 115200.
  3. Expect Connecting hubs, then opening for the gamepad. Each hub's LED goes green as it is claimed.
  4. Once both are up you get Hubs up - put the PS4 pad in pairing mode. Hold SHARE + PS on the controller until the light bar flashes.
  5. Onboard LED on GPIO2 goes solid when the pad and both hubs are all connected.

The order matters and is deliberate: gamepad discovery stays off until the hubs are connected, so Bluepad32's inquiry and LE scan are not running at the same time as our LE create-connection.

Troubleshooting

A missing header from btstack/3rd-party. Add another -I line for that directory and redo step 4.

Hubs never connect, no LE connection-complete in the log. In order of likelihood: the hubs went back to sleep (they time out after a couple of minutes — press the buttons again); the addresses are wrong; or the address type is wrong. For the last one, switch HUB_ADDR_TYPE to BD_ADDR_TYPE_LE_RANDOM.

One hub connects, the other never does. Expected if the first one is stuck mid-discovery. serviceHubConnections() only runs one at a time by design; check gActive is being released.

Motors stutter, or a hub drops off after a few seconds. Commands are going out faster than the hub can absorb. Raise MOTOR_MIN_GAP_MS.

Random crashes at unrelated places. Almost certainly a BTstack call added to loop(). BTstack is not thread-safe and loop() is not the BTstack thread — see the header comment in the sketch.

Turn on verbose BTstack logging before guessing at any of it. Nearly every failure mode here surfaces as a specific HCI event you can read.


Created by: Jess Rogerson (yelling commands at Claude.AI)