Files
Go2_navigation/unitree_ui/unitree_ui-main/docs/bluetooth-v1-v2.md

11 KiB
Raw Blame 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 — 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 — 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
Unitree G1 < 1.5.1 V1 / V2
Unitree G1 ≥ 1.5.1 V1 / V2 + V3 extension

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.

Table of Contents


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:

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:

sudo usermod -aG bluetooth $USER
# Log out and back in for group changes to take effect

Required Services

BlueZ daemon must be running:

sudo systemctl enable --now bluetooth
systemctl is-active bluetooth  # should print "active"

Bluetooth Adapter

Check your HCI adapter(s):

hciconfig -a

If the adapter shows DOWN (common for USB dongles after plug-in), bring it up:

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

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

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
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. 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. For the BLE remote control and the WebRTC relay that forwards its inputs to the robot, see remote-control.md.