Files

325 lines
11 KiB
Markdown
Raw Permalink Normal View History

# Bluetooth Protocol V1 / V2 — Unitree Robots
This document describes the legacy BLE provisioning protocol used to scan, connect, and configure Unitree robots (WiFi setup, serial number, AP MAC).
Related documents:
- [bluetooth-v3.md](bluetooth-v3.md) — V3 extension (`VERSION` 0xF1, `GCM_KEY` 0xF2) introduced on G1 firmware 1.5.1 and back-ported to Go2 1.1.15.
- [remote-control.md](remote-control.md) — BLE remote (`Unitree-*`) protocol and the WebRTC relay that forwards remote inputs to the robot.
## Firmware Compatibility
| Robot | Firmware | Service |
|---|---|---|
| Unitree Go2 | `< 1.1.11` | V1 / V2 (FFE0 service) |
| Unitree Go2 | `1.1.11 – 1.1.14` | V1 / V2 (NUS service) |
| Unitree Go2 | `≥ 1.1.15` | V1 / V2 (NUS) + [V3 extension](bluetooth-v3.md) |
| Unitree G1 | `< 1.5.1` | V1 / V2 |
| Unitree G1 | `≥ 1.5.1` | V1 / V2 + [V3 extension](bluetooth-v3.md) |
V1 and V2 share the same wire format, encryption, and command set. The only difference is the GATT service UUID used to expose the characteristics — see [Service UUIDs](#service-uuids).
## Table of Contents
- [System Requirements](#system-requirements)
- [Scanning](#scanning)
- [Service UUIDs](#service-uuids)
- [Encryption](#encryption)
- [Packet Format](#packet-format)
- [Robot Connection Flow](#robot-connection-flow)
- [Commands](#commands)
- [WiFi Configuration](#wifi-configuration)
---
## System Requirements
### Linux Packages
| Package | Purpose | Install |
|---|---|---|
| `bluez` | BlueZ daemon, `bluetoothctl`, `gatttool`, `hciconfig` | `sudo apt install bluez` |
The `bluez` package provides both the BlueZ D-Bus daemon (`bluetoothd`) used by `bleak` for the robot, and `gatttool` used by `pygatt` for the remote control.
### Python Packages
All listed in `server/requirements.txt`:
```
fastapi>=0.110.0 # REST API
uvicorn>=0.27.0 # ASGI server
bleak>=0.22.0 # BlueZ D-Bus BLE client (used for robot)
pycryptodome>=3.20.0 # AES-128-CFB encryption
pygatt>=4.0.0 # gatttool wrapper (used for remote control)
```
Install with:
```bash
pip install -r server/requirements.txt
```
### User Permissions
On **Ubuntu 22.04** (and some other distros), the default D-Bus policy at `/etc/dbus-1/system.d/bluetooth.conf` already allows any local user to communicate with `org.bluez`, so no group membership is needed.
On **Debian, Fedora, Arch, Ubuntu with hardened D-Bus policies**, add your user to the `bluetooth` group:
```bash
sudo usermod -aG bluetooth $USER
# Log out and back in for group changes to take effect
```
### Required Services
BlueZ daemon must be running:
```bash
sudo systemctl enable --now bluetooth
systemctl is-active bluetooth # should print "active"
```
### Bluetooth Adapter
Check your HCI adapter(s):
```bash
hciconfig -a
```
If the adapter shows `DOWN` (common for USB dongles after plug-in), bring it up:
```bash
sudo hciconfig hci0 up # or hci1, etc.
```
The BLE server exposes `/adapters` and `/adapter` endpoints to switch between adapters at runtime.
### What's NOT Required
- **Running as root** — not needed
- **`setcap CAP_NET_RAW` on binaries** — `gatttool` and `hcitool` delegate privileged ops to `bluetoothd` which already runs as root
- **Disabling AppArmor/SELinux** — default policies allow everything needed
### Troubleshooting
| Error | Fix |
|---|---|
| `Operation not permitted` during scan | Ensure `bluetoothd` is running (`systemctl status bluetooth`) |
| `org.bluez.Error.NotReady` | Adapter is DOWN — `sudo hciconfig hciX up` |
| `org.bluez.Error.NotAvailable: br-connection-profile-unavailable` (remote) | Normal for dual-mode remote on BlueZ 5.64; `pygatt` handles this automatically |
| `Could not connect to the system bus` | User not in `bluetooth` group on a distro that requires it |
| Adapter not found | USB dongle not recognized — check `dmesg | tail` for driver errors |
---
## Scanning
Robots and remotes are discovered via BLE advertisement.
### Device Name Prefixes
| Prefix | Device |
|--------|--------|
| `Go2_` | Unitree Go2 robot |
| `G1_` | Unitree G1 robot |
| `Unitree-` | BLE Remote Control (e.g. `Unitree-32KC0D`) — see [remote-control.md](remote-control.md) |
### Protocol Detection
The protocol variant is determined by which service UUID appears in the BLE advertisement:
| Service UUID | Variant | Firmware |
|---|---|---|
| `0000ffe0-0000-1000-8000-00805f9b34fb` | V1 (FFE0) | Go2 `< 1.1.11`, all G1 |
| `6e400001-b5a3-f393-e0a9-e50e24dcca9e` | V2 (NUS) | Go2 `≥ 1.1.11` |
---
## Service UUIDs
### V1 (FFE0)
| UUID | Role |
|---|---|
| `0000ffe0-0000-1000-8000-00805f9b34fb` | Service |
| `0000ffe1-0000-1000-8000-00805f9b34fb` | Notify (robot -> client) |
| `0000ffe2-0000-1000-8000-00805f9b34fb` | Write (client -> robot) |
### V2 (Nordic UART Service)
| UUID | Role |
|---|---|
| `6e400001-b5a3-f393-e0a9-e50e24dcca9e` | Service |
| `6e400002-b5a3-f393-e0a9-e50e24dcca9e` | Write / TX (client -> robot) |
| `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Notify / RX (robot -> client) |
Both variants use the same encryption, packet format, and command set — only the UUIDs differ.
---
## Encryption
All robot BLE packets are encrypted with AES-128-CFB before transmission and decrypted on receive.
| Parameter | Value |
|---|---|
| Algorithm | AES-128-CFB |
| Segment Size | 128 bits |
| Key | `df98b715d5c6ed2b25817b6f2554124a` |
| IV | `2841ae97419c2973296a0d4bdfe19a4f` |
```python
from Crypto.Cipher import AES
KEY = bytes.fromhex("df98b715d5c6ed2b25817b6f2554124a")
IV = bytes.fromhex("2841ae97419c2973296a0d4bdfe19a4f")
def encrypt(data: bytes) -> bytes:
return AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128).encrypt(data)
def decrypt(data: bytes) -> bytes:
return AES.new(KEY, AES.MODE_CFB, iv=IV, segment_size=128).decrypt(data)
```
> **Note:** The V3 extension (G1 ≥ 1.5.1, Go2 ≥ 1.1.15) sends a small set of additional commands **unencrypted** with a different magic prefix. See [bluetooth-v3.md](bluetooth-v3.md). On a V1/V2 connection, frames whose first byte is `0x00` (rather than the encrypted-frame distribution) belong to V3 and must be routed to the V3 handler before AES decryption is attempted.
---
## Packet Format
### Client -> Robot (Request)
After building the plaintext packet, it is AES-encrypted before writing to the GATT characteristic.
#### Simple Packet
```
[0x52] [length] [instruction] [data...] [checksum]
```
| Field | Size | Description |
|---|---|---|
| Header | 1 | Always `0x52` |
| Length | 1 | `len(data) + 4` (counts header, length, instruction, checksum) |
| Instruction | 1 | Command ID |
| Data | 0-N | Command payload |
| Checksum | 1 | `(-sum(header..data)) & 0xFF` |
#### Chunked Packet
Used when data exceeds the 14-byte chunk limit (SSID, password, handshake).
```
[0x52] [length] [instruction] [chunk_idx] [total_chunks] [data...] [checksum]
```
| Field | Size | Description |
|---|---|---|
| Header | 1 | Always `0x52` |
| Length | 1 | `len(data) + 6` (counts header through data + checksum) |
| Instruction | 1 | Command ID |
| Chunk Index | 1 | 1-based index of this chunk |
| Total Chunks | 1 | Total number of chunks |
| Data | 1-14 | Chunk payload (max `CHUNK_SIZE = 14` bytes) |
| Checksum | 1 | `(-sum(header..data)) & 0xFF` |
### Robot -> Client (Response)
Received as encrypted bytes on the notify characteristic, then AES-decrypted.
```
[0x51] [length] [instruction] [status] [data...] [checksum]
```
| Field | Size | Description |
|---|---|---|
| Header | 1 | Always `0x51` |
| Length | 1 | Payload length |
| Instruction | 1 | Echoed command ID |
| Status | 1 | `0x01` = success |
| Data | 0-N | Response payload |
| Checksum | 1 | `(-sum(header..data)) & 0xFF` |
Chunked responses (e.g. GET_SN) include `chunk_idx` and `total_chunks` after the instruction byte, same as the request format.
---
## Robot Connection Flow
```
1. BLE Scan -> Find device with name prefix Go2_ or G1_
2. GATT Connect -> Connect to the device
3. Detect Protocol -> Check services for NUS (V2) or FFE0 (V1)
4. Subscribe -> Enable notifications on the notify characteristic
5. Handshake -> Send chunked packet: instruction=0x01, data="unitree"
6. Verify -> Response status byte == 0x01 means success
```
### Handshake Detail
The handshake sends the string `"unitree"` (7 bytes) as a chunked packet with `idx=1, total=1`:
```
Plaintext: [0x52] [0x0D] [0x01] [0x01] [0x01] [u] [n] [i] [t] [r] [e] [e] [checksum]
header len=13 CMD idx total -------- "unitree" --------
```
This is then AES-encrypted and written to the write characteristic.
---
## Commands
| Command | ID | Format | Description |
|---|---|---|---|
| HANDSHAKE | `0x01` | Chunked | Auth with `"unitree"` |
| GET_SN | `0x02` | Simple (no data) | Request serial number (response is chunked) |
| WIFI_TYPE | `0x03` | Simple | Set WiFi mode: `0x01`=AP, `0x02`=STA |
| WIFI_SSID | `0x04` | Chunked | Send SSID (up to 14 bytes per chunk) |
| WIFI_PWD | `0x05` | Chunked | Send password (up to 14 bytes per chunk) |
| COUNTRY | `0x06` | Simple | Set country code: `[0x01] + "US\x00"` |
| GET_AP_MAC | `0x07` | Simple (no data) | Request AP MAC address |
| DISCONNECT | `0x08` | Simple (no data) | Disconnect BLE |
| HEARTBEAT | `0x0A` | Simple (no data) | Keep-alive |
### Response Parsing
- **GET_SN**: Comes as multiple chunked response packets. Reassemble chunks by index, decode as UTF-8.
- **GET_AP_MAC**: MAC bytes at `response[3:length-1]`, format as `XX:XX:XX:XX:XX:XX`.
- **WIFI_TYPE/SSID/PWD/COUNTRY**: Success if `response[3] == 0x01`.
---
## WiFi Configuration
Full WiFi setup sequence after handshake:
```
1. Set mode -> WIFI_TYPE (0x03) with 0x01 (AP) or 0x02 (STA)
2. Send SSID -> WIFI_SSID (0x04) chunked, 14 bytes per chunk, 50ms between
3. Send password -> WIFI_PWD (0x05) chunked, 14 bytes per chunk, 100ms between
4. Set country -> COUNTRY (0x06) with [0x01] + country_code + \x00
```
Each step waits for a success response before proceeding. The password step has a longer timeout (15s) as the robot applies the WiFi configuration.
> **Security note (CVE-2025-35027):** On Go2 firmware up to and including 1.1.11, the robot passes SSID and password to shell scripts via `system()` without sanitization, allowing command injection.
---
## Quick Reference
| Constant | Value |
|---|---|
| AES Key | `df98b715d5c6ed2b25817b6f2554124a` |
| AES IV | `2841ae97419c2973296a0d4bdfe19a4f` |
| Request header | `0x52` |
| Response header | `0x51` |
| Chunk size | 14 bytes |
| Handshake string | `"unitree"` |
| WiFi AP mode byte | `0x01` |
| WiFi STA mode byte | `0x02` |
| Success status | `0x01` |
For the V3 extension (`VERSION` 0xF1, `GCM_KEY` 0xF2 — G1 firmware ≥ 1.5.1 and Go2 firmware ≥ 1.1.15), see [bluetooth-v3.md](bluetooth-v3.md). For the BLE remote control and the WebRTC relay that forwards its inputs to the robot, see [remote-control.md](remote-control.md).