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

M5StackでBluetooth® LE通信を実現するまでの手順メモ③ペリフェラル通信編

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

M5StackでBluetooth® LE通信を実現するまでの手順メモ」の第3回目です。今回は「ペリフェラル通信編」です。

ご注意ください

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

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

ペリフェラル動作を試す

前回、非接続でデータをブロードキャストするブロードキャスターにチャレンジしました。

今回は、デバイスが1対1で接続し、双方向のデータ通信を行うペリフェラルにチャレンジします。

ブロードキャスターとペリフェラルの大きな違いは「接続」の有無です。ブロードキャスターが非接続型で一方的に情報発信するのに対し、ペリフェラルはセントラルからの接続を待ちます。接続が確立されると、双方向のデータ通信が可能になります。

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

前回同様、AIであるGemini 2.5 Proを利用してコーディングの支援をしてもらいます。前回わかったとおり、AIの生成コードは過信しすぎないようにします。

まず、どのようなペリフェラル機器にするかを検討する必要があります。今回は、M5StackをPCに接続し、スマートフォンとBluetooth LE通信を行うことで、PCとスマートフォンの間で双方向データ通信を実現することを目標とします。

プロンプト指示

前回のビーコン発信のときより、もう少し機能を明確にして以下のプロンプトを入力しました。

双方向データ通信(GATT通信)の仕様をイチから考えるのは大変なので、今回はNordic Semiconductor社が提唱するカスタムサービスであるNordic UART Service (NUS) を利用します。また、PCとM5Stackは仮想COMポートをつかって通信することにします。

手元に M5Stack Gray Core V1.0がある。
以下の条件のプログラムコードを提示してほしい。

開発環境
Arduino IDE

目的
Bluetooth LEのGATT通信を行うペリフェラル機器を作る

機能
アドバタイズ
タイプは接続型(ADV_IND)とする
周期は100msとする
Complete Local Nameを設定する("M5Stack NUS")

GATT通信
Nordic UART Service(NUS)をサポートする
Tx Characteristicをサポートする
Rx Characteristicをサポートする

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

M5Stackのディスプレイやそのほか機能は一切使わないものとする。

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

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Nordic UART Service (NUS) のためのUUID定義
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // M5Stackが受信する側 (Central -> Peripheral)
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // M5Stackが送信する側 (Peripheral -> Central)

// グローバル変数
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;

// BLEサーバーのイベントコールバック
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println(">>> Central device connected <<<");
    }

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("--- Central device disconnected ---");
    }
};

// M5Stackがデータを受信した時 (Rx Characteristicへの書き込み時) のコールバック
class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      if (rxValue.length() > 0) {
        // 受信したデータを仮想COMポート (Serial) に出力
        Serial.print("Received from BLE: ");
        Serial.println(rxValue.c_str());
      }
    }
};


void setup() {
  // 仮想COMポート (シリアル通信) を初期化
  Serial.begin(115200);
  Serial.println("Starting BLE Server for Nordic UART Service (NUS)...");

  // BLEデバイスを初期化し、アドバタイズ名を設定
  BLEDevice::init("M5Stack NUS");

  // GATTサーバーを作成
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Nordic UART Service (NUS) を作成
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // TX Characteristic (M5Stack -> Central) を作成
  // Property: Notify (Centralに値を通知する)
  pTxCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID_TX,
                      BLECharacteristic::PROPERTY_NOTIFY
                    );
  // Notifyを有効にするためのDescriptor(0x2902)を追加
  pTxCharacteristic->addDescriptor(new BLE2902());

  // RX Characteristic (Central -> M5Stack) を作成
  // Property: Write (Centralから書き込みを受け付ける)
  BLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
                                           CHARACTERISTIC_UUID_RX,
                                           BLECharacteristic::PROPERTY_WRITE
                                         );
  // Centralから書き込みがあった際のコールバックを設定
  pRxCharacteristic->setCallbacks(new MyCallbacks());

  // サービスを開始
  pService->start();

  // アドバタイジングの設定
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID); // アドバタイズパケットにサービスUUIDを含める
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x0); // iOSとの接続のために推奨される設定
  
  // アドバタイジング周期の設定 (100ms)
  // 値は0.625ms単位で指定する。 100ms / 0.625ms = 160
  uint16_t advInterval = 160;
  pAdvertising->setMinInterval(advInterval);
  pAdvertising->setMaxInterval(advInterval);

  // アドバタイジングを開始
  BLEDevice::startAdvertising();
  Serial.println("Waiting for a client connection...");
}

