# 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 Each test requires **two MCP servers** running simultaneously: | Server | Controls | Tool prefix | |--------|----------|-------------| | `mcbluetooth` | Linux BlueZ stack (hci0) | `bt_*` | | `mcbluetooth-esp32` | ESP32 peripheral via UART | `esp32_*` | 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. ### Prerequisites - 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 Both devices have `no_io` capability. Pairing should auto-complete with no user interaction. ### Setup **Step 1 -- Connect to the ESP32 and configure it as a no-IO device:** ``` esp32_connect(port="/dev/ttyUSB4") esp32_configure(name="JustWorks-Test", io_cap="no_io") esp32_classic_enable() esp32_classic_set_discoverable(discoverable=true) ``` **Step 2 -- Power on the Linux adapter:** ``` 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 Both devices have `display_yesno` capability. Both display a 6-digit passkey that the user (or LLM) must confirm matches. ### Setup **Step 1 -- Configure the ESP32 with display+yesno:** ``` esp32_connect(port="/dev/ttyUSB4") esp32_configure(name="NumComp-Test", io_cap="display_yesno") esp32_classic_enable() esp32_classic_set_discoverable(discoverable=true) ``` **Step 2 -- Ensure Linux adapter is powered:** ``` 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 One side displays a passkey, the other must enter it. Test both directions. ### Direction A: ESP32 displays, Linux enters **Setup:** ``` esp32_configure(name="PasskeyDisp-Test", io_cap="display_only") esp32_classic_enable() esp32_classic_set_discoverable(discoverable=true) ``` **Execute:** ``` 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=) ``` Verify `pair_complete` succeeds. --- ## Test 4: Legacy PIN ESP32 configured with a PIN code. Linux initiates pairing with that PIN. This tests pre-SSP pairing mode. ### Setup ``` esp32_connect(port="/dev/ttyUSB4") esp32_configure(name="LegacyPIN-Test", io_cap="no_io", pin_code="1234") esp32_classic_enable() esp32_classic_set_discoverable(discoverable=true) ``` ### Execute **Step 1 -- Scan and pair from Linux with the PIN:** ``` 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 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. ### Setup **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( service_handle=40, uuid="2A6E", properties=["read", "notify"], value="c409" ) ``` Returns: `{"handle": 42}` (example handle) The value `"c409"` encodes 25.00 C as a little-endian `int16` in hundredths of a degree (2500 = 0x09C4, LE = `c4 09`). **Step 3 -- Start advertising:** ``` 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 Subscribe Test notification subscription and delivery. The ESP32 pushes a value update to a subscribed Linux client. ### Setup Use the same GATT service structure from Test 5 (Environmental Sensing, Temperature characteristic with `notify` property). ``` 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) ``` The value `"0c0a"` encodes 25.72 C (2572 = 0x0A0C, LE = `0c 0a`). **Step 4 -- Verify the Linux client received the notification:** 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 Load different device personas on the ESP32 and verify that the device name and Bluetooth class are visible from a Linux scan. ### Execute **Step 1 -- List available personas:** ``` 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 ### Unit tests (no hardware required) Unit tests exercise the Python protocol layer, event queue, and MCP tool registration using mock serial connections. ```bash make test-unit ``` 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 Summary of all pairing tests and the IO capabilities required on each side. | Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | User Interaction | |------|----------|--------------|--------------|------------------| | 1. Just Works | Just Works | `no_io` | `no_io` | None (auto-accept) | | 2. Numeric Comparison | Numeric Comparison | `display_yesno` | `display_yesno` | Confirm passkey match on both sides | | 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 |