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

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

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

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

M5StackでBluetooth® LE通信を実現するまでの手順メモ(全5回)
ご注意ください

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

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

LINBLE-Z1とのBluetooth LEセントラル通信に挑戦

前回の記事では、2台のM5Stackを用いてNordic UART Service (NUS) ベースのBluetooth LE通信(ペリフェラル・セントラル構成)を実現しました。これは、ムセンコネクトのBluetooth® USBアダプタ「LINBLE-Z1 / LINBLE-Z2ドングル」LINBLE-Z1 / LINBLE-Z2 カンタンスターターキットを2台使用した構成と機能的に類似していることをお伝えしました。

当然、ここまで来ると「LINBLEとBluetooth LE通信ができるのでは?」という疑問が湧いてきますよね。ご安心ください、その期待にお応えします。

今回はLINBLE-Z1をペリフェラル、M5Stack CoreS3をセントラルとした構成で進めます。前回は通信の実現を優先し、性能はあまり重視しませんでした。(とりあえず接続してデータ送受信ができればOKというレベル)今回は実用レベルまで性能を追求します。

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

プロンプト指示

LINBLEシリーズのGATT定義としてLINBLE UART Serviceというカスタムサービスに対応する必要があります。このサービスはLINBLE-Z1 ユーザーマニュアルで公開されています。セントラル側にLINBLEシリーズを採用せず、ユーザー様ご自身でセントラル側の機器やスマートフォンアプリを開発される際には、このサービスに対応するように作りこんでいただく必要があります。

LINBLEシリーズはPCから仮想COMポートを介してコマンド制御しますが、セントラル側もPCから仮想COMポートを介してM5Stackと通信するようにします。この機能は前回のセントラル通信編と同様です。

それらを仕様に落とし込み、与えたプロンプトは以下のとおりです。

前回、M5CoreS3向けに

"「Bluetooth LEのGATT通信を行うペリフェラル機器」を対象として接続・通信するセントラル機器を作る"
という目的でプログラムコードを提示してもらった。

前回はGATT通信としてNordic UART Service(NUS)をサポートしたペリフェラル機器を対象としていたが
今回は、株式会社ムセンコネクトが製造・販売しているLINBLE-Z1をペリフェラル機器として対象とする。
LINBLE-Z1はLINBLE UART Serviceをサポートしている。

セントラル機器として仕様を以下に示す。
仕様を満たすプログラムコードを提示してほしい。


開発環境
・Arduino IDE


目的
・LINBLE-Z1を対象として接続・通信するセントラル機器を作る


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


2.GATT通信
・LINBLE UART Serviceをサポートする(UUID:27ADC9CA-35EB-465A-9154-B8FF9076F3E8)
※Nordic UART Service(NUS)のように、仮想的にUARTを実現するサービスと考えてよい
・Data From Peripheral Characteristicをサポートする(プロパティ:Notify、UUID:27ADC9CB-35EB-465A-9154-B8FF9076F3E8)
・Data To Peripheral Characteristicをサポートする(プロパティ:Write Without Response、UUID:27ADC9CC-35EB-465A-9154-B8FF9076F3E8)
・接続後、Data From Peripheral CharacteristicをEnableにする



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


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

今回も、接続状態を把握できるよう、ディスプレイ表示を変更する試みも行います。LINBLE UART Serviceと単語だけで実装されるのか不安だったため、UUIDやプロパティを丁寧に記載しました。

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

/**
 * @file M5CoreS3_LINBLE_Central.ino
 * @author Gemini
 * @brief M5CoreS3をセントラルとし、LINBLE-Z1とBLE GATT通信を行うプログラム
 * * @version 0.1
 * @date 2024-07-16
 * * @copyright Copyright (c) 2024
 * * 機能:
 * 1. Bluetooth LEのスキャンを行い、"LINBLE-Z1"という名前のデバイスを探す。
 * 2. 対象デバイスを発見後、自動で接続を試みる。
 * 3. 接続後は、LINBLE UART Serviceを介して双方向のデータ通信が可能になる。
 * 4. PCのシリアルモニタとLINBLE-Z1間でUART通信を中継する。
 * 5. 接続状態に応じてM5CoreS3のディスプレイ色を変更する(スキャン中:青、接続中:緑)。
 * 6. 接続が切断された場合、自動的にスキャンを再開する。
 */


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


// ----------------------------------------------------------------
// LINBLE UART ServiceのUUID定義
// ----------------------------------------------------------------
// Service UUID
static BLEUUID serviceUUID("27adc9ca-35eb-465a-9154-b8ff9076f3e8");
// Data From Peripheral Characteristic (Notify)
static BLEUUID charUUID_RX("27adc9cb-35eb-465a-9154-b8ff9076f3e8");
// Data To Peripheral Characteristic (Write Without Response)
static BLEUUID charUUID_TX("27adc9cc-35eb-465a-9154-b8ff9076f3e8");




