13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

本記事について

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

前提

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

コンセプト

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

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

BLE Scanner について

BLE Scanner App Store

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

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

IMG_5476.PNG

toio コアキューブ技術使用

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

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

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

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

  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

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. MyAdvertisedDeviceCallbacksonResultにスキャン結果が入ってくる。
  2. UUIDを比較することでスキャンしたデバイスが求めているものかを確かめる

という手順。

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

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

▶️ セントラル用コード

diff

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. MyAdvertisedDeviceCallbacksonResultにスキャン結果が入ってくる。
  2. スキャンしたペリフェラル(BLEAdvertisedDevice)を変数に入れておく
  3. BLEClientconnectでペリフェラルに接続
  4. 接続できたらコールバックが呼ばれる。

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

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

diff

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

セントラルの 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

一部コードを抜粋

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

一部コードを抜粋

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

参考

実行して接続後に 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

一部コードを抜粋

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 して送信

参考

▶️ セントラル用コード

diff

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周り)

13
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?