Skip to content

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 定義

wire
# 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 { ... }
フィールド型としての enumerror_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 フィールドにリトルエンディアンを規定していることに対応します。

混合エンディアンの構造が必要な場合は、個別フィールドに u16beu32be のような明示的なサフィックス型でオーバーライドできます。

型エイリアス

wire
type AttHandle = u16le
type Uuid16 = u16le

純粋な名前のエイリアスで、新しいワイヤレイアウトは導入しません。AttHandleu16le とまったく同じエンコーディングです。フィールド宣言に意味のある名前を付けて可読性を高めるためのものです。C ではエイリアスは透過的に扱われ、構造体フィールドもパース/シリアライズロジックも基底の uint16_t をそのまま使います。

フィールド型としての列挙型

wire
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 とタグ付きユニオン

wire
frame AttPdu = match opcode: u8 { ... }

frame はタグ付きユニオンを定義します。まずタグフィールド(opcode: u8)を読み、マッチするブランチにディスパッチします。どの値にもマッチせずワイルドカード _ があればそこに、なければ WIRESPEC_ERR_INVALID_TAG を返します。

C では AttPdukind 判別子 + ブランチ構造体のユニオンになります。

c
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]

wire
0x0b => ReadRsp { value: bytes[remaining] },

bytes[remaining] は現在のスコープの残りバイトをすべて消費します。トップレベルの frame ブランチでは、スコープは入力バッファ全体です。ブランチ内の最後のワイヤフィールドでなければならず、コンパイラが静的にチェックします。

C では valuewirespec_bytes_t{ const uint8_t *ptr; size_t len; })になり、元のバッファを直接指します。コピーは発生しません。

空ブランチ

wire
0x13 => WriteRsp {},

フィールドのないブランチも有効です。WriteRsp はゼロバイトのレスポンス確認で、パース時にはオペコードバイト(タグディスパッチで消費済み)以外を読みません。

コンパイル

bash
wirespec compile examples/ble/att.wspec -o build/

build/ble_att.hbuild/ble_att.c が生成されます。

生成される C API

c
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);

使用例

c
#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