株式会社Berry
📱

GATTを理解して、自作Bluetooth機器と通信してみよう

に公開

こんにちは、株式会社Berryの庭山です!
Berryでは、Vue/TypeScript/Supabaseアプリケーションの開発とFactory Automationを担当しております。

Bluetooth Low Energy(BLE)は、ウェアラブルデバイスやスマートホーム機器に広く利用されている[1]無線通信規格ですが、BtoB製品やオートメーションに採用する観点でも多くのメリットを持っています。

  • モバイル端末と通信が容易:ウェアラブルデバイス・スマートホーム機器
  • 産業用機器との互換性:RS-232C等[2]のシリアル通信を無線化できる
  • 世界共通規格[3]:市場を問わない

本記事では、BLEの概要とその中核であるGATT通信を簡単に解説し、実際に自作Bluetooth機器にプログラミング可能なコードを紹介してみます。

BLEの概要

BLEの通信はセントラル機器(モバイル端末など)とペリフェラル機器(スマートホーム機器など)の間で行われ、ライフサイクルは以下のようになります。

  1. ペリフェラル機器を接続待ちにする
  2. 接続待ちのペリフェラル機器にセントラル機器から接続要求する
  3. 接続要求を受け入れると1対1通信(GATT)が始まる
  4. GATT通信を行う
  5. セントラル機器から接続を解除すると、通信が終了する -> 1に戻る

ペリフェラル機器が接続待ちの状態を、アドバタイズといいます。セントラルはアドバタイズされているペリフェラルをスキャンによって発見することができます。

GATT通信が確立されている間は、セントラルからペリフェラルへ、必要な情報を提供するよう要求することができます。この意味合いでセントラルはBLEサービスにおけるクライアント、ペリフェラルはサーバーとみることができます。


Bluetooth Core Specification, Figure 2.2: Examples of configurationより引用

GATT通信の概要

GATT(Generic Attribute Profile)は、データの転送メカニズムにAttribute Protocol(ATT)を使用するサービスフレームワークです。Attribute Protocolは、ペリフェラル機器が公開する情報の公開方法と、公開データの構造化を属性として定義しています。


Bluetooth Core Specification, Figure 2.3: Attribute Protocol PDUより引用

GATTはAttribute Protocolの抽象化であり、BLTサービスの中でのそれぞれのAttibuteの役割を構造化します。例えば、AttibuteにはServiceとCharacteristicの役割があり、1つのServiceが複数のCharacteristicを持つという2階層の構造です。

アプリケーション開発者は、GATT層を通じて意味のある論理構造を見ることができ、低レイヤーの詳細を気にすることなくBLEサービスを開発できます。

ATT層での見え方

属性(Attribute)= {
  - ハンドル(Handle): 0x0001
  - タイプ(Type/UUID): 0x2803
  - 値(Value): バイト配列
  - パーミッション: 読み取り/書き込み
}

BLEサービス
→ Attribute 1
→ Attribute 2  
→ Attribute 3


ATT層では、全てのAttributeがフラットに見えるため、BLEサービスはAttributeの集合である

GATT層での見え方

Service
  └─ Characterictic
      ├─ 宣言(Declaration)
      ├─ 値(Value)
      └─ 記述子(Descriptor)

BLEサービス
├─ Attribute 1 = サービス宣言(Service Declaration)
│                - UUID: 0x2800(Primary Service)または 0x2801(Secondary Service)
│
├─ Attribute 2 = キャラクタリスティック宣言(Characteristic Declaration)  
│                - UUID: 0x2803
│                - 値: プロパティ、ハンドル、キャラクタリスティックUUID
│
└─ Attribute 3 = キャラクタリスティック値(Characteristic Value)
                 - UUID: カスタムまたは標準UUID
                 - 値: 実際のデータ


GATT層では、AttributeがServiceとCharacteristicに分かれた階層として見える

心拍数センサー

具体例として、心拍数を測定するウェアラブルデバイスを想定してみましょう。
測定する要素は、心拍数とセンサー位置の2種類とします。

GATT層レベルでは、1つのServiceの中に2つの測定値がCharacteristicとして定義でき分かりやすいです。一方、ATT層を通して見ると物理的には6つのAttributeが必要です。

GATT層での見え方

