【重要】LINBLE-Z2量産販売開始およびLINBLE-Z1使用部品変更予定のお知らせ

M5StackでBluetooth® LE通信を実現するまでの手順メモ④セントラル通信編

こんにちは、ムセンコネクトCCOの伊藤です。(プロフィール紹介はこちら

M5StackでBluetooth® LE(BLE)通信を実現するまでの手順メモ」の第4回目です。今回は「セントラル通信編」です。

ご注意ください

M5Stack自体はBluetooth認証を取得していないため、M5Stackで開発した最終製品のBluetooth認証取得は困難な場合があります。M5Stackを用いた製品化をご検討の際は、この点に十分ご注意ください。

M5Stackの公式サイトで製品を検索しても「Bluetooth」というキーワードでは見つからない
目次

セントラル動作を試す

前回、M5Stackをペリフェラル機器としてスマートフォンと双方向のデータ通信ができました。今回はセントラル機器として、前回つくったペリフェラル機器との通信に挑戦します。これでM5Stackを2台準備した意味が出てきます!

セントラルは、接続を待っているペリフェラルをスキャンして見つけ、接続を試みます。接続が確立されると、双方向のデータ通信が可能になります。

接続型の双方向データ通信(GATT通信)については、こちらのページでも解説していますので、併せてご確認ください。

恒例となりましたが、今回もAIである Gemini 2.5 Pro を利用してコーディングの支援をしてもらいます。繰り返しになりますが、AIの生成コードは過信しすぎないようにしましょう。

プロンプト指示

前回開発したペリフェラル機器とデータ通信を行うため、サービスはNordic Semiconductor社が提唱するカスタムサービスであるNordic UART Service (NUS) を採用する必要があります。また、セントラル側もPCから仮想COMポートを介してM5Stackと通信するようにします。

ペリフェラルはM5Stack Gray、セントラルはM5Stack CoreS3で動作させる構成とします。

これらの設定に至ったプロンプトは以下のとおりです。

前回、M5Stack Gray Core V1.0向けに

「Bluetooth LEのGATT通信を行うペリフェラル機器を作る」という目的でプログラムコードを提示してもらった。
今回、新たにM5CoreS3を入手したので、以下の条件のプログラムコードを提示してほしい。

開発環境
Arduino IDE

目的
前回の「Bluetooth LEのGATT通信を行うペリフェラル機器」を対象として接続・通信するセントラル機器を作る

機能
スキャン
電源投入後、Bluetooth LEのスキャンをし続ける
Complete Local Nameに"M5Stack NUS"と設定されているペリフェラル機器を見つけたら接続試行する
切断したら、再度スキャンを再開する

GATT通信
Nordic UART Service(NUS)をサポートする
Tx Characteristicをサポートする
Rx Characteristicをサポートする
接続後、ペリフェラル機器のTx CharacteristicのNotificationをEnableにする

Serial通信
仮想COMポートを利用して、上記GATT通信を経由してASCII文字列の送受信ができる

ディスプレイ表示
状態に応じてディスプレイの色を変更する
スキャン中:青
接続中:緑

ペリフェラル機器のTx CharacteristicのNotificationは、接続後に明示的に有効化しないと通知が届きません。また、接続状態を把握できるよう、ディスプレイ表示を変更する試みも行いました。

生成されたArduinoスケッチ(プログラムコード)は以下のとおりです。

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

// 接続ターゲットのBLEアドバタイズ名
#define TARGET_DEVICE_NAME "M5Stack NUS"

// Nordic UART Service (NUS) のUUID
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Centralが書き込む先 (PeripheralのRX)
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Centralが読み込む元 (PeripheralのTX)

// グローバル変数
static BLEAdvertisedDevice* pTargetDevice = nullptr;
static BLERemoteCharacteristic* pRemoteTxCharacteristic; // Peripheralからの送信を受け取る
static BLERemoteCharacteristic* pRemoteRxCharacteristic; // Peripheralへの送信で使う
static boolean doConnect = false;
static boolean connected = false;

// PeripheralからNotificationが届いたときの処理
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    // 受信データをSerialモニタに出力
    Serial.print("Received from BLE: ");
    Serial.write(pData, length);
    Serial.println();
}

// スキャンを開始する関数
void startScan() {
  Serial.println("Scanning for BLE devices...");
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(3);
  M5.Display.printf("Scanning for\n%s", TARGET_DEVICE_NAME);
  
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->start(5, false); // 5秒間スキャンする (見つかればコールバックで停止)
}

// Clientの接続/切断イベントを処理するコールバック
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    Serial.println(" - Connected to peripheral");
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println(" - Disconnected from peripheral");
    // 切断されたら再度スキャンを開始する
    startScan();
  }
};


