BLE ATT
Bluetooth Low Energy (BLE) uses the Attribute Protocol (ATT) to exchange data between a GATT client and server. ATT PDUs are sent over fixed L2CAP channel 4 (CID 0x0004) and are always little-endian — the opposite of most internet protocols.
This page shows the complete wirespec model for ATT and explains the features it exercises: @endian little, type aliases, enum as a field type, tagged frame unions, and bytes[remaining].
Protocol Background
ATT defines a small set of PDU types, identified by a 1-byte opcode. Common operations include reading and writing attribute values by handle, and server-initiated notifications. Error responses carry a structured body. All handles and MTU values are 16-bit little-endian integers.
Complete wirespec Definition
# BLE ATT Protocol (RFC / Bluetooth Core Spec Vol 3, Part F)
@endian little
module ble.att
type AttHandle = u16le
type Uuid16 = u16le
enum AttErrorCode: u8 {
InvalidHandle = 0x01,
ReadNotPermitted = 0x02,
WriteNotPermitted = 0x03,
AttributeNotFound = 0x0a,
}
frame AttPdu = match opcode: u8 {
0x01 => ErrorRsp {
request_opcode: u8,
handle: AttHandle,
error_code: AttErrorCode,
},
0x02 => ExchangeMtuReq { client_rx_mtu: u16le },
0x03 => ExchangeMtuRsp { server_rx_mtu: u16le },
0x0a => ReadReq { handle: AttHandle },
0x0b => ReadRsp { value: bytes[remaining] },
0x12 => WriteReq { handle: AttHandle, value: bytes[remaining] },
0x13 => WriteRsp {},
0x1b => Notification { handle: AttHandle, value: bytes[remaining] },
0x52 => WriteCmd { handle: AttHandle, value: bytes[remaining] },
_ => Unknown { data: bytes[remaining] },
}Features Demonstrated
| Feature | Where |
|---|---|
| Little-endian module default | @endian little |
| Type alias | type AttHandle = u16le |
enum definition | enum AttErrorCode: u8 { ... } |
enum as field type | error_code: AttErrorCode |
| Tagged frame union | frame AttPdu = match opcode: u8 { ... } |
| Fixed-width scalar | client_rx_mtu: u16le |
| Zero-copy byte span | value: bytes[remaining] |
| Empty branch | WriteRsp {} |
| Wildcard branch | _ => Unknown { ... } |
Feature Notes
@endian little
Placed at the top of the module, @endian little sets the default byte order for all multi-byte integer fields. A u16 field in this module is read and written in little-endian order without any extra annotation. This matches the Bluetooth Core Specification, which mandates little-endian for all ATT fields.
Individual fields can still override with explicit suffixed types (u16be, u32be) if a mixed-endian structure is needed.
Type Aliases
type AttHandle = u16le
type Uuid16 = u16leThese are pure naming aliases — no new wire layout is introduced. AttHandle encodes identically to u16le on the wire; the alias exists to make field declarations more readable and self-documenting. The alias appears transparently in generated C: both the struct field type and the parse/serialize logic use the underlying uint16_t.
Enum as a Field Type
enum AttErrorCode: u8 {
InvalidHandle = 0x01,
ReadNotPermitted = 0x02,
WriteNotPermitted = 0x03,
AttributeNotFound = 0x0a,
}AttErrorCode has an underlying wire type of u8. Using it as a field type (error_code: AttErrorCode) tells wirespec to read one byte and interpret it as an AttErrorCode value. In generated C, the field becomes a uint8_t with an associated typedef enum.
The enum does not perform exhaustiveness checking on the wire value — unrecognised opcode values are still parsed without error. The application layer decides how to handle unknown codes.
frame with Tagged Union
frame AttPdu = match opcode: u8 { ... }frame defines a tagged union. The compiler reads the tag field (opcode: u8) first, then dispatches to the matching branch. If the tag does not match any explicit value and a wildcard _ branch exists, the wildcard is taken. Without a wildcard, an unknown tag returns WIRESPEC_ERR_INVALID_TAG.
In C, AttPdu becomes a struct with a kind discriminant and a union of branch structs:
typedef enum {
ATT_PDU_ERROR_RSP = 0x01,
ATT_PDU_EXCHANGE_MTU_REQ = 0x02,
/* ... */
ATT_PDU_UNKNOWN,
} ble_att_att_pdu_kind_t;
typedef struct {
ble_att_att_pdu_kind_t kind;
union {
ble_att_error_rsp_t error_rsp;
ble_att_exchange_mtu_req_t exchange_mtu_req;
/* ... */
ble_att_unknown_t unknown;
};
} ble_att_att_pdu_t;bytes[remaining]
0x0b => ReadRsp { value: bytes[remaining] },bytes[remaining] consumes all bytes left in the current scope. For a top-level frame branch, the scope is the entire input buffer. The field must be the last wire field in its branch — the compiler enforces this statically.
In generated C, value becomes a wirespec_bytes_t — a { const uint8_t *ptr; size_t len; } pair pointing into the original input buffer. No copying occurs.
Empty Branch
0x13 => WriteRsp {},A branch with no fields is valid. WriteRsp is a zero-byte response acknowledgement. The generated C struct for it is empty, and parsing it consumes only the opcode byte (which was already consumed by the tag dispatch).
Compile
wirespec compile examples/ble/att.wspec -o build/This generates build/ble_att.h and build/ble_att.c.
Generated C API
wirespec_result_t ble_att_att_pdu_parse(
const uint8_t *buf, size_t len,
ble_att_att_pdu_t *out, size_t *consumed);
wirespec_result_t ble_att_att_pdu_serialize(
const ble_att_att_pdu_t *in,
uint8_t *buf, size_t cap, size_t *written);Usage Example
#include "ble_att.h"
void handle_att_pdu(const uint8_t *buf, size_t len) {
ble_att_att_pdu_t pdu;
size_t consumed;
wirespec_result_t r = ble_att_att_pdu_parse(buf, len, &pdu, &consumed);
if (r != WIRESPEC_OK) return;
switch (pdu.kind) {
case ATT_PDU_READ_REQ:
/* pdu.read_req.handle is a uint16_t attribute handle */
respond_with_attribute(pdu.read_req.handle);
break;
case ATT_PDU_WRITE_REQ:
/* pdu.write_req.value is a wirespec_bytes_t — zero-copy view */
write_attribute(pdu.write_req.handle,
pdu.write_req.value.ptr,
pdu.write_req.value.len);
break;
case ATT_PDU_EXCHANGE_MTU_REQ:
negotiate_mtu(pdu.exchange_mtu_req.client_rx_mtu);
break;
default:
break;
}
}What Next
- MQTT — continuation-bit VarInt, expression-based capsule tag, conditional fields
- TLS 1.3 —
enumtag types,u24primitive, fill arrays with byte bounds - Classic Protocols — UDP, TCP, Ethernet, IPv4