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)
264 lines
6.9 KiB
Markdown
264 lines
6.9 KiB
Markdown
# E2E Test Scenarios
|
|
|
|
## Overview
|
|
|
|
Tests require both MCP servers running simultaneously:
|
|
- **mcbluetooth** — controls the Linux BlueZ stack (host side)
|
|
- **mcbluetooth-esp32** — controls the ESP32 peripheral (device side)
|
|
|
|
An LLM orchestrates both servers, issuing tool calls to each side to execute the test flow.
|
|
|
|
## Prerequisites
|
|
|
|
```bash
|
|
# Terminal 1: Start ESP32 MCP server
|
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 uvx mcbluetooth-esp32
|
|
|
|
# Terminal 2: mcbluetooth is already running (or add to Claude Code config)
|
|
```
|
|
|
|
---
|
|
|
|
## Test 1: SSP Just Works
|
|
|
|
Both devices have `no_io` capability — pairing auto-completes without user interaction.
|
|
|
|
### Flow
|
|
|
|
```python
|
|
# ESP32 side: Configure as headset (no_io)
|
|
esp32_load_persona("headset") # io_cap=no_io → Just Works
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
# Linux side: Discover and pair
|
|
bt_scan(adapter="hci0", mode="classic", timeout=10)
|
|
# → Find "BT Headset" in scan results
|
|
|
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="auto")
|
|
# Just Works: auto-accepts on both sides
|
|
|
|
# Verify
|
|
esp32_wait_event("pair_complete", timeout=15)
|
|
# → {"address": "XX:XX:XX:XX:XX:XX", "success": true}
|
|
|
|
bt_device_info(adapter="hci0", address="D8:13:2A:7F:47:C0")
|
|
# → paired: true
|
|
```
|
|
|
|
### Expected Result
|
|
- Pairing completes without any passkey exchange
|
|
- Both sides report success
|
|
|
|
---
|
|
|
|
## Test 2: SSP Numeric Comparison
|
|
|
|
Both devices have `display_yesno` — both display a 6-digit passkey that must match.
|
|
|
|
### Flow
|
|
|
|
```python
|
|
# ESP32 side: Configure as phone (keyboard_display → numeric comparison)
|
|
esp32_load_persona("phone")
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
# Linux side: Scan and initiate pairing
|
|
bt_scan(adapter="hci0", mode="classic")
|
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
|
|
|
# Both sides receive the passkey
|
|
esp32_wait_event("pair_request", timeout=15)
|
|
# → {"type": "numeric_comparison", "passkey": 123456, "address": "..."}
|
|
|
|
bt_pairing_status()
|
|
# → passkey: 123456 (should match ESP32's passkey!)
|
|
|
|
# Both sides confirm
|
|
esp32_classic_pair_respond(address="XX:XX:XX:XX:XX:XX", accept=True)
|
|
bt_pair_confirm(adapter="hci0", address="D8:13:2A:7F:47:C0", accept=True)
|
|
|
|
# Verify
|
|
esp32_wait_event("pair_complete")
|
|
# → {"success": true}
|
|
```
|
|
|
|
### Expected Result
|
|
- Both sides display the same 6-digit passkey
|
|
- After both confirm, pairing succeeds
|
|
|
|
---
|
|
|
|
## Test 3: SSP Passkey Entry
|
|
|
|
One side displays a passkey, the other must enter it.
|
|
|
|
### Flow (ESP32 displays, Linux enters)
|
|
|
|
```python
|
|
# ESP32: display_only → shows passkey
|
|
esp32_configure(io_cap="display_only")
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
# Linux: Initiate pairing
|
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
|
|
|
# ESP32 displays passkey
|
|
esp32_wait_event("pair_request")
|
|
# → {"type": "passkey_display", "passkey": 654321}
|
|
|
|
# Linux enters the passkey
|
|
bt_pair_confirm(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
passkey=654321, accept=True)
|
|
|
|
# Verify
|
|
esp32_wait_event("pair_complete")
|
|
# → {"success": true}
|
|
```
|
|
|
|
### Flow (Linux displays, ESP32 enters)
|
|
|
|
```python
|
|
# ESP32: keyboard_only → must enter passkey
|
|
esp32_configure(io_cap="keyboard_only")
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
# Linux initiates pairing — Linux displays passkey
|
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
|
|
|
bt_pairing_status()
|
|
# → passkey: 789012 (displayed on Linux)
|
|
|
|
# ESP32 receives passkey entry request
|
|
esp32_wait_event("pair_request")
|
|
# → {"type": "passkey_entry"}
|
|
|
|
# ESP32 enters the passkey shown on Linux
|
|
esp32_classic_pair_respond(address="XX:XX:XX:XX:XX:XX", accept=True, passkey=789012)
|
|
|
|
# Verify
|
|
esp32_wait_event("pair_complete")
|
|
```
|
|
|
|
---
|
|
|
|
## Test 4: Legacy PIN
|
|
|
|
ESP32 configured with a legacy PIN code.
|
|
|
|
```python
|
|
esp32_configure(pin_code="1234")
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
|
|
|
# Linux enters PIN
|
|
bt_pair_confirm(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
pin="1234", accept=True)
|
|
```
|
|
|
|
---
|
|
|
|
## Test 5: BLE GATT Read/Write
|
|
|
|
ESP32 creates an Environmental Sensing service with a Temperature characteristic.
|
|
|
|
```python
|
|
# ESP32: Set up as sensor
|
|
esp32_load_persona("sensor")
|
|
esp32_gatt_add_service(uuid="0000181a-0000-1000-8000-00805f9b34fb", primary=True)
|
|
# → {"service_handle": 40}
|
|
|
|
esp32_gatt_add_characteristic(
|
|
service_handle=40,
|
|
uuid="00002a6e-0000-1000-8000-00805f9b34fb", # Temperature
|
|
properties=["read", "notify"],
|
|
value="e803" # 25.0°C (0x03e8 = 1000 in little-endian → 10.00°C? or raw)
|
|
)
|
|
# → {"char_handle": 42}
|
|
|
|
esp32_ble_advertise(enable=True)
|
|
|
|
# Linux: Scan, connect, read
|
|
bt_ble_scan(adapter="hci0", timeout=5)
|
|
bt_connect(adapter="hci0", address="D8:13:2A:7F:47:C0")
|
|
bt_ble_read(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
char_uuid="00002a6e-0000-1000-8000-00805f9b34fb")
|
|
# → {"hex": "e803", ...}
|
|
|
|
# ESP32: Update value
|
|
esp32_gatt_set_value(char_handle=42, value="f003") # 25.5°C
|
|
```
|
|
|
|
---
|
|
|
|
## Test 6: BLE GATT Notifications
|
|
|
|
```python
|
|
# Linux: Subscribe to temperature notifications
|
|
bt_ble_notify(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
char_uuid="00002a6e-0000-1000-8000-00805f9b34fb",
|
|
enable=True)
|
|
|
|
# ESP32: Verify subscription
|
|
esp32_wait_event("gatt_subscribe")
|
|
# → {"char_handle": 42, "subscribed": true}
|
|
|
|
# ESP32: Update and notify
|
|
esp32_gatt_set_value(char_handle=42, value="0004")
|
|
esp32_gatt_notify(char_handle=42)
|
|
|
|
# Linux should receive the updated value
|
|
bt_ble_read(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
char_uuid="00002a6e-0000-1000-8000-00805f9b34fb")
|
|
# → {"hex": "0004"}
|
|
```
|
|
|
|
---
|
|
|
|
## Test 7: Persona Switching
|
|
|
|
Verify device identity changes are visible from the Linux side.
|
|
|
|
```python
|
|
# Load different personas and scan each time
|
|
for persona in ["headset", "speaker", "keyboard", "sensor", "phone", "bare"]:
|
|
esp32_load_persona(persona)
|
|
esp32_classic_enable()
|
|
esp32_classic_set_discoverable(True)
|
|
|
|
bt_scan(adapter="hci0", mode="both", timeout=5)
|
|
# Verify device name and class match persona definition
|
|
|
|
esp32_classic_disable()
|
|
```
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# Unit tests only (no hardware needed)
|
|
make test-unit
|
|
|
|
# Integration tests (requires ESP32 on /dev/ttyUSB4)
|
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 make test-integration
|
|
|
|
# Full suite
|
|
make test
|
|
```
|
|
|
|
## Test Matrix
|
|
|
|
| Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | Auto? |
|
|
|------|----------|--------------|--------------|-------|
|
|
| Just Works | NoInputNoOutput | no_io | no_io | Yes |
|
|
| Numeric Comparison | NumericComparison | keyboard_display | display_yesno | No (confirm) |
|
|
| Passkey Entry (ESP32 displays) | PasskeyEntry | display_only | keyboard_only | No (enter) |
|
|
| Passkey Entry (Linux displays) | PasskeyEntry | keyboard_only | display_only | No (enter) |
|
|
| Legacy PIN | LegacyPIN | n/a | n/a | No (PIN) |
|