# UART Protocol Specification Authoritative reference for the JSON-over-UART protocol used between the Python MCP server (`mcbluetooth-esp32`) and the ESP32 firmware. ## Overview - **Format:** NDJSON (newline-delimited JSON) -- one JSON object per line, terminated by `\n` - **Baud rate:** 115200 - **Frame format:** 8N1 (8 data bits, no parity, 1 stop bit) - **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. - **Encoding:** UTF-8 - **Newlines:** `\n` only. Carriage returns (`\r`) are stripped by the firmware reader. There are three message types flowing over the link: | Direction | Type | Purpose | |-----------|------|---------| | Host --> ESP32 | `cmd` | Request an action or query | | ESP32 --> Host | `resp` | Direct reply to a command (correlated by `id`) | | ESP32 --> Host | `event` | Asynchronous notification (no correlation ID) | ## Message Formats ### Command (host --> ESP32) ```json {"type":"cmd","id":"1","cmd":"ping","params":{}} ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | yes | Always `"cmd"` | | `id` | string | yes | Monotonically increasing request ID. The ESP32 echoes this in the response. | | `cmd` | string | yes | Command name (see Command Reference below) | | `params` | object | no | Command-specific parameters. Omitted or `{}` if the command takes none. | ### Response (ESP32 --> host) ```json {"type":"resp","id":"1","status":"ok","data":{"pong":true}} ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | yes | Always `"resp"` | | `id` | string | yes | Echoed from the command that triggered this response | | `status` | string | yes | `"ok"` on success, `"error"` on failure | | `data` | object | yes | Command-specific payload. On error, contains `{"error":"description"}`. | ### Event (ESP32 --> host, unsolicited) ```json {"type":"event","event":"pair_request","data":{"address":"AA:BB:CC:DD:EE:FF","type":"numeric_comparison","passkey":123456},"ts":12345} ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | yes | Always `"event"` | | `event` | string | yes | Event name (see Event Reference below) | | `data` | object | yes | Event-specific payload | | `ts` | number | yes | Millisecond timestamp from `esp_timer_get_time() / 1000` (time since boot) | --- ## Command Reference ### System Commands | Command | Parameters | Response Data | Description | |---------|-----------|---------------|-------------| | `ping` | none | `{"pong":true}` | Heartbeat check. Verifies the serial link is alive. | | `reset` | none | `{}` | Reboots the ESP32. The response may not arrive before the link drops. | | `get_info` | none | `{"chip_model":"ESP32","features":["wifi","bt","ble"],"revision":3,"cores":2,"fw_version":"0.1.0","free_heap":240000,"bt_mac":"AA:BB:CC:DD:EE:FF"}` | Hardware and firmware details. | | `get_status` | none | `{"uptime_ms":12345,"free_heap":230000,"bt_enabled":false,"ble_enabled":false}` | Current device state. | ### Configuration Commands | Command | Parameters | Response Data | Description | |---------|-----------|---------------|-------------| | `configure` | `name` (string, optional), `io_cap` (string, optional), `device_class` (int, optional), `pin_code` (string, optional) | Applied config values | Set one or more Bluetooth properties atomically. Any parameter not sent retains its current value. | | `load_persona` | `persona` (string) | `{"persona":"headset","device_name":"BT Headset","io_cap":"no_io","classic":true,"ble":true,"device_class":"0x200404","services":[...]}` | Load a preset device profile. Sets name, IO capability, Class of Device, and advertised services in one operation. | | `list_personas` | none | `{"personas":[...]}` | List all available persona presets and their configurations. | | `classic_set_ssp_mode` | `mode` (string) | Applied mode | Set the SSP association model. See SSP Modes below. | **Valid `mode` values for `classic_set_ssp_mode`:** | Mode | Description | |------|-------------| | `just_works` | No user interaction. Lowest security. Both sides auto-accept. | | `numeric_comparison` | Both sides display a 6-digit passkey. User confirms they match. | | `passkey_entry` | Remote device must type a passkey displayed on the ESP32. | | `passkey_display` | ESP32 displays a passkey that the remote device enters. | ### Classic Bluetooth Commands | Command | Parameters | Response Data | Description | |---------|-----------|---------------|-------------| | `classic_enable` | none | BT state | Enable the BR/EDR (Classic Bluetooth) stack. | | `classic_disable` | none | BT state | Disable Classic Bluetooth. Device becomes invisible to inquiry scans. | | `classic_set_discoverable` | `discoverable` (bool), `timeout` (int, seconds, 0=forever) | Discoverable state | Control whether the ESP32 responds to Bluetooth inquiry scans. | | `classic_pair_respond` | `address` (string), `accept` (bool), `passkey` (int, optional), `pin` (string, optional) | Pairing result | Accept or reject an incoming pairing request from the given address. | ### BLE Commands | Command | Parameters | Response Data | Description | |---------|-----------|---------------|-------------| | `ble_enable` | none | BLE state | Initialize the BLE stack. Required before advertising or GATT operations. | | `ble_disable` | none | BLE state | Shut down BLE. Stops advertising and tears down GATT services. | | `ble_advertise` | `enable` (bool), `interval_ms` (int, default 100) | Advertising state | Start or stop BLE advertisement broadcasts. | | `ble_set_adv_data` | `name` (string, optional), `service_uuids` (string[], optional), `manufacturer_data` (hex string, optional) | Applied adv data | Configure the contents of BLE advertisement packets. Call before starting advertising. | ### GATT Commands | Command | Parameters | Response Data | Description | |---------|-----------|---------------|-------------| | `gatt_add_service` | `uuid` (string), `primary` (bool, default true) | `{"handle":N}` | Create a GATT service. Returns a handle used when adding characteristics. | | `gatt_add_characteristic` | `service_handle` (int), `uuid` (string), `properties` (string[]), `value` (hex string, optional) | `{"handle":N}` | Add a characteristic to a service. Properties: `"read"`, `"write"`, `"notify"`, `"indicate"`. | | `gatt_set_value` | `char_handle` (int), `value` (hex string) | `{}` | Update a characteristic's stored value. | | `gatt_notify` | `char_handle` (int) | `{}` | Push the current value to all subscribed BLE clients as a notification. | | `gatt_clear` | none | `{}` | Remove all services and characteristics, resetting the GATT server to blank. | **Hex string encoding:** Values are represented as lowercase hex strings without a prefix. For example, a temperature of 25.00 C encoded as a little-endian `int16` in hundredths of a degree would be `"c409"` (2500 = 0x09C4, little-endian = `c4 09`). --- ## Event Reference | Event | Data Fields | Trigger | |-------|-------------|---------| | `boot` | `fw_version` (string), `chip_model` (string), `cores` (int), `revision` (int), `free_heap` (int) | Emitted once immediately after firmware startup. | | `pair_request` | `address` (string), `type` (string: `"just_works"`, `"numeric_comparison"`, `"passkey_entry"`, `"legacy_pin"`), `passkey` (int) | A remote device has initiated pairing. Requires a `classic_pair_respond` to proceed. | | `pair_complete` | `address` (string), `success` (bool) | Pairing attempt finished (accepted, rejected, or timed out). | | `connect` | `address` (string), `transport` (string: `"classic"` or `"ble"`) | A remote device has established a connection. | | `disconnect` | `address` (string), `transport` (string: `"classic"` or `"ble"`) | A remote device has disconnected. | | `gatt_read` | `handle` (int), `address` (string) | A BLE client read a characteristic value. | | `gatt_write` | `handle` (int), `address` (string), `value` (hex string), `length` (int) | A BLE client wrote to a characteristic. | | `gatt_subscribe` | `handle` (int), `subscribed` (bool) | A BLE client enabled or disabled notifications on a characteristic. | --- ## IO Capabilities The `io_cap` parameter controls which Secure Simple Pairing (SSP) association model the Bluetooth stack negotiates. The pairing model is determined by the capabilities of **both** devices. | `io_cap` string | ESP-IDF Constant | Typical SSP Outcome | |-----------------|------------------|---------------------| | `display_only` | `ESP_IO_CAP_OUT` | Passkey Display (ESP32 shows, remote enters) | | `display_yesno` | `ESP_IO_CAP_IO` | Numeric Comparison (both display 6-digit code, user confirms) | | `keyboard_only` | `ESP_IO_CAP_IN` | Passkey Entry (remote displays, ESP32 enters) | | `no_io` | `ESP_IO_CAP_NONE` | Just Works (no user interaction, no MITM protection) | | `keyboard_display` | `ESP_IO_CAP_KBDISP` | Numeric Comparison or Passkey Entry depending on remote | **SSP negotiation matrix** (simplified, both sides must agree): | Local \ Remote | `no_io` | `display_only` | `display_yesno` | `keyboard_only` | `keyboard_display` | |----------------|---------|----------------|-----------------|-----------------|-------------------| | `no_io` | Just Works | Just Works | Just Works | Just Works | Just Works | | `display_only` | Just Works | Just Works | Just Works | Passkey Entry | Passkey Entry | | `display_yesno` | Just Works | Just Works | Numeric Comp | Passkey Entry | Numeric Comp | | `keyboard_only` | Just Works | Passkey Entry | Passkey Entry | Passkey Entry | Passkey Entry | | `keyboard_display` | Just Works | Passkey Entry | Numeric Comp | Passkey Entry | Numeric Comp | --- ## Error Handling ### Unknown commands An unrecognized command returns an error response with the original command echoed back: ```json {"type":"resp","id":"5","status":"error","data":{"error":"unknown_command","cmd":"foobar"}} ``` ### Invalid JSON If the host sends a line that does not parse as valid JSON, the firmware responds with `id` set to `"?"`: ```json {"type":"resp","id":"?","status":"error","data":"invalid JSON"} ``` ### Missing required fields If a command is missing required parameters, the firmware returns: ```json {"type":"resp","id":"3","status":"error","data":{"error":"missing 'persona' param"}} ``` ### Timeouts The host should use a **5-second default timeout** when waiting for a response. The Python `SerialClient` implements this via `asyncio.wait_for` and raises `CommandTimeout` on expiry. Exceptions: - The `reset` command may not produce a response at all (the device reboots before the reply flushes). Treat the link drop as success. - `classic_pair_respond` may take longer if the remote device is slow. Consider a 10-second timeout for pairing operations. ### Line overflow Lines exceeding 2048 bytes are discarded by both sides without any error response. The firmware resets its read buffer; the Python client logs a warning and drops the line. --- ## Wire Examples **Full ping/pong exchange:** ``` Host TX: {"type":"cmd","id":"1","cmd":"ping"}\n ESP32 TX: {"type":"resp","id":"1","status":"ok","data":{"pong":true}}\n ``` **Configure with IO capability:** ``` Host TX: {"type":"cmd","id":"2","cmd":"configure","params":{"name":"MyDevice","io_cap":"display_yesno"}}\n ESP32 TX: {"type":"resp","id":"2","status":"ok","data":{"name":"MyDevice","io_cap":"display_yesno"}}\n ``` **Boot event (ESP32 sends immediately after power-on):** ``` ESP32 TX: {"type":"event","event":"boot","data":{"fw_version":"0.1.0","chip_model":"ESP32","cores":2,"revision":3,"free_heap":283648},"ts":42}\n ``` **Pair request event followed by host response:** ``` ESP32 TX: {"type":"event","event":"pair_request","data":{"address":"AA:BB:CC:DD:EE:FF","type":"numeric_comparison","passkey":482901},"ts":15234}\n Host TX: {"type":"cmd","id":"7","cmd":"classic_pair_respond","params":{"address":"AA:BB:CC:DD:EE:FF","accept":true,"passkey":482901}}\n ESP32 TX: {"type":"resp","id":"7","status":"ok","data":{}}\n ESP32 TX: {"type":"event","event":"pair_complete","data":{"address":"AA:BB:CC:DD:EE:FF","success":true},"ts":15891}\n ``` --- ## Personas Personas are bundled device presets stored in the firmware. Loading one configures the device name, IO capability, Class of Device, and returns the list of GATT service UUIDs the persona expects. | Persona | Device Name | IO Capability | Class of Device | Classic | BLE | Services | |---------|------------|---------------|-----------------|---------|-----|----------| | `headset` | BT Headset | `no_io` | `0x200404` (Audio, Headset) | yes | yes | Battery (0x180F), Device Info (0x180A) | | `speaker` | BT Speaker | `no_io` | `0x200414` (Audio, Loudspeaker) | yes | yes | Battery (0x180F), Device Info (0x180A) | | `keyboard` | BT Keyboard | `keyboard_only` | `0x002540` (Peripheral, Keyboard) | yes | yes | HID (0x1812), Battery (0x180F) | | `sensor` | Environment Sensor | `no_io` | none (BLE only) | no | yes | Env Sensing (0x181A), Battery (0x180F) | | `phone` | Test Phone | `keyboard_display` | `0x5A020C` (Phone) | yes | yes | Phonebook (0x1130), Device Info (0x180A) | | `bare` | ESP32-Test | `display_yesno` | `0x1F00` (Uncategorized) | yes | yes | none |