Heart Rate Service(サービス)
├─ Heart Rate Measurement(Characteristic 1)
│   ├─ 宣言 (Attribute 0x0011)
│   ├─ 値 (Attribute 0x0012)
│   └─ CCCD記述子 (Attribute 0x0013)
│
└─ Body Sensor Location(Characteristic 2)
    ├─ 宣言 (Attribute 0x0014)
    └─ 値 (Attribute 0x0015)

ATT層での見え方

Heart Rate Service
├─ Attribute (Handle: 0x0010) → サービス宣言
├─ Attribute (Handle: 0x0011) → 心拍数測定キャラクタリスティック宣言
├─ Attribute (Handle: 0x0012) → 心拍数測定値
├─ Attribute (Handle: 0x0013) → Client Characteristic Configuration記述子
├─ Attribute (Handle: 0x0014) → ボディセンサー位置キャラクタリスティック宣言
└─ Attribute (Handle: 0x0015) → ボディセンサー位置値

実サービスでは、例えばバッテリー残量をセントラルに送ったり、セントラルとの接続確認用に定期的にデータを送ったりするCharactericticなどが追加で必要になります。

Propertyの種類と使用法

Characteristicにおけるデータ送受信の方式をPropertyといいます。開発者は送受信したい情報に適したPropertyを選択することができます。

主要なPropertyを5つ列挙します。

  • Read: 現在の状態を取得したい場合
  • Write: 確実にコマンドを送りたい場合
  • Write Without Response: 大量データや低遅延が必要な場合
  • Notify: センサーデータなど頻繁な更新が必要な場合
  • Indicate: アラートなど確実に届ける必要がある場合

それぞれの特徴は以下のようになります。

プロパティ 定数 動作 応答 用途
Read CHR_PROPS_READ クライアント→サーバー値の読み取り 必須(同期的) センサーの現在値、設定値の取得 バッテリーレベル、デバイス名
Write CHR_PROPS_WRITE クライアント→サーバー値の書き込み 必須(Write Response) 確実に送信したいコマンド、設定変更 LED制御コマンド、設定値の変更
Write Without Response CHR_PROPS_WRITE_WO_RESP クライアント→サーバー値の書き込み なし(高速) リアルタイムデータ、大量データの送信 ストリーミングデータ、マウスの動き
Notify CHR_PROPS_NOTIFY サーバー→クライアント自発的データ送信 なし(確認不要) リアルタイムデータ、頻繁な更新 心拍数、加速度センサー
Indicate CHR_PROPS_INDICATE サーバー→クライアント自発的データ送信 必須(Confirmation) 重要なイベント、確実に届ける必要があるデータ アラート、重要な状態変化

GATT通信の概要 まとめ

  • GATTはATTを抽象化したサービスフレームワーク
  • サービス開発者はGATT層を通じて、ServiceとCharacteristicでサービスを構造化できる
  • データ送受信の方式はPropertyで選択できる

次節では、ライブラリを使用して簡単なBLEサービスをプログラムしてみます。

Bluefruitライブラリで簡単にBLEサービスを作ろう

BluefruitライブラリはAdafruit社(Github)が開発しているBLEサービス用ライブラリです。

今回は、Nordic Semicondor社のSoC、nRF52840[4]を搭載したボードをペリフェラル機器としてプログラミングすることを想定します。

また、プログラミングするBLEサービスは、Bluetooth経由でPWM制御をON/OFFするだけの単純なアプリケーションとします。

入手性・レファレンス多さから、私は今回秋月電子通商のnRF52840使用BLEマイコンボード、AE-NRF52840を使用しましたが、nRF52シリーズを使用したボードにはバリエーションがあります[5]ので、用途に適したものを選定してください。

AE-NRF52840は、Adafruit Feather nRF52840 Expressの互換ボードです。使用する際にはAdafruit Feather nRF52840 Expressのレファレンス[6]に従って、事前に設定を済ませてください。

AE-NRF52840のピンアサインはこちら:https://akizukidenshi.com/goodsaffix/AE-NRF52840.pdf

BLEサービスの全体

全体の流れとしては以下のようになります。

  1. ServiceとCharacteristicを定義
  2. CharacteristicごとにPropertyを定義
  3. BLEサービス上のイベントに対応するコールバックを定義
  4. ペリフェラルをアドバタイズする

また、今回はService1個、Characteristic2個のシンプルな設計です。

Characteristic Property 役割
notifyCharacteristic Notify セントラルとの接続を監視する
writeCharacteristic Write PWM制御のON/OFFを切り替える
ble_pwm.ino
#include <Arduino.h>
#include <bluefruit.h>

