diff --git a/docs/test-scenarios.md b/docs/test-scenarios.md index d5b64ea..1a8667a 100644 --- a/docs/test-scenarios.md +++ b/docs/test-scenarios.md @@ -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 -Tests require both MCP servers running simultaneously: -- **mcbluetooth** — controls the Linux BlueZ stack (host side) -- **mcbluetooth-esp32** — controls the ESP32 peripheral (device side) +Each test requires **two MCP servers** running simultaneously: -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 -# Terminal 1: Start ESP32 MCP server -ESP32_SERIAL_PORT=/dev/ttyUSB4 uvx mcbluetooth-esp32 +### Prerequisites -# 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 -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 -# ESP32 side: Configure as headset (no_io) -esp32_load_persona("headset") # io_cap=no_io → Just Works +**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(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 +esp32_classic_set_discoverable(discoverable=true) ``` -### Expected Result -- Pairing completes without any passkey exchange -- Both sides report success +**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` — 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 -# ESP32 side: Configure as phone (keyboard_display → numeric comparison) -esp32_load_persona("phone") +**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(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} +esp32_classic_set_discoverable(discoverable=true) ``` -### Expected Result -- Both sides display the same 6-digit passkey -- After both confirm, pairing succeeds +**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. +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 -# ESP32: display_only → shows passkey -esp32_configure(io_cap="display_only") +**Setup:** + +``` +esp32_configure(name="PasskeyDisp-Test", 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} +esp32_classic_set_discoverable(discoverable=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=) +``` + +Verify `pair_complete` succeeds. --- ## 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 -esp32_configure(pin_code="1234") +### 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(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 -bt_pair_confirm(adapter="hci0", address="D8:13:2A:7F:47:C0", - pin="1234", accept=True) +**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. +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 -# ESP32: Set up as sensor -esp32_load_persona("sensor") -esp32_gatt_add_service(uuid="0000181a-0000-1000-8000-00805f9b34fb", primary=True) -# → {"service_handle": 40} +### 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="00002a6e-0000-1000-8000-00805f9b34fb", # Temperature + uuid="2A6E", 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 -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", ...} +The value `"c409"` encodes 25.00 C as a little-endian `int16` in hundredths of a degree (2500 = 0x09C4, LE = `c4 09`). -# ESP32: Update value -esp32_gatt_set_value(char_handle=42, value="f003") # 25.5°C +**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 Notifications +## Test 6: BLE GATT Subscribe -```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) +Test notification subscription and delivery. The ESP32 pushes a value update to a subscribed Linux client. -# ESP32: Verify subscription -esp32_wait_event("gatt_subscribe") -# → {"char_handle": 42, "subscribed": true} +### Setup -# ESP32: Update and notify -esp32_gatt_set_value(char_handle=42, value="0004") +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) +``` -# 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"} +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 -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 -# 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) +### Execute - bt_scan(adapter="hci0", mode="both", timeout=5) - # Verify device name and class match persona definition +**Step 1 -- List available personas:** - 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 +### Unit tests (no hardware required) + +Unit tests exercise the Python protocol layer, event queue, and MCP tool registration using mock serial connections. + ```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 ``` +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 | 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) | +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 |