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

T
1 bit
reserved
7 bits
cmd_name_len
8 bits
cmd_name
variable
data_len
16 bits (LE)
data
variable
FieldBitsDescription
type10 = request, 1 = response
reserved7Zero-filled
cmd_name_len8Length of the command name in bytes
cmd_namevariableASCII command name (e.g., echo, flash_read)
data_len16Length of the protobuf data (bytes, little-endian)
datavariableProtocol Buffers encoded payload

Command Discovery

The code generator derives commands from your .proto file in one of two ways:

Message PairCommand Name
EchoRequest + EchoResponseecho
FlashReadRequest + FlashReadResponseflash_read
DataWriteRequest + DataWriteResponsedata_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:

transaction_id
8 bits
sequence_number
8 bits
type
2
control_cmd
4
reserved
2
total_length
16 bits (LE)
payload_len
8 bits
payload
variable
FieldBitsDescription
transaction_id8Unique ID for this request/response pair. Resets per transaction.
sequence_number8Increments with each container sent. Resets per transaction.
type20b00 = first
control_cmd40x0 (unused for data containers)
reserved2Zero-filled
total_length16Total payload size across all containers (bytes, little-endian)
payload_len8Payload size in this container (bytes)
payloadvariableFragment 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:

transaction_id
8 bits
sequence_number
8 bits
type
2
control_cmd
4
reserved
2
payload_len
8 bits
payload
variable

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
bits 7-6
control_cmd
bits 5-2
reserved
bits 1-0

Container Splitting Example

A 500-byte command payload with MTU=247 (244 usable after ATT overhead):

ContainerTypeHeaderPayloadTotal
1FIRST6 bytes238 bytes244 bytes
2SUBSEQUENT4 bytes240 bytes244 bytes
3SUBSEQUENT4 bytes22 bytes26 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:

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)

FieldValue
type0b11 (control)
control_cmd0x1
payload_len0x00

Response (Peripheral → Central)

FieldValue
type0b11 (control)
control_cmd0x1
payload_len0x02
payloadtimeout_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)

FieldValue
type0b11 (control)
control_cmd0x4
payload_len0x06
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)

FieldValue
type0b11 (control)
control_cmd0x4
payload_len0x06
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_cmdDirectionDescription
0x2Central → PeripheralCentral ends the upload stream
0x3Peripheral → CentralPeripheral 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).

FieldValue
type0b11 (control)
control_cmd0x5
payload_len0x01
payload[0]Error code

Error codes:

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.

FieldValue
type0b11 (control)
control_cmd0x6
payload_lenvariable
payloadKey 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.