こんな悩みはありませんか?
「BLEのサンプルコードは動かせた。でも仕様書を読み始めた途端、GAPだのGATTだのCharacteristicだの、用語が多すぎて頭に入ってこない…」
BLEは用語の数が多く、しかも階層関係がわかりにくいのが入門の壁です。
でも、全体像を2層に分けて捉えるだけで、霧が一気に晴れます。
この記事を読めば、次の3つがクリアになります。
- GAPとGATTがそれぞれ「いつ」働くのか
- Service / Characteristic / Descriptor の階層関係
- 実際にデータが流れるまでの手順
最後に、初心者がほぼ全員ハマる落とし穴も共有します。
まず押さえるべき: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 になりますが、仕様上はどちらが何になっても構いません。両方持つこともあります。
混同しやすいので、表で整理しておきましょう。
| GAP | GATT | |
|---|---|---|
| 担当 | 接続まで | 接続後の通信 |
| ペア | Peripheral ↔ Central | Server ↔ 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も作れます。
| UUID | Service名 |
|---|---|
| 0x180D | Heart Rate Service |
| 0x180F | Battery Service |
| 0x180A | Device Information Service |
Characteristic
データ本体です。それぞれ Properties で「何ができるか」が決まっています。
| Property | 方向 | 動作 |
|---|---|---|
| Read | Client → Server | 値を読む |
| Write | Client → Server | 値を書く(ACKあり) |
| Write Without Response | Client → Server | 値を書く(ACK無し、高速) |
| Notify | Server → Client | Serverから一方的にpush(ACK無し) |
| Indicate | Server → Client | Serverから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点です。
- BLEは「接続前 (GAP)」と「接続後 (GATT)」の2層構造
- GATTのデータは Service → Characteristic → Descriptor の3階層
- Notify は CCCD を有効化しないと届かない
特に1つ目の「2層構造」を頭に入れておくと、仕様書を読むときも、デバッグでつまずいたときも、「これはGAPの問題?それともGATTの問題?」という切り分けが一瞬でできるようになります。
次のステップとして、自分でCustom GATT Serviceを作ってみるのがおすすめです。Service UUID、Characteristic UUID、Propertiesを自分で決めて、ReadとNotifyを実装してみると、ここで解説した階層構造が手に馴染んできます。
BLEは仕様の幅が広いですが、入り口の構造さえ掴んでしまえば、あとは積み上げていくだけです。ぜひ手を動かして、BLE開発を楽しんでください。


コメント