Expand test scenarios with step-by-step procedures
Rewrite test-scenarios.md with detailed per-step instructions including exact tool calls, expected responses, negative test cases, teardown procedures, and an environment variable reference.
This commit is contained in:
parent
6398a5223a
commit
73d3d438a2
@ -1,263 +1,655 @@
|
|||||||
# E2E Test Scenarios
|
# End-to-End Test Scenarios
|
||||||
|
|
||||||
|
Tests that exercise the full Bluetooth stack across two devices: a Linux host running `mcbluetooth` (BlueZ) and an ESP32 running the `mcbluetooth-esp32` firmware.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Tests require both MCP servers running simultaneously:
|
Each test requires **two 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.
|
| Server | Controls | Tool prefix |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| `mcbluetooth` | Linux BlueZ stack (hci0) | `bt_*` |
|
||||||
|
| `mcbluetooth-esp32` | ESP32 peripheral via UART | `esp32_*` |
|
||||||
|
|
||||||
## Prerequisites
|
The LLM orchestrates both sides, acting as the test conductor. It issues commands to one side, observes events on the other, and verifies that the protocol exchange matches expectations.
|
||||||
|
|
||||||
```bash
|
### Prerequisites
|
||||||
# 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)
|
- ESP32 connected and reachable at `ESP32_SERIAL_PORT` (default `/dev/ttyUSB4`)
|
||||||
```
|
- BlueZ adapter powered on (`bt_adapter_power(adapter="hci0", on=true)`)
|
||||||
|
- Both MCP servers registered in the Claude Code session
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 1: SSP Just Works
|
## Test 1: SSP Just Works
|
||||||
|
|
||||||
Both devices have `no_io` capability — pairing auto-completes without user interaction.
|
Both devices have `no_io` capability. Pairing should auto-complete with no user interaction.
|
||||||
|
|
||||||
### Flow
|
### Setup
|
||||||
|
|
||||||
```python
|
**Step 1 -- Connect to the ESP32 and configure it as a no-IO device:**
|
||||||
# ESP32 side: Configure as headset (no_io)
|
|
||||||
esp32_load_persona("headset") # io_cap=no_io → Just Works
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_configure(name="JustWorks-Test", io_cap="no_io")
|
||||||
esp32_classic_enable()
|
esp32_classic_enable()
|
||||||
esp32_classic_set_discoverable(True)
|
esp32_classic_set_discoverable(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
|
**Step 2 -- Power on the Linux adapter:**
|
||||||
- Pairing completes without any passkey exchange
|
|
||||||
- Both sides report success
|
```
|
||||||
|
bt_adapter_power(adapter="hci0", on=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute
|
||||||
|
|
||||||
|
**Step 3 -- Scan from Linux to discover the ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify that a device named `"JustWorks-Test"` appears in the scan results. Note its Bluetooth address (e.g., `"AA:BB:CC:DD:EE:FF"`).
|
||||||
|
|
||||||
|
**Step 4 -- Initiate pairing from Linux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="auto")
|
||||||
|
```
|
||||||
|
|
||||||
|
Since both sides have `no_io`, SSP Just Works is negotiated. No passkey exchange occurs.
|
||||||
|
|
||||||
|
**Step 5 -- Wait for the pair_request event on the ESP32 side:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_request", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"event":"pair_request","data":{"address":"...","type":"just_works","passkey":0},...}`
|
||||||
|
|
||||||
|
**Step 6 -- Accept the pairing on the ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 7 -- Verify pairing completed on both sides:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_complete", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"event":"pair_complete","data":{"address":"AA:BB:CC:DD:EE:FF","success":true},...}`
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_list_devices(adapter="hci0", filter="paired")
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the ESP32 address appears in the paired devices list.
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_unpair(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
esp32_classic_set_discoverable(discoverable=false)
|
||||||
|
esp32_classic_disable()
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 2: SSP Numeric Comparison
|
## Test 2: SSP Numeric Comparison
|
||||||
|
|
||||||
Both devices have `display_yesno` — both display a 6-digit passkey that must match.
|
Both devices have `display_yesno` capability. Both display a 6-digit passkey that the user (or LLM) must confirm matches.
|
||||||
|
|
||||||
### Flow
|
### Setup
|
||||||
|
|
||||||
```python
|
**Step 1 -- Configure the ESP32 with display+yesno:**
|
||||||
# ESP32 side: Configure as phone (keyboard_display → numeric comparison)
|
|
||||||
esp32_load_persona("phone")
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_configure(name="NumComp-Test", io_cap="display_yesno")
|
||||||
esp32_classic_enable()
|
esp32_classic_enable()
|
||||||
esp32_classic_set_discoverable(True)
|
esp32_classic_set_discoverable(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
|
**Step 2 -- Ensure Linux adapter is powered:**
|
||||||
- Both sides display the same 6-digit passkey
|
|
||||||
- After both confirm, pairing succeeds
|
```
|
||||||
|
bt_adapter_power(adapter="hci0", on=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute
|
||||||
|
|
||||||
|
**Step 3 -- Scan and discover:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
```
|
||||||
|
|
||||||
|
Locate `"NumComp-Test"` in results.
|
||||||
|
|
||||||
|
**Step 4 -- Initiate pairing from Linux in interactive mode:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns a pairing status indicating it is awaiting confirmation with a passkey.
|
||||||
|
|
||||||
|
**Step 5 -- Capture the passkey from the ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_request", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"event":"pair_request","data":{"address":"...","type":"numeric_comparison","passkey":482901},...}`
|
||||||
|
|
||||||
|
Note the `passkey` value (e.g., `482901`).
|
||||||
|
|
||||||
|
**Step 6 -- Verify the passkeys match and confirm on both sides:**
|
||||||
|
|
||||||
|
Check that the passkey from the Linux pairing status matches the ESP32 event.
|
||||||
|
|
||||||
|
Accept on ESP32:
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true, passkey=482901)
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm on Linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair_confirm(adapter="hci0", address="AA:BB:CC:DD:EE:FF", accept=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 7 -- Verify completion:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_complete", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"data":{"address":"AA:BB:CC:DD:EE:FF","success":true}}`
|
||||||
|
|
||||||
|
### Negative case
|
||||||
|
|
||||||
|
Repeat the flow but provide mismatched confirmation: accept on one side, reject on the other. Verify `pair_complete` reports `"success":false`.
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_unpair(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
esp32_classic_set_discoverable(discoverable=false)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 3: SSP Passkey Entry
|
## Test 3: SSP Passkey Entry
|
||||||
|
|
||||||
One side displays a passkey, the other must enter it.
|
One side displays a passkey, the other must enter it. Test both directions.
|
||||||
|
|
||||||
### Flow (ESP32 displays, Linux enters)
|
### Direction A: ESP32 displays, Linux enters
|
||||||
|
|
||||||
```python
|
**Setup:**
|
||||||
# ESP32: display_only → shows passkey
|
|
||||||
esp32_configure(io_cap="display_only")
|
```
|
||||||
|
esp32_configure(name="PasskeyDisp-Test", io_cap="display_only")
|
||||||
esp32_classic_enable()
|
esp32_classic_enable()
|
||||||
esp32_classic_set_discoverable(True)
|
esp32_classic_set_discoverable(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)
|
**Execute:**
|
||||||
|
|
||||||
```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")
|
|
||||||
```
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
||||||
|
```
|
||||||
|
|
||||||
|
The ESP32 receives a `pair_request` event with `"type":"passkey_entry"` and a `passkey` value.
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_request", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the passkey (e.g., `731205`). Then send it from the Linux side:
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair_confirm(adapter="hci0", address="AA:BB:CC:DD:EE:FF", passkey=731205, accept=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accept on ESP32:
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `pair_complete` succeeds.
|
||||||
|
|
||||||
|
### Direction B: Linux displays, ESP32 enters
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_configure(name="PasskeyEntry-Test", io_cap="keyboard_only")
|
||||||
|
esp32_classic_enable()
|
||||||
|
esp32_classic_set_discoverable(discoverable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Execute:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
||||||
|
```
|
||||||
|
|
||||||
|
The Linux side generates a passkey for the ESP32 to enter. The ESP32 receives a `pair_request` event. Forward the passkey from the Linux pairing status to the ESP32 response:
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_request", timeout=10)
|
||||||
|
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true, passkey=<passkey_from_linux>)
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify `pair_complete` succeeds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 4: Legacy PIN
|
## Test 4: Legacy PIN
|
||||||
|
|
||||||
ESP32 configured with a legacy PIN code.
|
ESP32 configured with a PIN code. Linux initiates pairing with that PIN. This tests pre-SSP pairing mode.
|
||||||
|
|
||||||
```python
|
### Setup
|
||||||
esp32_configure(pin_code="1234")
|
|
||||||
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_configure(name="LegacyPIN-Test", io_cap="no_io", pin_code="1234")
|
||||||
esp32_classic_enable()
|
esp32_classic_enable()
|
||||||
esp32_classic_set_discoverable(True)
|
esp32_classic_set_discoverable(discoverable=true)
|
||||||
|
```
|
||||||
|
|
||||||
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
### Execute
|
||||||
|
|
||||||
# Linux enters PIN
|
**Step 1 -- Scan and pair from Linux with the PIN:**
|
||||||
bt_pair_confirm(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
||||||
pin="1234", accept=True)
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2 -- Handle the pair request on ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_request", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `"type":"legacy_pin"`
|
||||||
|
|
||||||
|
**Step 3 -- Respond with the PIN on the ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true, pin="1234")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4 -- Provide the PIN from Linux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_pair_confirm(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pin="1234", accept=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5 -- Verify:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="pair_complete", timeout=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Negative case
|
||||||
|
|
||||||
|
Provide a wrong PIN from the Linux side (e.g., `"0000"`). Verify pairing fails with `"success":false`.
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_unpair(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 5: BLE GATT Read/Write
|
## Test 5: BLE GATT Read/Write
|
||||||
|
|
||||||
ESP32 creates an Environmental Sensing service with a Temperature characteristic.
|
ESP32 creates an Environmental Sensing service with a Temperature characteristic. Linux reads the value and verifies correctness. ESP32 updates the value and Linux reads again.
|
||||||
|
|
||||||
```python
|
### Setup
|
||||||
# ESP32: Set up as sensor
|
|
||||||
esp32_load_persona("sensor")
|
|
||||||
esp32_gatt_add_service(uuid="0000181a-0000-1000-8000-00805f9b34fb", primary=True)
|
|
||||||
# → {"service_handle": 40}
|
|
||||||
|
|
||||||
|
**Step 1 -- Prepare the ESP32 as a BLE sensor:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_configure(name="Temp-Sensor", io_cap="no_io")
|
||||||
|
esp32_ble_enable()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2 -- Create the GATT service and characteristic:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_gatt_add_service(uuid="181A", primary=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns: `{"handle": 40}` (example handle)
|
||||||
|
|
||||||
|
```
|
||||||
esp32_gatt_add_characteristic(
|
esp32_gatt_add_characteristic(
|
||||||
service_handle=40,
|
service_handle=40,
|
||||||
uuid="00002a6e-0000-1000-8000-00805f9b34fb", # Temperature
|
uuid="2A6E",
|
||||||
properties=["read", "notify"],
|
properties=["read", "notify"],
|
||||||
value="e803" # 25.0°C (0x03e8 = 1000 in little-endian → 10.00°C? or raw)
|
value="c409"
|
||||||
)
|
)
|
||||||
# → {"char_handle": 42}
|
```
|
||||||
|
|
||||||
esp32_ble_advertise(enable=True)
|
Returns: `{"handle": 42}` (example handle)
|
||||||
|
|
||||||
# Linux: Scan, connect, read
|
The value `"c409"` encodes 25.00 C as a little-endian `int16` in hundredths of a degree (2500 = 0x09C4, LE = `c4 09`).
|
||||||
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
|
**Step 3 -- Start advertising:**
|
||||||
esp32_gatt_set_value(char_handle=42, value="f003") # 25.5°C
|
|
||||||
|
```
|
||||||
|
esp32_ble_set_adv_data(name="Temp-Sensor", service_uuids=["181A"])
|
||||||
|
esp32_ble_advertise(enable=true, interval_ms=100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
**Step 4 -- Scan from Linux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_scan(adapter="hci0", timeout=10, name_filter="Temp-Sensor")
|
||||||
|
```
|
||||||
|
|
||||||
|
Locate the device and note its address.
|
||||||
|
|
||||||
|
**Step 5 -- Connect and read the characteristic:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_connect(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
bt_ble_read(adapter="hci0", address="AA:BB:CC:DD:EE:FF", char_uuid="2A6E")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: hex value `"c409"`, decoded as temperature 25.00 C.
|
||||||
|
|
||||||
|
**Step 6 -- Verify the ESP32 received a GATT read event:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_get_events(event_name="gatt_read")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: an event with `{"handle":42,"address":"..."}`.
|
||||||
|
|
||||||
|
### Write (update from ESP32, re-read from Linux)
|
||||||
|
|
||||||
|
**Step 7 -- Update the value on the ESP32:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_gatt_set_value(char_handle=42, value="d007")
|
||||||
|
```
|
||||||
|
|
||||||
|
The value `"d007"` encodes 20.00 C (2000 = 0x07D0, LE = `d0 07`).
|
||||||
|
|
||||||
|
**Step 8 -- Re-read from Linux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_read(adapter="hci0", address="AA:BB:CC:DD:EE:FF", char_uuid="2A6E")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `"d007"` (20.00 C).
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_disconnect(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
esp32_ble_advertise(enable=false)
|
||||||
|
esp32_gatt_clear()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 6: BLE GATT Notifications
|
## Test 6: BLE GATT Subscribe
|
||||||
|
|
||||||
```python
|
Test notification subscription and delivery. The ESP32 pushes a value update to a subscribed Linux client.
|
||||||
# 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
|
### Setup
|
||||||
esp32_wait_event("gatt_subscribe")
|
|
||||||
# → {"char_handle": 42, "subscribed": true}
|
|
||||||
|
|
||||||
# ESP32: Update and notify
|
Use the same GATT service structure from Test 5 (Environmental Sensing, Temperature characteristic with `notify` property).
|
||||||
esp32_gatt_set_value(char_handle=42, value="0004")
|
|
||||||
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_ble_enable()
|
||||||
|
esp32_gatt_add_service(uuid="181A", primary=true)
|
||||||
|
# -> handle: 40
|
||||||
|
esp32_gatt_add_characteristic(
|
||||||
|
service_handle=40,
|
||||||
|
uuid="2A6E",
|
||||||
|
properties=["read", "notify"],
|
||||||
|
value="c409"
|
||||||
|
)
|
||||||
|
# -> handle: 42
|
||||||
|
esp32_ble_set_adv_data(name="Notify-Sensor", service_uuids=["181A"])
|
||||||
|
esp32_ble_advertise(enable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execute
|
||||||
|
|
||||||
|
**Step 1 -- Connect from Linux and subscribe to notifications:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_scan(adapter="hci0", timeout=10, name_filter="Notify-Sensor")
|
||||||
|
bt_connect(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
bt_ble_notify(adapter="hci0", address="AA:BB:CC:DD:EE:FF", char_uuid="2A6E", enable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2 -- Verify the ESP32 received a subscribe event:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_wait_event(event_name="gatt_subscribe", timeout=5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"handle":42,"subscribed":true}`
|
||||||
|
|
||||||
|
**Step 3 -- Update the value on the ESP32 and send a notification:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_gatt_set_value(char_handle=42, value="0c0a")
|
||||||
esp32_gatt_notify(char_handle=42)
|
esp32_gatt_notify(char_handle=42)
|
||||||
|
```
|
||||||
|
|
||||||
# Linux should receive the updated value
|
The value `"0c0a"` encodes 25.72 C (2572 = 0x0A0C, LE = `0c 0a`).
|
||||||
bt_ble_read(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
|
||||||
char_uuid="00002a6e-0000-1000-8000-00805f9b34fb")
|
**Step 4 -- Verify the Linux client received the notification:**
|
||||||
# → {"hex": "0004"}
|
|
||||||
|
The BLE notification should arrive as an updated characteristic value on the Linux side. Read the cached value:
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_read(adapter="hci0", address="AA:BB:CC:DD:EE:FF", char_uuid="2A6E")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `"0c0a"`.
|
||||||
|
|
||||||
|
**Step 5 -- Unsubscribe:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_notify(adapter="hci0", address="AA:BB:CC:DD:EE:FF", char_uuid="2A6E", enable=false)
|
||||||
|
esp32_wait_event(event_name="gatt_subscribe", timeout=5)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `{"handle":42,"subscribed":false}`
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_disconnect(adapter="hci0", address="AA:BB:CC:DD:EE:FF")
|
||||||
|
esp32_ble_advertise(enable=false)
|
||||||
|
esp32_gatt_clear()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 7: Persona Switching
|
## Test 7: Persona Switching
|
||||||
|
|
||||||
Verify device identity changes are visible from the Linux side.
|
Load different device personas on the ESP32 and verify that the device name and Bluetooth class are visible from a Linux scan.
|
||||||
|
|
||||||
```python
|
### Execute
|
||||||
# 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)
|
**Step 1 -- List available personas:**
|
||||||
# Verify device name and class match persona definition
|
|
||||||
|
|
||||||
esp32_classic_disable()
|
```
|
||||||
|
esp32_connect(port="/dev/ttyUSB4")
|
||||||
|
esp32_list_personas()
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the response contains all six personas: `headset`, `speaker`, `keyboard`, `sensor`, `phone`, `bare`.
|
||||||
|
|
||||||
|
**Step 2 -- Load the headset persona:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_load_persona(persona="headset")
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"persona": "headset",
|
||||||
|
"device_name": "BT Headset",
|
||||||
|
"io_cap": "no_io",
|
||||||
|
"classic": true,
|
||||||
|
"ble": true,
|
||||||
|
"device_class": "0x200404",
|
||||||
|
"services": ["0000180f-...", "0000180a-..."]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3 -- Enable Classic BT and make discoverable:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_enable()
|
||||||
|
esp32_classic_set_discoverable(discoverable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4 -- Scan from Linux and verify the device appears as "BT Headset":**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that the scan results include a device named `"BT Headset"`.
|
||||||
|
|
||||||
|
**Step 5 -- Switch to the keyboard persona:**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_load_persona(persona="keyboard")
|
||||||
|
esp32_classic_set_discoverable(discoverable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6 -- Scan again and verify the name changed:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that the device now appears as `"BT Keyboard"`.
|
||||||
|
|
||||||
|
**Step 7 -- Load the sensor persona (BLE only):**
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_classic_disable()
|
||||||
|
esp32_load_persona(persona="sensor")
|
||||||
|
esp32_ble_enable()
|
||||||
|
esp32_ble_set_adv_data(name="Environment Sensor", service_uuids=["181A"])
|
||||||
|
esp32_ble_advertise(enable=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 8 -- Scan via BLE from Linux:**
|
||||||
|
|
||||||
|
```
|
||||||
|
bt_ble_scan(adapter="hci0", timeout=10, name_filter="Environment Sensor")
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the device appears in the BLE scan results.
|
||||||
|
|
||||||
|
### Teardown
|
||||||
|
|
||||||
|
```
|
||||||
|
esp32_ble_advertise(enable=false)
|
||||||
|
esp32_ble_disable()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
|
### Unit tests (no hardware required)
|
||||||
|
|
||||||
|
Unit tests exercise the Python protocol layer, event queue, and MCP tool registration using mock serial connections.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Unit tests only (no hardware needed)
|
|
||||||
make test-unit
|
make test-unit
|
||||||
|
|
||||||
# Integration tests (requires ESP32 on /dev/ttyUSB4)
|
|
||||||
ESP32_SERIAL_PORT=/dev/ttyUSB4 make test-integration
|
|
||||||
|
|
||||||
# Full suite
|
|
||||||
make test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/ -v --ignore=tests/integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration tests (requires ESP32 on serial port)
|
||||||
|
|
||||||
|
Integration tests require a physical ESP32 connected and flashed with the firmware.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 uv run pytest tests/integration/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full E2E tests (requires both MCP servers)
|
||||||
|
|
||||||
|
The test scenarios in this document are designed to be executed by an LLM with both MCP servers available. Configure your Claude Code session with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the ESP32 MCP server
|
||||||
|
claude mcp add mcbluetooth-esp32 -- uvx mcbluetooth-esp32
|
||||||
|
|
||||||
|
# The mcbluetooth server (Linux BlueZ) should already be available
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the serial port via environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ESP32_SERIAL_PORT=/dev/ttyUSB4
|
||||||
|
```
|
||||||
|
|
||||||
|
Then instruct the LLM to run through the test scenarios, e.g.:
|
||||||
|
|
||||||
|
> "Run Test 1 (SSP Just Works) from the test scenarios document. Both mcbluetooth and mcbluetooth-esp32 MCP servers are available."
|
||||||
|
|
||||||
|
### Test environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ESP32_SERIAL_PORT` | `/dev/ttyUSB0` | Serial device path for the ESP32 |
|
||||||
|
| `ESP32_SERIAL_BAUD` | `115200` | Baud rate (should not need changing) |
|
||||||
|
| `BT_ADAPTER` | `hci0` | Linux Bluetooth adapter for mcbluetooth |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Matrix
|
## Test Matrix
|
||||||
|
|
||||||
| Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | Auto? |
|
Summary of all pairing tests and the IO capabilities required on each side.
|
||||||
|------|----------|--------------|--------------|-------|
|
|
||||||
| Just Works | NoInputNoOutput | no_io | no_io | Yes |
|
| Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | User Interaction |
|
||||||
| Numeric Comparison | NumericComparison | keyboard_display | display_yesno | No (confirm) |
|
|------|----------|--------------|--------------|------------------|
|
||||||
| Passkey Entry (ESP32 displays) | PasskeyEntry | display_only | keyboard_only | No (enter) |
|
| 1. Just Works | Just Works | `no_io` | `no_io` | None (auto-accept) |
|
||||||
| Passkey Entry (Linux displays) | PasskeyEntry | keyboard_only | display_only | No (enter) |
|
| 2. Numeric Comparison | Numeric Comparison | `display_yesno` | `display_yesno` | Confirm passkey match on both sides |
|
||||||
| Legacy PIN | LegacyPIN | n/a | n/a | No (PIN) |
|
| 3a. Passkey Entry (ESP32 displays) | Passkey Entry | `display_only` | `keyboard_only` | Linux enters passkey shown on ESP32 |
|
||||||
|
| 3b. Passkey Entry (Linux displays) | Passkey Entry | `keyboard_only` | `display_only` | ESP32 enters passkey shown on Linux |
|
||||||
|
| 4. Legacy PIN | Legacy PIN | n/a | n/a | Both sides provide pre-shared PIN |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user