// サーバーへの接続処理
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(pTargetDevice->getAddress().toString().c_str());

  BLEClient* pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());
  
  // 接続試行
  if (!pClient->connect(pTargetDevice)) {
    Serial.println(" - Connection failed.");
    return false;
  }
  
  // サービスの取得
  BLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
  if (pRemoteService == nullptr) {
    Serial.print(" - Failed to find service UUID: ");
    Serial.println(SERVICE_UUID);
    pClient->disconnect();
    return false;
  }
  
  // Characteristicの取得
  pRemoteTxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_TX);
  pRemoteRxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_RX);

  if (pRemoteTxCharacteristic == nullptr || pRemoteRxCharacteristic == nullptr) {
    Serial.println(" - Failed to find one or both of the characteristics.");
    pClient->disconnect();
    return false;
  }

  // Peripheralからの通知(Notify)を有効化
  if(pRemoteTxCharacteristic->canNotify()) {
    pRemoteTxCharacteristic->registerForNotify(notifyCallback);
    Serial.println(" - Notifications enabled for TX characteristic.");
  }

  connected = true;
  M5.Display.fillScreen(GREEN);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(4);
  M5.Display.println("Connected!");
  M5.Display.setTextSize(2);
  M5.Display.println("Ready for UART communication.");
  return true;
}


// スキャン結果を処理するコールバック
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // Complete Local Nameがターゲットと一致するか確認
    if (advertisedDevice.haveName() && advertisedDevice.getName() == TARGET_DEVICE_NAME) {
      BLEDevice::getScan()->stop();
      pTargetDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true; // 接続フラグを立てる
      Serial.print("Found target device: ");
      Serial.println(advertisedDevice.toString().c_str());
    }
  }
};


void setup() {
  M5.begin();
  Serial.begin(115200);

  BLEDevice::init(""); // Centralなのでデバイス名は不要

  // スキャナを取得し、コールバックを設定
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);

  // 最初のスキャンを開始
  startScan();
}

void loop() {
  M5.update();

  // 接続フラグが立ったら、接続処理を実行
  if (doConnect) {
    doConnect = false;
    if (!connectToServer()) {
      // 接続に失敗したら、再度スキャンを開始
      Serial.println("Will retry scanning...");
      startScan();
    }
  }

  // 接続中、かつSerialモニタからデータが送信された場合
  if (connected) {
    if (Serial.available()) {
      // Serialからの文字列を読み取り、BLE経由でPeripheralに送信
      String txString = Serial.readStringUntil('\n');
      if (txString.length() > 0) {
        pRemoteRxCharacteristic->writeValue(txString.c_str(), txString.length());
        Serial.print("Sent via BLE: ");
        Serial.println(txString);
      }
    }
  }
}

生成されたコードがプロンプトの指示に沿っていない点にお気づきでしょうか?

「電源投入後、Bluetooth LEのスキャンをし続ける」と指示したにもかかわらず、5秒間しかスキャンしない箇所です。この点についてプロンプトで指摘します。

確認ですが、このセントラルは5秒間しかスキャンしないのではないでしょうか?
目的の”M5Stack NUS”が見つかるまで永続的にスキャンするような作りですか?

やはり間違えていたようです。

はい、ご指摘の通りです。鋭いご質問ありがとうございます。
現在のコードは、pBLEScan->start(5, false); の部分で5秒間だけスキャンを行い、
その間に "M5Stack NUS" が見つからなかった場合はスキャンを停止して待機状態になってしまいます。
目的のデバイスが見つかるまで永続的にスキャンし続けるように修正する必要があります。

修正されたコードは以下のとおりです。

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

// 接続ターゲットのBLEアドバタイズ名
#define TARGET_DEVICE_NAME "M5Stack NUS"

// Nordic UART Service (NUS) のUUID
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // Centralが書き込む先 (PeripheralのRX)
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // Centralが読み込む元 (PeripheralのTX)

// グローバル変数
static BLEAdvertisedDevice* pTargetDevice = nullptr;
static BLERemoteCharacteristic* pRemoteTxCharacteristic; // Peripheralからの送信を受け取る
static BLERemoteCharacteristic* pRemoteRxCharacteristic; // Peripheralへの送信で使う
static boolean doConnect = false;
static boolean connected = false;

// PeripheralからNotificationが届いたときの処理
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    // 受信データをSerialモニタに出力
    Serial.print("Received from BLE: ");
    Serial.write(pData, length);
    Serial.println();
}

// スキャンを開始する関数
void startScan() {
  Serial.println("Scanning for BLE devices indefinitely...");
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(3);
  M5.Display.printf("Scanning for\n%s", TARGET_DEVICE_NAME);
  
  BLEScan* pBLEScan = BLEDevice::getScan();
  
  // ★★★ 修正点: スキャン時間を 0 にして無期限スキャンに変更 ★★★
  pBLEScan->start(0, false); // 0秒指定 = 無期限スキャン
}


// Clientの接続/切断イベントを処理するコールバック
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    Serial.println(" - Connected to peripheral");
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println(" - Disconnected from peripheral");
    // 切断されたら再度スキャンを開始する
    startScan();
  }
};


