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.

graph TD
    A["Application"] --> B["Command Layer<br/>Protobuf encode/decode + command metadata"]
    B --> E["Encryption Layer (optional)<br/>AES-128-GCM encrypt/decrypt"]
    E --> C["Container Layer<br/>MTU-based split/reassemble + sequencing"]
    C --> D["BLE GATT<br/>(single characteristic)"]
      

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.

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
ctrl_cmd
4
rsv
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
ctrl_cmd
4
rsv
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

The sequence_number is 8 bits, limiting a single transaction to ~255 containers. With a typical MTU of 247 (240 bytes per subsequent container), the practical maximum payload per transaction is approximately 60 KB. For larger transfers, use multiple request/response cycles.

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

When flags is absent (4-byte payload for backward compatibility), no optional features are available.

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.

Encrypted Data Container

When encryption is active, the payload of data containers (FIRST / SUBSEQUENT) is replaced by the encrypted form of the serialized command:

counter
32 bits (LE)
ciphertext
variable
tag
128 bits
FieldSizeDescription
counter4 bytesMonotonically increasing counter (little-endian). Used as AES-GCM nonce and for replay detection.
ciphertextN bytesAES-128-GCM encrypted serialized command
tag16 bytesAES-GCM authentication tag

Total overhead: 20 bytes (4 counter + 16 tag). The ciphertext is then split into containers as usual.

Command Layer

The command layer wraps Protocol Buffers-encoded data with RPC metadata. 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

Commands are automatically generated from .proto file definitions. Matching *Request / *Response message pairs produce a command whose name is the prefix converted to snake_case:

Message PairCommand Name
EchoRequest + EchoResponseecho
FlashReadRequest + FlashReadResponseflash_read
DataWriteRequest + DataWriteResponsedata_write

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:

sequenceDiagram
    participant C as Central
    participant P as Peripheral
    Note over C: 1. Protobuf encode request
    Note over C: 2. Wrap in command header
    Note over C: 3. Split into containers
    C->>P: Write Without Response (FIRST)
    C->>P: Write Without Response (SUBSEQUENT...)
    Note over P: 4. Reassemble containers
    Note over P: 5. Decode command + Protobuf
    Note over P: 6. Execute handler
    Note over P: 7. Encode response + split
    P->>C: Notify (FIRST)
    P->>C: Notify (SUBSEQUENT...)
    Note over C: 8. Reassemble + decode response
      

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.

sequenceDiagram
    participant C as Central
    participant P as Peripheral
    C->>P: Request
    P->>C: Response 1 (Notify)
    P->>C: Response 2 (Notify)
    P->>C: Response 3 (Notify)
    P-->>C: STREAM_END_P2C (control)
      

Central → Peripheral Stream (Client Streaming)

Multiple requests are sent, followed by a STREAM_END_C2P control container. The peripheral responds with a final message.

sequenceDiagram
    participant C as Central
    participant P as Peripheral
    C->>P: Request 1
    C->>P: Request 2
    C->>P: Request 3
    C-->>P: STREAM_END_C2P (control)
    P->>C: Final Response (Notify)
      

Connection Setup Sequence

When a central connects to a peripheral, it performs these initialization steps:

sequenceDiagram
    participant C as Central
    participant P as Peripheral
    C->>P: BLE Connect
    C->>P: MTU Exchange
    C->>P: Discover Services
    C->>P: Enable Notifications
    C->>P: Timeout Request (ctrl)
    P->>C: Timeout Response (e.g. 100ms)
    C->>P: Capability Request (ctrl)
    P->>C: Capability Response (max_req, max_resp, flags)
    opt Encryption (flags & 0x01)
        C->>P: Key Exchange Step 1 (ctrl 0x6)
        P->>C: Key Exchange Step 2 (ctrl 0x6)
        C->>P: Key Exchange Step 3 (ctrl 0x6)
        P->>C: Key Exchange Step 4 (ctrl 0x6)
        Note over C,P: AES-128-GCM session established
    end
    Note over C,P: Ready for RPC calls