Implements Classic Bluetooth HID Device profile for keyboard/mouse emulation:
Firmware:
- bt_hid.c/h: HID device driver with combo keyboard/mouse HID descriptor
- cmd_hid_{enable,disable,connect,disconnect}: HID lifecycle management
- cmd_hid_send_keyboard: Send keyboard reports (modifier + up to 6 keys)
- cmd_hid_send_mouse: Send mouse reports (buttons + relative X/Y)
- cmd_hid_status: Query HID state (enabled, registered, connected)
Python MCP tools:
- esp32_hid_enable/disable: Control HID device mode
- esp32_hid_connect/disconnect: Manage HID host connections
- esp32_hid_send_keyboard/send_mouse: Send HID reports
- esp32_hid_status: Get connection state
Config:
- Enable BT_HID_ENABLED + BT_HID_DEVICE_ENABLED in sdkconfig.defaults
- Add bt_hid.c to CMakeLists.txt
Tested E2E: Linux (hci1) connects to ESP32 HID device, keyboard and
mouse reports sent successfully.
242 lines
8.4 KiB
Markdown
242 lines
8.4 KiB
Markdown
# mcbluetooth-esp32
|
|
|
|
ESP32 Bluetooth test harness MCP server — UART-controlled peripheral for automated E2E Bluetooth testing.
|
|
|
|
## Overview
|
|
|
|
This project turns an ESP32 into a programmable Bluetooth peripheral that can be controlled via MCP (Model Context Protocol). Combined with [mcbluetooth](https://github.com/supported-systems/mcbluetooth) (Linux BlueZ MCP server), it enables fully automated end-to-end Bluetooth testing orchestrated by an LLM.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ LLM (Claude, etc.) │
|
|
│ Orchestrates both MCP servers │
|
|
└─────────────────────────┬───────────────────────────────────────┘
|
|
│
|
|
┌───────────────┴───────────────┐
|
|
│ │
|
|
┌───────┴───────┐ ┌───────┴────────┐
|
|
│ mcbluetooth │ │mcbluetooth-esp32│
|
|
│ (bt_* tools)│ │ (esp32_* tools) │
|
|
└───────┬───────┘ └───────┬────────┘
|
|
│ │
|
|
D-Bus/BlueZ Serial/UART
|
|
│ │
|
|
┌───────┴───────┐ ┌───────┴────────┐
|
|
│ Linux Host │◄── Bluetooth ──►│ ESP32 │
|
|
│ (hci0/1) │ (over air) │ (peripheral) │
|
|
└───────────────┘ └────────────────┘
|
|
```
|
|
|
|
## ESP32 Capabilities
|
|
|
|
The ESP32 can emulate various Bluetooth devices for testing:
|
|
|
|
| Capability | Description |
|
|
|------------|-------------|
|
|
| **Classic BT Pairing** | All 4 SSP modes: Just Works, Numeric Comparison, Passkey Entry, Legacy PIN |
|
|
| **SPP (Serial Port)** | Bidirectional data transfer over Classic Bluetooth virtual serial port |
|
|
| **HID Device** | Keyboard/mouse combo emulation over Classic Bluetooth |
|
|
| **BLE GATT Server** | Dynamic service/characteristic creation at runtime |
|
|
| **Device Personas** | Presets for headset, speaker, keyboard, sensor, phone |
|
|
| **IO Capabilities** | Configurable: `no_io`, `display_only`, `display_yesno`, `keyboard_only`, `keyboard_display` |
|
|
| **Event Reporting** | Real-time events: pair_request, pair_complete, spp_data, hid_connect, gatt_read, gatt_write |
|
|
|
|
### Hardware Requirements
|
|
|
|
- **Original ESP32** (ESP32-D0WD or similar) — must have Classic Bluetooth support
|
|
- ESP32-S3, C3, H2, S2 will NOT work (no BR/EDR radio)
|
|
- USB cable for serial communication
|
|
|
|
### Verified Hardware
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Chip | ESP32-D0WD-V3 (rev 3.1) |
|
|
| Features | Wi-Fi, BT Classic + BLE, Dual Core 240MHz |
|
|
| Flash | 4MB |
|
|
| Protocol | NDJSON over UART @ 115200 baud |
|
|
|
|
## Quick Start
|
|
|
|
### 1. Flash the firmware
|
|
|
|
```bash
|
|
# Install ESP-IDF v5.x first (see docs/hardware-setup.md)
|
|
cd firmware
|
|
idf.py set-target esp32
|
|
idf.py -p /dev/ttyUSB0 flash
|
|
```
|
|
|
|
### 2. Run the MCP server
|
|
|
|
```bash
|
|
# Install and run
|
|
uvx mcbluetooth-esp32
|
|
|
|
# Or with explicit port
|
|
ESP32_SERIAL_PORT=/dev/ttyUSB0 uvx mcbluetooth-esp32
|
|
```
|
|
|
|
### 3. Add to Claude Code
|
|
|
|
```bash
|
|
claude mcp add mcbluetooth-esp32 -- uvx mcbluetooth-esp32
|
|
```
|
|
|
|
### 4. Verify connection
|
|
|
|
```python
|
|
esp32_connect() # → connected=true, ready=true
|
|
esp32_ping() # → {pong: true}
|
|
esp32_get_info() # → chip, fw_version, bt_mac
|
|
```
|
|
|
|
## MCP Tools
|
|
|
|
### Connection & System
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_connect` | Open serial connection to ESP32 |
|
|
| `esp32_disconnect` | Close serial connection |
|
|
| `esp32_ping` | Verify UART link is alive |
|
|
| `esp32_status` | Get current BT/BLE state |
|
|
| `esp32_get_info` | Get chip model, firmware version, MAC |
|
|
| `esp32_reset` | Reboot the ESP32 |
|
|
|
|
### Configuration
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_configure` | Set device name, IO capabilities, PIN |
|
|
| `esp32_set_ssp_mode` | Configure SSP pairing mode with optional auto-accept |
|
|
| `esp32_load_persona` | Load preset device profile (headset, speaker, etc.) |
|
|
|
|
### Classic Bluetooth
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_classic_enable` | Enable BR/EDR radio |
|
|
| `esp32_classic_disable` | Disable BR/EDR radio |
|
|
| `esp32_classic_set_discoverable` | Make device visible for pairing |
|
|
| `esp32_classic_pair_respond` | Accept/reject incoming pairing |
|
|
|
|
### SPP (Serial Port Profile)
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_spp_send` | Send data (text or hex) over SPP connection |
|
|
| `esp32_spp_disconnect` | Close the active SPP connection |
|
|
| `esp32_spp_status` | Get SPP connection status and remote address |
|
|
|
|
### HID (Human Interface Device)
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_hid_enable` | Enable HID device (keyboard/mouse combo) |
|
|
| `esp32_hid_disable` | Disable HID device |
|
|
| `esp32_hid_connect` | Connect to a specific HID host |
|
|
| `esp32_hid_disconnect` | Disconnect from current host |
|
|
| `esp32_hid_send_keyboard` | Send keyboard report (keys + modifiers) |
|
|
| `esp32_hid_send_mouse` | Send mouse report (movement + buttons) |
|
|
| `esp32_hid_status` | Get HID connection status |
|
|
|
|
### BLE / GATT
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_ble_enable` | Enable BLE radio |
|
|
| `esp32_ble_disable` | Disable BLE radio |
|
|
| `esp32_ble_advertise` | Start/stop BLE advertising |
|
|
| `esp32_ble_set_adv_data` | Configure advertisement data |
|
|
| `esp32_gatt_add_service` | Create GATT service |
|
|
| `esp32_gatt_add_characteristic` | Add characteristic to service |
|
|
| `esp32_gatt_set_value` | Set characteristic value |
|
|
| `esp32_gatt_notify` | Send notification to subscribers |
|
|
| `esp32_gatt_clear` | Remove all GATT services |
|
|
|
|
### Events
|
|
| Tool | Description |
|
|
|------|-------------|
|
|
| `esp32_get_events` | Retrieve event history |
|
|
| `esp32_wait_event` | Block until specific event occurs |
|
|
| `esp32_clear_events` | Clear event history |
|
|
|
|
## E2E Testing
|
|
|
|
Run automated tests using Claude CLI in headless mode:
|
|
|
|
```bash
|
|
# Setup test environment
|
|
mkdir -p /tmp/bt-e2e-test && cd /tmp/bt-e2e-test
|
|
git init
|
|
|
|
# Create MCP config with both servers
|
|
cat > .mcp.json << 'EOF'
|
|
{
|
|
"mcpServers": {
|
|
"esp32": {
|
|
"type": "stdio",
|
|
"command": "uvx",
|
|
"args": ["mcbluetooth-esp32"],
|
|
"env": {"ESP32_SERIAL_PORT": "/dev/ttyUSB0"}
|
|
},
|
|
"bluez": {
|
|
"type": "stdio",
|
|
"command": "uvx",
|
|
"args": ["mcbluetooth"]
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
# Run test suite
|
|
PROMPT=$(cat tests/prompts/test-prompt-v5.md)
|
|
claude -p "$PROMPT" \
|
|
--mcp-config .mcp.json \
|
|
--allowedTools "mcp__esp32__*,mcp__bluez__*" \
|
|
--output-format json
|
|
```
|
|
|
|
### Test Coverage (v5 - 76 tests)
|
|
|
|
| Phase | Tests | Coverage |
|
|
|-------|-------|----------|
|
|
| ESP32 Connection | 1-4 | connect, ping, info, status |
|
|
| BlueZ Adapter | 5-8 | list, info, pairable, discoverable |
|
|
| Classic BT + SSP | 9-24 | Full pairing workflow with auto-accept |
|
|
| BLE GATT Setup | 30-42 | Battery Service, Environmental Sensing |
|
|
| HCI Capture | 43-55 | btsnoop capture and analysis |
|
|
| GATT Operations | 56-63 | read, write, notify, subscribe |
|
|
| Cleanup | 64-76 | Adapter management, reset |
|
|
|
|
### Recent Test Results
|
|
|
|
| Version | Tests | Result | Duration |
|
|
|---------|-------|--------|----------|
|
|
| v4 | 71 | 66 PASS, 5 PARTIAL | 8.6 min |
|
|
| v5 | 76 | **76/76 PASS (100%)** | 7.6 min |
|
|
|
|
## Documentation
|
|
|
|
| Document | Description |
|
|
|----------|-------------|
|
|
| [hardware-setup.md](docs/hardware-setup.md) | ESP-IDF installation, flashing, wiring |
|
|
| [protocol-spec.md](docs/protocol-spec.md) | NDJSON UART protocol specification |
|
|
| [test-scenarios.md](docs/test-scenarios.md) | Manual E2E test procedures |
|
|
| [automated-e2e-testing.md](docs/automated-e2e-testing.md) | Claude CLI headless testing guide |
|
|
|
|
## Development
|
|
|
|
```bash
|
|
# Install dev dependencies
|
|
uv sync --all-extras
|
|
|
|
# Run unit tests
|
|
uv run pytest tests/ -v --ignore=tests/integration
|
|
|
|
# Run integration tests (requires ESP32)
|
|
ESP32_SERIAL_PORT=/dev/ttyUSB0 uv run pytest tests/integration/ -v
|
|
|
|
# Lint
|
|
uv run ruff check src/
|
|
```
|
|
|
|
## License
|
|
|
|
MIT
|