Files

325 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).