BLEの基本構造を図解で理解する【GAP / GATT / Service / Characteristic】 –>

こんな悩みはありませんか?

「BLEのサンプルコードは動かせた。でも仕様書を読み始めた途端、GAPだのGATTだのCharacteristicだの、用語が多すぎて頭に入ってこない…」

BLEは用語の数が多く、しかも階層関係がわかりにくいのが入門の壁です。
でも、全体像を2層に分けて捉えるだけで、霧が一気に晴れます

この記事を読めば、次の3つがクリアになります。

  1. GAPとGATTがそれぞれ「いつ」働くのか
  2. Service / Characteristic / Descriptor の階層関係
  3. 実際にデータが流れるまでの手順

最後に、初心者がほぼ全員ハマる落とし穴も共有します。


まず押さえるべき:BLEは「接続前」と「接続後」の2層構造

BLE通信は、大きく2つのフェーズに分かれています。

フェーズプロトコル役割
接続前GAP (Generic Access Profile)デバイスの発見と接続
接続後GATT (Generic Attribute Profile)データのやり取り
[フェーズ1: GAP]              [フェーズ2: GATT]
 デバイスを見つける             データをやり取りする
 ────────────────────→         ────────────────────→
 Advertising                    Service Discovery
 Scanning                       Read / Write / Notify
 Connecting

「GAPで繋いで、GATTで話す」 — この一文を頭に入れておくと、仕様書を読むときに迷子になりません。


GAP:デバイスの発見と接続

4つのロール

GAPでは、デバイスがどう振る舞うかを4つのロールで定義します。

ロール動作代表例
Broadcasterアドバタイズするだけ(接続不可)ビーコン
Observerアドバタイズを受信するだけビーコン受信器
Peripheralアドバタイズ + 接続受け入れセンサー、ウェアラブル
Centralスキャン + 接続開始スマホ、ゲートウェイ

接続型のBLE通信は、基本的に Peripheral と Central のペア で動きます。
組み込み機器が Peripheral、それを操作するスマホが Central、というのが典型パターンです。

接続が成立するまでの流れ

[Peripheral]                       [Central]
     │                                 │
     ├── Advertising Packet ────────→ │ Scanning
     │   (20ms〜数秒間隔で送信)        │
     │                                 │
     │ ←─── Connection Request ───────┤
     │                                 │
     │ ←────── Connected ──────→       │

Peripheralが「ここに居ますよ」とアドバタイズパケットを撒き、Centralがそれをスキャンで拾って接続要求を送る。これがGAPフェーズの中身です。

ここまでは「ただ繋がっただけ」で、まだデータは何も流れません。
データのやり取りは、ここからGATTの出番になります。


GATT:データのやり取りの構造

Client / Server モデル

GATTでは、データを 持っている側(Server)要求する側(Client) に分かれます。

役割説明典型例
GATT Serverデータを保持・提供センサー側
GATT Clientデータを要求・読み書きアプリ側

ここで重要なポイントを1つ。

GATTのClient/Server と GAPのCentral/Peripheral は、別の概念です。

多くの場合 Peripheral=Server / Central=Client になりますが、仕様上はどちらが何になっても構いません。両方持つこともあります。
混同しやすいので、表で整理しておきましょう。

GAPGATT
担当接続まで接続後の通信
ペアPeripheral ↔ CentralServer ↔ Client
典型センサー (P) ↔ スマホ (C)センサー (S) ↔ スマホ (C)

GATTの階層構造:Service / Characteristic / Descriptor

GATT Server が持つデータは、3階層で構成されます。

Service(機能のまとまり)
  └─ Characteristic(個別のデータ項目)
       ├─ Value(実際のデータ)
       └─ Descriptor(補助情報)

「フォルダ(Service)の中にファイル(Characteristic)があり、ファイルには本文(Value)と属性情報(Descriptor)がある」というイメージで掴むとわかりやすいです。

Service

関連するCharacteristicをまとめた単位です。Bluetooth SIGが標準Serviceを多数定義しており、独自Serviceも作れます。

UUIDService名
0x180DHeart Rate Service
0x180FBattery Service
0x180ADevice Information Service

Characteristic

データ本体です。それぞれ Properties で「何ができるか」が決まっています。

