BLE ATT
Bluetooth Low Energy(BLE)の Attribute Protocol(ATT)は、GATT クライアント/サーバー間のデータ交換に使われます。ATT PDU は固定 L2CAP チャネル 4(CID 0x0004)で送受信され、バイトオーダーは常にリトルエンディアンです。インターネット系プロトコルの多くとは逆になります。
このページでは ATT の wirespec モデル全体を示し、@endian little、型エイリアス、フィールド型としての enum、タグ付きフレームユニオン、bytes[remaining] といった機能を解説します。
プロトコルの背景
ATT は 1 バイトのオペコードで識別される PDU タイプの集まりです。主な操作はハンドルによる属性値の読み書きと、サーバーからの通知です。エラーレスポンスには構造化されたボディがあります。ハンドルと MTU 値はすべて 16 ビットリトルエンディアン整数です。
wirespec 定義
# 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] },
}使われている機能
| 機能 | 該当箇所 |
|---|---|
| リトルエンディアンのモジュールデフォルト | @endian little |
| 型エイリアス | type AttHandle = u16le |
enum 定義 | enum AttErrorCode: u8 { ... } |
フィールド型としての enum | error_code: AttErrorCode |
| タグ付きフレームユニオン | frame AttPdu = match opcode: u8 { ... } |
| 固定幅スカラー | client_rx_mtu: u16le |
| ゼロコピーバイトスパン | value: bytes[remaining] |
| 空ブランチ | WriteRsp {} |
| ワイルドカードブランチ | _ => Unknown { ... } |
各機能の解説
@endian little
モジュール先頭に @endian little を置くと、マルチバイト整数フィールドのデフォルトバイトオーダーがリトルエンディアンになります。このモジュール内の u16 は追加のアノテーションなしにリトルエンディアンで読み書きされます。Bluetooth Core Specification が ATT フィールドにリトルエンディアンを規定していることに対応します。
混合エンディアンの構造が必要な場合は、個別フィールドに u16be、u32be のような明示的なサフィックス型でオーバーライドできます。
型エイリアス
type AttHandle = u16le
type Uuid16 = u16le純粋な名前のエイリアスで、新しいワイヤレイアウトは導入しません。AttHandle は u16le とまったく同じエンコーディングです。フィールド宣言に意味のある名前を付けて可読性を高めるためのものです。C ではエイリアスは透過的に扱われ、構造体フィールドもパース/シリアライズロジックも基底の uint16_t をそのまま使います。
フィールド型としての列挙型
enum AttErrorCode: u8 {
InvalidHandle = 0x01,
ReadNotPermitted = 0x02,
WriteNotPermitted = 0x03,
AttributeNotFound = 0x0a,
}AttErrorCode の基底ワイヤ型は u8 です。フィールド型として使う(error_code: AttErrorCode)と、wirespec は 1 バイトを読んで AttErrorCode 値として解釈します。C では uint8_t フィールドに対応する typedef enum が生成されます。
enum はワイヤ値の網羅性チェックを行いません。認識されないオペコード値もエラーにはならず、未知のコードの処理はアプリケーション層に委ねられます。
frame とタグ付きユニオン
frame AttPdu = match opcode: u8 { ... }frame はタグ付きユニオンを定義します。まずタグフィールド(opcode: u8)を読み、マッチするブランチにディスパッチします。どの値にもマッチせずワイルドカード _ があればそこに、なければ WIRESPEC_ERR_INVALID_TAG を返します。
C では AttPdu は kind 判別子 + ブランチ構造体のユニオンになります。
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] は現在のスコープの残りバイトをすべて消費します。トップレベルの frame ブランチでは、スコープは入力バッファ全体です。ブランチ内の最後のワイヤフィールドでなければならず、コンパイラが静的にチェックします。
C では value は wirespec_bytes_t({ const uint8_t *ptr; size_t len; })になり、元のバッファを直接指します。コピーは発生しません。
空ブランチ
0x13 => WriteRsp {},フィールドのないブランチも有効です。WriteRsp はゼロバイトのレスポンス確認で、パース時にはオペコードバイト(タグディスパッチで消費済み)以外を読みません。
コンパイル
wirespec compile examples/ble/att.wspec -o build/build/ble_att.h と build/ble_att.c が生成されます。
生成される 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);使用例
#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 は uint16_t の属性ハンドル */
respond_with_attribute(pdu.read_req.handle);
break;
case ATT_PDU_WRITE_REQ:
/* pdu.write_req.value は wirespec_bytes_t -- ゼロコピービュー */
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;
}
}次のステップ
- MQTT -- 継続ビット VarInt、式ベースのカプセルタグ、条件フィールド
- TLS 1.3 --
enumタグ型、u24プリミティブ、バイト境界付き fill 配列 - クラシックプロトコル -- UDP、TCP、Ethernet、IPv4