#define USER_SERVICE_UUID "0f8cda8a-c133-49f0-8666-b2ee8fc381f5"
#define WRITE_CHARACTERISTIC_UUID "e855f407-69c7-449d-85c2-3d2260249ea2"
#define NOTIFY_CHARACTERISTIC_UUID "7058504e-d414-4c73-9d70-579c46c1ed77"
#define GREEN_PIN 13
#define RED_PIN 12
#define PWM_PIN 9

BLEService service;
BLECharacteristic notifyCharacteristic;
BLECharacteristic writeCharacteristic;

uint8_t num = 0;

void setup() {

  pinMode(GREEN_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  pinMode(PWM_PIN, OUTPUT);

  digitalWrite(GREEN_PIN, LOW);
  digitalWrite(PWM_PIN, LOW);
  digitalWrite(RED_PIN, HIGH);

  Serial.begin(115200);
  while (!Serial) delay(10);
  
  Serial.println("AE-NRF52840 with Bluefruit Library.");
  Serial.println("------------------------------------\n");

  Bluefruit.begin();

  //消費電力モードを設定
  Bluefruit.setTxPower(4);

  Bluefruit.setName("AE-NRF52840 PWM switch");

  //接続時コールバック、接続切断時コールバックを定義
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  Serial.println("Configuring Service.");
  setupService();

  startAdv();
}

void loop() {
  uint8_t test = 0x01;

  if ( Bluefruit.connected() ){
    if (notifyCharacteristic.notify(&test, 1)){
      Serial.println("Notify data SUCCESS.");
    } else {
      Serial.println("ERROR: Notify not set in the CCCD or not connected.");
    }
  }
}

void strUUID2Bytes(String strUUID, uint8_t binUUID[]){
  String hexString = String(strUUID);
  hexString.replace("-", "");
  
  for (int i = 16; i != 0; i--){
    binUUID[i - 1] = hex2c(hexString[(16 - i) * 2], hexString[((16 - i) * 2) + 1]);
  }
}

char hex2c(char c1, char c2){
  return (nibble2c(c1) << 4) + nibble2c(c2);
}

char nibble2c(char c) {
  if ((c >= '0') && (c <= '9'))
    return c = '0';
  if ((c >= 'A') && (c <= 'F'))
    return c + 10 - 'A';
  if ((c >= 'a') && (c <= 'f'))
    return c + 10 - 'a';
  return 0;
}

void setupService(void) {
  //stringを変換して16バイトのuuidを用意
  uint8_t userServiceUUID[16];
  uint8_t notifyCharacteristicUUID[16];
  uint8_t writeCharacteristicUUID[16];

  strUUID2Bytes(USER_SERVICE_UUID, userServiceUUID);
  strUUID2Bytes(NOTIFY_CHARACTERISTIC_UUID, notifyCharacteristicUUID);
  strUUID2Bytes(WRITE_CHARACTERISTIC_UUID, writeCharacteristicUUID);

  service = BLEService(userServiceUUID);
  service.begin();

  notifyCharacteristic = BLECharacteristic(notifyCharacteristicUUID);
  notifyCharacteristic.setProperties(CHR_PROPS_NOTIFY);
  notifyCharacteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  notifyCharacteristic.setMaxLen(5);
  notifyCharacteristic.setCccdWriteCallback(cccd_callback); // Optional: capture cccd update
  notifyCharacteristic.begin();

  writeCharacteristic = BLECharacteristic(writeCharacteristicUUID);
  writeCharacteristic.setProperties(CHR_PROPS_WRITE);
  writeCharacteristic.setPermission(SECMODE_NO_ACCESS, SECMODE_OPEN);
  writeCharacteristic.setFixedLen(1);
  writeCharacteristic.setWriteCallback(write_callback);
  writeCharacteristic.begin();
}

//サービスのアドバタイズ
void startAdv(void) {
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addName();

  Bluefruit.Advertising.addService(service);
  Bluefruit.Advertising.setInterval(32, 244);
  Bluefruit.Advertising.setFastTimeout(30);
  Bluefruit.Advertising.start(0);
}

void connect_callback(uint16_t conn_handle){
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  Serial.print("Connected to ");
  Serial.println(central_name);
}

void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
  (void) conn_handle;
  (void) reason;
  
  Serial.print("Disconnected, reason=0x"); Serial.println(reason, HEX);
  Serial.println("Advertising!");
}