// ----------------------------------------------------------------
// グローバル変数
// ----------------------------------------------------------------
static boolean doConnect = false;         // 接続試行フラグ
static boolean deviceConnected = false;   // 接続状態フラグ
static BLEAdvertisedDevice* myDevice;     // 発見したデバイスの情報を保持
static BLERemoteCharacteristic* pRemoteCharacteristicTX; // Write用
static BLERemoteCharacteristic* pRemoteCharacteristicRX; // Notify用
static BLEClient* pClient;                // BLEクライアント




// ----------------------------------------------------------------
// プロトタイプ宣言
// ----------------------------------------------------------------
bool connectToServer();
void startScan();




/**
 * @brief ペリフェラルからのNotify受信時に呼び出されるコールバック関数
 */
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    // 受信したデータをそのままシリアルモニタに出力
    Serial.write(pData, length);
}


/**
 * @brief BLEクライアントのイベント(接続/切断)を処理するコールバッククラス
 */
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    deviceConnected = true;
    M5.Display.fillScreen(GREEN);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.println("Connected to LINBLE-Z1");
    Serial.println("Connected to LINBLE-Z1.");
  }


  void onDisconnect(BLEClient* pclient) {
    deviceConnected = false;
    M5.Display.fillScreen(BLUE);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.println("Disconnected.");
    M5.Display.println("Restarting scan...");
    Serial.println("Disconnected. Restarting scan...");
   
    // スキャンを再開
    startScan();
  }
};


/**
 * @brief スキャンで見つかったデバイスを処理するコールバッククラス
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // デバイス名が"LINBLE-Z1"であるかを確認
    if (advertisedDevice.getName() == "LINBLE-Z1") {
      Serial.print("Found LINBLE-Z1: ");
      Serial.println(advertisedDevice.getAddress().toString().c_str());
     
      // スキャンを停止
      BLEDevice::getScan()->stop();
     
      // 発見したデバイスの情報をコピーして保持
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
     
      // 接続フラグを立てる
      doConnect = true;
    }
  }
};


/**
 * @brief BLEスキャンを開始する
 */
void startScan() {
  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pScan->setActiveScan(true); // デバイス名取得のためにアクティブスキャンを有効化
  pScan->start(0, false);     // 0秒 = 無限スキャン
  Serial.println("Scan started...");
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(2);
  M5.Display.println("Scanning for LINBLE-Z1...");
}


/**
 * @brief サーバー(ペリフェラル)に接続する
 * @return bool 接続に成功した場合はtrue、失敗した場合はfalse
 */
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(myDevice->getAddress().toString().c_str());


  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());


  // 接続試行
  if (!pClient->connect(myDevice)) {
    Serial.println("Connection failed.");
    return false;
  }
  Serial.println("Connected to server.");


  // サービスの取得
  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.");


  // Write用キャラクタリスティックの取得
  pRemoteCharacteristicTX = pRemoteService->getCharacteristic(charUUID_TX);
  if (pRemoteCharacteristicTX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (TX): ");
    Serial.println(charUUID_TX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (TX).");


  // Notify用キャラクタリスティックの取得
  pRemoteCharacteristicRX = pRemoteService->getCharacteristic(charUUID_RX);
  if (pRemoteCharacteristicRX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (RX): ");
    Serial.println(charUUID_RX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (RX).");


  // Notifyの有効化
  if (pRemoteCharacteristicRX->canNotify()) {
    pRemoteCharacteristicRX->registerForNotify(notifyCallback);
   
    // CCCD (0x2902) ディスクリプタに書き込んでNotifyを有効にする
    BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(BLEUUID((uint16_t)0x2902));
    if (pDescriptor != nullptr) {
      uint8_t value[2] = {0x01, 0x00}; // Notify有効
      pDescriptor->writeValue(value, 2, true);
      Serial.println("Registered for notifications.");
    }
  }


  return true;
}


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Central for LINBLE-Z1...");


  BLEDevice::init(""); // BLEデバイスを初期化(セントラルなので名前は空)
 
  startScan(); // スキャンを開始
}


/**
 * @brief メインループ
 */
void loop() {
  // 接続フラグが立っている場合、接続を試みる
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("Connection successful.");
    } else {
      Serial.println("Failed to connect. Restarting scan...");
      startScan(); // 接続に失敗したらスキャンを再開
    }
    doConnect = false;
  }


  // デバイスに接続中の場合
  if (deviceConnected) {
    // シリアルモニタからデータが送信されたら、ペリフェラルに送信する
    if (Serial.available()) {
      String str = Serial.readString();
      if (str.length() > 0) {
        pRemoteCharacteristicTX->writeValue(str.c_str(), str.length());
      }
    }
  }
 
  delay(100); // CPU負荷を軽減
}
エラーなく書き込み完了

