Compare commits
No commits in common. "61da375a0c9e08f31eb8edba07de70174028e30e" and "6398a5223a89b3c7598f0d14d4338b4767f70f42" have entirely different histories.
61da375a0c
...
6398a5223a
229
README.md
229
README.md
@ -1,229 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
| **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, gatt_read, gatt_write, gatt_subscribe |
|
|
||||||
|
|
||||||
### 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 |
|
|
||||||
|
|
||||||
### 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
|
|
||||||
@ -1,287 +0,0 @@
|
|||||||
# Automated E2E Testing with Claude CLI
|
|
||||||
|
|
||||||
This document describes how to run fully automated end-to-end Bluetooth tests using the Claude CLI in headless mode. The tests exercise the complete Bluetooth stack across two devices: a Linux host running `mcbluetooth` (BlueZ) and an ESP32 running the `mcbluetooth-esp32` firmware.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌───────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Claude CLI (headless mode) │
|
|
||||||
│ Orchestrates both MCP servers │
|
|
||||||
└───────────────────────────┬───────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌───────────────┴───────────────┐
|
|
||||||
│ │
|
|
||||||
┌───────┴───────┐ ┌───────┴───────┐
|
|
||||||
│ mcbluetooth │ │mcbluetooth-esp32│
|
|
||||||
│ MCP Server │ │ MCP Server │
|
|
||||||
│ (bt_* tools)│ │ (esp32_* tools)│
|
|
||||||
└───────┬───────┘ └───────┬────────┘
|
|
||||||
│ │
|
|
||||||
D-Bus/BlueZ Serial/UART
|
|
||||||
│ │
|
|
||||||
┌───────┴───────┐ ┌───────┴────────┐
|
|
||||||
│ Linux Host │◄── Bluetooth ──►│ ESP32 │
|
|
||||||
│ (hci1) │ (over air) │ (peripheral) │
|
|
||||||
└───────────────┘ └────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
### Hardware
|
|
||||||
- ESP32 dev board connected via USB (typically `/dev/ttyUSB0` or `/dev/ttyUSB4`)
|
|
||||||
- Linux host with Bluetooth adapter (typically `hci0` or `hci1`)
|
|
||||||
|
|
||||||
### Software
|
|
||||||
- ESP32 flashed with mcbluetooth-esp32 firmware
|
|
||||||
- Both MCP servers installed and accessible via `uvx`
|
|
||||||
- Claude CLI installed
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
For HCI packet capture tests, grant btmon the required capability:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo setcap cap_net_raw+ep /usr/bin/btmon
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Environment Setup
|
|
||||||
|
|
||||||
### 1. Create a test directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /tmp/bt-e2e-test
|
|
||||||
cd /tmp/bt-e2e-test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create MCP configuration
|
|
||||||
|
|
||||||
Create `.mcp.json` with both MCP servers:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"esp32": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "uvx",
|
|
||||||
"args": ["mcbluetooth-esp32"],
|
|
||||||
"env": {
|
|
||||||
"ESP32_SERIAL_PORT": "/dev/ttyUSB4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bluez": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "uvx",
|
|
||||||
"args": ["mcbluetooth"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Initialize git (required for Claude CLI)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git init
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### Basic Command Structure
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude -p "$(cat test-prompt.md)" \
|
|
||||||
--mcp-config .mcp.json \
|
|
||||||
--allowedTools "mcp__esp32__*,mcp__bluez__*" \
|
|
||||||
--output-format json \
|
|
||||||
2>/dev/null | tee results.json | jq -r '.result'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key flags:**
|
|
||||||
- `-p`: Print/headless mode (non-interactive)
|
|
||||||
- `--mcp-config`: Path to MCP server configuration
|
|
||||||
- `--allowedTools`: Glob patterns for permitted tools (required in headless mode)
|
|
||||||
- `--output-format json`: Machine-parseable output
|
|
||||||
|
|
||||||
### Full Test Suite (76 tests)
|
|
||||||
|
|
||||||
The comprehensive test suite covers:
|
|
||||||
- ESP32 connection and system commands
|
|
||||||
- BlueZ adapter management
|
|
||||||
- Classic Bluetooth SSP pairing with auto-accept
|
|
||||||
- BLE GATT service creation (Environmental Sensing + Battery Service)
|
|
||||||
- HCI packet capture and analysis
|
|
||||||
- GATT read/write/notify operations
|
|
||||||
- Device management (trust, block, alias)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude -p "$(cat test-prompt-v5.md)" \
|
|
||||||
--mcp-config .mcp.json \
|
|
||||||
--allowedTools "mcp__esp32__*,mcp__bluez__*" \
|
|
||||||
--output-format json 2>/dev/null | tee results-v5.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Analyzing Results
|
|
||||||
|
|
||||||
Extract the summary:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jq -r '.result' results-v5.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Check pass/fail statistics:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jq -r '.result' results-v5.json | grep -E "(PASS|FAIL|Total)"
|
|
||||||
```
|
|
||||||
|
|
||||||
View full metrics:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
jq '{
|
|
||||||
duration_ms: .duration_ms,
|
|
||||||
num_turns: .num_turns,
|
|
||||||
total_cost_usd: .total_cost_usd,
|
|
||||||
success: .is_error == false
|
|
||||||
}' results-v5.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Phases
|
|
||||||
|
|
||||||
The test suite is organized into phases that must run sequentially:
|
|
||||||
|
|
||||||
| Phase | Tests | Coverage |
|
|
||||||
|-------|-------|----------|
|
|
||||||
| 1. ESP32 Connection | 1-4 | connect, ping, get_info, status |
|
|
||||||
| 2. BlueZ Adapter | 5-8 | list_adapters, adapter_info, pairable, discoverable |
|
|
||||||
| 3. Classic BT + SSP | 9-24 | enable, configure, SSP mode, scan, pair, device management |
|
|
||||||
| 4. Classic Cleanup | 25-29 | disable, events, clear_events |
|
|
||||||
| 5. BLE GATT Setup | 30-42 | Battery Service, Environmental Sensing, advertising |
|
|
||||||
| 6. HCI Capture + Discovery | 43-51 | capture_start, BLE scan, connect, services, characteristics |
|
|
||||||
| 7. Analyze Capture | 52-55 | capture_stop, parse, analyze, read_raw |
|
|
||||||
| 8. GATT Write + Notify | 56-63 | write, subscribe, notify, unsubscribe |
|
|
||||||
| 9. BLE Cleanup | 64-68 | stop advertising, clear GATT, disable BLE |
|
|
||||||
| 10. Adapter Management | 69-73 | set_alias, restore, disable discoverable |
|
|
||||||
| 11. Final Cleanup | 74-76 | ESP32 reset, disconnect, final check |
|
|
||||||
|
|
||||||
## SSP Pairing: The auto_accept Flag
|
|
||||||
|
|
||||||
Numeric Comparison SSP requires **both sides** to confirm the passkey. In headless mode, this creates a deadlock:
|
|
||||||
|
|
||||||
1. Linux calls `bt_pair()` which blocks waiting for ESP32 confirmation
|
|
||||||
2. ESP32 can't receive the confirmation command because the LLM is blocked
|
|
||||||
|
|
||||||
**Solution:** The ESP32 firmware supports `auto_accept` mode:
|
|
||||||
|
|
||||||
```
|
|
||||||
esp32_set_ssp_mode(mode="numeric_comparison", auto_accept=true)
|
|
||||||
```
|
|
||||||
|
|
||||||
This makes the ESP32 automatically confirm SSP pairings, breaking the deadlock.
|
|
||||||
|
|
||||||
## Battery Service Test
|
|
||||||
|
|
||||||
The test suite creates a standard Battery Service (UUID 0x180F) on the ESP32:
|
|
||||||
|
|
||||||
1. Add Battery Service as primary GATT service
|
|
||||||
2. Add Battery Level characteristic (UUID 0x2A19) with read property
|
|
||||||
3. Set value to "4b" (75% in hex)
|
|
||||||
4. After BLE connection, call `bt_ble_battery` on Linux
|
|
||||||
5. Verify it returns 75
|
|
||||||
|
|
||||||
This tests the dedicated `bt_ble_battery` tool in mcbluetooth which reads from the standard Battery Level characteristic.
|
|
||||||
|
|
||||||
## HCI Packet Capture
|
|
||||||
|
|
||||||
Tests 43-55 exercise the btsnoop capture functionality:
|
|
||||||
|
|
||||||
```
|
|
||||||
bt_capture_start(adapter="hci1", output_file="/tmp/ble-gatt-capture.btsnoop")
|
|
||||||
# ... BLE operations ...
|
|
||||||
bt_capture_stop(capture_id="...")
|
|
||||||
bt_capture_parse(filepath="...", max_packets=50)
|
|
||||||
bt_capture_analyze(filepath="...")
|
|
||||||
bt_capture_read_raw(filepath="...", count=20)
|
|
||||||
```
|
|
||||||
|
|
||||||
Typical captures include 100-150 packets covering:
|
|
||||||
- HCI commands (LE scanning, connection)
|
|
||||||
- ACL data (GATT operations)
|
|
||||||
- HCI events (connection complete, encryption)
|
|
||||||
|
|
||||||
## Test Prompt Format
|
|
||||||
|
|
||||||
Test prompts follow a structured format:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Test Suite Title
|
|
||||||
|
|
||||||
## Phase N: Phase Name (Tests X-Y)
|
|
||||||
|
|
||||||
N. **Test Name**: Call `tool_name` with params — expected result
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
After all tests, print a DETAILED summary table:
|
|
||||||
|
|
||||||
| # | Test | Result | Notes |
|
|
||||||
|---|------|--------|-------|
|
|
||||||
| 1 | Connect | PASS/FAIL | ... |
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Serial port busy
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: could not open port /dev/ttyUSB4
|
|
||||||
```
|
|
||||||
|
|
||||||
Check for other processes using the port:
|
|
||||||
```bash
|
|
||||||
lsof /dev/ttyUSB4
|
|
||||||
```
|
|
||||||
|
|
||||||
### btmon permission denied
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: Failed to open HCI raw socket
|
|
||||||
```
|
|
||||||
|
|
||||||
Grant capability:
|
|
||||||
```bash
|
|
||||||
sudo setcap cap_net_raw+ep /usr/bin/btmon
|
|
||||||
```
|
|
||||||
|
|
||||||
### ESP32 not responding
|
|
||||||
|
|
||||||
Power cycle the ESP32 and check the firmware is flashed:
|
|
||||||
```bash
|
|
||||||
# Monitor serial output
|
|
||||||
screen /dev/ttyUSB4 115200
|
|
||||||
```
|
|
||||||
|
|
||||||
Press reset button — should see boot event JSON.
|
|
||||||
|
|
||||||
### Pairing timeout
|
|
||||||
|
|
||||||
Ensure `auto_accept=true` is set for SSP numeric comparison mode before initiating pairing from Linux.
|
|
||||||
|
|
||||||
## Example Results
|
|
||||||
|
|
||||||
A successful v5 run produces:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "result",
|
|
||||||
"subtype": "success",
|
|
||||||
"is_error": false,
|
|
||||||
"duration_ms": 320000,
|
|
||||||
"num_turns": 88,
|
|
||||||
"result": "All 76 tests passed..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Key metrics from successful runs:
|
|
||||||
- Duration: ~5-6 minutes
|
|
||||||
- API turns: 80-90
|
|
||||||
- HCI packets captured: 100-150
|
|
||||||
- Cost: ~$1.50-1.70 USD
|
|
||||||
@ -22,13 +22,41 @@ Any ESP32 board based on the original ESP32 chip should work. Commonly available
|
|||||||
|
|
||||||
## Wiring
|
## Wiring
|
||||||
|
|
||||||
### USB only (default)
|
### Default: USB only
|
||||||
|
|
||||||
A single USB cable handles both flashing and NDJSON protocol communication. The ESP32 dev board's built-in USB-to-UART bridge (typically CP2102 or CH340) connects to UART0 (TX=GPIO1, RX=GPIO3).
|
For most development, a single USB cable handles both flashing and protocol communication. The ESP32's built-in USB-to-UART bridge (typically CP2102 or CH340) provides the serial link.
|
||||||
|
|
||||||
The firmware uses **UART0** for the NDJSON protocol. The ESP-IDF console is disabled (`CONFIG_ESP_CONSOLE_NONE=y`) so there is no conflict -- the firmware owns UART0 exclusively. No additional wiring or USB-UART adapters are needed.
|
The firmware uses **UART1** (GPIO4/GPIO5) for the NDJSON protocol and keeps **UART0** for ESP-IDF console logging. When using USB only, the USB bridge connects to UART0 by default -- so you will need either:
|
||||||
|
|
||||||
The dev board appears as `/dev/ttyUSB*` on the host. Use this device path for `ESP32_SERIAL_PORT`.
|
1. A board that routes UART1 through the USB bridge (uncommon), or
|
||||||
|
2. A separate USB-UART adapter connected to GPIO4/GPIO5 (described below)
|
||||||
|
|
||||||
|
For quick testing with the firmware's default pin assignment, connect a USB-UART adapter.
|
||||||
|
|
||||||
|
### Dedicated UART (GPIO4/GPIO5)
|
||||||
|
|
||||||
|
Connect a USB-UART adapter (e.g., FTDI FT232R, CP2102, CH340) to the ESP32:
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 GPIO4 (TX) ----> USB-UART adapter RX
|
||||||
|
ESP32 GPIO5 (RX) <---- USB-UART adapter TX
|
||||||
|
ESP32 GND ----> USB-UART adapter GND
|
||||||
|
```
|
||||||
|
|
||||||
|
The adapter appears as a second `/dev/ttyUSB*` device on the host. Use this device path for `ESP32_SERIAL_PORT`.
|
||||||
|
|
||||||
|
Do not connect voltage lines (VCC/3V3) between the adapter and the ESP32 if the board is already powered via its own USB port.
|
||||||
|
|
||||||
|
### Pin reassignment
|
||||||
|
|
||||||
|
If GPIO4/GPIO5 conflict with other peripherals on your board, change the pin definitions in `firmware/main/uart_handler.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define UART_TX_PIN GPIO_NUM_4
|
||||||
|
#define UART_RX_PIN GPIO_NUM_5
|
||||||
|
```
|
||||||
|
|
||||||
|
Rebuild and reflash after changing pins.
|
||||||
|
|
||||||
## ESP-IDF Setup
|
## ESP-IDF Setup
|
||||||
|
|
||||||
@ -87,13 +115,13 @@ make flash SERIAL_PORT=/dev/ttyUSB4
|
|||||||
|
|
||||||
### 5. Monitor (optional)
|
### 5. Monitor (optional)
|
||||||
|
|
||||||
Open the ESP-IDF serial monitor to watch raw UART traffic. Since the firmware owns UART0 (console is disabled), you will see NDJSON protocol messages rather than ESP-IDF log output:
|
Open the ESP-IDF serial monitor to see console logs from UART0:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
idf.py -p /dev/ttyUSB4 monitor
|
idf.py -p /dev/ttyUSB4 monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
Press `Ctrl+]` to exit the monitor. Note: while the monitor is open, the MCP server cannot use the same serial port.
|
Press `Ctrl+]` to exit the monitor.
|
||||||
|
|
||||||
### 6. Flash and monitor in one step
|
### 6. Flash and monitor in one step
|
||||||
|
|
||||||
@ -156,13 +184,13 @@ The project ships `firmware/sdkconfig.defaults` with the required Bluetooth conf
|
|||||||
| `CONFIG_BT_BLUEDROID_ENABLED` | y | Use Bluedroid host stack |
|
| `CONFIG_BT_BLUEDROID_ENABLED` | y | Use Bluedroid host stack |
|
||||||
| `CONFIG_BT_CLASSIC_ENABLED` | y | Enable BR/EDR (Classic BT) |
|
| `CONFIG_BT_CLASSIC_ENABLED` | y | Enable BR/EDR (Classic BT) |
|
||||||
| `CONFIG_BT_BLE_ENABLED` | y | Enable BLE |
|
| `CONFIG_BT_BLE_ENABLED` | y | Enable BLE |
|
||||||
|
| `CONFIG_BT_SSP_ENABLED` | y | Enable Secure Simple Pairing |
|
||||||
| `CONFIG_BT_SPP_ENABLED` | y | Enable Serial Port Profile |
|
| `CONFIG_BT_SPP_ENABLED` | y | Enable Serial Port Profile |
|
||||||
| `CONFIG_BT_GATTS_ENABLE` | y | Enable GATT Server |
|
| `CONFIG_BT_GATTS_ENABLE` | y | Enable GATT Server |
|
||||||
| `CONFIG_BTDM_CTRL_MODE_BTDM` | y | Dual-mode controller (Classic + BLE simultaneously) |
|
| `CONFIG_BTDM_CTRL_MODE_BTDM` | y | Dual-mode controller (Classic + BLE simultaneously) |
|
||||||
| `CONFIG_ESP_CONSOLE_NONE` | y | Disable ESP-IDF console so firmware owns UART0 |
|
| `CONFIG_NVS_ENABLED` | y | Non-volatile storage for bonding data |
|
||||||
| `CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE` | y | 1.5MB app partition (dual-mode BT stack needs >1MB) |
|
|
||||||
|
|
||||||
Do not modify these unless you understand the implications. Disabling `CONFIG_BT_CLASSIC_ENABLED` breaks all Classic BT pairing tests.
|
Do not modify these unless you understand the implications. Disabling `CONFIG_BT_CLASSIC_ENABLED` breaks all Classic BT pairing tests. Disabling `CONFIG_BT_SSP_ENABLED` forces legacy PIN-only pairing.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@ -204,13 +232,13 @@ If the board has auto-download circuitry (most DevKitC boards do), this should n
|
|||||||
|
|
||||||
### No response over UART
|
### No response over UART
|
||||||
|
|
||||||
1. **Check baud rate.** Both sides must use 115200. Verify in `screen` or your terminal emulator.
|
1. **Verify TX/RX pin assignment.** The firmware uses UART1 on GPIO4 (TX) and GPIO5 (RX). If your adapter is connected to different pins, update `uart_handler.c`.
|
||||||
|
|
||||||
2. **Make sure nothing else is using the port.** The ESP-IDF monitor, `screen`, another MCP server instance, or any other serial tool will lock the device. Only one process can open `/dev/ttyUSB*` at a time.
|
2. **Check baud rate.** Both sides must use 115200. Verify in `screen` or your terminal emulator.
|
||||||
|
|
||||||
3. **Send valid JSON.** The firmware expects complete JSON objects terminated by `\n`. A bare `ping` won't work -- send `{"type":"cmd","id":"1","cmd":"ping"}\n`.
|
3. **Check the correct serial device.** If the board has two USB-UART interfaces (one for UART0 console, one for UART1 protocol), make sure you are talking to the right one.
|
||||||
|
|
||||||
4. **Verify the firmware booted.** After flashing, the firmware should emit a `boot` event within ~2 seconds. If you see nothing at all, try pressing the EN (reset) button on the board.
|
4. **Look at UART0 console output.** Connect the ESP-IDF monitor to the console port. Boot messages and error logs appear there. If you see `UART1 ready (TX=4 RX=5 @ 115200 baud)` in the log, the firmware started correctly.
|
||||||
|
|
||||||
### Build errors about missing Bluetooth headers
|
### Build errors about missing Bluetooth headers
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Authoritative reference for the JSON-over-UART protocol used between the Python
|
|||||||
- **Baud rate:** 115200
|
- **Baud rate:** 115200
|
||||||
- **Frame format:** 8N1 (8 data bits, no parity, 1 stop bit)
|
- **Frame format:** 8N1 (8 data bits, no parity, 1 stop bit)
|
||||||
- **Max line length:** 2048 bytes (lines exceeding this are silently dropped)
|
- **Max line length:** 2048 bytes (lines exceeding this are silently dropped)
|
||||||
- **UART peripheral:** ESP32 UART0 via the dev board's USB-to-UART bridge (TX=GPIO1, RX=GPIO3). ESP-IDF console is disabled (`CONFIG_ESP_CONSOLE_NONE=y`) so the firmware owns UART0 exclusively.
|
- **UART peripheral:** ESP32 UART1 on GPIO4 (TX) and GPIO5 (RX), keeping UART0 free for ESP-IDF console logging
|
||||||
- **Encoding:** UTF-8
|
- **Encoding:** UTF-8
|
||||||
- **Newlines:** `\n` only. Carriage returns (`\r`) are stripped by the firmware reader.
|
- **Newlines:** `\n` only. Carriage returns (`\r`) are stripped by the firmware reader.
|
||||||
|
|
||||||
|
|||||||
@ -1,655 +1,263 @@
|
|||||||
# End-to-End Test Scenarios
|
# E2E 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
|
||||||
|
|
||||||
Each test requires **two MCP servers** running simultaneously:
|
Tests require both MCP servers running simultaneously:
|
||||||
|
- **mcbluetooth** — controls the Linux BlueZ stack (host side)
|
||||||
|
- **mcbluetooth-esp32** — controls the ESP32 peripheral (device side)
|
||||||
|
|
||||||
| Server | Controls | Tool prefix |
|
An LLM orchestrates both servers, issuing tool calls to each side to execute the test flow.
|
||||||
|--------|----------|-------------|
|
|
||||||
| `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
|
||||||
|
|
||||||
### Prerequisites
|
```bash
|
||||||
|
# Terminal 1: Start ESP32 MCP server
|
||||||
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 uvx mcbluetooth-esp32
|
||||||
|
|
||||||
- ESP32 connected and reachable at `ESP32_SERIAL_PORT` (default `/dev/ttyUSB4`)
|
# Terminal 2: mcbluetooth is already running (or add to Claude Code config)
|
||||||
- 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 should auto-complete with no user interaction.
|
Both devices have `no_io` capability — pairing auto-completes without user interaction.
|
||||||
|
|
||||||
### Setup
|
### Flow
|
||||||
|
|
||||||
**Step 1 -- Connect to the ESP32 and configure it as a no-IO device:**
|
```python
|
||||||
|
# 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(discoverable=true)
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step 2 -- Power on the Linux adapter:**
|
### Expected Result
|
||||||
|
- 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` capability. Both display a 6-digit passkey that the user (or LLM) must confirm matches.
|
Both devices have `display_yesno` — both display a 6-digit passkey that must match.
|
||||||
|
|
||||||
### Setup
|
### Flow
|
||||||
|
|
||||||
**Step 1 -- Configure the ESP32 with display+yesno:**
|
```python
|
||||||
|
# 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(discoverable=true)
|
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}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step 2 -- Ensure Linux adapter is powered:**
|
### Expected Result
|
||||||
|
- 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. Test both directions.
|
One side displays a passkey, the other must enter it.
|
||||||
|
|
||||||
### Direction A: ESP32 displays, Linux enters
|
### Flow (ESP32 displays, Linux enters)
|
||||||
|
|
||||||
**Setup:**
|
```python
|
||||||
|
# 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(discoverable=true)
|
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}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Execute:**
|
### Flow (Linux displays, ESP32 enters)
|
||||||
|
|
||||||
```
|
```python
|
||||||
bt_scan(adapter="hci0", timeout=10, mode="classic")
|
# ESP32: keyboard_only → must enter passkey
|
||||||
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
esp32_configure(io_cap="keyboard_only")
|
||||||
```
|
|
||||||
|
|
||||||
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_enable()
|
||||||
esp32_classic_set_discoverable(discoverable=true)
|
esp32_classic_set_discoverable(True)
|
||||||
```
|
|
||||||
|
|
||||||
**Execute:**
|
# Linux initiates pairing — Linux displays passkey
|
||||||
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
||||||
|
|
||||||
```
|
bt_pairing_status()
|
||||||
bt_pair(adapter="hci0", address="AA:BB:CC:DD:EE:FF", pairing_mode="interactive")
|
# → passkey: 789012 (displayed on Linux)
|
||||||
```
|
|
||||||
|
|
||||||
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 receives passkey entry request
|
||||||
|
esp32_wait_event("pair_request")
|
||||||
|
# → {"type": "passkey_entry"}
|
||||||
|
|
||||||
```
|
# ESP32 enters the passkey shown on Linux
|
||||||
esp32_wait_event(event_name="pair_request", timeout=10)
|
esp32_classic_pair_respond(address="XX:XX:XX:XX:XX:XX", accept=True, passkey=789012)
|
||||||
esp32_classic_pair_respond(address="AA:BB:CC:DD:EE:FF", accept=true, passkey=<passkey_from_linux>)
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify `pair_complete` succeeds.
|
# Verify
|
||||||
|
esp32_wait_event("pair_complete")
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test 4: Legacy PIN
|
## Test 4: Legacy PIN
|
||||||
|
|
||||||
ESP32 configured with a PIN code. Linux initiates pairing with that PIN. This tests pre-SSP pairing mode.
|
ESP32 configured with a legacy PIN code.
|
||||||
|
|
||||||
### Setup
|
```python
|
||||||
|
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(discoverable=true)
|
esp32_classic_set_discoverable(True)
|
||||||
```
|
|
||||||
|
|
||||||
### Execute
|
bt_pair(adapter="hci0", address="D8:13:2A:7F:47:C0", pairing_mode="interactive")
|
||||||
|
|
||||||
**Step 1 -- Scan and pair from Linux with the PIN:**
|
# Linux enters 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. Linux reads the value and verifies correctness. ESP32 updates the value and Linux reads again.
|
ESP32 creates an Environmental Sensing service with a Temperature characteristic.
|
||||||
|
|
||||||
### Setup
|
```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}
|
||||||
|
|
||||||
**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="2A6E",
|
uuid="00002a6e-0000-1000-8000-00805f9b34fb", # Temperature
|
||||||
properties=["read", "notify"],
|
properties=["read", "notify"],
|
||||||
value="c409"
|
value="e803" # 25.0°C (0x03e8 = 1000 in little-endian → 10.00°C? or raw)
|
||||||
)
|
)
|
||||||
```
|
# → {"char_handle": 42}
|
||||||
|
|
||||||
Returns: `{"handle": 42}` (example handle)
|
esp32_ble_advertise(enable=True)
|
||||||
|
|
||||||
The value `"c409"` encodes 25.00 C as a little-endian `int16` in hundredths of a degree (2500 = 0x09C4, LE = `c4 09`).
|
# 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", ...}
|
||||||
|
|
||||||
**Step 3 -- Start advertising:**
|
# ESP32: Update value
|
||||||
|
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 Subscribe
|
## Test 6: BLE GATT Notifications
|
||||||
|
|
||||||
Test notification subscription and delivery. The ESP32 pushes a value update to a subscribed Linux client.
|
```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)
|
||||||
|
|
||||||
### Setup
|
# ESP32: Verify subscription
|
||||||
|
esp32_wait_event("gatt_subscribe")
|
||||||
|
# → {"char_handle": 42, "subscribed": true}
|
||||||
|
|
||||||
Use the same GATT service structure from Test 5 (Environmental Sensing, Temperature characteristic with `notify` property).
|
# ESP32: Update and notify
|
||||||
|
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)
|
||||||
```
|
|
||||||
|
|
||||||
The value `"0c0a"` encodes 25.72 C (2572 = 0x0A0C, LE = `0c 0a`).
|
# Linux should receive the updated value
|
||||||
|
bt_ble_read(adapter="hci0", address="D8:13:2A:7F:47:C0",
|
||||||
**Step 4 -- Verify the Linux client received the notification:**
|
char_uuid="00002a6e-0000-1000-8000-00805f9b34fb")
|
||||||
|
# → {"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
|
||||||
|
|
||||||
Load different device personas on the ESP32 and verify that the device name and Bluetooth class are visible from a Linux scan.
|
Verify device identity changes are visible from the Linux side.
|
||||||
|
|
||||||
### Execute
|
```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)
|
||||||
|
|
||||||
**Step 1 -- List available personas:**
|
bt_scan(adapter="hci0", mode="both", timeout=5)
|
||||||
|
# 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
|
||||||
```
|
|
||||||
|
|
||||||
Or directly:
|
# Integration tests (requires ESP32 on /dev/ttyUSB4)
|
||||||
|
|
||||||
```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
|
ESP32_SERIAL_PORT=/dev/ttyUSB4 make test-integration
|
||||||
|
|
||||||
|
# Full suite
|
||||||
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
Summary of all pairing tests and the IO capabilities required on each side.
|
| Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | Auto? |
|
||||||
|
|------|----------|--------------|--------------|-------|
|
||||||
| Test | SSP Mode | ESP32 IO Cap | Linux IO Cap | User Interaction |
|
| Just Works | NoInputNoOutput | no_io | no_io | Yes |
|
||||||
|------|----------|--------------|--------------|------------------|
|
| Numeric Comparison | NumericComparison | keyboard_display | display_yesno | No (confirm) |
|
||||||
| 1. Just Works | Just Works | `no_io` | `no_io` | None (auto-accept) |
|
| Passkey Entry (ESP32 displays) | PasskeyEntry | display_only | keyboard_only | No (enter) |
|
||||||
| 2. Numeric Comparison | Numeric Comparison | `display_yesno` | `display_yesno` | Confirm passkey match on both sides |
|
| Passkey Entry (Linux displays) | PasskeyEntry | keyboard_only | display_only | No (enter) |
|
||||||
| 3a. Passkey Entry (ESP32 displays) | Passkey Entry | `display_only` | `keyboard_only` | Linux enters passkey shown on ESP32 |
|
| Legacy PIN | LegacyPIN | n/a | n/a | No (PIN) |
|
||||||
| 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 |
|
|
||||||
|
|||||||
@ -114,6 +114,14 @@ static int hex_to_bytes(const char *hex, uint8_t *out, int max_len)
|
|||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void bytes_to_hex(const uint8_t *data, int len, char *out)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
sprintf(out + i * 2, "%02x", data[i]);
|
||||||
|
}
|
||||||
|
out[len * 2] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
static uint8_t properties_from_json(cJSON *arr)
|
static uint8_t properties_from_json(cJSON *arr)
|
||||||
{
|
{
|
||||||
uint8_t props = 0;
|
uint8_t props = 0;
|
||||||
@ -416,15 +424,6 @@ static void gatts_event_handler(esp_gatts_cb_event_t event,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* State query */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
bool bt_ble_is_enabled(void)
|
|
||||||
{
|
|
||||||
return s_ble.enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Command handlers */
|
/* Command handlers */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
/* Initialize BLE subsystem (Bluedroid GATTS) */
|
/* Initialize BLE subsystem (Bluedroid GATTS) */
|
||||||
void bt_ble_init(void);
|
void bt_ble_init(void);
|
||||||
|
|
||||||
/* State query */
|
|
||||||
bool bt_ble_is_enabled(void);
|
|
||||||
|
|
||||||
/* Command handlers (called from cmd_dispatcher) */
|
/* Command handlers (called from cmd_dispatcher) */
|
||||||
void cmd_ble_enable(const char *id, cJSON *params);
|
void cmd_ble_enable(const char *id, cJSON *params);
|
||||||
void cmd_ble_disable(const char *id, cJSON *params);
|
void cmd_ble_disable(const char *id, cJSON *params);
|
||||||
|
|||||||
@ -46,7 +46,6 @@ static struct {
|
|||||||
esp_bt_io_cap_t io_cap;
|
esp_bt_io_cap_t io_cap;
|
||||||
char pin_code[17];
|
char pin_code[17];
|
||||||
bool ssp_enabled;
|
bool ssp_enabled;
|
||||||
bool auto_accept; /* Auto-confirm SSP pairing (for testing) */
|
|
||||||
/* Pending pairing state */
|
/* Pending pairing state */
|
||||||
char pending_pair_address[18];
|
char pending_pair_address[18];
|
||||||
char pending_pair_cmd_id[32];
|
char pending_pair_cmd_id[32];
|
||||||
@ -54,7 +53,6 @@ static struct {
|
|||||||
pair_type_t pair_type;
|
pair_type_t pair_type;
|
||||||
/* SPP handle for the listening server */
|
/* SPP handle for the listening server */
|
||||||
uint32_t spp_handle;
|
uint32_t spp_handle;
|
||||||
char spp_remote_addr[18]; /* Connected peer address */
|
|
||||||
} classic_state = {
|
} classic_state = {
|
||||||
.io_cap = ESP_BT_IO_CAP_IO, /* display+yesno (numeric comparison) */
|
.io_cap = ESP_BT_IO_CAP_IO, /* display+yesno (numeric comparison) */
|
||||||
.ssp_enabled = true,
|
.ssp_enabled = true,
|
||||||
@ -107,9 +105,9 @@ static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
|
|||||||
bd_addr_to_str(param->auth_cmpl.bda, addr_str);
|
bd_addr_to_str(param->auth_cmpl.bda, addr_str);
|
||||||
bool ok = (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS);
|
bool ok = (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "auth_cmpl: %s %s (lk_type=%d)",
|
ESP_LOGI(TAG, "auth_cmpl: %s %s (mode=%d)",
|
||||||
addr_str, ok ? "success" : "FAIL",
|
addr_str, ok ? "success" : "FAIL",
|
||||||
param->auth_cmpl.lk_type);
|
param->auth_cmpl.auth_mode);
|
||||||
|
|
||||||
event_report_pair_complete(addr_str, ok);
|
event_report_pair_complete(addr_str, ok);
|
||||||
|
|
||||||
@ -134,16 +132,10 @@ static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
|
|||||||
|
|
||||||
event_report_pair_request(addr_str, "numeric_comparison", (int)passkey);
|
event_report_pair_request(addr_str, "numeric_comparison", (int)passkey);
|
||||||
|
|
||||||
if (classic_state.auto_accept) {
|
/* Stash address so pair_respond can reply */
|
||||||
/* Auto-confirm for automated E2E testing */
|
strncpy(classic_state.pending_pair_address, addr_str,
|
||||||
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
|
sizeof(classic_state.pending_pair_address) - 1);
|
||||||
ESP_LOGI(TAG, "cfm_req: auto-accepted (auto_accept=true)");
|
classic_state.pair_type = PAIR_TYPE_NUMERIC_COMPARISON;
|
||||||
} else {
|
|
||||||
/* Stash address so pair_respond can reply */
|
|
||||||
strncpy(classic_state.pending_pair_address, addr_str,
|
|
||||||
sizeof(classic_state.pending_pair_address) - 1);
|
|
||||||
classic_state.pair_type = PAIR_TYPE_NUMERIC_COMPARISON;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,37 +236,24 @@ static void spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
|
|||||||
case ESP_SPP_SRV_OPEN_EVT:
|
case ESP_SPP_SRV_OPEN_EVT:
|
||||||
bd_addr_to_str(param->srv_open.rem_bda, addr_str);
|
bd_addr_to_str(param->srv_open.rem_bda, addr_str);
|
||||||
classic_state.spp_handle = param->srv_open.handle;
|
classic_state.spp_handle = param->srv_open.handle;
|
||||||
strncpy(classic_state.spp_remote_addr, addr_str,
|
|
||||||
sizeof(classic_state.spp_remote_addr) - 1);
|
|
||||||
classic_state.spp_remote_addr[sizeof(classic_state.spp_remote_addr) - 1] = '\0';
|
|
||||||
ESP_LOGI(SPP_TAG, "SPP client connected: %s (handle=%" PRIu32 ")",
|
ESP_LOGI(SPP_TAG, "SPP client connected: %s (handle=%" PRIu32 ")",
|
||||||
addr_str, param->srv_open.handle);
|
addr_str, param->srv_open.handle);
|
||||||
/* Report specific SPP connect event with more detail */
|
event_report_connect(addr_str, "classic");
|
||||||
{
|
|
||||||
cJSON *d = cJSON_CreateObject();
|
|
||||||
cJSON_AddStringToObject(d, "address", addr_str);
|
|
||||||
cJSON_AddNumberToObject(d, "handle", (double)param->srv_open.handle);
|
|
||||||
cJSON_AddStringToObject(d, "transport", "spp");
|
|
||||||
event_report(EVT_SPP_CONNECT, d);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESP_SPP_CLOSE_EVT:
|
case ESP_SPP_CLOSE_EVT:
|
||||||
ESP_LOGI(SPP_TAG, "SPP disconnected (handle=%" PRIu32 ")",
|
ESP_LOGI(SPP_TAG, "SPP disconnected (handle=%" PRIu32 ")",
|
||||||
param->close.handle);
|
param->close.handle);
|
||||||
/* Report SPP disconnect with saved remote address */
|
/* We don't have the remote address in CLOSE_EVT on all IDF versions,
|
||||||
|
* so report with handle info. */
|
||||||
{
|
{
|
||||||
cJSON *d = cJSON_CreateObject();
|
cJSON *d = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(d, "handle", (double)param->close.handle);
|
cJSON_AddNumberToObject(d, "handle", (double)param->close.handle);
|
||||||
cJSON_AddStringToObject(d, "transport", "spp");
|
cJSON_AddStringToObject(d, "transport", "classic");
|
||||||
if (classic_state.spp_remote_addr[0] != '\0') {
|
event_report(EVT_DISCONNECT, d);
|
||||||
cJSON_AddStringToObject(d, "address", classic_state.spp_remote_addr);
|
|
||||||
}
|
|
||||||
event_report(EVT_SPP_DISCONNECT, d);
|
|
||||||
}
|
}
|
||||||
if (classic_state.spp_handle == param->close.handle) {
|
if (classic_state.spp_handle == param->close.handle) {
|
||||||
classic_state.spp_handle = 0;
|
classic_state.spp_handle = 0;
|
||||||
classic_state.spp_remote_addr[0] = '\0';
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -288,46 +267,9 @@ static void spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ESP_SPP_DATA_IND_EVT:
|
case ESP_SPP_DATA_IND_EVT:
|
||||||
/* Data received over SPP -- report as event */
|
/* Data received over SPP -- log but don't process for now. */
|
||||||
ESP_LOGI(SPP_TAG, "SPP data: %d bytes on handle %" PRIu32,
|
ESP_LOGI(SPP_TAG, "SPP data: %d bytes on handle %" PRIu32,
|
||||||
param->data_ind.len, param->data_ind.handle);
|
param->data_ind.len, param->data_ind.handle);
|
||||||
{
|
|
||||||
cJSON *d = cJSON_CreateObject();
|
|
||||||
cJSON_AddNumberToObject(d, "handle", (double)param->data_ind.handle);
|
|
||||||
cJSON_AddNumberToObject(d, "length", param->data_ind.len);
|
|
||||||
if (classic_state.spp_remote_addr[0] != '\0') {
|
|
||||||
cJSON_AddStringToObject(d, "address", classic_state.spp_remote_addr);
|
|
||||||
}
|
|
||||||
/* Encode data as hex string */
|
|
||||||
size_t hex_len = param->data_ind.len * 2 + 1;
|
|
||||||
char *hex_str = malloc(hex_len);
|
|
||||||
if (hex_str) {
|
|
||||||
for (int i = 0; i < param->data_ind.len; i++) {
|
|
||||||
sprintf(hex_str + i * 2, "%02x", param->data_ind.data[i]);
|
|
||||||
}
|
|
||||||
hex_str[param->data_ind.len * 2] = '\0';
|
|
||||||
cJSON_AddStringToObject(d, "data_hex", hex_str);
|
|
||||||
free(hex_str);
|
|
||||||
}
|
|
||||||
/* Also try UTF-8 if printable */
|
|
||||||
bool printable = true;
|
|
||||||
for (int i = 0; i < param->data_ind.len && printable; i++) {
|
|
||||||
uint8_t c = param->data_ind.data[i];
|
|
||||||
if (c < 0x20 && c != '\n' && c != '\r' && c != '\t') {
|
|
||||||
printable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (printable && param->data_ind.len < 256) {
|
|
||||||
char *text = malloc(param->data_ind.len + 1);
|
|
||||||
if (text) {
|
|
||||||
memcpy(text, param->data_ind.data, param->data_ind.len);
|
|
||||||
text[param->data_ind.len] = '\0';
|
|
||||||
cJSON_AddStringToObject(d, "data_text", text);
|
|
||||||
free(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event_report(EVT_SPP_DATA, d);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -633,151 +575,14 @@ void cmd_classic_set_ssp_mode(const char *id, cJSON *params)
|
|||||||
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
|
esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap,
|
||||||
sizeof(iocap));
|
sizeof(iocap));
|
||||||
|
|
||||||
/* Optional auto_accept flag for automated testing */
|
ESP_LOGI(TAG, "SSP mode set to '%s' (io_cap=%d)", mode, new_cap);
|
||||||
const cJSON *j_auto = cJSON_GetObjectItem(params, "auto_accept");
|
|
||||||
if (cJSON_IsBool(j_auto)) {
|
|
||||||
classic_state.auto_accept = cJSON_IsTrue(j_auto);
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "SSP mode set to '%s' (io_cap=%d, auto_accept=%d)",
|
|
||||||
mode, new_cap, classic_state.auto_accept);
|
|
||||||
|
|
||||||
cJSON *data = cJSON_CreateObject();
|
cJSON *data = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(data, "mode", mode);
|
cJSON_AddStringToObject(data, "mode", mode);
|
||||||
cJSON_AddNumberToObject(data, "io_cap", new_cap);
|
cJSON_AddNumberToObject(data, "io_cap", new_cap);
|
||||||
cJSON_AddBoolToObject(data, "auto_accept", classic_state.auto_accept);
|
|
||||||
uart_send_response(id, STATUS_OK, data);
|
uart_send_response(id, STATUS_OK, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* SPP command handlers */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
static bool hex_to_bytes(const char *hex, uint8_t *out, size_t *out_len, size_t max_len)
|
|
||||||
{
|
|
||||||
size_t hex_len = strlen(hex);
|
|
||||||
if (hex_len % 2 != 0) return false;
|
|
||||||
size_t bytes = hex_len / 2;
|
|
||||||
if (bytes > max_len) return false;
|
|
||||||
for (size_t i = 0; i < bytes; i++) {
|
|
||||||
unsigned int b;
|
|
||||||
if (sscanf(hex + i * 2, "%2x", &b) != 1) return false;
|
|
||||||
out[i] = (uint8_t)b;
|
|
||||||
}
|
|
||||||
*out_len = bytes;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void cmd_spp_send(const char *id, cJSON *params)
|
|
||||||
{
|
|
||||||
if (!classic_state.enabled) {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("classic BT not enabled"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classic_state.spp_handle == 0) {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("no SPP connection"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cJSON *j_data = cJSON_GetObjectItem(params, "data");
|
|
||||||
const cJSON *j_hex = cJSON_GetObjectItem(params, "data_hex");
|
|
||||||
|
|
||||||
uint8_t buf[512];
|
|
||||||
size_t len = 0;
|
|
||||||
|
|
||||||
if (cJSON_IsString(j_hex)) {
|
|
||||||
/* Hex-encoded data */
|
|
||||||
if (!hex_to_bytes(j_hex->valuestring, buf, &len, sizeof(buf))) {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("invalid hex data"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (cJSON_IsString(j_data)) {
|
|
||||||
/* Plain text data */
|
|
||||||
len = strlen(j_data->valuestring);
|
|
||||||
if (len > sizeof(buf)) {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("data too long (max 512 bytes)"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
memcpy(buf, j_data->valuestring, len);
|
|
||||||
} else {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("missing 'data' or 'data_hex'"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t err = esp_spp_write(classic_state.spp_handle, len, buf);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGE(SPP_TAG, "spp_write failed: %s", esp_err_to_name(err));
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString(esp_err_to_name(err)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(SPP_TAG, "SPP sent %zu bytes", len);
|
|
||||||
|
|
||||||
cJSON *data = cJSON_CreateObject();
|
|
||||||
cJSON_AddNumberToObject(data, "bytes_sent", (double)len);
|
|
||||||
uart_send_response(id, STATUS_OK, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void cmd_spp_disconnect(const char *id, cJSON *params)
|
|
||||||
{
|
|
||||||
(void)params;
|
|
||||||
|
|
||||||
if (!classic_state.enabled) {
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString("classic BT not enabled"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (classic_state.spp_handle == 0) {
|
|
||||||
uart_send_response(id, STATUS_OK,
|
|
||||||
cJSON_CreateString("no active connection"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t handle = classic_state.spp_handle;
|
|
||||||
esp_err_t err = esp_spp_disconnect(handle);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGE(SPP_TAG, "spp_disconnect failed: %s", esp_err_to_name(err));
|
|
||||||
uart_send_response(id, STATUS_ERROR,
|
|
||||||
cJSON_CreateString(esp_err_to_name(err)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(SPP_TAG, "SPP disconnect initiated (handle=%" PRIu32 ")", handle);
|
|
||||||
uart_send_response(id, STATUS_OK, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
void cmd_spp_status(const char *id, cJSON *params)
|
|
||||||
{
|
|
||||||
(void)params;
|
|
||||||
|
|
||||||
cJSON *data = cJSON_CreateObject();
|
|
||||||
cJSON_AddBoolToObject(data, "connected", classic_state.spp_handle != 0);
|
|
||||||
if (classic_state.spp_handle != 0) {
|
|
||||||
cJSON_AddNumberToObject(data, "handle", (double)classic_state.spp_handle);
|
|
||||||
if (classic_state.spp_remote_addr[0] != '\0') {
|
|
||||||
cJSON_AddStringToObject(data, "remote_address", classic_state.spp_remote_addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uart_send_response(id, STATUS_OK, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* State query */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
bool bt_classic_is_enabled(void)
|
|
||||||
{
|
|
||||||
return classic_state.enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Configure helpers (called from the configure command handler) */
|
/* Configure helpers (called from the configure command handler) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@ -800,16 +605,11 @@ void bt_classic_set_io_cap(const char *io_cap_str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void bt_classic_set_device_class(uint32_t cod_raw)
|
void bt_classic_set_device_class(uint32_t cod)
|
||||||
{
|
{
|
||||||
esp_bt_cod_t cod;
|
esp_err_t err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_ALL);
|
||||||
memset(&cod, 0, sizeof(cod));
|
|
||||||
cod.minor = (cod_raw >> 2) & 0x3F;
|
|
||||||
cod.major = (cod_raw >> 8) & 0x1F;
|
|
||||||
cod.service = (cod_raw >> 13) & 0x7FF;
|
|
||||||
esp_err_t err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR | ESP_BT_SET_COD_SERVICE_CLASS);
|
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "CoD set to 0x%06" PRIx32, cod_raw);
|
ESP_LOGI(TAG, "CoD set to 0x%06" PRIx32, cod);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "set_cod failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "set_cod failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
void bt_classic_init(void);
|
void bt_classic_init(void);
|
||||||
@ -17,14 +14,6 @@ void cmd_classic_set_discoverable(const char *id, cJSON *params);
|
|||||||
void cmd_classic_pair_respond(const char *id, cJSON *params);
|
void cmd_classic_pair_respond(const char *id, cJSON *params);
|
||||||
void cmd_classic_set_ssp_mode(const char *id, cJSON *params);
|
void cmd_classic_set_ssp_mode(const char *id, cJSON *params);
|
||||||
|
|
||||||
/* SPP command handlers */
|
|
||||||
void cmd_spp_send(const char *id, cJSON *params);
|
|
||||||
void cmd_spp_disconnect(const char *id, cJSON *params);
|
|
||||||
void cmd_spp_status(const char *id, cJSON *params);
|
|
||||||
|
|
||||||
/* State query */
|
|
||||||
bool bt_classic_is_enabled(void);
|
|
||||||
|
|
||||||
/* Called by configure command to set IO capabilities */
|
/* Called by configure command to set IO capabilities */
|
||||||
void bt_classic_set_io_cap(const char *io_cap_str);
|
void bt_classic_set_io_cap(const char *io_cap_str);
|
||||||
void bt_classic_set_device_class(uint32_t cod_raw);
|
void bt_classic_set_device_class(uint32_t cod);
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
#include "cmd_dispatcher.h"
|
#include "cmd_dispatcher.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "uart_handler.h"
|
#include "uart_handler.h"
|
||||||
#include "bt_classic.h"
|
|
||||||
#include "bt_ble.h"
|
|
||||||
#include "personas.h"
|
|
||||||
|
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "esp_chip_info.h"
|
#include "esp_chip_info.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_mac.h"
|
#include "esp_mac.h"
|
||||||
#include "esp_gap_bt_api.h"
|
#include "esp_bt.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -26,6 +23,11 @@ typedef struct {
|
|||||||
cmd_handler_t handler;
|
cmd_handler_t handler;
|
||||||
} cmd_entry_t;
|
} cmd_entry_t;
|
||||||
|
|
||||||
|
/* Forward declarations for handlers in other modules */
|
||||||
|
extern void bt_classic_register_commands(void);
|
||||||
|
extern void bt_ble_register_commands(void);
|
||||||
|
extern void personas_register_commands(void);
|
||||||
|
|
||||||
/* --- System command handlers --- */
|
/* --- System command handlers --- */
|
||||||
|
|
||||||
static void handle_ping(const char *id, cJSON *params)
|
static void handle_ping(const char *id, cJSON *params)
|
||||||
@ -95,92 +97,30 @@ static void handle_get_status(const char *id, cJSON *params)
|
|||||||
cJSON *data = cJSON_CreateObject();
|
cJSON *data = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(data, "uptime_ms", (double)uptime_ms);
|
cJSON_AddNumberToObject(data, "uptime_ms", (double)uptime_ms);
|
||||||
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
|
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
|
||||||
cJSON_AddBoolToObject(data, "bt_enabled", bt_classic_is_enabled());
|
cJSON_AddBoolToObject(data, "bt_enabled", cJSON_False);
|
||||||
cJSON_AddBoolToObject(data, "ble_enabled", bt_ble_is_enabled());
|
cJSON_AddBoolToObject(data, "ble_enabled", cJSON_False);
|
||||||
|
|
||||||
uart_send_response(id, STATUS_OK, data);
|
uart_send_response(id, STATUS_OK, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Configure command: sets device name, IO capabilities, CoD --- */
|
|
||||||
|
|
||||||
static void handle_configure(const char *id, cJSON *params)
|
|
||||||
{
|
|
||||||
if (!params) {
|
|
||||||
cJSON *err = cJSON_CreateObject();
|
|
||||||
cJSON_AddStringToObject(err, "error", "params required");
|
|
||||||
uart_send_response(id, STATUS_ERROR, err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cJSON *applied = cJSON_CreateObject();
|
|
||||||
|
|
||||||
cJSON *name = cJSON_GetObjectItem(params, "name");
|
|
||||||
if (name && cJSON_IsString(name)) {
|
|
||||||
esp_bt_gap_set_device_name(name->valuestring);
|
|
||||||
cJSON_AddStringToObject(applied, "name", name->valuestring);
|
|
||||||
}
|
|
||||||
|
|
||||||
cJSON *io_cap = cJSON_GetObjectItem(params, "io_cap");
|
|
||||||
if (io_cap && cJSON_IsString(io_cap)) {
|
|
||||||
bt_classic_set_io_cap(io_cap->valuestring);
|
|
||||||
cJSON_AddStringToObject(applied, "io_cap", io_cap->valuestring);
|
|
||||||
}
|
|
||||||
|
|
||||||
cJSON *device_class = cJSON_GetObjectItem(params, "device_class");
|
|
||||||
if (device_class && cJSON_IsNumber(device_class)) {
|
|
||||||
bt_classic_set_device_class((uint32_t)device_class->valuedouble);
|
|
||||||
cJSON_AddNumberToObject(applied, "device_class", device_class->valuedouble);
|
|
||||||
}
|
|
||||||
|
|
||||||
uart_send_response(id, STATUS_OK, applied);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Command table --- */
|
/* --- Command table --- */
|
||||||
|
|
||||||
static const cmd_entry_t cmd_table[] = {
|
static const cmd_entry_t cmd_table[] = {
|
||||||
/* System */
|
{ CMD_PING, handle_ping },
|
||||||
{ CMD_PING, handle_ping },
|
{ CMD_RESET, handle_reset },
|
||||||
{ CMD_RESET, handle_reset },
|
{ CMD_GET_INFO, handle_get_info },
|
||||||
{ CMD_GET_INFO, handle_get_info },
|
{ CMD_GET_STATUS, handle_get_status },
|
||||||
{ CMD_GET_STATUS, handle_get_status },
|
|
||||||
|
|
||||||
/* Configuration */
|
|
||||||
{ CMD_CONFIGURE, handle_configure },
|
|
||||||
{ CMD_LOAD_PERSONA, cmd_load_persona },
|
|
||||||
{ CMD_LIST_PERSONAS, cmd_list_personas },
|
|
||||||
|
|
||||||
/* Classic BT */
|
|
||||||
{ CMD_CLASSIC_ENABLE, cmd_classic_enable },
|
|
||||||
{ CMD_CLASSIC_DISABLE, cmd_classic_disable },
|
|
||||||
{ CMD_CLASSIC_SET_DISCOVERABLE,cmd_classic_set_discoverable },
|
|
||||||
{ CMD_CLASSIC_PAIR_RESPOND, cmd_classic_pair_respond },
|
|
||||||
{ CMD_CLASSIC_SET_SSP_MODE, cmd_classic_set_ssp_mode },
|
|
||||||
|
|
||||||
/* SPP */
|
|
||||||
{ CMD_SPP_SEND, cmd_spp_send },
|
|
||||||
{ CMD_SPP_DISCONNECT, cmd_spp_disconnect },
|
|
||||||
{ CMD_SPP_STATUS, cmd_spp_status },
|
|
||||||
|
|
||||||
/* BLE */
|
|
||||||
{ CMD_BLE_ENABLE, cmd_ble_enable },
|
|
||||||
{ CMD_BLE_DISABLE, cmd_ble_disable },
|
|
||||||
{ CMD_BLE_ADVERTISE, cmd_ble_advertise },
|
|
||||||
{ CMD_BLE_SET_ADV_DATA, cmd_ble_set_adv_data },
|
|
||||||
|
|
||||||
/* GATT */
|
|
||||||
{ CMD_GATT_ADD_SERVICE, cmd_gatt_add_service },
|
|
||||||
{ CMD_GATT_ADD_CHARACTERISTIC, cmd_gatt_add_characteristic },
|
|
||||||
{ CMD_GATT_SET_VALUE, cmd_gatt_set_value },
|
|
||||||
{ CMD_GATT_NOTIFY, cmd_gatt_notify },
|
|
||||||
{ CMD_GATT_CLEAR, cmd_gatt_clear },
|
|
||||||
|
|
||||||
{ NULL, NULL } /* sentinel */
|
{ NULL, NULL } /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
void cmd_dispatcher_init(void)
|
void cmd_dispatcher_init(void)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "command dispatcher ready (%d commands)",
|
ESP_LOGI(TAG, "command dispatcher ready");
|
||||||
(int)(sizeof(cmd_table) / sizeof(cmd_table[0])) - 1);
|
|
||||||
|
/* Future: these will register their own handlers into the table */
|
||||||
|
/* bt_classic_register_commands(); */
|
||||||
|
/* bt_ble_register_commands(); */
|
||||||
|
/* personas_register_commands(); */
|
||||||
}
|
}
|
||||||
|
|
||||||
void cmd_dispatch(const char *id, const char *cmd, cJSON *params)
|
void cmd_dispatch(const char *id, const char *cmd, cJSON *params)
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "uart_handler.h"
|
#include "uart_handler.h"
|
||||||
#include "event_reporter.h"
|
|
||||||
#include "cmd_dispatcher.h"
|
#include "cmd_dispatcher.h"
|
||||||
|
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
@ -58,7 +57,6 @@ void app_main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
uart_handler_init();
|
uart_handler_init();
|
||||||
event_reporter_init();
|
|
||||||
send_boot_event();
|
send_boot_event();
|
||||||
cmd_dispatcher_init();
|
cmd_dispatcher_init();
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
#include "bt_ble.h"
|
#include "bt_ble.h"
|
||||||
|
|
||||||
#include "esp_bt_device.h"
|
#include "esp_bt_device.h"
|
||||||
#include "esp_gap_bt_api.h"
|
|
||||||
#include "esp_gap_ble_api.h"
|
#include "esp_gap_ble_api.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
@ -175,7 +174,7 @@ void cmd_load_persona(const char *id, cJSON *params)
|
|||||||
|
|
||||||
/* --- Set device name on both stacks --- */
|
/* --- Set device name on both stacks --- */
|
||||||
if (p->classic_enabled) {
|
if (p->classic_enabled) {
|
||||||
esp_bt_gap_set_device_name(p->device_name);
|
esp_bt_dev_set_device_name(p->device_name);
|
||||||
}
|
}
|
||||||
if (p->ble_enabled) {
|
if (p->ble_enabled) {
|
||||||
esp_ble_gap_set_device_name(p->device_name);
|
esp_ble_gap_set_device_name(p->device_name);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
|
|
||||||
/* UART configuration */
|
/* UART configuration */
|
||||||
#define PROTO_UART_NUM UART_NUM_0
|
#define PROTO_UART_NUM UART_NUM_1
|
||||||
#define PROTO_BAUD_RATE 115200
|
#define PROTO_BAUD_RATE 115200
|
||||||
#define PROTO_TX_BUF_SIZE 4096
|
#define PROTO_TX_BUF_SIZE 4096
|
||||||
#define PROTO_RX_BUF_SIZE 4096
|
#define PROTO_RX_BUF_SIZE 4096
|
||||||
@ -36,11 +36,6 @@
|
|||||||
#define CMD_CLASSIC_SET_DISCOVERABLE "classic_set_discoverable"
|
#define CMD_CLASSIC_SET_DISCOVERABLE "classic_set_discoverable"
|
||||||
#define CMD_CLASSIC_PAIR_RESPOND "classic_pair_respond"
|
#define CMD_CLASSIC_PAIR_RESPOND "classic_pair_respond"
|
||||||
|
|
||||||
/* SPP commands */
|
|
||||||
#define CMD_SPP_SEND "spp_send"
|
|
||||||
#define CMD_SPP_DISCONNECT "spp_disconnect"
|
|
||||||
#define CMD_SPP_STATUS "spp_status"
|
|
||||||
|
|
||||||
/* BLE commands */
|
/* BLE commands */
|
||||||
#define CMD_BLE_ENABLE "ble_enable"
|
#define CMD_BLE_ENABLE "ble_enable"
|
||||||
#define CMD_BLE_DISABLE "ble_disable"
|
#define CMD_BLE_DISABLE "ble_disable"
|
||||||
@ -64,11 +59,6 @@
|
|||||||
#define EVT_GATT_WRITE "gatt_write"
|
#define EVT_GATT_WRITE "gatt_write"
|
||||||
#define EVT_GATT_SUBSCRIBE "gatt_subscribe"
|
#define EVT_GATT_SUBSCRIBE "gatt_subscribe"
|
||||||
|
|
||||||
/* SPP events */
|
|
||||||
#define EVT_SPP_DATA "spp_data"
|
|
||||||
#define EVT_SPP_CONNECT "spp_connect"
|
|
||||||
#define EVT_SPP_DISCONNECT "spp_disconnect"
|
|
||||||
|
|
||||||
/* SSP IO capabilities */
|
/* SSP IO capabilities */
|
||||||
#define IO_CAP_DISPLAY_ONLY "display_only"
|
#define IO_CAP_DISPLAY_ONLY "display_only"
|
||||||
#define IO_CAP_DISPLAY_YESNO "display_yesno"
|
#define IO_CAP_DISPLAY_YESNO "display_yesno"
|
||||||
|
|||||||
@ -14,11 +14,9 @@
|
|||||||
static const char *TAG = "uart";
|
static const char *TAG = "uart";
|
||||||
static SemaphoreHandle_t tx_mutex;
|
static SemaphoreHandle_t tx_mutex;
|
||||||
|
|
||||||
/*
|
/* Pin assignments -- keep UART0 free for ESP-IDF console/logging */
|
||||||
* Use UART0 — the dev board's USB bridge connects here.
|
#define UART_TX_PIN GPIO_NUM_4
|
||||||
* ESP-IDF console is disabled (CONFIG_ESP_CONSOLE_NONE=y) so we own UART0.
|
#define UART_RX_PIN GPIO_NUM_5
|
||||||
* Default UART0 pins (TX=GPIO1, RX=GPIO3) route through the USB bridge.
|
|
||||||
*/
|
|
||||||
|
|
||||||
void uart_handler_init(void)
|
void uart_handler_init(void)
|
||||||
{
|
{
|
||||||
@ -32,7 +30,8 @@ void uart_handler_init(void)
|
|||||||
};
|
};
|
||||||
|
|
||||||
ESP_ERROR_CHECK(uart_param_config(PROTO_UART_NUM, &cfg));
|
ESP_ERROR_CHECK(uart_param_config(PROTO_UART_NUM, &cfg));
|
||||||
/* UART0 default pins (TX=1, RX=3) — no set_pin needed */
|
ESP_ERROR_CHECK(uart_set_pin(PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
||||||
ESP_ERROR_CHECK(uart_driver_install(PROTO_UART_NUM,
|
ESP_ERROR_CHECK(uart_driver_install(PROTO_UART_NUM,
|
||||||
PROTO_RX_BUF_SIZE, PROTO_TX_BUF_SIZE,
|
PROTO_RX_BUF_SIZE, PROTO_TX_BUF_SIZE,
|
||||||
0, NULL, 0));
|
0, NULL, 0));
|
||||||
@ -40,8 +39,8 @@ void uart_handler_init(void)
|
|||||||
tx_mutex = xSemaphoreCreateMutex();
|
tx_mutex = xSemaphoreCreateMutex();
|
||||||
assert(tx_mutex);
|
assert(tx_mutex);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "UART%d ready (USB bridge @ %d baud)",
|
ESP_LOGI(TAG, "UART%d ready (TX=%d RX=%d @ %d baud)",
|
||||||
PROTO_UART_NUM, PROTO_BAUD_RATE);
|
PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN, PROTO_BAUD_RATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
char *uart_read_line(void)
|
char *uart_read_line(void)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ CONFIG_BT_CLASSIC_ENABLED=y
|
|||||||
CONFIG_BT_BLE_ENABLED=y
|
CONFIG_BT_BLE_ENABLED=y
|
||||||
CONFIG_BT_A2DP_ENABLE=n
|
CONFIG_BT_A2DP_ENABLE=n
|
||||||
CONFIG_BT_SPP_ENABLED=y
|
CONFIG_BT_SPP_ENABLED=y
|
||||||
|
CONFIG_BT_SSP_ENABLED=y
|
||||||
|
|
||||||
# GAP & GATTS
|
# GAP & GATTS
|
||||||
CONFIG_BT_GATTS_ENABLE=y
|
CONFIG_BT_GATTS_ENABLE=y
|
||||||
@ -13,19 +14,22 @@ CONFIG_BT_GATTC_ENABLE=n
|
|||||||
# Bluetooth controller
|
# Bluetooth controller
|
||||||
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
||||||
|
|
||||||
# UART — disable ESP-IDF console so our protocol handler owns UART0
|
# UART
|
||||||
CONFIG_ESP_CONSOLE_NONE=y
|
CONFIG_ESP_CONSOLE_UART_NUM=0
|
||||||
|
|
||||||
# Stack sizes (Bluetooth needs generous stacks)
|
# Stack sizes (Bluetooth needs generous stacks)
|
||||||
CONFIG_BT_BTU_TASK_STACK_SIZE=8192
|
CONFIG_BT_BTU_TASK_STACK_SIZE=8192
|
||||||
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
|
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
|
||||||
|
|
||||||
|
# NVS (for bonding storage)
|
||||||
|
CONFIG_NVS_ENABLED=y
|
||||||
|
|
||||||
# Logging — keep it reasonable
|
# Logging — keep it reasonable
|
||||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||||
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
||||||
|
|
||||||
# Partition table — large (1.5MB app) needed for dual-mode BT stack
|
# Partition table
|
||||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
CONFIG_PARTITION_TABLE_SINGLE_APP=y
|
||||||
|
|
||||||
# FreeRTOS
|
# FreeRTOS
|
||||||
CONFIG_FREERTOS_HZ=1000
|
CONFIG_FREERTOS_HZ=1000
|
||||||
|
|||||||
@ -60,11 +60,6 @@ CMD_CLASSIC_DISABLE = "classic_disable"
|
|||||||
CMD_CLASSIC_SET_DISCOVERABLE = "classic_set_discoverable"
|
CMD_CLASSIC_SET_DISCOVERABLE = "classic_set_discoverable"
|
||||||
CMD_CLASSIC_PAIR_RESPOND = "classic_pair_respond"
|
CMD_CLASSIC_PAIR_RESPOND = "classic_pair_respond"
|
||||||
|
|
||||||
# SPP (Serial Port Profile)
|
|
||||||
CMD_SPP_SEND = "spp_send"
|
|
||||||
CMD_SPP_DISCONNECT = "spp_disconnect"
|
|
||||||
CMD_SPP_STATUS = "spp_status"
|
|
||||||
|
|
||||||
# BLE
|
# BLE
|
||||||
CMD_BLE_ENABLE = "ble_enable"
|
CMD_BLE_ENABLE = "ble_enable"
|
||||||
CMD_BLE_DISABLE = "ble_disable"
|
CMD_BLE_DISABLE = "ble_disable"
|
||||||
@ -88,11 +83,6 @@ EVT_GATT_READ = "gatt_read"
|
|||||||
EVT_GATT_WRITE = "gatt_write"
|
EVT_GATT_WRITE = "gatt_write"
|
||||||
EVT_GATT_SUBSCRIBE = "gatt_subscribe"
|
EVT_GATT_SUBSCRIBE = "gatt_subscribe"
|
||||||
|
|
||||||
# SPP Events
|
|
||||||
EVT_SPP_DATA = "spp_data"
|
|
||||||
EVT_SPP_CONNECT = "spp_connect"
|
|
||||||
EVT_SPP_DISCONNECT = "spp_disconnect"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Protocol constants
|
# Protocol constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -148,15 +138,11 @@ class Response:
|
|||||||
def from_json(cls, line: str) -> Response:
|
def from_json(cls, line: str) -> Response:
|
||||||
"""Parse a JSON line known to be a response."""
|
"""Parse a JSON line known to be a response."""
|
||||||
obj = json.loads(line)
|
obj = json.loads(line)
|
||||||
raw_data = obj.get("data", {})
|
|
||||||
# Firmware may return a bare string on some error paths — normalise to dict
|
|
||||||
if isinstance(raw_data, str):
|
|
||||||
raw_data = {"error": raw_data}
|
|
||||||
return cls(
|
return cls(
|
||||||
type=MsgType(obj["type"]),
|
type=MsgType(obj["type"]),
|
||||||
id=obj["id"],
|
id=obj["id"],
|
||||||
status=Status(obj["status"]),
|
status=Status(obj["status"]),
|
||||||
data=raw_data if isinstance(raw_data, dict) else {},
|
data=obj.get("data", {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -217,14 +203,11 @@ def parse_message(line: str) -> Command | Response | Event:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if msg_type == MsgType.RESP:
|
if msg_type == MsgType.RESP:
|
||||||
raw_data = obj.get("data", {})
|
|
||||||
if isinstance(raw_data, str):
|
|
||||||
raw_data = {"error": raw_data}
|
|
||||||
return Response(
|
return Response(
|
||||||
type=MsgType.RESP,
|
type=MsgType.RESP,
|
||||||
id=obj["id"],
|
id=obj["id"],
|
||||||
status=Status(obj["status"]),
|
status=Status(obj["status"]),
|
||||||
data=raw_data if isinstance(raw_data, dict) else {},
|
data=obj.get("data", {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if msg_type == MsgType.EVENT:
|
if msg_type == MsgType.EVENT:
|
||||||
|
|||||||
@ -263,7 +263,7 @@ class SerialClient:
|
|||||||
_client: SerialClient | None = None
|
_client: SerialClient | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_client() -> SerialClient:
|
async def get_client() -> SerialClient:
|
||||||
"""Get the singleton serial client.
|
"""Get the singleton serial client.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
|||||||
@ -35,7 +35,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
response = await client.send_command(CMD_BLE_ENABLE)
|
response = await client.send_command(CMD_BLE_ENABLE)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -52,7 +52,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
response = await client.send_command(CMD_BLE_DISABLE)
|
response = await client.send_command(CMD_BLE_DISABLE)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -76,7 +76,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"enable": enable,
|
"enable": enable,
|
||||||
"interval_ms": interval_ms,
|
"interval_ms": interval_ms,
|
||||||
@ -108,7 +108,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if name is not None:
|
if name is not None:
|
||||||
params["name"] = name
|
params["name"] = name
|
||||||
@ -141,7 +141,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Dict containing the assigned service handle.
|
Dict containing the assigned service handle.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"uuid": uuid,
|
"uuid": uuid,
|
||||||
"primary": primary,
|
"primary": primary,
|
||||||
@ -175,7 +175,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Dict containing the assigned characteristic handle.
|
Dict containing the assigned characteristic handle.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"service_handle": service_handle,
|
"service_handle": service_handle,
|
||||||
"uuid": uuid,
|
"uuid": uuid,
|
||||||
@ -207,7 +207,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
"char_handle": char_handle,
|
"char_handle": char_handle,
|
||||||
"value": value,
|
"value": value,
|
||||||
@ -231,7 +231,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {"char_handle": char_handle}
|
params: dict[str, Any] = {"char_handle": char_handle}
|
||||||
response = await client.send_command(CMD_GATT_NOTIFY, params)
|
response = await client.send_command(CMD_GATT_NOTIFY, params)
|
||||||
return response.data
|
return response.data
|
||||||
@ -250,7 +250,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status dict from the ESP32.
|
Status dict from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
response = await client.send_command(CMD_GATT_CLEAR)
|
response = await client.send_command(CMD_GATT_CLEAR)
|
||||||
return response.data
|
return response.data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@ -11,9 +11,6 @@ from ..protocol import (
|
|||||||
CMD_CLASSIC_ENABLE,
|
CMD_CLASSIC_ENABLE,
|
||||||
CMD_CLASSIC_PAIR_RESPOND,
|
CMD_CLASSIC_PAIR_RESPOND,
|
||||||
CMD_CLASSIC_SET_DISCOVERABLE,
|
CMD_CLASSIC_SET_DISCOVERABLE,
|
||||||
CMD_SPP_DISCONNECT,
|
|
||||||
CMD_SPP_SEND,
|
|
||||||
CMD_SPP_STATUS,
|
|
||||||
Status,
|
Status,
|
||||||
)
|
)
|
||||||
from ..serial_client import get_client
|
from ..serial_client import get_client
|
||||||
@ -33,7 +30,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data from the ESP32 including current BT state.
|
Response data from the ESP32 including current BT state.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
response = await client.send_command(CMD_CLASSIC_ENABLE)
|
response = await client.send_command(CMD_CLASSIC_ENABLE)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
return {"status": "ok", **response.data}
|
return {"status": "ok", **response.data}
|
||||||
@ -52,7 +49,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data from the ESP32 confirming BT is disabled.
|
Response data from the ESP32 confirming BT is disabled.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
response = await client.send_command(CMD_CLASSIC_DISABLE)
|
response = await client.send_command(CMD_CLASSIC_DISABLE)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
return {"status": "ok", **response.data}
|
return {"status": "ok", **response.data}
|
||||||
@ -78,7 +75,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data confirming the new discoverable state.
|
Response data confirming the new discoverable state.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params = {"discoverable": discoverable, "timeout": timeout}
|
params = {"discoverable": discoverable, "timeout": timeout}
|
||||||
response = await client.send_command(CMD_CLASSIC_SET_DISCOVERABLE, params)
|
response = await client.send_command(CMD_CLASSIC_SET_DISCOVERABLE, params)
|
||||||
if response.status == Status.OK:
|
if response.status == Status.OK:
|
||||||
@ -113,7 +110,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Response data with the pairing result from the ESP32.
|
Response data with the pairing result from the ESP32.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = await get_client()
|
||||||
params: dict[str, Any] = {"address": address, "accept": accept}
|
params: dict[str, Any] = {"address": address, "accept": accept}
|
||||||
if passkey is not None:
|
if passkey is not None:
|
||||||
params["passkey"] = passkey
|
params["passkey"] = passkey
|
||||||
@ -125,81 +122,3 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"error": str(exc)}
|
return {"error": str(exc)}
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
# SPP (Serial Port Profile) tools
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def esp32_spp_send(
|
|
||||||
data: str | None = None,
|
|
||||||
data_hex: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Send data over the SPP (Serial Port Profile) connection.
|
|
||||||
|
|
||||||
SPP provides a virtual serial port over Bluetooth. After a Linux host
|
|
||||||
connects via rfcomm or similar, this tool sends data to that connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Plain text data to send (UTF-8 encoded).
|
|
||||||
data_hex: Hex-encoded binary data to send (e.g., "48656c6c6f" for "Hello").
|
|
||||||
Use this for binary protocols. Takes precedence over 'data'.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response with bytes_sent count on success.
|
|
||||||
"""
|
|
||||||
if data is None and data_hex is None:
|
|
||||||
return {"error": "Either 'data' or 'data_hex' must be provided"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
client = get_client()
|
|
||||||
params: dict[str, Any] = {}
|
|
||||||
if data_hex is not None:
|
|
||||||
params["data_hex"] = data_hex
|
|
||||||
elif data is not None:
|
|
||||||
params["data"] = data
|
|
||||||
|
|
||||||
response = await client.send_command(CMD_SPP_SEND, params)
|
|
||||||
if response.status == Status.OK:
|
|
||||||
return {"status": "ok", **response.data}
|
|
||||||
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
|
||||||
except Exception as exc:
|
|
||||||
return {"error": str(exc)}
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def esp32_spp_disconnect() -> dict[str, Any]:
|
|
||||||
"""Disconnect the current SPP connection.
|
|
||||||
|
|
||||||
Terminates the active SPP (Serial Port Profile) connection if one exists.
|
|
||||||
The SPP server will continue listening for new connections.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response confirming the disconnection.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client = get_client()
|
|
||||||
response = await client.send_command(CMD_SPP_DISCONNECT)
|
|
||||||
if response.status == Status.OK:
|
|
||||||
return {"status": "ok", **response.data}
|
|
||||||
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
|
||||||
except Exception as exc:
|
|
||||||
return {"error": str(exc)}
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def esp32_spp_status() -> dict[str, Any]:
|
|
||||||
"""Get the current SPP connection status.
|
|
||||||
|
|
||||||
Returns information about whether an SPP connection is active,
|
|
||||||
the connection handle, and the remote device address.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response with connected (bool), handle, and remote_address if connected.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client = get_client()
|
|
||||||
response = await client.send_command(CMD_SPP_STATUS)
|
|
||||||
if response.status == Status.OK:
|
|
||||||
return {"status": "ok", **response.data}
|
|
||||||
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
|
||||||
except Exception as exc:
|
|
||||||
return {"error": str(exc)}
|
|
||||||
|
|||||||
@ -74,10 +74,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def esp32_set_ssp_mode(
|
async def esp32_set_ssp_mode(mode: str) -> dict[str, Any]:
|
||||||
mode: str,
|
|
||||||
auto_accept: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Set the Secure Simple Pairing (SSP) mode on the ESP32.
|
"""Set the Secure Simple Pairing (SSP) mode on the ESP32.
|
||||||
|
|
||||||
SSP mode determines which pairing association model the ESP32
|
SSP mode determines which pairing association model the ESP32
|
||||||
@ -93,11 +90,6 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
io_cap).
|
io_cap).
|
||||||
"passkey_display" — ESP32 displays a passkey for the
|
"passkey_display" — ESP32 displays a passkey for the
|
||||||
remote device to enter.
|
remote device to enter.
|
||||||
auto_accept: When true, the ESP32 automatically confirms
|
|
||||||
numeric comparison pairing requests without waiting for
|
|
||||||
a classic_pair_respond command. Useful for automated
|
|
||||||
E2E testing where both sides need to confirm but the
|
|
||||||
MCP tool calls are sequential.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response data confirming the new SSP mode, or an error dict
|
Response data confirming the new SSP mode, or an error dict
|
||||||
@ -107,13 +99,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
if mode not in valid_modes:
|
if mode not in valid_modes:
|
||||||
return {"error": f"Invalid SSP mode '{mode}'. Must be one of: {sorted(valid_modes)}"}
|
return {"error": f"Invalid SSP mode '{mode}'. Must be one of: {sorted(valid_modes)}"}
|
||||||
|
|
||||||
params: dict[str, Any] = {"mode": mode}
|
|
||||||
if auto_accept:
|
|
||||||
params["auto_accept"] = True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = get_client()
|
client = get_client()
|
||||||
resp = await client.send_command(CMD_CLASSIC_SET_SSP_MODE, params)
|
resp = await client.send_command(CMD_CLASSIC_SET_SSP_MODE, {"mode": mode})
|
||||||
if resp.status == Status.OK:
|
if resp.status == Status.OK:
|
||||||
return resp.data
|
return resp.data
|
||||||
return {"error": resp.data.get("error", "Failed to set SSP mode")}
|
return {"error": resp.data.get("error", "Failed to set SSP mode")}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
from ..protocol import CMD_GET_INFO, CMD_GET_STATUS, CMD_PING, CMD_RESET, Status
|
from ..protocol import CMD_GET_INFO, CMD_GET_STATUS, CMD_PING, CMD_RESET, Status
|
||||||
from ..serial_client import CommandTimeout, NotConnected, get_client, init_client
|
from ..serial_client import NotConnected, get_client, init_client
|
||||||
|
|
||||||
|
|
||||||
def register_tools(mcp: FastMCP) -> None:
|
def register_tools(mcp: FastMCP) -> None:
|
||||||
@ -32,10 +32,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Connection status including port, baudrate, and whether the
|
Connection status including port, baudrate, and whether the
|
||||||
device responded with a boot event.
|
device responded with a boot event.
|
||||||
"""
|
"""
|
||||||
from ..serial_client import get_client_or_none
|
try:
|
||||||
|
client = get_client()
|
||||||
client = get_client_or_none()
|
except NotConnected:
|
||||||
if client is None:
|
|
||||||
client = init_client(port=port, baudrate=baudrate)
|
client = init_client(port=port, baudrate=baudrate)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -46,30 +45,16 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
# Give the ESP32 a moment to send its boot event
|
# Give the ESP32 a moment to send its boot event
|
||||||
boot_received = False
|
boot_received = False
|
||||||
try:
|
try:
|
||||||
event = await client.event_queue.wait_for(event_name="boot", timeout=2.0)
|
event = await asyncio.wait_for(client.wait_event("boot"), timeout=2.0)
|
||||||
boot_received = event is not None
|
boot_received = event is not None
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Ready probe: retry ping until the firmware's command handler is up.
|
|
||||||
# The boot event fires early in app_main, before the UART command
|
|
||||||
# task is fully initialised, so the first command can get lost.
|
|
||||||
ready = False
|
|
||||||
for attempt in range(5):
|
|
||||||
try:
|
|
||||||
resp = await client.send_command(CMD_PING, timeout=1.0)
|
|
||||||
if resp.status == Status.OK:
|
|
||||||
ready = True
|
|
||||||
break
|
|
||||||
except (CommandTimeout, NotConnected):
|
|
||||||
await asyncio.sleep(0.3)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"connected": True,
|
"connected": True,
|
||||||
"port": port,
|
"port": port,
|
||||||
"baudrate": baudrate,
|
"baudrate": baudrate,
|
||||||
"boot_event": boot_received,
|
"boot_event": boot_received,
|
||||||
"ready": ready,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
# E2E Test Prompts
|
|
||||||
|
|
||||||
These markdown files are test prompts for running automated E2E Bluetooth tests using Claude CLI in headless mode.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /tmp/bt-e2e-test
|
|
||||||
claude -p "$(cat test-prompt-v5.md)" \
|
|
||||||
--mcp-config .mcp.json \
|
|
||||||
--allowedTools "mcp__esp32__*,mcp__bluez__*" \
|
|
||||||
--output-format json
|
|
||||||
```
|
|
||||||
|
|
||||||
See [automated-e2e-testing.md](../../docs/automated-e2e-testing.md) for full setup instructions.
|
|
||||||
|
|
||||||
## Test Prompts
|
|
||||||
|
|
||||||
| File | Tests | Coverage |
|
|
||||||
|------|-------|----------|
|
|
||||||
| `test-prompt-v4.md` | 71 | Classic BT, BLE GATT, HCI capture |
|
|
||||||
| `test-prompt-v5.md` | 76 | v4 + Battery Service, bt_ble_battery |
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- ESP32 with mcbluetooth-esp32 firmware
|
|
||||||
- Linux host with Bluetooth adapter
|
|
||||||
- Both MCP servers configured in `.mcp.json`
|
|
||||||
- btmon with CAP_NET_RAW for HCI capture tests
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
# ESP32 + BlueZ Full E2E Bluetooth Test Suite (v4 - Extended Coverage)
|
|
||||||
|
|
||||||
You have two MCP servers available:
|
|
||||||
- **esp32** (prefix: `mcp__esp32__`) — controls an ESP32 dev board via serial/UART
|
|
||||||
- **bluez** (prefix: `mcp__bluez__`) — controls the Linux BlueZ Bluetooth stack
|
|
||||||
|
|
||||||
Run ALL tests IN ORDER. For each test, report PASS or FAIL with actual response data.
|
|
||||||
If a test fails, note the error and continue with remaining tests.
|
|
||||||
|
|
||||||
**Important**: The Linux adapter is `hci1` (not hci0). Use `hci1` for all BlueZ calls.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: ESP32 Connection & System (Tests 1-4)
|
|
||||||
|
|
||||||
1. **Connect**: Call `esp32_connect` — should return connected=true AND ready=true
|
|
||||||
2. **Ping**: Call `esp32_ping` — expect `{"pong": true}`
|
|
||||||
3. **Get Info**: Call `esp32_get_info` — note the chip model, FW version, and BT MAC address (you'll need the MAC later)
|
|
||||||
4. **Get Status**: Call `esp32_status` — confirm bt_enabled=false, ble_enabled=false
|
|
||||||
|
|
||||||
## Phase 2: BlueZ Adapter Deep Dive (Tests 5-8)
|
|
||||||
|
|
||||||
5. **List Adapters**: Call `bt_list_adapters` — find the powered adapter, note its name (probably hci1)
|
|
||||||
6. **Adapter Info**: Call `bt_adapter_info` with the adapter name — report full details including the adapter's MAC address
|
|
||||||
7. **Set Pairable**: Call `bt_adapter_pairable` with adapter, on=true — enable pairing acceptance
|
|
||||||
8. **Set Discoverable**: Call `bt_adapter_discoverable` with adapter, on=true, timeout=300 — make Linux visible
|
|
||||||
|
|
||||||
## Phase 3: Classic BT + SSP Pairing (Tests 9-24)
|
|
||||||
|
|
||||||
IMPORTANT: Enable Classic BT FIRST, then configure IO capabilities.
|
|
||||||
|
|
||||||
9. **Enable Classic**: Call `esp32_classic_enable`
|
|
||||||
10. **Configure ESP32**: Call `esp32_configure` with name="SSP-Test-Device", io_cap="display_yesno"
|
|
||||||
11. **Set SSP Mode**: Call `esp32_set_ssp_mode` with mode="numeric_comparison", auto_accept=true
|
|
||||||
12. **Check Status**: Call `esp32_status` — confirm bt_enabled=true
|
|
||||||
13. **Set ESP32 Discoverable**: Call `esp32_classic_set_discoverable` with discoverable=true, timeout=120
|
|
||||||
14. **Scan from Linux**: Call `bt_scan` with adapter="hci1", mode="classic", timeout=10 — find the ESP32 by its MAC
|
|
||||||
|
|
||||||
Now initiate pairing:
|
|
||||||
|
|
||||||
15. **Check Pairing Status Before**: Call `bt_pairing_status` — should show no pending requests
|
|
||||||
16. **Start Pairing**: Call `bt_pair` with adapter="hci1", address=<ESP32 MAC>, pairing_mode="auto"
|
|
||||||
17. **Check ESP32 Pair Events**: Call `esp32_get_events` — look for pair_request and pair_complete events
|
|
||||||
18. **Verify Paired on Linux**: Call `bt_device_info` with adapter="hci1", address=<ESP32 MAC> — check paired=true
|
|
||||||
|
|
||||||
Post-pairing device management:
|
|
||||||
|
|
||||||
19. **List Known Devices**: Call `bt_list_devices` with adapter="hci1", filter="paired" — ESP32 should be in the list
|
|
||||||
20. **Trust Device**: Call `bt_trust` with adapter="hci1", address=<ESP32 MAC>, trusted=true
|
|
||||||
21. **Set Alias**: Call `bt_device_set_alias` with adapter="hci1", address=<ESP32 MAC>, alias="My ESP32 Test Device"
|
|
||||||
22. **Block Device**: Call `bt_block` with adapter="hci1", address=<ESP32 MAC>, blocked=true — block the device
|
|
||||||
23. **Unblock Device**: Call `bt_block` with adapter="hci1", address=<ESP32 MAC>, blocked=false — unblock it
|
|
||||||
24. **Unpair**: Call `bt_unpair` with adapter="hci1", address=<ESP32 MAC> — clean up pairing
|
|
||||||
|
|
||||||
## Phase 4: Classic Cleanup + Event System (Tests 25-29)
|
|
||||||
|
|
||||||
25. **Disable Classic**: Call `esp32_classic_disable`
|
|
||||||
26. **Get All Events**: Call `esp32_get_events` — report ALL events captured so far
|
|
||||||
27. **Clear Events**: Call `esp32_clear_events`
|
|
||||||
28. **Verify Cleared**: Call `esp32_get_events` — should return empty list
|
|
||||||
29. **Check Status**: Call `esp32_status` — confirm bt_enabled=false
|
|
||||||
|
|
||||||
## Phase 5: BLE GATT Setup (Tests 30-38)
|
|
||||||
|
|
||||||
Set up ESP32 as a BLE peripheral with multiple characteristics:
|
|
||||||
|
|
||||||
30. **Enable BLE**: Call `esp32_ble_enable`
|
|
||||||
31. **Add Service**: Call `esp32_gatt_add_service` with uuid="0000181a-0000-1000-8000-00805f9b34fb" (Environmental Sensing), primary=true — save the service_handle
|
|
||||||
32. **Add Read+Notify Char**: Call `esp32_gatt_add_characteristic` with the service_handle, uuid="00002a6e-0000-1000-8000-00805f9b34fb" (Temperature), properties=["read","notify"] — save as temp_handle
|
|
||||||
33. **Add Read+Write Char**: Call `esp32_gatt_add_characteristic` with the service_handle, uuid="00002a6f-0000-1000-8000-00805f9b34fb" (Humidity), properties=["read","write"] — save as humidity_handle
|
|
||||||
34. **Set Temperature**: Call `esp32_gatt_set_value` with char_handle=temp_handle, value="e803" (25.0°C)
|
|
||||||
35. **Set Humidity**: Call `esp32_gatt_set_value` with char_handle=humidity_handle, value="3200" (50%)
|
|
||||||
36. **Set Adv Data**: Call `esp32_ble_set_adv_data` with name="BLE-Sensor-Test", service_uuids=["0000181a-0000-1000-8000-00805f9b34fb"]
|
|
||||||
37. **Start Advertising**: Call `esp32_ble_advertise` with enable=true
|
|
||||||
38. **Clear Events**: Call `esp32_clear_events` — clear event history before BLE tests
|
|
||||||
|
|
||||||
## Phase 6: HCI Capture + BLE Discovery (Tests 39-47)
|
|
||||||
|
|
||||||
Start HCI packet capture BEFORE connecting, then analyze it afterwards:
|
|
||||||
|
|
||||||
39. **Start HCI Capture**: Call `bt_capture_start` with adapter="hci1", output_file="/tmp/ble-gatt-capture.btsnoop"
|
|
||||||
40. **List Active Captures**: Call `bt_capture_list_active` — verify capture is running
|
|
||||||
41. **BLE Scan**: Call `bt_ble_scan` with adapter="hci1", timeout=10 — find the ESP32, verify name="BLE-Sensor-Test"
|
|
||||||
42. **Connect BLE**: Call `bt_connect` with adapter="hci1", address=<ESP32 MAC>
|
|
||||||
|
|
||||||
Wait 3 seconds for service discovery to complete, then:
|
|
||||||
|
|
||||||
43. **List Services**: Call `bt_ble_services` with adapter="hci1", address=<ESP32 MAC> — should see Environmental Sensing (181a)
|
|
||||||
44. **List Characteristics**: Call `bt_ble_characteristics` with adapter="hci1", address=<ESP32 MAC> — should see Temperature (2a6e) and Humidity (2a6f)
|
|
||||||
45. **Read Temperature**: Call `bt_ble_read` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb" — should return hex "e803"
|
|
||||||
46. **Check Read Event on ESP32**: Call `esp32_get_events` — look for a gatt_read event
|
|
||||||
47. **Stop HCI Capture**: Call `bt_capture_stop` with capture_id from test 39
|
|
||||||
|
|
||||||
## Phase 7: Analyze HCI Capture (Tests 48-50)
|
|
||||||
|
|
||||||
48. **Parse Capture**: Call `bt_capture_parse` with filepath="/tmp/ble-gatt-capture.btsnoop", max_packets=50 — report packet count and types
|
|
||||||
49. **Analyze Capture**: Call `bt_capture_analyze` with filepath="/tmp/ble-gatt-capture.btsnoop" — report statistics
|
|
||||||
50. **Read Raw Packets**: Call `bt_capture_read_raw` with filepath="/tmp/ble-gatt-capture.btsnoop", count=20 — show decoded packets
|
|
||||||
|
|
||||||
## Phase 8: BLE GATT Write + Notifications (Tests 51-58)
|
|
||||||
|
|
||||||
51. **Write Humidity**: Call `bt_ble_write` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6f-0000-1000-8000-00805f9b34fb", value="4b00", value_type="hex"
|
|
||||||
52. **Check Write Event**: Call `esp32_get_events` — look for a gatt_write event with the written value
|
|
||||||
53. **Subscribe Notifications**: Call `bt_ble_notify` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb", enable=true
|
|
||||||
54. **Check Subscribe Event**: Call `esp32_get_events` — look for a gatt_subscribe event
|
|
||||||
55. **Push Notification**: First call `esp32_gatt_set_value` with char_handle=temp_handle, value="f401" (50.0°C), then call `esp32_gatt_notify` with char_handle=temp_handle
|
|
||||||
56. **Read Updated Value**: Call `bt_ble_read` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb" — should now return "f401"
|
|
||||||
57. **Unsubscribe**: Call `bt_ble_notify` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb", enable=false
|
|
||||||
58. **Disconnect BLE**: Call `bt_disconnect` with adapter="hci1", address=<ESP32 MAC>
|
|
||||||
|
|
||||||
## Phase 9: BLE Cleanup (Tests 59-63)
|
|
||||||
|
|
||||||
59. **Stop Advertising**: Call `esp32_ble_advertise` with enable=false
|
|
||||||
60. **Clear GATT**: Call `esp32_gatt_clear`
|
|
||||||
61. **Disable BLE**: Call `esp32_ble_disable`
|
|
||||||
62. **Final Status**: Call `esp32_status` — bt_enabled=false, ble_enabled=false
|
|
||||||
63. **Clear ESP32 Events**: Call `esp32_clear_events`
|
|
||||||
|
|
||||||
## Phase 10: Adapter Self-Management (Tests 64-68)
|
|
||||||
|
|
||||||
These tests verify adapter configuration tools. Done at the end to not interfere with earlier tests.
|
|
||||||
|
|
||||||
64. **Get Original Alias**: Call `bt_adapter_info` with adapter="hci1" — note the current alias
|
|
||||||
65. **Set New Alias**: Call `bt_adapter_set_alias` with adapter="hci1", alias="E2E-Test-Adapter" — change the name
|
|
||||||
66. **Verify Alias Changed**: Call `bt_adapter_info` with adapter="hci1" — confirm alias is "E2E-Test-Adapter"
|
|
||||||
67. **Restore Original Alias**: Call `bt_adapter_set_alias` with adapter="hci1", alias=<original alias from test 64>
|
|
||||||
68. **Disable Discoverable**: Call `bt_adapter_discoverable` with adapter="hci1", on=false — clean up
|
|
||||||
|
|
||||||
## Phase 11: Final Cleanup (Tests 69-71)
|
|
||||||
|
|
||||||
69. **Reset ESP32**: Call `esp32_reset`
|
|
||||||
70. **Disconnect Serial**: Call `esp32_disconnect`
|
|
||||||
71. **Final Adapter Check**: Call `bt_adapter_info` with adapter="hci1" — verify adapter is still powered and healthy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
After all tests, print a DETAILED summary table:
|
|
||||||
|
|
||||||
| # | Test | Result | Notes |
|
|
||||||
|---|------|--------|-------|
|
|
||||||
| 1 | Connect | PASS/FAIL | ... |
|
|
||||||
| ... | ... | ... | ... |
|
|
||||||
|
|
||||||
Report:
|
|
||||||
- Total PASS/FAIL counts
|
|
||||||
- Whether pairing succeeded (phase 3)
|
|
||||||
- What ESP32 events were captured (phases 4, 6, 8)
|
|
||||||
- Whether GATT read returned expected value (test 45)
|
|
||||||
- Whether GATT write succeeded (test 51)
|
|
||||||
- Whether the updated value was readable after notification (test 56)
|
|
||||||
- HCI capture statistics (tests 48-50): packet count, types captured, any errors
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
# ESP32 + BlueZ Full E2E Bluetooth Test Suite (v5 - Complete Coverage)
|
|
||||||
|
|
||||||
You have two MCP servers available:
|
|
||||||
- **esp32** (prefix: `mcp__esp32__`) — controls an ESP32 dev board via serial/UART
|
|
||||||
- **bluez** (prefix: `mcp__bluez__`) — controls the Linux BlueZ Bluetooth stack
|
|
||||||
|
|
||||||
Run ALL tests IN ORDER. For each test, report PASS or FAIL with actual response data.
|
|
||||||
If a test fails, note the error and continue with remaining tests.
|
|
||||||
|
|
||||||
**Important**: The Linux adapter is `hci1` (not hci0). Use `hci1` for all BlueZ calls.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: ESP32 Connection & System (Tests 1-4)
|
|
||||||
|
|
||||||
1. **Connect**: Call `esp32_connect` — should return connected=true AND ready=true
|
|
||||||
2. **Ping**: Call `esp32_ping` — expect `{"pong": true}`
|
|
||||||
3. **Get Info**: Call `esp32_get_info` — note the chip model, FW version, and BT MAC address (you'll need the MAC later)
|
|
||||||
4. **Get Status**: Call `esp32_status` — confirm bt_enabled=false, ble_enabled=false
|
|
||||||
|
|
||||||
## Phase 2: BlueZ Adapter Deep Dive (Tests 5-8)
|
|
||||||
|
|
||||||
5. **List Adapters**: Call `bt_list_adapters` — find the powered adapter, note its name (probably hci1)
|
|
||||||
6. **Adapter Info**: Call `bt_adapter_info` with the adapter name — report full details including the adapter's MAC address
|
|
||||||
7. **Set Pairable**: Call `bt_adapter_pairable` with adapter, on=true — enable pairing acceptance
|
|
||||||
8. **Set Discoverable**: Call `bt_adapter_discoverable` with adapter, on=true, timeout=300 — make Linux visible
|
|
||||||
|
|
||||||
## Phase 3: Classic BT + SSP Pairing (Tests 9-24)
|
|
||||||
|
|
||||||
IMPORTANT: Enable Classic BT FIRST, then configure IO capabilities.
|
|
||||||
|
|
||||||
9. **Enable Classic**: Call `esp32_classic_enable`
|
|
||||||
10. **Configure ESP32**: Call `esp32_configure` with name="SSP-Test-Device", io_cap="display_yesno"
|
|
||||||
11. **Set SSP Mode**: Call `esp32_set_ssp_mode` with mode="numeric_comparison", auto_accept=true
|
|
||||||
12. **Check Status**: Call `esp32_status` — confirm bt_enabled=true
|
|
||||||
13. **Set ESP32 Discoverable**: Call `esp32_classic_set_discoverable` with discoverable=true, timeout=120
|
|
||||||
14. **Scan from Linux**: Call `bt_scan` with adapter="hci1", mode="classic", timeout=10 — find the ESP32 by its MAC
|
|
||||||
|
|
||||||
Now initiate pairing:
|
|
||||||
|
|
||||||
15. **Check Pairing Status Before**: Call `bt_pairing_status` — should show no pending requests
|
|
||||||
16. **Start Pairing**: Call `bt_pair` with adapter="hci1", address=<ESP32 MAC>, pairing_mode="auto"
|
|
||||||
17. **Check ESP32 Pair Events**: Call `esp32_get_events` — look for pair_request and pair_complete events
|
|
||||||
18. **Verify Paired on Linux**: Call `bt_device_info` with adapter="hci1", address=<ESP32 MAC> — check paired=true
|
|
||||||
|
|
||||||
Post-pairing device management:
|
|
||||||
|
|
||||||
19. **List Known Devices**: Call `bt_list_devices` with adapter="hci1", filter="paired" — ESP32 should be in the list
|
|
||||||
20. **Trust Device**: Call `bt_trust` with adapter="hci1", address=<ESP32 MAC>, trusted=true
|
|
||||||
21. **Set Alias**: Call `bt_device_set_alias` with adapter="hci1", address=<ESP32 MAC>, alias="My ESP32 Test Device"
|
|
||||||
22. **Block Device**: Call `bt_block` with adapter="hci1", address=<ESP32 MAC>, blocked=true — block the device
|
|
||||||
23. **Unblock Device**: Call `bt_block` with adapter="hci1", address=<ESP32 MAC>, blocked=false — unblock it
|
|
||||||
24. **Unpair**: Call `bt_unpair` with adapter="hci1", address=<ESP32 MAC> — clean up pairing
|
|
||||||
|
|
||||||
## Phase 4: Classic Cleanup + Event System (Tests 25-29)
|
|
||||||
|
|
||||||
25. **Disable Classic**: Call `esp32_classic_disable`
|
|
||||||
26. **Get All Events**: Call `esp32_get_events` — report ALL events captured so far
|
|
||||||
27. **Clear Events**: Call `esp32_clear_events`
|
|
||||||
28. **Verify Cleared**: Call `esp32_get_events` — should return empty list
|
|
||||||
29. **Check Status**: Call `esp32_status` — confirm bt_enabled=false
|
|
||||||
|
|
||||||
## Phase 5: BLE GATT Setup with Battery Service (Tests 30-42)
|
|
||||||
|
|
||||||
Set up ESP32 as a BLE peripheral with Environmental Sensing AND Battery Service:
|
|
||||||
|
|
||||||
30. **Enable BLE**: Call `esp32_ble_enable`
|
|
||||||
|
|
||||||
First, add Battery Service (0x180F) for bt_ble_battery test:
|
|
||||||
|
|
||||||
31. **Add Battery Service**: Call `esp32_gatt_add_service` with uuid="0000180f-0000-1000-8000-00805f9b34fb" (Battery Service), primary=true — save as battery_svc_handle
|
|
||||||
32. **Add Battery Level Char**: Call `esp32_gatt_add_characteristic` with service_handle=battery_svc_handle, uuid="00002a19-0000-1000-8000-00805f9b34fb" (Battery Level), properties=["read"] — save as battery_char_handle
|
|
||||||
33. **Set Battery Level**: Call `esp32_gatt_set_value` with char_handle=battery_char_handle, value="4b" (75%)
|
|
||||||
|
|
||||||
Now add Environmental Sensing Service:
|
|
||||||
|
|
||||||
34. **Add Env Service**: Call `esp32_gatt_add_service` with uuid="0000181a-0000-1000-8000-00805f9b34fb" (Environmental Sensing), primary=true — save as env_svc_handle
|
|
||||||
35. **Add Temperature Char**: Call `esp32_gatt_add_characteristic` with service_handle=env_svc_handle, uuid="00002a6e-0000-1000-8000-00805f9b34fb" (Temperature), properties=["read","notify"] — save as temp_handle
|
|
||||||
36. **Add Humidity Char**: Call `esp32_gatt_add_characteristic` with service_handle=env_svc_handle, uuid="00002a6f-0000-1000-8000-00805f9b34fb" (Humidity), properties=["read","write"] — save as humidity_handle
|
|
||||||
37. **Set Temperature**: Call `esp32_gatt_set_value` with char_handle=temp_handle, value="e803" (25.0°C)
|
|
||||||
38. **Set Humidity**: Call `esp32_gatt_set_value` with char_handle=humidity_handle, value="3200" (50%)
|
|
||||||
|
|
||||||
Configure advertising:
|
|
||||||
|
|
||||||
39. **Set Adv Data**: Call `esp32_ble_set_adv_data` with name="BLE-Sensor-Test", service_uuids=["0000180f-0000-1000-8000-00805f9b34fb", "0000181a-0000-1000-8000-00805f9b34fb"]
|
|
||||||
40. **Start Advertising**: Call `esp32_ble_advertise` with enable=true
|
|
||||||
41. **Clear Events**: Call `esp32_clear_events` — clear event history before BLE tests
|
|
||||||
42. **Check BLE Status**: Call `esp32_status` — confirm ble_enabled=true
|
|
||||||
|
|
||||||
## Phase 6: HCI Capture + BLE Discovery (Tests 43-51)
|
|
||||||
|
|
||||||
Start HCI packet capture BEFORE connecting:
|
|
||||||
|
|
||||||
43. **Start HCI Capture**: Call `bt_capture_start` with adapter="hci1", output_file="/tmp/ble-gatt-capture.btsnoop"
|
|
||||||
44. **List Active Captures**: Call `bt_capture_list_active` — verify capture is running
|
|
||||||
45. **BLE Scan**: Call `bt_ble_scan` with adapter="hci1", timeout=10 — find the ESP32, verify name="BLE-Sensor-Test"
|
|
||||||
46. **Connect BLE**: Call `bt_connect` with adapter="hci1", address=<ESP32 MAC>
|
|
||||||
|
|
||||||
Wait 3 seconds for service discovery to complete, then:
|
|
||||||
|
|
||||||
47. **List Services**: Call `bt_ble_services` with adapter="hci1", address=<ESP32 MAC> — should see Battery (180f) and Environmental Sensing (181a)
|
|
||||||
48. **List Characteristics**: Call `bt_ble_characteristics` with adapter="hci1", address=<ESP32 MAC> — should see Battery Level (2a19), Temperature (2a6e), Humidity (2a6f)
|
|
||||||
49. **Read Battery Level**: Call `bt_ble_battery` with adapter="hci1", address=<ESP32 MAC> — should return 75 (the value we set as "4b")
|
|
||||||
50. **Read Temperature**: Call `bt_ble_read` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb" — should return hex "e803"
|
|
||||||
51. **Check Read Events**: Call `esp32_get_events` — look for gatt_read events
|
|
||||||
|
|
||||||
Stop capture for analysis:
|
|
||||||
|
|
||||||
## Phase 7: Analyze HCI Capture (Tests 52-55)
|
|
||||||
|
|
||||||
52. **Stop HCI Capture**: Call `bt_capture_stop` with capture_id from test 43
|
|
||||||
53. **Parse Capture**: Call `bt_capture_parse` with filepath="/tmp/ble-gatt-capture.btsnoop", max_packets=50 — report packet count and types
|
|
||||||
54. **Analyze Capture**: Call `bt_capture_analyze` with filepath="/tmp/ble-gatt-capture.btsnoop" — report statistics
|
|
||||||
55. **Read Raw Packets**: Call `bt_capture_read_raw` with filepath="/tmp/ble-gatt-capture.btsnoop", count=20 — show decoded packets
|
|
||||||
|
|
||||||
## Phase 8: BLE GATT Write + Notifications (Tests 56-63)
|
|
||||||
|
|
||||||
56. **Write Humidity**: Call `bt_ble_write` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6f-0000-1000-8000-00805f9b34fb", value="4b00", value_type="hex"
|
|
||||||
57. **Check Write Event**: Call `esp32_get_events` — look for a gatt_write event with the written value
|
|
||||||
58. **Subscribe Notifications**: Call `bt_ble_notify` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb", enable=true
|
|
||||||
59. **Check Subscribe Event**: Call `esp32_get_events` — look for a gatt_subscribe event
|
|
||||||
60. **Push Notification**: First call `esp32_gatt_set_value` with char_handle=temp_handle, value="f401" (50.0°C), then call `esp32_gatt_notify` with char_handle=temp_handle
|
|
||||||
61. **Read Updated Value**: Call `bt_ble_read` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb" — should now return "f401"
|
|
||||||
62. **Unsubscribe**: Call `bt_ble_notify` with adapter="hci1", address=<ESP32 MAC>, char_uuid="00002a6e-0000-1000-8000-00805f9b34fb", enable=false
|
|
||||||
63. **Disconnect BLE**: Call `bt_disconnect` with adapter="hci1", address=<ESP32 MAC>
|
|
||||||
|
|
||||||
## Phase 9: BLE Cleanup (Tests 64-68)
|
|
||||||
|
|
||||||
64. **Stop Advertising**: Call `esp32_ble_advertise` with enable=false
|
|
||||||
65. **Clear GATT**: Call `esp32_gatt_clear`
|
|
||||||
66. **Disable BLE**: Call `esp32_ble_disable`
|
|
||||||
67. **Final Status**: Call `esp32_status` — bt_enabled=false, ble_enabled=false
|
|
||||||
68. **Clear ESP32 Events**: Call `esp32_clear_events`
|
|
||||||
|
|
||||||
## Phase 10: Adapter Self-Management (Tests 69-73)
|
|
||||||
|
|
||||||
These tests verify adapter configuration tools. Done at the end to not interfere with earlier tests.
|
|
||||||
|
|
||||||
69. **Get Original Alias**: Call `bt_adapter_info` with adapter="hci1" — note the current alias
|
|
||||||
70. **Set New Alias**: Call `bt_adapter_set_alias` with adapter="hci1", alias="E2E-Test-Adapter" — change the name
|
|
||||||
71. **Verify Alias Changed**: Call `bt_adapter_info` with adapter="hci1" — confirm alias is "E2E-Test-Adapter"
|
|
||||||
72. **Restore Original Alias**: Call `bt_adapter_set_alias` with adapter="hci1", alias=<original alias from test 69>
|
|
||||||
73. **Disable Discoverable**: Call `bt_adapter_discoverable` with adapter="hci1", on=false — clean up
|
|
||||||
|
|
||||||
## Phase 11: Final Cleanup (Tests 74-76)
|
|
||||||
|
|
||||||
74. **Reset ESP32**: Call `esp32_reset`
|
|
||||||
75. **Disconnect Serial**: Call `esp32_disconnect`
|
|
||||||
76. **Final Adapter Check**: Call `bt_adapter_info` with adapter="hci1" — verify adapter is still powered and healthy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
After all tests, print a DETAILED summary table:
|
|
||||||
|
|
||||||
| # | Test | Result | Notes |
|
|
||||||
|---|------|--------|-------|
|
|
||||||
| 1 | Connect | PASS/FAIL | ... |
|
|
||||||
| ... | ... | ... | ... |
|
|
||||||
|
|
||||||
Report:
|
|
||||||
- Total PASS/FAIL counts
|
|
||||||
- Whether pairing succeeded (phase 3)
|
|
||||||
- What ESP32 events were captured (phases 4, 6, 8)
|
|
||||||
- Whether bt_ble_battery returned the expected value 75 (test 49)
|
|
||||||
- Whether GATT read returned expected value (test 50)
|
|
||||||
- Whether GATT write succeeded (test 56)
|
|
||||||
- Whether the updated value was readable after notification (test 61)
|
|
||||||
- HCI capture statistics (tests 53-55): packet count, types captured, any errors
|
|
||||||
Loading…
x
Reference in New Issue
Block a user