void cccd_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint16_t cccd_value){
  Serial.print("CCCD Updated: ");
  Serial.print(cccd_value);
  Serial.println("");

  if (chr->uuid == notifyCharacteristic.uuid){
    if (chr->notifyEnabled(conn_hdl)){
      Serial.println("'Notify' enabled");
    } else {
      Serial.println("'Notify' disabled");
    }
  }
}

//受信したらやること
//セントラルから受信したデータはdataで受け取る
//複数バイトを受信する場合はdata[i]とする
void write_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint8_t* data, uint16_t len){
  Serial.println(data[0]);
  digitalToggle(GREEN_PIN);
  digitalToggle(RED_PIN);

  if (digitalRead(PWM_PIN) == HIGH) {
    // PWMをON(例:50%デューティサイクル)
    analogWrite(PWM_PIN, 128);
  } else {
    // PWMをOFF
    analogWrite(PWM_PIN, 0);
  }
}

BLEサービスの詳細

ServiceとCharacteristicの定義です。それぞれにuuidが必要なので、ランダムなuuidを生成して設定しています。

ble_pwm.ino
#define USER_SERVICE_UUID "0f8cda8a-c133-49f0-8666-b2ee8fc381f5"
#define WRITE_CHARACTERISTIC_UUID "e855f407-69c7-449d-85c2-3d2260249ea2"
#define NOTIFY_CHARACTERISTIC_UUID "7058504e-d414-4c73-9d70-579c46c1ed77"

BLEService service;
BLECharacteristic notifyCharacteristic;
BLECharacteristic writeCharacteristic;

setup()の中で、

  1. デバイス名(≠サービス名)の設定
  2. 接続/切断時のコールバック
  3. アドバタイズの開始

を行っています。デバイス名は、セントラルのBLEアプリでデバイス名を見るために設定しています。

void setup(){
  // 省略

  Bluefruit.begin();
  //消費電力モードを設定
  Bluefruit.setTxPower(4);
  Bluefruit.setName("AE-NRF52840 PWM switch");
  //接続時コールバック、接続切断時コールバックを定義
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
  Serial.println("Configuring Service.");
  setupService();
  startAdv();
}

setupService()は、ServiceとCharacteristicの定義です。Propertyの設定や読み取りモード・セキュリティの設定が可能です。

pwm_ble
void setupService(void) {
  //省略
  service = BLEService(userServiceUUID);
  service.begin();

  notifyCharacteristic = BLECharacteristic(notifyCharacteristicUUID);
  notifyCharacteristic.setProperties(CHR_PROPS_NOTIFY);
  notifyCharacteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  notifyCharacteristic.setMaxLen(5);
  notifyCharacteristic.setCccdWriteCallback(cccd_callback); // Optional: capture cccd update
  notifyCharacteristic.begin();

  writeCharacteristic = BLECharacteristic(writeCharacteristicUUID);
  writeCharacteristic.setProperties(CHR_PROPS_WRITE);
  writeCharacteristic.setPermission(SECMODE_NO_ACCESS, SECMODE_OPEN);
  writeCharacteristic.setFixedLen(1);
  writeCharacteristic.setWriteCallback(write_callback);
  writeCharacteristic.begin();
}

write_callback()で、WriteCharacteristicに値の書き込みがあったときの動作を定義しています。

  • OFF時には赤色LEDが点灯
  • ON(稼働)時には緑色LEDが点灯
  • ON/OFFとPWM制御が同期(デューティー比50%)

としてみました。

ble_pwm
void write_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint8_t* data, uint16_t len){
  Serial.println(data[0]);
  digitalToggle(GREEN_PIN);
  digitalToggle(RED_PIN);

  if (digitalRead(PWM_PIN) == HIGH) {
    // PWMをON(例:50%デューティサイクル)
    analogWrite(PWM_PIN, 128);
  } else {
    // PWMをOFF
    analogWrite(PWM_PIN, 0);
  }
}

loop()では、常にnotifyCharacteristicからデータ0x01が送られてきているかを確認し、シリアルモニタへの出力で接続状態を確認できるようにしています。

ble_pwm
void loop() {
  uint8_t test = 0x01;

  if ( Bluefruit.connected() ){
    if (notifyCharacteristic.notify(&test, 1)){
      Serial.println("Notify data SUCCESS.");
    } else {
      Serial.println("ERROR: Notify not set in the CCCD or not connected.");
    }
  }
}