さて、書き込みが完了したのでさっそく動作確認を、と思いましたが初期の段階で問題点を見つけてしまいました。

電源投入後、Bluetooth LEのスキャンを開始しますがディスプレイが青色表示になっていませんでした。ディスプレイの更新周りと予想はつきますが、Geminiに仕事をしてもらうためプロンプトで指示をだします。

プログラムを書き込み、動作させたところ初回のスキャン開始時にディスプレイが青色表示にならなかった。

コードの問題点を確認し、修正を加え再提示してください。

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

コメントにバージョン表記や、変更点をまとめてくれるのはありがたいですね(日付の年が間違っていますが、それは最後に指摘することにします)。やはりディスプレイ更新処理に問題があったようです。合わせて切断時の処理もstartScan()関数に任せられるよう変更したようです。

/**
 * @file M5CoreS3_LINBLE_Central.ino
 * @author Gemini
 * @brief M5CoreS3をセントラルとし、LINBLE-Z1とBLE GATT通信を行うプログラム
 * @version 0.2
 * @date 2024-07-16
 * @copyright Copyright (c) 2024
 *
 * @note v0.2変更点:
 * - 初回スキャン開始時にディスプレイ表示が更新されない問題を修正。
 * - startScan()関数内で、BLEスキャン処理の前にディスプレイ更新処理を行うように順序を変更。
 * - onDisconnect時の処理を簡素化。
 *
 * 機能:
 * 1. Bluetooth LEのスキャンを行い、"LINBLE-Z1"という名前のデバイスを探す。
 * 2. 対象デバイスを発見後、自動で接続を試みる。
 * 3. 接続後は、LINBLE UART Serviceを介して双方向のデータ通信が可能になる。
 * 4. PCのシリアルモニタとLINBLE-Z1間でUART通信を中継する。
 * 5. 接続状態に応じてM5CoreS3のディスプレイ色を変更する(スキャン中:青、接続中:緑)。
 * 6. 接続が切断された場合、自動的にスキャンを再開する。
 */


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


// ----------------------------------------------------------------
// LINBLE UART ServiceのUUID定義
// ----------------------------------------------------------------
// Service UUID
static BLEUUID serviceUUID("27adc9ca-35eb-465a-9154-b8ff9076f3e8");
// Data From Peripheral Characteristic (Notify)
static BLEUUID charUUID_RX("27adc9cb-35eb-465a-9154-b8ff9076f3e8");
// Data To Peripheral Characteristic (Write Without Response)
static BLEUUID charUUID_TX("27adc9cc-35eb-465a-9154-b8ff9076f3e8");




// ----------------------------------------------------------------
// グローバル変数
// ----------------------------------------------------------------
static boolean doConnect = false;         // 接続試行フラグ
static boolean deviceConnected = false;   // 接続状態フラグ
static BLEAdvertisedDevice* myDevice;     // 発見したデバイスの情報を保持
static BLERemoteCharacteristic* pRemoteCharacteristicTX; // Write用
static BLERemoteCharacteristic* pRemoteCharacteristicRX; // Notify用
static BLEClient* pClient;                // BLEクライアント




// ----------------------------------------------------------------
// プロトタイプ宣言
// ----------------------------------------------------------------
bool connectToServer();
void startScan();




/**
 * @brief ペリフェラルからのNotify受信時に呼び出されるコールバック関数
 */
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    // 受信したデータをそのままシリアルモニタに出力
    Serial.write(pData, length);
}


/**
 * @brief BLEクライアントのイベント(接続/切断)を処理するコールバッククラス
 */
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    deviceConnected = true;
    M5.Display.fillScreen(GREEN);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.println("Connected to LINBLE-Z1");
    Serial.println("Connected to LINBLE-Z1.");
  }


  void onDisconnect(BLEClient* pclient) {
    deviceConnected = false;
    Serial.println("Disconnected. Restarting scan...");
   
    // スキャンを再開
    startScan();
  }
};


/**
 * @brief スキャンで見つかったデバイスを処理するコールバッククラス
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // デバイス名が"LINBLE-Z1"であるかを確認
    if (advertisedDevice.getName() == "LINBLE-Z1") {
      Serial.print("Found LINBLE-Z1: ");
      Serial.println(advertisedDevice.getAddress().toString().c_str());
     
      // スキャンを停止
      BLEDevice::getScan()->stop();
     
      // 発見したデバイスの情報をコピーして保持
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
     
      // 接続フラグを立てる
      doConnect = true;
    }
  }
};


/**
 * @brief BLEスキャンを開始する
 */