void loop() {
  // Centralとの接続/切断状態が変化した場合にメッセージを表示
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;
  }
  if (!deviceConnected && oldDeviceConnected) {
      oldDeviceConnected = deviceConnected;
      Serial.println("Advertising restarted. Waiting for a client connection...");
      // 切断されたらアドバタイジングを再開する
      BLEDevice::startAdvertising(); 
  }

  // Centralと接続中、かつ仮想COMポートからデータが送信された場合
  if (deviceConnected) {
    if (Serial.available()) {
      // 仮想COMポートからの文字列を読み取り、BLE経由で送信
      String txString = Serial.readStringUntil('\n');
      pTxCharacteristic->setValue(txString.c_str());
      pTxCharacteristic->notify(); // Centralに値を通知
      Serial.print("Sent via BLE: ");
      Serial.println(txString);
    }
  }
  
  // CPUを少し休ませる
  delay(10);
}

書き込みをしようとすると、エラーが出ました。ESP32のBLEライブラリが返す値と、それを格納する変数の型が一致していないようです。

C:\Users\genki.ito\Documents\Arduino\sketch_peripheral_nus_ai_generated_gemini2.5pro\sketch_peripheral_nus_ai_generated_gemini2.5pro.ino: In member function 'virtual void MyCallbacks::onWrite(BLECharacteristic*)':
C:\Users\genki.ito\Documents\Arduino\sketch_peripheral_nus_ai_generated_gemini2.5pro\sketch_peripheral_nus_ai_generated_gemini2.5pro.ino:32:54: error: conversion from 'String' to non-scalar type 'std::string' {aka 'std::__cxx11::basic_string<char>'} requested
   32 |       std::string rxValue = pCharacteristic->getValue();
      |                             ~~~~~~~~~~~~~~~~~~~~~~~~~^~
exit status 1

Compilation error: conversion from 'String' to non-scalar type 'std::string' {aka 'std::__cxx11::basic_string<char>'} requested

このエラー自体をAIにプロンプトとして与えてみると、型変換について修正したコードが提示されました。

修正点もまとめてくれていて、かつコード上に修正箇所がわかるよう目印をつけてくれていますね。ビーコン発信の際に私が★で目印をつけたのと同じように記載していて驚きました!私の行動を学習しているのかもしれませんね。

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

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Nordic UART Service (NUS) のためのUUID定義
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // M5Stackが受信する側 (Central -> Peripheral)
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // M5Stackが送信する側 (Peripheral -> Central)

// グローバル変数
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;

// BLEサーバーのイベントコールバック
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println(">>> Central device connected <<<");
    }

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("--- Central device disconnected ---");
    }
};

// M5Stackがデータを受信した時 (Rx Characteristicへの書き込み時) のコールバック
class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      // ★★★ 修正点1: 値をArduinoのStringオブジェクトとして受け取る ★★★
      // .c_str()を経由することで、ライブラリのバージョン差異による型問題を吸収します。
      String rxValue = pCharacteristic->getValue().c_str();

      if (rxValue.length() > 0) {
        // 受信したデータを仮想COMポート (Serial) に出力
        Serial.print("Received from BLE: ");
        // ★★★ 修正点2: Stringオブジェクトを直接printlnに渡す ★★★
        Serial.println(rxValue);
      }
    }
};