// サーバーへの接続処理
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(pTargetDevice->getAddress().toString().c_str());

  BLEClient* pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());
  
  // 接続試行
  if (!pClient->connect(pTargetDevice)) {
    Serial.println(" - Connection failed.");
    return false;
  }
  
  // サービスの取得
  BLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
  if (pRemoteService == nullptr) {
    Serial.print(" - Failed to find service UUID: ");
    Serial.println(SERVICE_UUID);
    pClient->disconnect();
    return false;
  }
  
  // Characteristicの取得
  pRemoteTxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_TX);
  pRemoteRxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_RX);

  if (pRemoteTxCharacteristic == nullptr || pRemoteRxCharacteristic == nullptr) {
    Serial.println(" - Failed to find one or both of the characteristics.");
    pClient->disconnect();
    return false;
  }

  // Peripheralからの通知(Notify)を有効化
  if(pRemoteTxCharacteristic->canNotify()) {
    pRemoteTxCharacteristic->registerForNotify(notifyCallback);
    Serial.println(" - Notifications enabled for TX characteristic.");
  }

  connected = true;
  M5.Display.fillScreen(GREEN);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(4);
  M5.Display.println("Connected!");
  M5.Display.setTextSize(2);
  M5.Display.println("Ready for UART communication.");
  return true;
}


// スキャン結果を処理するコールバック
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // Complete Local Nameがターゲットと一致するか確認
    if (advertisedDevice.haveName() && advertisedDevice.getName() == TARGET_DEVICE_NAME) {
      BLEDevice::getScan()->stop();
      pTargetDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true; // 接続フラグを立てる
      Serial.print("Found target device: ");
      Serial.println(advertisedDevice.toString().c_str());
    }
  }
};


void setup() {
  M5.begin();
  Serial.begin(115200);

  BLEDevice::init(""); // Centralなのでデバイス名は不要

  // スキャナを取得し、コールバックを設定
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->setInterval(100);
  pBLEScan->setWindow(99);

  // 最初のスキャンを開始
  startScan();
}

void loop() {
  M5.update();

  // 接続フラグが立ったら、接続処理を実行
  if (doConnect) {
    doConnect = false;
    if (!connectToServer()) {
      // 接続に失敗したら、再度スキャンを開始
      Serial.println("Will retry scanning...");
      startScan();
    }
  }

  // 接続中、かつSerialモニタからデータが送信された場合
  if (connected) {
    if (Serial.available()) {
      // Serialからの文字列を読み取り、BLE経由でPeripheralに送信
      String txString = Serial.readStringUntil('\n');
      if (txString.length() > 0) {
        pRemoteRxCharacteristic->writeValue(txString.c_str(), txString.length());
        Serial.print("Sent via BLE: ");
        Serial.println(txString);
      }
    }
  }
}

動作確認

提案されたコードのコンパイル、および書き込みが完了したので、実際に試してみます。

まず、セントラルとなるM5Stack CoreS3を起動します。

ディスプレイが青く表示され、”M5Stack NUS”という名前のペリフェラルを継続的にスキャンしていることがわかります。

スキャン中のセントラル

それでは、ペリフェラルであるM5Stack Grayも起動します。M5Stack Grayは前回から変更がないため、ディスプレイなどで状態を確認することはできませんが、ペリフェラルを起動すると、セントラルがスキャンで発見し、接続を試行します。セントラルのディスプレイが青から緑に変わり、「Connected!」と表示され、接続が確立されたようです。

接続した

この接続状態を維持したまま、セントラルとペリフェラルの間でBluetooth LEを介したシリアルデータ送受信が可能か試します。

全体の構成は以下の通りです。PC1台で検証しているため、見た目が良くない点はご容赦ください。

構成

結論からお伝えすると、両デバイスからの送受信することを確認できました!

データ送受信確認

まとめ

送受信はできたものの、複数回試行してみると、シリアル通信が正常に表示されなかったり、再接続に失敗したりと、まだまだ改善の余地があることが分かりました。

Nordic UART Serviceを使用することで、Bluetooth LE通信自体はスムーズに行えました。しかし、それ以外の部分でどのように動作させるかについては、さらに検討が必要です。今回は試すことが主な目的だったため、詳細は割愛します。

Bluetooth LE通信は無線通信であるため、通信状態は周囲の環境に依存します。今回は未確認ですが、意図しない切断が発生した場合や、通信中に距離が離れて通信が確立できなくなった場合の処理も考慮する必要があります。また、今回は接続してデータ送信ができただけにすぎず、どの程度のスループットが出せるかなどの性能は追求していません。さらに、どの程度の距離まで通信できるかも未検証です。

冒頭にもお伝えしましたが、M5Stack自体はBluetooth認証を取得していないため、M5Stackで製品を開発しようとすると、最後のBluetooth認証取得の際に困難が待ち構えています。M5Stackを用いた製品化をご検討の際は、この点に十分ご注意ください。

前回同様、お気づきの方もおられるかもしれませんが、今回試してみたセントラル・ペリフェラルの両機能は、ムセンコネクトのBluetooth® USBアダプタ「LINBLE-Z1 / LINBLE-Z2ドングル」LINBLE-Z1 / LINBLE-Z2 カンタンスターターキットを2台つかった構成に似通った機能となっています。

LINBLEシリーズは、上記のような、通信距離やスループット、消費電流などの各情報を公開しております。ぜひ参考にしてください。

次回は、LINBLE-Z1を相手に双方向のデータ通信を実現したいと思います!

よろしければシェアをお願いします
目次