void startScan() {
  // ★修正点: 先にディスプレイ表示を更新する
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(2);
  M5.Display.println("Scanning for LINBLE-Z1...");
  Serial.println("Scan started...");


  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pScan->setActiveScan(true); // デバイス名取得のためにアクティブスキャンを有効化
  pScan->start(0, false);     // 0秒 = 無限スキャン
}


/**
 * @brief サーバー(ペリフェラル)に接続する
 * @return bool 接続に成功した場合はtrue、失敗した場合はfalse
 */
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(myDevice->getAddress().toString().c_str());


  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());


  // 接続試行
  if (!pClient->connect(myDevice)) {
    Serial.println("Connection failed.");
    return false;
  }
  Serial.println("Connected to server.");


  // サービスの取得
  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.");


  // Write用キャラクタリスティックの取得
  pRemoteCharacteristicTX = pRemoteService->getCharacteristic(charUUID_TX);
  if (pRemoteCharacteristicTX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (TX): ");
    Serial.println(charUUID_TX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (TX).");


  // Notify用キャラクタリスティックの取得
  pRemoteCharacteristicRX = pRemoteService->getCharacteristic(charUUID_RX);
  if (pRemoteCharacteristicRX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (RX): ");
    Serial.println(charUUID_RX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (RX).");


  // Notifyの有効化
  if (pRemoteCharacteristicRX->canNotify()) {
    pRemoteCharacteristicRX->registerForNotify(notifyCallback);
   
    // CCCD (0x2902) ディスクリプタに書き込んでNotifyを有効にする
    BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(BLEUUID((uint16_t)0x2902));
    if (pDescriptor != nullptr) {
      uint8_t value[2] = {0x01, 0x00}; // Notify有効
      pDescriptor->writeValue(value, 2, true);
      Serial.println("Registered for notifications.");
    }
  }


  return true;
}


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Central for LINBLE-Z1...");


  BLEDevice::init(""); // BLEデバイスを初期化(セントラルなので名前は空)
 
  startScan(); // スキャンを開始
}


/**
 * @brief メインループ
 */
void loop() {
  // 接続フラグが立っている場合、接続を試みる
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("Connection successful.");
    } else {
      Serial.println("Failed to connect. Restarting scan...");
      startScan(); // 接続に失敗したらスキャンを再開
    }
    doConnect = false;
  }


  // デバイスに接続中の場合
  if (deviceConnected) {
    // シリアルモニタからデータが送信されたら、ペリフェラルに送信する
    if (Serial.available()) {
      String str = Serial.readString();
      if (str.length() > 0) {
        pRemoteCharacteristicTX->writeValue(str.c_str(), str.length());
      }
    }
  }
 
  delay(100); // CPU負荷を軽減
}

エラーなく書き込みが完了できたので、修正点も含めてVer0.2の動作を確認していきます。

ディスプレイ更新が入り、青色表示でスキャン状態が見分けられるようになりました。
指摘した部分が修正されたことを確認できました。

では、LINBLE-Z1をPCに接続し、Teratermで操作してペリフェラル動作を行います。LINBLE-Z1をご利用いただいているユーザー様の用途としては自動モード(ペリフェラル動作)で使われていることが多いかもしれませんが、今回は動的にアドバタイズの開始・停止を制御したかったため、コマンド制御可能な通常モードで動作させます。

BTA	//アドバタイズの開始
@@@BTD	//アドバタイズの停止・接続中であれば通信切断

LINBLE-Z1からアドバタイズを開始すると、自動で発見・接続確立することを確認しました。
接続直後にData From Peripheral CharacteristicのNotify Enableが実施され、LINBLE-Z1に「CONN」が通知されました。

ここまでは順調でしたが、不具合を見つけてしまいました。

LINBLE-Z1からの通知はシリアル通信で受け取れるのですが、PCからM5Stackへシリアル通信で送信したデータがLINBLE-Z1に届いていません。おそらく、シリアル通信でM5Stackに送ったデータが、うまくData To Peripheral CharacteristicへWriteできていないような挙動でした。

さっそく、この挙動をプロンプトに記載し、修正を指示します。

プログラムを書き込み、動作させたところ初回のスキャン開始時にディスプレイが青色表示された。
指摘した部分は修正されていた。

LINBLE-Z1を見つけ、接続確立することを確認した。
LINBLE-Z1からのNotifyデータ(Data From Peripheral Characteristic)はシリアル通信で受信できた。
しかし、M5Stackへシリアル通信で送信したデータが、LINBLE-Z1へ届いていないようだ。
Data To Peripheral CharacteristicへのWriteがうまくいっていないような挙動であった。

コードの問題点を確認し、修正を加え再提示してください。

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

データ送信処理を修正したようですね。ぱっと確認した限り、満足いく性能ではないにしても、必要十分な機能になったのではないでしょうか。それでは書き込んで確認してみましょう。