void setup() {
  // 仮想COMポート (シリアル通信) を初期化
  Serial.begin(115200);
  Serial.println("Starting BLE Server for Nordic UART Service (NUS)...");

  // BLEデバイスを初期化し、アドバタイズ名を設定
  BLEDevice::init("M5Stack NUS");

  // GATTサーバーを作成
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Nordic UART Service (NUS) を作成
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // TX Characteristic (M5Stack -> Central) を作成
  // Property: Notify (Centralに値を通知する)
  pTxCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID_TX,
                      BLECharacteristic::PROPERTY_NOTIFY
                    );
  // Notifyを有効にするためのDescriptor(0x2902)を追加
  pTxCharacteristic->addDescriptor(new BLE2902());

  // RX Characteristic (Central -> M5Stack) を作成
  // Property: Write (Centralから書き込みを受け付ける)
  BLECharacteristic *pRxCharacteristic = pService->createCharacteristic(
                                           CHARACTERISTIC_UUID_RX,
                                           BLECharacteristic::PROPERTY_WRITE
                                         );
  // Centralから書き込みがあった際のコールバックを設定
  pRxCharacteristic->setCallbacks(new MyCallbacks());

  // サービスを開始
  pService->start();

  // アドバタイジングの設定
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID); // アドバタイズパケットにサービスUUIDを含める
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x0); // iOSとの接続のために推奨される設定
  
  // アドバタイジング周期の設定 (100ms)
  // 値は0.625ms単位で指定する。 100ms / 0.625ms = 160
  uint16_t advInterval = 160;
  pAdvertising->setMinInterval(advInterval);
  pAdvertising->setMaxInterval(advInterval);

  // アドバタイジングを開始
  BLEDevice::startAdvertising();
  Serial.println("Waiting for a client connection...");
}

void loop() {
  // Centralとの接続/切断状態が変化した場合にメッセージを表示
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;
  }
  if (!deviceConnected && oldDeviceConnected) {
      oldDeviceConnected = deviceConnected;
      Serial.println("Advertising restarted. Waiting for a client connection...");
      // 切断されたらアドバタイジングを再開する
      BLEDevice::startAdvertising(); 
  }

  // Centralと接続中、かつ仮想COMポートからデータが送信された場合
  if (deviceConnected) {
    if (Serial.available()) {
      // 仮想COMポートからの文字列を読み取り、BLE経由で送信
      String txString = Serial.readStringUntil('\n');
      pTxCharacteristic->setValue(txString.c_str());
      pTxCharacteristic->notify(); // Centralに値を通知
      Serial.print("Sent via BLE: ");
      Serial.println(txString);
    }
  }
  
  // CPUを少し休ませる
  delay(10);
}

コンパイルが通って、M5Stack Grayに書き込みが成功しました。さっそく、ペリフェラルとして接続・双方向データ通信ができるのか動作の確認をしてみます。

前回同様、確認用のアプリケーションとしてnRF Connect for Mobileを使用します。セントラルの役割をしてもらいます。今回、GATT通信としてNordic UART Service(NUS)を採用したのは、nRF Connect for Mobileと相性が良いからです。

動作確認

M5Stack Grayのペリフェラル動作

NUS機器として専用アイコンで表示されています。期待が持てますね、ではさっそく接続してみましょう。

サービスとしてNUSが認識されている

Nordic UART Serviceが正しく認識されています。接続したことで、Ardunino IDEのSerial Monitorにも接続されたことを示すログが表示されました。

では、実際にPCとスマートフォン間で双方向のデータ通信ができるのか試してみましょう。

事前に、TX Characteristicの↓↓↓(↓矢印)をタップして、Notificationsを有効化しておきます。これは、PC(M5Stack側)からのデータを受け取るために必要な手順になります。

まずは、Ardunino IDEのSerial Monitorから「1234」と入力し、スマートフォン側に通知されることが確認できました!

TX Characteristicに「1234」と受信した

続いて、スマートフォン側からRX Characteristicに「abcd」と入力、送信し、PC側に通知されることも確認できました!

RX Characteristicに「abcd」と送信する
PC側のログ

ここまでわずか30分程度です。今回はプロンプトに機能を細かく書いたことが功を奏したかもしれません。一度の修正でやりたかったことが実現できました。

なお、今回もプロンプトではM5Stack Gray向けで指示しましたが、M5Stack CoreS3 SEでも同一のプログラムコードで動作することも確認できました。

まとめ

無事、ペリフェラル機器として双方向のデータ通信ができました。独自のGATT通信仕様ではなく、Nordic UART Serviceを利用したことで、相互接続性が高い品質を作り出せたようです。

また、Nordic UART ServiceはWebにも情報が整理されており、AIも学習済みだったのではないかと思います。

今回、M5StackをPCにつないで、ターミナルソフト等からBluetooth LE通信ができるようになりました。

お気づきの方もおられるかもしれませんが、この機能は、ムセンコネクトのBluetooth® USBアダプタ「LINBLE-Z1 / LINBLE-Z2ドングル」LINBLE-Z1 / LINBLE-Z2 カンタンスターターキットに近い機能となっています。

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

次回は、今回スマートフォン側だったセントラルにチャレンジします。お楽しみに!

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