mcbluetooth-esp32/docs/hardware-setup.md
Ryan Malloy 6398a5223a ESP32 Bluetooth test harness MCP server
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)
2026-02-02 15:12:28 -07:00

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 mcbluetooth MCP 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:

  1. A board that routes UART1 through the USB bridge (uncommon), or
  2. 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:

  1. esp32_connect -- opens the serial link
  2. esp32_ping -- should return {"pong": true}
  3. 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

  1. 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.

  2. Check baud rate. Both sides must use 115200. Verify in screen or your terminal emulator.

  3. 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.

  4. 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.