本記事について
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デバイスを探すアプリ
どんなデータを送っているのかを確認することができる。
Near By
のタブで周辺の BLE デバイスを探し、Connect
ボタンを押すと中身が見れる。
toioのアドバタイズデータを見てみると以下のようになった。
ADVERTIMENT DATA の中に Service UUIDs という値があるのがポイント。
ステップ1 アドバタイズする (M5AtomS3)
まずはペリフェラルデバイスから自身の情報を周囲に飛ばすプログラムを作り、そのデータを BLE スキャナーで確認する。
UUIDの部分はここで生成して差し替えることを推奨
// 参考
// 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 を使うと以下のように表示される
-
BLEDevice::Init
で定義した Device Local Nameが表示されている -
pAdvertising->addServiceUUID(SERVICE_UUID);
でセットしたServiceUUIDが表示されている -
pCharacteristic->setValue("Hello World");
でセットした値が表示されている
BLE Scanner を使うことでアドバタイズできていることを確認することができた。
ステップ2 セントラルデバイスからペリフェラルデバイスを探す(M5AtomLite)
// 参考
// 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()
{
}
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!」 と出力されていることから対象のデバイスを検出できていることがわかる。
-
MyAdvertisedDeviceCallbacks
のonResult
にスキャン結果が入ってくる。 - UUIDを比較することでスキャンしたデバイスが求めているものかを確かめる
という手順。
M5Stack CoreS3 をセントラルとして使ってみたところ対象デバイスを検出できなかった。
本コードでデバイス検出できない、かつ BLE Scanner で検出できる場合にはセントラルとして使っているデバイスを変えてみたり設定を確認するのが良さそう。
ステップ3 セントラルからペリフェラルに接続する
▶️ セントラル用コード
// 参考
// 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;
}
}
-
MyAdvertisedDeviceCallbacks
のonResult
にスキャン結果が入ってくる。 - スキャンしたペリフェラル(BLEAdvertisedDevice)を変数に入れておく
-
BLEClient
のconnect
でペリフェラルに接続 - 接続できたらコールバックが呼ばれる。
STATE_CONNECTED が 3 なのは間に STATE_CONNECTING が入りそうだからというだけで深い意味はない。
▶️ ペリフェラル用コード
// 参考
// 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 という文字列を読む
pCharacteristic->addDescriptor(new BLE2902());
pCharacteristic->setValue("Hello World"); // <- これ
セントラルの connect() を以下に変える
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
セントラルからペリフェラルにデータ送信
▶️ セントラル用コード
一部コードを抜粋
+ 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);
+ }
+ }
-
connect
時に RemoteCharacteristic を変数に格納して保持 - ボタンを押したら RemoteCharacteristic の
writeValue
を呼び出す
ローカルとリモートの Characteristic に値を書き込む方法の違い
ペリフェラル | セントラル | |
---|---|---|
ローカル | リモート | |
メソッド | setValue | writeValue |
▶️ ペリフェラル用コード
Write を検知するためにコールバック登録。来たら画面に表示
一部コードを抜粋
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
ペリフェラルからセントラルにデータ送信
▶️ ペリフェラル用コード
一部コードを抜粋
+ #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();
+}
- 新しく Notify 用の Characteristic を作成
- ボタンを押したら value を set して送信
参考
▶️ セントラル用コード
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");
}
- static メソッドで Notify されたときに呼ばれるメソッドを定義
- 登録
実行して接続し、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周り)