【M5Stack】段階的に動かして学ぶBluetooth通信

## 本記事について

2台の M5Stack を BLE(Bluetooth Low Energy) 通信のコネクトモードによってデータを送受信するプログラムを書くことによって BLE通信のコンセプトや流れ、実装方法について理解することを目的とした記事。


本記事で扱うのは BLE であり、Bluetooth Classic については扱わない。

## 環境・使ったもの

MacBook Air M2
macOS 13.3
Atom S3, Atom Lite
VSCode, PlatformIO
toio (BLEで送信する情報の参考として使った。なくて問題ない)
iPhone

## 前提

https://qiita.com/Teach/items/18013e1e34d649c7947d

https://qiita.com/Teach/items/a3ce9f2df73064575fc6

BLEについて0知識だと厳しいため、ある程度基礎知識を調べておくこと。

## コンセプト

紛らわしい役割をあらかじめまとめておく

||ペリフェラル|セントラル|
|:--|:--|:--|
||BLEServer|BLEClient|
||センサの情報送る|センサの情報受け取る|
||アドバタイズする|BLEデバイスを探す|
||Notify|Write, Read|

## BLE Scanner について

[BLE Scanner App Store](https://apps.apple.com/jp/app/ble-scanner-4-0/id1221763603)

周辺のBLEデバイスを探すアプリ
どんなデータを送っているのかを確認することができる。

`Near By`のタブで周辺の BLE デバイスを探し、`Connect`ボタンを押すと中身が見れる。
toioのアドバタイズデータを見てみると以下のようになった。

![IMG_5476.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/6d893e23-1cbe-7721-2c2a-5705c375b03d.png)

[toio コアキューブ技術使用](https://toio.github.io/toio-spec/docs/ble_communication_overview/)

ADVERTIMENT DATA の中に Service UUIDs という値があるのがポイント。

## ステップ1 アドバタイズする (M5AtomS3)

まずはペリフェラルデバイスから自身の情報を周囲に飛ばすプログラムを作り、そのデータを BLE スキャナーで確認する。
UUIDの部分は[ここ](https://www.uuidgenerator.net/)で生成して差し替えることを推奨

```main.cpp

// 参考
// https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_server/BLE_server.ino

#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLE2902.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID "068c47b7-fc04-4d47-975a-7952be1a576f"
#define CHARACTERISTIC_UUID "e3737b3f-a08d-405b-b32d-35a8f6c64c5d"

void startService(BLEServer *pServer)
{
  BLEService *pService = pServer->createService(SERVICE_UUID);

  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID,
      BLECharacteristic::PROPERTY_READ |
          BLECharacteristic::PROPERTY_WRITE);
  pCharacteristic->addDescriptor(new BLE2902()); // Descriptorを定義しておかないとClient側でエラーログが出力される
  pCharacteristic->setValue("Hello World");

  pService->start();
}

void startAdvertising()
{
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true); // trueにしないと、Advertising DataにService UUIDが含まれない。
  // minIntervalはデフォルトの20でとくに問題なさそうなため、setMinPreferredは省略
  BLEDevice::startAdvertising();
}

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);
  USBSerial.begin(115200);
  M5.Display.setTextSize(2);

  BLEDevice::init("M5AtomS3 BLE Server");
  BLEServer *pServer = BLEDevice::createServer();
  startService(pServer);
  startAdvertising();

  M5.Display.println("Advertising!");
}

void loop()
{
}
```

実行して BLE Scanner を使うと以下のように表示される

![スクリーンショット_2023-07-15_23_45_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/bd1897b3-959a-8e6b-9a36-f4ebacfd5d47.png)

1. `BLEDevice::Init`で定義した Device Local Nameが表示されている
2. `pAdvertising->addServiceUUID(SERVICE_UUID);`でセットしたServiceUUIDが表示されている
3. `pCharacteristic->setValue("Hello World");`でセットした値が表示されている

BLE Scanner を使うことでアドバタイズできていることを確認することができた。

## ステップ2 セントラルデバイスからペリフェラルデバイスを探す(M5AtomLite)

```main.cpp
// 参考
// https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_client/BLE_client.ino

#include <M5Unified.h>
#include <BLEDevice.h>

#define SERVICE_UUID "068c47b7-fc04-4d47-975a-7952be1a576f"
#define CHARACTERISTIC_UUID "e3737b3f-a08d-405b-b32d-35a8f6c64c5d"

static BLEUUID serviceUUID(SERVICE_UUID);
static BLEUUID charUUID(CHARACTERISTIC_UUID);

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
  void onResult(BLEAdvertisedDevice advertisedDevice)
  {
    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID))
    {
      Serial.println("Device found!");
      advertisedDevice.getScan()->stop();
    }
  }
};

void scan()
{
  BLEScan *pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  // Interval, Windowはdefaultの値で動作して問題なさそうなため設定しない。
  // アドバタイズを受信するだけのためパッシブスキャン
  // trueにすると高速にペリフェラルを検出できるかもしれないが、パッシブでもすぐ検出できるため必要性は感じていない
  // https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_scan/BLE_scan.ino#L27
  pBLEScan->setActiveScan(false);

  // スキャン5秒には特に意味はない。
  // スキャン結果を残しておく必要がないため、終わったクリアする。そのためにis_continueはfalseにする
  pBLEScan->start(5, false);
}

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);

  BLEDevice::init("M5AtomLite BLE Client");

  // setupで単発実行。繰り返し実行するならloopに配置する必要がある
  scan();
}

void loop()
{
}

```

![スクリーンショット 2023-07-16 15.48.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/99a96cd9-4522-f3f5-036f-07a81e432b79.png)

`Upload and Monitor`で AtomLite に流し込む。
流し込んでる途中に、ステップ1のプログラムを別電源から起動しておく。

すると流し込みが終わったら以下のようなログが流れる。

実行ログ

```
Advertised Device: Name:...
Advertised Device: Name: , Address: dc:54:75:c8:c9:dd, serviceUUID: 068c47b7-fc04-4d47-975a-7952be1a576f, rssi: -36 
Device found!
```

「"Device found!」 と出力されていることから対象のデバイスを検出できていることがわかる。

1. `MyAdvertisedDeviceCallbacks`の`onResult`にスキャン結果が入ってくる。
2. UUIDを比較することでスキャンしたデバイスが求めているものかを確かめる

という手順。

M5Stack CoreS3 をセントラルとして使ってみたところ対象デバイスを検出できなかった。
本コードでデバイス検出できない、かつ BLE Scanner で検出できる場合にはセントラルとして使っているデバイスを変えてみたり設定を確認するのが良さそう。

## ステップ3 セントラルからペリフェラルに接続する

▶️ セントラル用コード

[diff](https://github.com/teach310/AtomLiteSample/commit/0e205fd4a5298709d233c2c52d52d9e4d907550a)

```main.cpp
// 参考
// https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_client/BLE_client.ino

#include <M5Unified.h>
#include <BLEDevice.h>

#define SERVICE_UUID "068c47b7-fc04-4d47-975a-7952be1a576f"
#define CHARACTERISTIC_UUID "e3737b3f-a08d-405b-b32d-35a8f6c64c5d"

static BLEUUID serviceUUID(SERVICE_UUID);
static BLEUUID charUUID(CHARACTERISTIC_UUID);
static BLEAdvertisedDevice *pPeripheral;

static int8_t state = 0;

#define STATE_IDLE 0
#define STATE_DO_CONNECT 1
#define STATE_CONNECTED 3

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
  void onResult(BLEAdvertisedDevice advertisedDevice)
  {
    Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str());
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID))
    {
      Serial.println("Device found!");
      pPeripheral = new BLEAdvertisedDevice(advertisedDevice);
      advertisedDevice.getScan()->stop();
      state = STATE_DO_CONNECT;
    }
  }
};

class MyClientCallbacks : public BLEClientCallbacks
{
  void onConnect(BLEClient *pclient)
  {
    Serial.println("onConnect");
    state = STATE_CONNECTED;
  }

  void onDisconnect(BLEClient *pclient)
  {
    Serial.println("onDisconnect");
    state = STATE_IDLE;
  }
};

void scan()
{
  BLEScan *pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  // Interval, Windowはdefaultの値で動作して問題なさそうなため設定しない。
  // アドバタイズを受信するだけのためパッシブスキャン
  // trueにすると高速にペリフェラルを検出できるかもしれないが、パッシブでもすぐ検出できるため必要性は感じていない
  // https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_scan/BLE_scan.ino#L27
  pBLEScan->setActiveScan(false);

  // スキャン5秒には特に意味はない。
  // スキャン結果を残しておく必要がないため、終わったクリアする。そのためにis_continueはfalseにする
  pBLEScan->start(5, false);
}

bool connect()
{
  BLEClient *pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallbacks());
  return pClient->connect(pPeripheral);
}

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);

  BLEDevice::init("M5AtomLite BLE Client");

  // setupで単発実行。繰り返し実行するならloopに配置する必要がある
  scan();
}

void loop()
{
  switch (state)
  {
  case STATE_DO_CONNECT:
    if (connect())
    {
      Serial.println("Connected to server");
    }
    else
    {
      Serial.println("Failed to connect");
      state = STATE_IDLE;
    }
    break;
  default:
    break;
  }
}

```

1. `MyAdvertisedDeviceCallbacks`の`onResult`にスキャン結果が入ってくる。
2. スキャンしたペリフェラル(BLEAdvertisedDevice)を変数に入れておく
3. `BLEClient`の`connect`でペリフェラルに接続
4. 接続できたらコールバックが呼ばれる。

STATE_CONNECTED が 3 なのは間に STATE_CONNECTING が入りそうだからというだけで深い意味はない。

▶️ ペリフェラル用コード

[diff](https://github.com/teach310/AtomS3Sample/commit/ce0662235fc1439c069fd5bd32948ed34e737f20)

```main.cpp

// 参考
// https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_server/BLE_server.ino

#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLE2902.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID "068c47b7-fc04-4d47-975a-7952be1a576f"
#define CHARACTERISTIC_UUID "e3737b3f-a08d-405b-b32d-35a8f6c64c5d"

static String text = "";
static bool redraw = false;

class MyServerCallbacks : public BLEServerCallbacks
{
  void onConnect(BLEServer *pServer)
  {
    USBSerial.println("onConnect");
    text = "Connected!";
    redraw = true;
  };

  void onDisconnect(BLEServer *pServer)
  {
    USBSerial.println("onDisconnect");
    text = "Disconnected!";
    redraw = true;
  }
};

void startService(BLEServer *pServer)
{
  BLEService *pService = pServer->createService(SERVICE_UUID);

  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID,
      BLECharacteristic::PROPERTY_READ |
          BLECharacteristic::PROPERTY_WRITE);
  pCharacteristic->addDescriptor(new BLE2902()); // Descriptorを定義しておかないとClient側でエラーログが出力される
  pCharacteristic->setValue("Hello World");

  pService->start();
}

void startAdvertising()
{
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true); // trueにしないと、Advertising DataにService UUIDが含まれない。
  // minIntervalはデフォルトの20でとくに問題なさそうなため、setMinPreferredは省略
  BLEDevice::startAdvertising();
}

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);
  USBSerial.begin(115200);
  M5.Display.setTextSize(2);

  BLEDevice::init("M5AtomS3 BLE Server");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  startService(pServer);
  startAdvertising();

  text = "Advertising!";
  redraw = true;
}

void loop()
{
  if (!redraw)
  {
    return;
  }
  redraw = false;

  M5.Display.clear();
  M5.Display.setCursor(0, 0);
  M5.Display.println(text);
}
```

コールバックを追加することで接続されたかどうかを取得。
あとは状態をディスプレイに表示。


上記2つのプログラムをスキャンと同様の方法で実行すると AtomLite 側では以下のような実行ログがでて接続できたことがわかる。

実行ログ

```
Advertised Device: Name: , Address: dc:54:75:c8:c9:dd, serviceUUID: 068c47b7-fc04-4d47-975a-7952be1a576f, rssi: -43 
Device found!
onConnect
Connected to server
```

また、AtomS3 には Connected! と表示されたら成功

## ステップ4 Read

ペリフェラルから送られたデータをセントラルで読む。
具体的には、アドバタイズ時に設定している Hello World という文字列を読む

```.cpp
  pCharacteristic->addDescriptor(new BLE2902());
  pCharacteristic->setValue("Hello World"); // <- これ
```

[diff](https://github.com/teach310/AtomLiteSample/commit/acb4815f8c3c57920dc7045209286f1d334f3b8b)

セントラルの connect() を以下に変える

```main.cpp
bool connect()
{
  BLEClient *pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallbacks());
  if (!pClient->connect(pPeripheral))
  {
    return false;
  }

  BLERemoteService *pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr)
  {
    Serial.print("Failed to find our service UUID: ");
    Serial.println(serviceUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found our service");

  BLERemoteCharacteristic *pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
  if (pRemoteCharacteristic == nullptr)
  {
    Serial.print("Failed to find our characteristic UUID: ");
    Serial.println(charUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found our characteristic");

  if (pRemoteCharacteristic->canRead())
  {
    std::string value = pRemoteCharacteristic->readValue();
    Serial.print("The characteristic value was: ");
    Serial.println(value.c_str());
  }

  return true;
}
```

実行ログ

```
Advertised Device: Name: , Address: dc:54:75:c8:c9:dd, serviceUUID: 068c47b7-fc04-4d47-975a-7952be1a576f, rssi: -28 
Device found!
onConnect
 - Found our service
 - Found our characteristic
The characteristic value was: Hello World
Connected to server
```

Hello World　という文字が表示できていれば成功

`BLERemoteService`, `BLERemoteCharacteristic` を使うことでデータにアクセスできる。

## ステップ5 Write
セントラルからペリフェラルにデータ送信

▶️ セントラル用コード

[diff](https://github.com/teach310/AtomLiteSample/commit/45de0b838cbf59b1b3c57488cd05aaa5075b6d82)

一部コードを抜粋

```main.cpp.diff
+ static BLERemoteCharacteristic *pRemoteCharacteristic;

...

bool connect()
{
  BLEClient *pClient = BLEDevice::createClient();
...
- BLERemoteCharacteristic *pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
+ // Writeで使うためにCharacteristicを保持しておく
+ pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID)

...

+ void connectedLoop()
+ {
+   M5.update();
+   if (M5.BtnA.wasClicked())
+   {
+     String value = "Write Data " + String(random(100, 999));
+     pRemoteCharacteristic->writeValue(value.c_str(), value.length());
+     Serial.println("Write: " + value);
+   }
+ }
```

1. `connect`時に RemoteCharacteristic を変数に格納して保持
2. ボタンを押したら RemoteCharacteristic の `writeValue` を呼び出す

ローカルとリモートの Characteristic に値を書き込む方法の違い

||ペリフェラル|セントラル|
|:--|:--|:--|
||ローカル|リモート|
|メソッド|setValue|writeValue|

▶️ ペリフェラル用コード

Write を検知するためにコールバック登録。来たら画面に表示

[diff](https://github.com/teach310/AtomS3Sample/commit/782d0e7f85442361542d29de2b50aad8d1eab841)

一部コードを抜粋

```main.cpp
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    std::string value = pCharacteristic->getValue();

    if (value.length() > 0)
    {
      USBSerial.print("onWrite: ");
      USBSerial.println(value.c_str());
      text = value.c_str();
      redraw = true;
    }
  }
};

...
  pCharacteristic->setValue("Hello World");
  pCharacteristic->setCallbacks(new MyCharacteristicCallbacks());
```

参考

https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_write/BLE_write.ino#L17-L31

実行して接続後に AtomLite のボタンを押すと以下のようなログが出る。

```
Advertised Device: Name: , Address: dc:54:75:c8:c9:dd, serviceUUID: 068c47b7-fc04-4d47-975a-7952be1a576f, rssi: -52 
Device found!
onConnect
 - Found our service
 - Found our characteristic
The characteristic value was: Hello World
Connected to server
Write: Write Data 443
Write: Write Data 207
Write: Write Data 956
Write: Write Data 426
```

また AtomS3 の画面に「Write Data 443」のように表示されたらデータ送信成功

## ステップ6 Notify

ペリフェラルからセントラルにデータ送信

▶️ ペリフェラル用コード

[diff](https://github.com/teach310/AtomS3Sample/commit/09e0662deb46d8cd57c0ff3dd93bc641ec948851)

一部コードを抜粋

```main.cpp.diff
+ #define NOTIFY_CHARACTERISTIC_UUID "c9da2ce8-d119-40d5-90f7-ef24627e8193"

+ BLECharacteristic *pNotifyCharacteristic;

...

+  // 権限を最小にするためにNotify用のCharacteristicはReadWrite用とは別に定義
+  pNotifyCharacteristic = pService->createCharacteristic(
+      NOTIFY_CHARACTERISTIC_UUID,
+      BLECharacteristic::PROPERTY_NOTIFY);
+  pNotifyCharacteristic->addDescriptor(new BLE2902());

...

+void onClickBtnA()
+{
+  if (!connected)
+  {
+    return;
+  }

+  String value = "Notify Data " + String(random(100, 999));
+  pNotifyCharacteristic->setValue(value.c_str());
+  pNotifyCharacteristic->notify();
+}

```

1. 新しく Notify 用の Characteristic を作成
2. ボタンを押したら value を set して送信

参考

https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE_notify/BLE_notify.ino#L93-L94

▶️ セントラル用コード

[diff](https://github.com/teach310/AtomLiteSample/commit/d275df573c59a7d9ced38330498d182313b69bd8)

```main.cpp
static void notifyCallback(
    BLERemoteCharacteristic *pBLERemoteCharacteristic,
    uint8_t *pData,
    size_t length,
    bool isNotify)
{
  Serial.print("Notify callback for characteristic ");
  Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
  Serial.print(" of data length ");
  Serial.println(length);
  Serial.print("data: ");
  Serial.println((char *)pData);
}

...

  pNotifyCharacteristic = pRemoteService->getCharacteristic(notifyCharUUID);
  if (pNotifyCharacteristic == nullptr)
  {
    Serial.print("Failed to find our notify characteristic UUID: ");
    Serial.println(notifyCharUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }

  if (pNotifyCharacteristic->canNotify())
  {
    pNotifyCharacteristic->registerForNotify(notifyCallback);
    Serial.println(" - Registered for notify");
  }
```

1. static メソッドで Notify されたときに呼ばれるメソッドを定義
2. 登録

実行して接続し、M5AtomS3 のボタンを押す。
そして、M5AtomLite 側で以下のようなログが出力されたら成功

```
Advertised Device: Name: , Address: dc:54:75:c8:c9:dd, serviceUUID: 068c47b7-fc04-4d47-975a-7952be1a576f, rssi: -49 
Device found!
onConnect
 - Found our service
 - Found our characteristic
The characteristic value was: Hello World
 - Registered for notify
Connected to server
Notify callback for characteristic c9da2ce8-d119-40d5-90f7-ef24627e8193 of data length 15
␆ata: Notify Data 700
```

## おわりに

有効なサンプルを見つけるところ、BLE のコンセプト、接続が難しく時間がかかった。
途中わからない部分や、引数どうすればいいか等は ChatGPT に聞いたり定義ジャンプで直接元コードを読むことで、どう書くべきかを判断した。

一気に理解するのは難しい！と思っている方に本記事の段階的なプログラムが役に立てば幸いです。
学習始めたばかりで間違っているところもあるかもしれないためそこはご了承ください。(特にDescriptor周り)