/**
 * @file M5CoreS3_LINBLE_Central.ino
 * @author Gemini
 * @brief M5CoreS3をセントラルとし、LINBLE-Z1とBLE GATT通信を行うプログラム
 * @version 0.3
 * @date 2024-07-16
 * @copyright Copyright (c) 2024
 *
 * @note v0.3変更点:
 * - シリアルからのデータ送信が失敗する問題を修正。
 * - loop()関数内のデータ送信処理を、Stringオブジェクトではなく、
 * バイトバッファ(uint8_t[])を経由する方法に変更し、堅牢性を向上。
 *
 * 機能:
 * 1. Bluetooth LEのスキャンを行い、"LINBLE-Z1"という名前のデバイスを探す。
 * 2. 対象デバイスを発見後、自動で接続を試みる。
 * 3. 接続後は、LINBLE UART Serviceを介して双方向のデータ通信が可能になる。
 * 4. PCのシリアルモニタとLINBLE-Z1間でUART通信を中継する。
 * 5. 接続状態に応じてM5CoreS3のディスプレイ色を変更する(スキャン中:青、接続中:緑)。
 * 6. 接続が切断された場合、自動的にスキャンを再開する。
 */


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


// ----------------------------------------------------------------
// LINBLE UART ServiceのUUID定義
// ----------------------------------------------------------------
// Service UUID
static BLEUUID serviceUUID("27adc9ca-35eb-465a-9154-b8ff9076f3e8");
// Data From Peripheral Characteristic (Notify)
static BLEUUID charUUID_RX("27adc9cb-35eb-465a-9154-b8ff9076f3e8");
// Data To Peripheral Characteristic (Write Without Response)
static BLEUUID charUUID_TX("27adc9cc-35eb-465a-9154-b8ff9076f3e8");




// ----------------------------------------------------------------
// グローバル変数
// ----------------------------------------------------------------
static boolean doConnect = false;         // 接続試行フラグ
static boolean deviceConnected = false;   // 接続状態フラグ
static BLEAdvertisedDevice* myDevice;     // 発見したデバイスの情報を保持
static BLERemoteCharacteristic* pRemoteCharacteristicTX; // Write用
static BLERemoteCharacteristic* pRemoteCharacteristicRX; // Notify用
static BLEClient* pClient;                // BLEクライアント




// ----------------------------------------------------------------
// プロトタイプ宣言
// ----------------------------------------------------------------
bool connectToServer();
void startScan();




/**
 * @brief ペリフェラルからのNotify受信時に呼び出されるコールバック関数
 */
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    // 受信したデータをそのままシリアルモニタに出力
    Serial.write(pData, length);
}


/**
 * @brief BLEクライアントのイベント(接続/切断)を処理するコールバッククラス
 */
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    deviceConnected = true;
    M5.Display.fillScreen(GREEN);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.println("Connected to LINBLE-Z1");
    Serial.println("Connected to LINBLE-Z1.");
  }


  void onDisconnect(BLEClient* pclient) {
    deviceConnected = false;
    Serial.println("Disconnected. Restarting scan...");
   
    // スキャンを再開
    startScan();
  }
};


/**
 * @brief スキャンで見つかったデバイスを処理するコールバッククラス
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    // デバイス名が"LINBLE-Z1"であるかを確認
    if (advertisedDevice.getName() == "LINBLE-Z1") {
      Serial.print("Found LINBLE-Z1: ");
      Serial.println(advertisedDevice.getAddress().toString().c_str());
     
      // スキャンを停止
      BLEDevice::getScan()->stop();
     
      // 発見したデバイスの情報をコピーして保持
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
     
      // 接続フラグを立てる
      doConnect = true;
    }
  }
};


/**
 * @brief BLEスキャンを開始する
 */
void startScan() {
  // 先にディスプレイ表示を更新する
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(2);
  M5.Display.println("Scanning for LINBLE-Z1...");
  Serial.println("Scan started...");


  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pScan->setActiveScan(true); // デバイス名取得のためにアクティブスキャンを有効化
  pScan->start(0, false);     // 0秒 = 無限スキャン
}


