UART-controlled ESP32 peripheral for automated E2E Bluetooth testing. Dual-mode (Classic BT + BLE) via Bluedroid on original ESP32. Firmware (ESP-IDF v5.x, 2511 lines C): - NDJSON protocol over UART1 (115200 baud) - System commands: ping, reset, get_info, get_status - Classic BT: GAP, SPP, all 4 SSP pairing modes - BLE: GATTS, advertising, GATT service/characteristic management - 6 device personas: headset, speaker, keyboard, sensor, phone, bare - Event reporter: thread-safe async event queue to host Python MCP server (FastMCP, 1626 lines): - Async serial client with command/response correlation - Event queue with wait_for pattern matching - Tools: connection, configure, classic, ble, persona, events - MCP resources: esp32://status, esp32://events, esp32://personas Tests: 74 unit tests passing, 5 integration test stubs (skip without hardware)
7.2 KiB
Hardware Setup Guide
How to set up the ESP32 hardware and build environment for the mcbluetooth-esp32 test harness.
Requirements
- ESP32 dev board -- must be an original ESP32 (ESP32-D0WD or similar) with Classic Bluetooth support. ESP32-S3, ESP32-C3, ESP32-H2, and ESP32-S2 lack the BR/EDR radio and will not work for Classic BT pairing tests.
- USB cable (USB-A to micro-USB or USB-C depending on your board)
- Linux host with BlueZ installed (for the
mcbluetoothMCP server on the other side of E2E tests) - ESP-IDF v5.x toolchain
Verified Hardware
| Property | Value |
|---|---|
| Chip | ESP32-D0WD-V3 (rev 3.1) |
| Flash | 4MB |
| Crystal | 40MHz |
| Features | Wi-Fi, Bluetooth (dual-mode), Dual Core, 240MHz |
Any ESP32 board based on the original ESP32 chip should work. Commonly available boards include ESP32-DevKitC, ESP32-WROOM-32, and NodeMCU-32S.
Wiring
Default: USB only
For most development, a single USB cable handles both flashing and protocol communication. The ESP32's built-in USB-to-UART bridge (typically CP2102 or CH340) provides the serial link.
The firmware uses UART1 (GPIO4/GPIO5) for the NDJSON protocol and keeps UART0 for ESP-IDF console logging. When using USB only, the USB bridge connects to UART0 by default -- so you will need either:
- A board that routes UART1 through the USB bridge (uncommon), or
- A separate USB-UART adapter connected to GPIO4/GPIO5 (described below)
For quick testing with the firmware's default pin assignment, connect a USB-UART adapter.
Dedicated UART (GPIO4/GPIO5)
Connect a USB-UART adapter (e.g., FTDI FT232R, CP2102, CH340) to the ESP32:
ESP32 GPIO4 (TX) ----> USB-UART adapter RX
ESP32 GPIO5 (RX) <---- USB-UART adapter TX
ESP32 GND ----> USB-UART adapter GND
The adapter appears as a second /dev/ttyUSB* device on the host. Use this device path for ESP32_SERIAL_PORT.
Do not connect voltage lines (VCC/3V3) between the adapter and the ESP32 if the board is already powered via its own USB port.
Pin reassignment
If GPIO4/GPIO5 conflict with other peripherals on your board, change the pin definitions in firmware/main/uart_handler.c:
#define UART_TX_PIN GPIO_NUM_4
#define UART_RX_PIN GPIO_NUM_5
Rebuild and reflash after changing pins.
ESP-IDF Setup
1. Install ESP-IDF v5.x
Follow the official installation guide: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/
On Arch Linux:
# Install dependencies
sudo pacman -S cmake ninja python
# Clone ESP-IDF
mkdir -p ~/esp && cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32
# Activate the environment (add to .bashrc or run each session)
. ~/esp/esp-idf/export.sh
2. Set the target
cd firmware
idf.py set-target esp32
This only needs to be done once per checkout. It configures the build system for the ESP32 chip.
3. Build
idf.py build
Or using the project Makefile from the repository root:
make build
4. Flash
idf.py -p /dev/ttyUSB4 flash
Or:
make flash SERIAL_PORT=/dev/ttyUSB4
5. Monitor (optional)
Open the ESP-IDF serial monitor to see console logs from UART0:
idf.py -p /dev/ttyUSB4 monitor
Press Ctrl+] to exit the monitor.
6. Flash and monitor in one step
make flash-monitor SERIAL_PORT=/dev/ttyUSB4
Quick Verification
Using the MCP server
# Set the serial port and start the server
ESP32_SERIAL_PORT=/dev/ttyUSB4 uvx mcbluetooth-esp32
Then from a Claude Code session with the MCP server configured, call:
esp32_connect-- opens the serial linkesp32_ping-- should return{"pong": true}esp32_get_info-- should return chip model, firmware version, MAC address
Using the Makefile ping target
make ping SERIAL_PORT=/dev/ttyUSB4
This runs a standalone Python script that connects, sends a ping command, prints the response, and disconnects.
Raw serial check
If everything else fails, use screen or minicom to send raw JSON:
screen /dev/ttyUSB4 115200
Type (all on one line, then press Enter):
{"type":"cmd","id":"1","cmd":"ping"}
You should see:
{"type":"resp","id":"1","status":"ok","data":{"pong":true}}
Press Ctrl+A then K then Y to exit screen.
sdkconfig
The project ships firmware/sdkconfig.defaults with the required Bluetooth configuration pre-set:
| Setting | Value | Purpose |
|---|---|---|
CONFIG_BT_ENABLED |
y | Enable Bluetooth controller |
CONFIG_BT_BLUEDROID_ENABLED |
y | Use Bluedroid host stack |
CONFIG_BT_CLASSIC_ENABLED |
y | Enable BR/EDR (Classic BT) |
CONFIG_BT_BLE_ENABLED |
y | Enable BLE |
CONFIG_BT_SSP_ENABLED |
y | Enable Secure Simple Pairing |
CONFIG_BT_SPP_ENABLED |
y | Enable Serial Port Profile |
CONFIG_BT_GATTS_ENABLE |
y | Enable GATT Server |
CONFIG_BTDM_CTRL_MODE_BTDM |
y | Dual-mode controller (Classic + BLE simultaneously) |
CONFIG_NVS_ENABLED |
y | Non-volatile storage for bonding data |
Do not modify these unless you understand the implications. Disabling CONFIG_BT_CLASSIC_ENABLED breaks all Classic BT pairing tests. Disabling CONFIG_BT_SSP_ENABLED forces legacy PIN-only pairing.
Troubleshooting
Permission denied on /dev/ttyUSB*
Add your user to the dialout group (or uucp on Arch Linux):
# Debian/Ubuntu
sudo usermod -aG dialout $USER
# Arch Linux
sudo usermod -aG uucp $USER
Log out and back in for the group change to take effect.
Device not found
Check what serial devices are present:
ls -la /dev/ttyUSB* /dev/ttyACM*
If nothing appears, verify the USB cable is a data cable (not charge-only) and that the board's USB-UART chip driver is loaded:
dmesg | tail -20
Look for lines mentioning cp210x, ch341, or ftdi_sio.
Flash fails or hangs
Some ESP32 boards require holding the BOOT button during the initial flash sequence. Hold BOOT, press and release EN (reset), then release BOOT. The flash should proceed.
If the board has auto-download circuitry (most DevKitC boards do), this should not be necessary.
No response over UART
-
Verify TX/RX pin assignment. The firmware uses UART1 on GPIO4 (TX) and GPIO5 (RX). If your adapter is connected to different pins, update
uart_handler.c. -
Check baud rate. Both sides must use 115200. Verify in
screenor your terminal emulator. -
Check the correct serial device. If the board has two USB-UART interfaces (one for UART0 console, one for UART1 protocol), make sure you are talking to the right one.
-
Look at UART0 console output. Connect the ESP-IDF monitor to the console port. Boot messages and error logs appear there. If you see
UART1 ready (TX=4 RX=5 @ 115200 baud)in the log, the firmware started correctly.
Build errors about missing Bluetooth headers
Make sure idf.py set-target esp32 was run. The ESP32-S3 and ESP32-C3 targets do not expose Classic BT APIs, which causes build failures.
NVS errors on first boot
If the console shows NVS partition issue, erasing and re-initializing, this is expected on first flash or after a partition table change. The firmware handles it automatically by erasing and reinitializing NVS.