Property方向動作
ReadClient → Server値を読む
WriteClient → Server値を書く(ACKあり)
Write Without ResponseClient → Server値を書く(ACK無し、高速)
NotifyServer → ClientServerから一方的にpush(ACK無し)
IndicateServer → ClientServerからpush(ACKあり)

Descriptor

Characteristicに付随する補助情報です。中でも最も重要なのが CCCD(Client Characteristic Configuration Descriptor) で、Notify / Indicate を有効化するスイッチの役割を持ちます。

このCCCD、後で出てくる「ハマりポイント第1位」の主役です。


具体例:心拍計で全体を繋げる

理論だけだとピンとこないので、標準Profileの「心拍計」を例に流れを追ってみます。

心拍計のGATT構造

Heart Rate Service (0x180D)
├─ Heart Rate Measurement (0x2A37)    [Notify]
│   └─ CCCD (0x2902)
├─ Body Sensor Location (0x2A38)      [Read]
└─ Heart Rate Control Point (0x2A39)  [Write]

動作の流れ

1. [GAP]    Peripheral がアドバタイズ
2. [GAP]    Central がスキャン → 接続
3. [GATT]   Central が Service Discovery で 0x180D を発見
4. [GATT]   Central が CCCD (0x2902) に 0x0001 を書き込み
            (= Notify有効化)
5. [GATT]   Peripheral が心拍値を Notify で送信
6. [GATT]   Central が受信して表示

ポイントは、ステップ4でCCCDを書き込むまで、Peripheralがいくらデータを送ってもCentralには届かないことです。
これが次の章のハマりポイントに直結します。


初心者が必ずハマる4つの落とし穴

1. Notify が届かない → 8割は CCCD の書き込み忘れ

「Server側で notify 関数を呼んでいるのに、Client側に何も来ない…」

このパターン、本当に多いです。原因はほぼ Client側の CCCD 書き込み忘れ。
nRF Connectなどのデバッグツールでは、CharacteristicのNotifyボタンを押すと自動でCCCDに書き込んでくれるので、まずはツールで動作確認するのがおすすめです。

2. 「Peripheral = Server」と思い込む

多くの場合そうですが、仕様上は独立した概念です。
両ロール持ち(Central + Peripheral同時動作)の機器もあり得るので、設計時には「このデバイスはGAP的にはX、GATT的にはY」と分けて考える癖をつけましょう。

3. 独自Serviceに16-bit UUIDを使う

16-bit UUID は Bluetooth SIG が管理する Assigned Numbers の一部で、申請しないと使えない公式UUIDです(取得には$3,450 USDの申請費用がかかります)。
独自Serviceには必ず 128-bit UUID を使ってください。uuidgen コマンドや各種ジェネレータで生成できます。

$ uuidgen
12345678-1234-5678-1234-56789ABCDEF0

Assigned Numbersの仕組みや申請手続きの詳細については、別記事で詳しく解説していますので、興味のある方はそちらもご覧ください。
Assigned Numbers

4. 「接続したらすぐデータが流れる」と思う

接続成立後、実際にデータをやり取りするには
Service Discovery → CCCD設定 → 通信開始 という手順が必要です。
接続イベントを受け取った瞬間に send 関数を叩いても、相手側はまだ準備できていません。


まとめ

この記事でお伝えしたかったのは、次の3点です。

  1. BLEは「接続前 (GAP)」と「接続後 (GATT)」の2層構造
  2. GATTのデータは Service → Characteristic → Descriptor の3階層
  3. Notify は CCCD を有効化しないと届かない

特に1つ目の「2層構造」を頭に入れておくと、仕様書を読むときも、デバッグでつまずいたときも、「これはGAPの問題?それともGATTの問題?」という切り分けが一瞬でできるようになります。

次のステップとして、自分でCustom GATT Serviceを作ってみるのがおすすめです。Service UUID、Characteristic UUID、Propertiesを自分で決めて、ReadとNotifyを実装してみると、ここで解説した階層構造が手に馴染んできます。

BLEは仕様の幅が広いですが、入り口の構造さえ掴んでしまえば、あとは積み上げていくだけです。ぜひ手を動かして、BLE開発を楽しんでください。

コメント

タイトルとURLをコピーしました