/**
 * @brief サーバー(ペリフェラル)に接続する
 * @return bool 接続に成功した場合はtrue、失敗した場合はfalse
 */
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(myDevice->getAddress().toString().c_str());


  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());


  // 接続試行
  if (!pClient->connect(myDevice)) {
    Serial.println("Connection failed.");
    return false;
  }
  Serial.println("Connected to server.");


  // サービスの取得
  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.");


  // Write用キャラクタリスティックの取得
  pRemoteCharacteristicTX = pRemoteService->getCharacteristic(charUUID_TX);
  if (pRemoteCharacteristicTX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (TX): ");
    Serial.println(charUUID_TX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (TX).");


  // Notify用キャラクタリスティックの取得
  pRemoteCharacteristicRX = pRemoteService->getCharacteristic(charUUID_RX);
  if (pRemoteCharacteristicRX == nullptr) {
    Serial.print("Failed to find our characteristic UUID (RX): ");
    Serial.println(charUUID_RX.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println("Found our characteristic (RX).");


  // Notifyの有効化
  if (pRemoteCharacteristicRX->canNotify()) {
    pRemoteCharacteristicRX->registerForNotify(notifyCallback);
   
    // CCCD (0x2902) ディスクリプタに書き込んでNotifyを有効にする
    BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(BLEUUID((uint16_t)0x2902));
    if (pDescriptor != nullptr) {
      uint8_t value[2] = {0x01, 0x00}; // Notify有効
      pDescriptor->writeValue(value, 2, true);
      Serial.println("Registered for notifications.");
    }
  }


  return true;
}


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Central for LINBLE-Z1...");


  BLEDevice::init(""); // BLEデバイスを初期化(セントラルなので名前は空)
 
  startScan(); // スキャンを開始
}


/**
 * @brief メインループ
 */
void loop() {
  // 接続フラグが立っている場合、接続を試みる
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("Connection successful.");
    } else {
      Serial.println("Failed to connect. Restarting scan...");
      startScan(); // 接続に失敗したらスキャンを再開
    }
    doConnect = false;
  }


  // デバイスに接続中の場合
  if (deviceConnected && pRemoteCharacteristicTX != nullptr) {
    // ★修正点: Serial.readString()から、より堅牢なバッファ経由の読み書きに変更
    // シリアルモニタからデータが送信されたら、ペリフェラルに送信する
    if (Serial.available() > 0) {
      // 送信用のバッファを準備 (BLEの最大長は512バイト)
      uint8_t txBuffer[512];
      // シリアルから読み込めるだけのデータをバッファに読み込む
      // readBytesは指定したサイズに達するか、タイムアウト(デフォルト1秒)するまで読み込む
      size_t len = Serial.readBytes(txBuffer, sizeof(txBuffer));


      if (len > 0) {
        // 読み込んだデータをそのままペリフェラルに書き込む
        // 第3引数にfalseを指定し、Write Without Responseを明示
        pRemoteCharacteristicTX->writeValue(txBuffer, len, false);
      }
    }
  }
 
  delay(100); // CPU負荷を軽減
}

エラーなく書き込めたので、Ver0.3の動作を確認します。

指摘した部分ですが、シリアル通信でPCからM5Stackに送ったデータが、LINBLE-Z1に表示されることを確認できました。
これで、LINBLE-Z1とM5Stackの双方向のデータ通信が実現できました!

M5StackとLINBLE-Z1のBluetooth LE通信を経由したシリアル通信

性能の向上

さて、やりたいことは実現しましたが、最初に「今回は実用レベルまで性能を追求します」と言い切りましたので、もう少し機能を追い込みたいと思います。

先ほどのコードは「ぱっと確認した限り、満足いく性能ではないにしても、必要十分な機能」と申した通り、機能は満たしているが、性能としては満足できませんでした。

現状、シリアル通信で書き込んだデータ列がLINBLE-Z1に届くタイミングは

  • 送信用バッファ512byteがフルになる
  • 1秒のタイムアウトが発生する

のいずれかとなります。

動画:M5Stack_vs_LINBLE-Z1_Ver0.3.mp4

どういう用途で使うか次第ではありますが、大量のデータでもない限り、現状は1秒毎しかデータを送信できないこととなりますので、リアルタイム性の観点ではイマイチです。

上の動画のとおり、M5Stack(セントラル)からのデータ送信は、一拍置いて届いているのがわかります。ですので、以下のプロンプトに記載し、アップデートを指示します。

ここからは、こちらから指示したプロンプトをダイジェストで一挙公開します。

スループット向上

プログラムを書き込み、動作させたところ Writeできるようになっていた。
指摘した部分は修正されていた。

現在のデータ送信は、512byteのデータバッファがフルに達するか、1秒のタイムアウト毎に行われていると認識している。

Bluetooth LEで実現可能なスループットから考えると、1秒毎にデータを刈り取るのは頻度が遅い。
タイムアウトを20msに修正し、再提示してください。

再接続の不具合修正①

プログラムを書き込み、動作させたところ高速でデータが送信されることを確認した。
指摘した部分は修正されていた。

LINBLE-Z1から接続を切断すると、セントラル側は再スキャンを実施する。
その後、再度LINBLE-Z1がアドバタイズを開始しても、再接続試行をしなかった。

再接続に関連した挙動に問題がありそうです。
問題点を明確にし、コードを修正して再提示してください。

再接続の不具合修正②

プログラムを書き込み、動作させたところやはり切断後の再スキャン以降、再接続が行われていません。

