Protocol Specification

bleRPC uses a two-layer protocol stack on top of BLE GATT. The Command Layer wraps Protocol Buffers payloads with RPC metadata, 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 --> 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 to the central.

Request (Central → Peripheral)

FieldValue
type0b11 (control)
control_cmd0x4
payload_len0x00

Response (Peripheral → Central)

FieldValue
type0b11 (control)
control_cmd0x4
payload_len0x04
payload[0:2]max_request_payload_size (16-bit LE)
payload[2:4]max_response_payload_size (16-bit LE)

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:

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)
    Note over C,P: Ready for RPC calls