Protocol Specification
bleRPC uses a layered protocol stack on top of BLE GATT. The Command Layer wraps Protocol Buffers payloads with RPC metadata, the optional Encryption Layer encrypts the serialized command, and the Container Layer fragments them into MTU-sized packets for BLE transmission.
Connection Setup Sequence
Before any RPC calls, a central runs a short setup sequence with the peripheral. This is the overall timeline — each step is detailed in the sections below (and in Encryption for the key exchange).
Command Layer
The command layer wraps Protocol Buffers-encoded data with RPC metadata into a CommandPacket. Commands are identified by name (ASCII string), enabling flexible, human-readable dispatch.
Command Format
| Field | Bits | Description |
|---|---|---|
type | 1 | 0 = request, 1 = response |
reserved | 7 | Zero-filled |
cmd_name_len | 8 | Length of the command name in bytes |
cmd_name | variable | ASCII command name (e.g., echo, flash_read) |
data_len | 16 | Length of the protobuf data (bytes, little-endian) |
data | variable | Protocol Buffers encoded payload |
Command Discovery
The code generator derives commands from your .proto file in one of two ways:
- Service definitions (preferred) — if the file declares a
service { ... }block, eachrpcmethod becomes a command. The command name is the method name insnake_case, and its request/response types are taken from the method signature. - Naming convention (fallback) — when no service is defined, matching
*Request/*Responsemessage pairs produce a command whose name is the prefix converted tosnake_case:
| Message Pair | Command Name |
|---|---|
EchoRequest + EchoResponse | echo |
FlashReadRequest + FlashReadResponse | flash_read |
DataWriteRequest + DataWriteResponse | data_write |
Streaming commands are registered separately — either via streaming rpc methods in a service block, or by listing them in proto/streaming.txt (e.g. counter_stream p2c, counter_upload c2p). These generate dedicated streaming APIs (see Streaming) instead of a plain request/response method.
Encryption Layer
Encryption is optional. When a session is active (see Encryption), the serialized command is encrypted with AES-128-GCM before it is handed to the container layer. The encrypted payload is [counter:4][ciphertext:N][tag:16] — a 20-byte overhead (4-byte counter + 16-byte GCM tag) — which is then split into containers exactly like a plaintext command.
The counter doubles as the AES-GCM nonce and as a replay guard. See Encryption for the handshake, key derivation, nonce construction, and replay rules.
Container Layer
The container layer splits large payloads into MTU-sized packets and reassembles them on the receiving side. Each container carries a transaction ID and sequence number for tracking and ordering.
Container Format
All multi-byte fields are little-endian.
First Container (type=0b00)
The first container in a transaction includes the total payload length:
| Field | Bits | Description |
|---|---|---|
transaction_id | 8 | Unique ID for this request/response pair. Resets per transaction. |
sequence_number | 8 | Increments with each container sent. Resets per transaction. |
type | 2 | 0b00 = first |
control_cmd | 4 | 0x0 (unused for data containers) |
reserved | 2 | Zero-filled |
total_length | 16 | Total payload size across all containers (bytes, little-endian) |
payload_len | 8 | Payload size in this container (bytes) |
payload | variable | Fragment of the command data |
Header size: 6 bytes (transaction_id + sequence_number + flags + total_length + payload_len)
Subsequent Container (type=0b01)
Continuation containers omit total_length:
Header size: 4 bytes (no total_length field)
Byte 2 Bitfield Detail
The third byte of every container encodes the type, control command, and reserved bits:
type:00= first,01= subsequent,11= controlcontrol_cmd:0x0–0x6(only valid when type=11)reserved: zero-filled
Container Splitting Example
A 500-byte command payload with MTU=247 (244 usable after ATT overhead):
| Container | Type | Header | Payload | Total |
|---|---|---|---|---|
| 1 | FIRST | 6 bytes | 238 bytes | 244 bytes |
| 2 | SUBSEQUENT | 4 bytes | 240 bytes | 244 bytes |
| 3 | SUBSEQUENT | 4 bytes | 22 bytes | 26 bytes |
Total payload: 500 bytes (238 + 240 + 22)
Maximum Payload
Two limits bound a single transaction. The sequence_number is 8 bits, capping it at ~255 containers; with a typical MTU of 247 (240 bytes per subsequent container) that is a practical maximum of approximately 60 KB. The 16-bit total_length field also imposes an absolute ceiling of 65,535 bytes. For larger transfers, use multiple request/response cycles.
BLE Transport
All communication uses a single GATT Characteristic:
- Requests: Central writes via Write Without Response
- Responses: Peripheral sends via Notify
MTU is negotiated automatically via the BLE ATT MTU Exchange procedure. The application uses the negotiated MTU (minus 3 bytes ATT overhead) to determine the maximum container payload size.
Default timeout is 100ms, configurable via the timeout control container.
Request/Response Flow
A complete RPC call involves encoding at the command layer, splitting at the container layer, BLE transmission, and the reverse on the receiving side:
Control Containers
Control containers (type=0b11) carry protocol-level commands instead of application data.
Timeout Sharing (control_cmd=0x1)
Allows the peripheral to communicate its processing timeout to the central.
Request (Central → Peripheral)
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x1 |
| payload_len | 0x00 |
Response (Peripheral → Central)
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x1 |
| payload_len | 0x02 |
| payload | timeout_ms (16-bit LE, milliseconds) |
Capability Sharing (control_cmd=0x4)
Allows the peripheral to communicate its buffer constraints and feature flags to the central.
Request (Central → Peripheral)
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x4 |
| payload_len | 0x06 |
| payload[0:2] | max_request_payload_size (16-bit LE, typically 0) |
| payload[2:4] | max_response_payload_size (16-bit LE, typically 0) |
| payload[4:6] | flags (16-bit LE, typically 0) |
Response (Peripheral → Central)
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x4 |
| payload_len | 0x06 |
| payload[0:2] | max_request_payload_size (16-bit LE) |
| payload[2:4] | max_response_payload_size (16-bit LE) |
| payload[4:6] | flags (16-bit LE) — bit 0: encryption supported |
The capabilities payload is always 6 bytes. A peripheral with no optional features simply reports flags = 0x0000.
Stream End (control_cmd=0x2, 0x3)
Signals the end of a streaming sequence. Either side can terminate a stream.
| control_cmd | Direction | Description |
|---|---|---|
0x2 | Central → Peripheral | Central ends the upload stream |
0x3 | Peripheral → Central | Peripheral ends the download stream |
Both have payload_len=0x00 (no payload).
Error Notification (control_cmd=0x5)
Sent by the peripheral when an error occurs (e.g., response exceeds buffer limits).
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x5 |
| payload_len | 0x01 |
| payload[0] | Error code |
Error codes:
0x01— RESPONSE_TOO_LARGE: Response payload exceedsmax_response_payload_size0x02— BUSY: The peripheral has no free request slot (it is still handling another transaction); the central should retry
Key Exchange (control_cmd=0x6)
Carries key exchange handshake payloads for establishing an encrypted session. Used when the peripheral advertises encryption support via the capabilities flags field.
| Field | Value |
|---|---|
| type | 0b11 (control) |
| control_cmd | 0x6 |
| payload_len | variable |
| payload | Key exchange step data (see Encryption) |
Four containers are exchanged (Step 1–4) to complete the handshake. After completion, all subsequent data containers carry encrypted payloads.
Streaming
bleRPC supports two streaming patterns beyond simple request/response:
Peripheral → Central Stream (Server Streaming)
One request triggers multiple responses. The stream ends with a STREAM_END_P2C control container.
Central → Peripheral Stream (Client Streaming)
Multiple requests are sent, followed by a STREAM_END_C2P control container. The peripheral responds with a final message.