Scan started...
というログが出力されていることから、再スキャンは動作しているようです。

Connecting to
というログが以降に出力されないので、接続試行に至っていないと予想します。

Disconnected. Restarting scan...
というログが出ていませんでした。

しかし、Scan started... がでているので、これはシリアル通信上の表示と考えます。
セントラル・ペリフェラルどちらからの切断でも切断は検知できる仕組みになっていますか?
全体的に接続フラグ管理はただしいでしょうか?

見直して、必要に応じて修正し、コードを再提示してください。

コンパイルエラー対応

C:\Users\genki.ito\Documents\Arduino\sketch_central_lus_for_m5stack_cores3_ai_generated_gemini2.5pro\sketch_central_lus_for_m5stack_cores3_ai_generated_gemini2.5pro.ino: In function 'void loop()':

C:\Users\genki.ito\Documents\Arduino\sketch_central_lus_for_m5stack_cores3_ai_generated_gemini2.5pro\sketch_central_lus_for_m5stack_cores3_ai_generated_gemini2.5pro.ino:213:32: error: 'class BLEScan' has no member named 'isScanning'

  213 |     if (!BLEDevice::getScan()->isScanning()) {

      |                                ^~~~~~~~~~

exit status 1



Compilation error: 'class BLEScan' has no member named 'isScanning'

正式リリース・コメント修正

期待する処理になっていました!

コードの処理自体は修正しなくてよいので、@noteのバージョン履歴を0.1から0.7すべて記載し、
1.0として正式リリース、と記載してください。
ずっと黙っていましたが今年は2025年です。
2025年に修正したものをVer1.0として提示してください。

正式リリース

最終的に生成されたVer1.0のArduinoスケッチ(プログラムコード)のみを掲示します。

/**
 * @file M5CoreS3_LINBLE_Central.ino
 * @author Gemini
 * @brief M5CoreS3をセントラルとし、LINBLE-Z1とBLE GATT通信を行うプログラム
 * @version 1.0
 * @date 2025-07-17
 * @copyright Copyright (c) 2025
 *
 * @note
 * v1.0 (2025-07-17): 正式リリース。日付を2025年に修正。
 * -----------------------------------------------------------------
 * @note << Version History >>
 * v0.7: isScanning()が存在しないことによるコンパイルエラーを修正。
 * スキャン状態を管理するフラグ(isScanning)を独自に導入し、ロジックを置き換え。
 * v0.6: 再接続ロジックを修正。状態管理をメインループに集約し、安定性を向上。
 * v0.5: 再接続に失敗するメモリリークの問題を修正。BLEリソース管理を改善。
 * v0.4: データ送信の遅延を削減するため、シリアル通信のタイムアウト値を20msに変更。
 * v0.3: シリアルからのデータ送信(Write)が失敗する問題を修正。
 * v0.2: 初回起動時にディスプレイ表示が更新されない問題を修正。
 * v0.1: 初期バージョンの作成。
 *
 * 機能:
 * 1. Bluetooth LEのスキャンを行い、"LINBLE-Z1"という名前のデバイスを探す。
 * 2. 対象デバイスを発見後、自動で接続を試みる。
 * 3. 接続後は、LINBLE UART Serviceを介して双方向のデータ通信が可能になる。
 * 4. PCのシリアルモニタとLINBLE-Z1間でUART通信を中継する。
 * 5. 接続状態に応じてM5CoreS3のディスプレイ色を変更する(スキャン中:青、接続中:緑)。
 * 6. 接続が切断された場合、自動的にスキャンを再開する。
 */


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


// ----------------------------------------------------------------
// LINBLE UART ServiceのUUID定義
// ----------------------------------------------------------------
static BLEUUID serviceUUID("27adc9ca-35eb-465a-9154-b8ff9076f3e8");
static BLEUUID charUUID_RX("27adc9cb-35eb-465a-9154-b8ff9076f3e8");
static BLEUUID charUUID_TX("27adc9cc-35eb-465a-9154-b8ff9076f3e8");




// ----------------------------------------------------------------
// グローバル変数
// ----------------------------------------------------------------
static boolean doConnect = false;
static boolean deviceConnected = false;
static boolean isScanning = false;
static BLEAdvertisedDevice* myDevice = nullptr;
static BLERemoteCharacteristic* pRemoteCharacteristicTX = nullptr;
static BLERemoteCharacteristic* pRemoteCharacteristicRX = nullptr;
static BLEClient* pClient = nullptr;




// ----------------------------------------------------------------
// プロトタイプ宣言とコールバッククラス定義
// ----------------------------------------------------------------
bool connectToServer();
void startScan();


/**
 * @brief ペリフェラルからのNotify受信時に呼び出されるコールバック関数
 */
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.write(pData, length);
}