補足:CCCD (Client Characteristic Configuration Descriptor)

Notify Property, Indicator Propertyには、CCCD(Client Characteristic Configuration Descriptor)の設定が必要です。CCCDは、クライアントがサーバーからの通知(Notify/Indicate)を受け取るかどうかを制御するための特別な記述子です。

例えば、Notify PropertyのCCCDを記述すると、セントラル側でNotify Propertyで送られてくる情報を受信するかどうか(=Subscribeするかどうか)を選択することができるようになります。

プロパティ 通信方向 CCCD必要 速度 信頼性
Read Client→Server 不要
Write Client→Server 不要
Write Without Response Client→Server 不要
Notify Server→Client 必要
Indicate Server→Client 必要

サービス中では、CCCDの切り替え(Subscribe <-> No Subscribe)が起こった時にシリアルモニタへ通知するようにcccd_callback()で定義しています。

ble_pwm
void cccd_callback(uint16_t conn_hdl, BLECharacteristic* chr, uint16_t cccd_value){
  Serial.print("CCCD Updated: ");
  Serial.print(cccd_value);
  Serial.println("");

  if (chr->uuid == notifyCharacteristic.uuid){
    if (chr->notifyEnabled(conn_hdl)){
      Serial.println("'Notify' enabled");
    } else {
      Serial.println("'Notify' disabled");
    }
  }
}

モバイル端末でBLEサービスに接続する

前章のBLEサービスをハードウェアにプログラミングしたら、スマートフォンから接続してみましょう。セントラル側に動作確認用のiOSアプリをインストールします。

代表的なものに、Nordic社のnRF Connect for Mobileや、punchthrough社LightBlueなどがあります。[7]

スマートフォンで動作確認用アプリを起動し、自作のペリフェラル機器に接続してみてください。
例えばLightBlueでは、下画像のようにWrite Characteristicにアクセス可能です。


LightBlueのCharacteristic詳細画面。
画像はApp Store(https://apps.apple.com/jp/app/lightblue/id557428110)から引用

応用例:モーターを動かしてみよう

ハードウェアの実装についてはバリエーションがあるため、ここでは省略します。
私は、Write Characteristicの値が上書きされるたびに、モーター正回転がON/OFFされるようなアプリケーションとなるようにハードウェアを実装しました。

GATT通信は柔軟であるため、色々試されると楽しんでいただけるかと思います。

以上で本記事の内容を終わります。お疲れさまでした。

まとめ

  • BLE(Bluetooth low Energy)はウェアラブル・スマートホーム端末から産業用途、オートメーションにも応用可能な無線通信規格
  • BLEサービスを作成するには、GATT(Generic Attribute Profile)の理解が肝要
  • 製品組み込み前のデバッグツールとして、Nordic社やpunchthrough社のモバイルアプリが利用できる
  • ハードウェアの実装次第で、発展的アプリケーションも実現できる

応募待っています

WEBエンジニア募集中です!医療業界での経験や3Dの知見は問いません。Berryでは様々な経験をお持ちの方が活躍できる環境があります。

Berryの考え方や製品に少しでも興味が持てた方はお気軽に応募下さい。
https://www.wantedly.com/projects/2141780

脚注
  1. Apple Watch Series 11は最新規格Bluetooth 5.3を採用しています。Bluetooth 5.3は、標準でBLEをサポートしているBluetooth最新規格です(2025年10月現在)。 ↩︎

  2. シリアル通信を扱うRS-232Cは、非常に低レイヤーな産業機器にも対応の多い通信規格です。例えば、電子天秤でも利用可能です。 ↩︎

  3. BLEは2.4GHz帯(2.402〜2.480GHz)のISMバンドを利用しているため、ライセンス無しで利用できることが多いです。 ↩︎

  4. https://www.nordicsemi.jp/products/nrf52840/ ↩︎

  5. Seeed Studio社のXIAO nRF52840(Seeed Studio Wiki)などは、小型で製品に組み込みやすいラインナップです。 ↩︎

  6. 一例ですが、Bluefruit nRF52 Feather Learning Guideは非常に有用なレファレンスです。 ↩︎

  7. nRF Connect for MobileやLightBlueは、単にペリフェラルを発見するだけでなく、BLE機器の電波強度などを測定可能なので、本格的な実製品組み込み前のデバッグツールとしても有用です。 ↩︎

株式会社Berry
株式会社Berry

Discussion