/**
 * @brief BLEクライアントのイベント(接続/切断)を処理するコールバッククラス
 */
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
    deviceConnected = true;
    M5.Display.fillScreen(GREEN);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextSize(2);
    M5.Display.println("Connected to LINBLE-Z1");
    Serial.println("Connected to LINBLE-Z1.");
  }


  void onDisconnect(BLEClient* pclient) {
    deviceConnected = false;
    Serial.println("Disconnected.");
  }
};


/**
 * @brief スキャンで見つかったデバイスを処理するコールバッククラス
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    if (advertisedDevice.getName() == "LINBLE-Z1") {
      Serial.print("Found LINBLE-Z1: ");
      Serial.println(advertisedDevice.getAddress().toString().c_str());
     
      BLEDevice::getScan()->stop();
      isScanning = false;
     
      if (myDevice != nullptr) {
        delete myDevice;
        myDevice = nullptr;
      }
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
     
      doConnect = true;
    }
  }
};


static MyClientCallback clientCallback;
static MyAdvertisedDeviceCallbacks advertisedDeviceCallbacks;


/**
 * @brief BLEスキャンを開始する
 */
void startScan() {
  M5.Display.fillScreen(BLUE);
  M5.Display.setCursor(10, 10);
  M5.Display.setTextSize(2);
  M5.Display.println("Scanning for LINBLE-Z1...");
  Serial.println("Scan started...");


  isScanning = true;
  BLEDevice::getScan()->start(0, false);
}


/**
 * @brief サーバー(ペリフェラル)に接続する
 */
bool connectToServer() {
  Serial.print("Connecting to ");
  Serial.println(myDevice->getAddress().toString().c_str());


  if (!pClient->connect(myDevice)) {
    Serial.println("Connection failed.");
    return false;
  }
  Serial.println("Connected to server.");


  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.");


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


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


  if (pRemoteCharacteristicRX->canNotify()) {
    pRemoteCharacteristicRX->registerForNotify(notifyCallback);
    BLERemoteDescriptor* pDescriptor = pRemoteCharacteristicRX->getDescriptor(BLEUUID((uint16_t)0x2902));
    if (pDescriptor != nullptr) {
      uint8_t value[2] = {0x01, 0x00};
      pDescriptor->writeValue(value, 2, true);
      Serial.println("Registered for notifications.");
    }
  }


  return true;
}


/**
 * @brief 初期設定
 */
void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Central for LINBLE-Z1...");
  Serial.setTimeout(20);


  BLEDevice::init("");


  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(&clientCallback);


  BLEScan* pScan = BLEDevice::getScan();
  pScan->setAdvertisedDeviceCallbacks(&advertisedDeviceCallbacks);
  pScan->setActiveScan(true);
}


/**
 * @brief メインループ
 */
void loop() {
  // 1. 接続試行フラグが立っている場合、接続を試みる
  if (doConnect) {
    if (connectToServer()) {
      Serial.println("Connection successful.");
    } else {
      Serial.println("Failed to connect. Will restart scan.");
    }
    // 接続試行が完了したのでフラグをリセット
    doConnect = false;
  }


  // 2. 接続されておらず、接続試行中でもない場合、スキャンを開始する
  if (!deviceConnected && !doConnect) {
    // スキャンが実行中でなければ、スキャンを開始する
    if (!isScanning) {
      startScan();
    }
  }


  // 3. 接続中の場合、シリアルデータを処理する
  if (deviceConnected && pRemoteCharacteristicTX != nullptr) {
    if (Serial.available() > 0) {
      uint8_t txBuffer[512];
      size_t len = Serial.readBytes(txBuffer, sizeof(txBuffer));
      if (len > 0) {
        pRemoteCharacteristicTX->writeValue(txBuffer, len, false);
      }
    }
  }
 
  delay(10);
}

さて、正式リリースとしてデータ送受信のリアルタイム性や、再接続性などを向上させることができました。

同一PCでM5Stack(セントラル)とLINBLE-Z1(ペリフェラル)をコントロール
リアルタイム性も再接続性もクリア
動画:M5Stack_vs_LINBLE-Z1_Ver1.0.mp4

まとめ

前回、M5Stack同士の通信では、性能が不十分な点がありましたが今回はその点をしっかりカバーして品質もあげられました。LINBLE-Z1 ユーザーマニュアルに記載してあるLINBLE UART Serviceに対応することで、LINBLE-Z1とのBluetooth LE通信がリアルタイム性をもって行えました。また、予期せぬ切断などに対する再接続性の面もしっかり考慮できました。

LINBLE-Z1が売れなくなるとお叱りを受けそう(笑)ですが、性能的に良いものができたと思います。

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

今回の検証は一旦ここまでですが、今後もM5Stackにセンサーをつけてセンサービーコンにしてみたり、いろいろチャレンジしたいと思います。お楽しみに!

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