【M5】M5STACK<->M5STICK間でBLE通信を行う

M5Stack・Arduino

M5STACKのボタンを押すと値をBLE経由でM5STICKPlusに送信し、 M5StickPlusでは受信した内容を画面に表示する

構成

プロジェクト名:BLE_Sample_M5Stack.ino/BLE_Sample_M5StickPlus.ino

デバイス:M5STACK、M5STICK CPLUS

動作確認

M5STACK側

接続が完了で以下の情報がシリアル出力されることを確認。

17:38:01.038 -> Device Connected!

M5STICK側

接続を完了し、受信したボタン情報がシリアル出力されることを確認。

18:03:19.627 -> Forming a connection to 08:3a:f2:43:ab:06
18:03:20.150 ->  - Created client
18:03:20.222 ->  - Connected to server
18:03:20.690 ->  - Found our service
18:03:20.690 ->  - Found our characteristic
18:03:20.730 -> We are now connected to the BLE Server.
18:03:25.598 -> Received: A=1, B=0, C=0
18:03:26.853 -> Received: A=1, B=2, C=0
18:03:27.728 -> Received: A=1, B=2, C=3

プログラム

M5STACK

//--------------------------------------------
// ライブラリ
//--------------------------------------------
#include <M5Stack.h>
//BLE関連
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// BLEサーバー本体のオブジェクト。クライアント(M5StickCなど)からの接続を受け付ける中心的な役割です。
BLEServer* pServer = NULL;

// データを読み書きするための「特性(窓口)」。ここに値をセットしてNotify(通知)を飛ばします。
BLECharacteristic* pCharacteristic = NULL;

// 現在、デバイス(クライアント)が接続されているかどうかを保持するフラグです。
bool deviceConnected = false;

// 1つ前のループ時点での接続状態。これと比較することで「接続された瞬間」や「切断された瞬間」を判定します。
bool oldDeviceConnected = false;

// 送信データを格納するバッファ(情報の入れ物)。ボタンの状態などをこの配列に書き込んで送信します。
uint8_t buf[100];

#define SERVICE_UUID        "6f3b8a12-4d5e-4b9a-9c2f-7a1b3c5d8e01"
#define CHARACTERISTIC_UUID "a1b2c3d4-e5f6-4a5b-bc6d-7e8f9a0b1c2d"

//--------------------------------------------
//コールバック関数
//--------------------------------------------
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
    }
};

//--------------------------------------------
//初期化処理
//--------------------------------------------
void setup()
{
  M5.begin();
  WiFi.mode(WIFI_OFF); 
  Serial.print("");

  // Create the BLE Device
  // 初期化処理を行ってBLEデバイスを初期化する
  // Device Local Name
  BLEDevice::init("BLEMaster"); 

  // Serverオブジェクトを作成してコールバックを設定する  
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Serviceオブジェクトを作成して準備処理を実行する
  BLEService *pService = pServer->createService(SERVICE_UUID);
  // Create a BLE Characteristic
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ   |
                      BLECharacteristic::PROPERTY_WRITE  |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_INDICATE
                    );

  // Create a BLE Descriptor
  pCharacteristic->addDescriptor(new BLE2902());

// --- Master側の setup() 内の修正 ---

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

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  
  // 重要:ここでサービスUUIDをアドバタイジングデータに確実に追加する
  pAdvertising->addServiceUUID(SERVICE_UUID); 
  
  // Scan Responseをtrueに設定(名前やUUIDをより確実に送るため)
  pAdvertising->setScanResponse(true); 
  
  // iPhoneなどでも見つけやすくするための推奨設定
  pAdvertising->setMinPreferred(0x06);  
  pAdvertising->setMaxPreferred(0x12);

  // アドバタイジングの開始
  BLEDevice::startAdvertising();
  //画面初期化
  M5.Lcd.clear(WHITE);
  M5.Lcd.setCursor(90, 100);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(BLUE);
  M5.Lcd.println("BLEMaster");
}

//--------------------------------------------
//メイン処理
//--------------------------------------------
void loop()
{
  M5.update();

  if (M5.BtnA.wasPressed()) { //Aボタン押下時
     M5.Lcd.clear(RED);
      if(buf[0]==0x00)
        buf[0]=(uint8_t)(0x01);
      else
        buf[0]=(uint8_t)(0x00);
      pCharacteristic->setValue(buf,sizeof buf);
      pCharacteristic->notify();
      delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
  } 
  if (M5.BtnB.wasPressed()) { //Bボタン押下時
      M5.Lcd.clear(GREEN);
      if(buf[1]==0x00)
        buf[1]=(uint8_t)(0x02);
      else
        buf[1]=(uint8_t)(0x00);
      pCharacteristic->setValue(buf,sizeof buf);
      pCharacteristic->notify();
      delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
  } 
  if (M5.BtnC.wasPressed()) { //Cボタン押下時
      if(buf[2]==0x00)
        buf[2]=(uint8_t)(0x03);
      else
        buf[2]=(uint8_t)(0x00);
      pCharacteristic->setValue(buf,sizeof buf);
      pCharacteristic->notify();
      delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
      M5.Lcd.clear(BLACK);
  } 

  delay(300);

    // BLEの接続状態が変わった瞬間の処理(管理)
    // notify changed value
    
    //接続の維持(Connecting): oldDeviceConnected という変数を使って、
    //前回は切れていたけど、今はつながっている」という変化の瞬間を捉えています。
    //ここで「Connected!」と表示させることで、Slave側(StickC)が
    //正しく見つけて接続しに来たことが確認できます。
    if (deviceConnected) {
        // ここはボタンを押した時以外に、自動でデータを送り続けたい場合に記述します。
        // 今はボタン押下時に送信しているので、空のままでOKです。
    }
    // 【切断された瞬間】の処理
    //自動再開(Disconnecting): BLEは一度切れると、Master側が再び「募集中(Advertising)」を
    //始めないとSlaveから見えなくなります。
    //このコードがあるおかげで、StickCの電源を入れ直したり、
    //距離が離れて切れたりしても、自動で再接続できる状態に戻ります。
    if (!deviceConnected && oldDeviceConnected) {
        delay(500); 
        pServer->startAdvertising(); // アドバタイジング(再募集)を再開
        
        // --- デバッグ表示を追加 ---
        Serial.println("Device Disconnected. Restarting advertising...");
        M5.Lcd.clear(BLACK);
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.println("Disconnected");
        // -----------------------

        oldDeviceConnected = deviceConnected;
    }
  // 【接続された瞬間】の処理
    if (deviceConnected && !oldDeviceConnected) {
        // --- デバッグ表示を追加 ---
        Serial.println("Device Connected!");
        M5.Lcd.clear(GREEN);
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.println("Connected!");
        // -----------------------

        oldDeviceConnected = deviceConnected;
    }
}

M5STICK CPLUS

//--------------------------------------------
// ライブラリ
//--------------------------------------------
#include <M5StickCPlus.h>
#include "BLEDevice.h"


// 接続先(M5Stack)が提供している「サービス」の固有ID。Master側と一致させる必要があります。
static BLEUUID serviceUUID("6f3b8a12-4d5e-4b9a-9c2f-7a1b3c5d8e01");

// 読み書きや通知を受け取る「窓口(特性)」の固有ID。Master側と一致させる必要があります。
static BLEUUID    charUUID("a1b2c3d4-e5f6-4a5b-bc6d-7e8f9a0b1c2d");

// 目的のデバイスが見つかり、「今から接続処理を開始していいですよ」という合図(フラグ)です。
static boolean doConnect = false;

// 現在、実際にMasterとの通信が確立されているかどうかを保持します。
static boolean connected = false;

// 一度切断された後などに、再び周囲のデバイスを探す(スキャンする)必要があるかを示します。
static boolean doScan = false;

// 接続先のMasterが持つデータの窓口を操作するためのオブジェクトです。ここを通じて値を取得します。
static BLERemoteCharacteristic* pRemoteCharacteristic;

// スキャンで見つかったMasterの情報を一時的に保存しておく場所です。
static BLEAdvertisedDevice myDevice; 

// Masterから受信した数値を保存する変数。これをLCDに表示します。
uint16_t val;


//--------------------------------------------
// notifyCallback 関数は、**「Master(M5Stack)から
//新しいデータが送られてきた瞬間に、自動で呼び出される処理
//--------------------------------------------
static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
  //pData: 届いたデータの塊(配列)の先頭アドレスです。
  //length: 届いたデータの長さ(今回の場合は100バイト)です。
  // 配列の各要素をデバッグ表示
  Serial.printf("Received: A=%d, B=%d, C=%d\n", pData[0], pData[1], pData[2]);

    // 画面表示用の変数 val に何を代入するか決めます
  // 例:最後に変化があった値を表示する場合
  if (pData[0] != 0) val = pData[0];
  else if (pData[1] != 0) val = pData[1];
  else if (pData[2] != 0) val = pData[2];
  else val = 0;
}
//--------------------------------------------
//Slave(M5StickC)側から見た、接続状態の変化を監視する
//--------------------------------------------
class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};
//--------------------------------------------
//M5StickCがMaster(M5Stack)を見つけた後、**「実際に通信ができる状態までセットアップを完了させる
//--------------------------------------------
bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice.getAddress().toString().c_str());

    //「BLE通信専用の窓口(クライアント)」を作ります
    BLEClient* pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");
    pClient->setClientCallbacks(new MyClientCallback());

    //スキャンで見つけておいたMaster(myDevice)に対して接続を試みます。
    //重要点: ここで成功すると、Master側の画面には「Connected!」と表示されます。
    pClient->connect(&myDevice); 
    Serial.println(" - Connected to server");

    //今後、ボタンの値を受け取る相手はこの pRemoteCharacteristic
    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");

    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);

    //これが非常に重要です。Masterに対して**「値が変わったら(ボタンが押されたら)、
    //その都度勝手に知らせて(Notify)ください」**と予約します。
    //これにより、Masterでボタンが押されるたびに、
    //自動的に notifyCallback 関数が動くようになります。
    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->canNotify())
      pRemoteCharacteristic->registerForNotify(notifyCallback);

    connected = true;
    return true; // 戻り値もお忘れなく
}
//--------------------------------------------
//周囲のBluetoothデバイスをスキャン
//--------------------------------------------
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  //デバイスを1つ見つけるたびに、この関数が自動で実行されます
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    //まず、相手の名前が "BLEMaster" かどうかを確認します。
    if (advertisedDevice.getName() == "BLEMaster") {
      if (advertisedDevice.isAdvertisingService(serviceUUID)) {
        //目的の相手が見つかったので、バッテリーとメモリを節約するために
        //スキャン(検索)を停止します。
        BLEDevice::getScan()->stop();
        //見つかった相手の情報を myDevice という変数にコピーして保存します。
        myDevice = advertisedDevice; 
        doConnect = true;
        doScan = true;
      }
    }
  }
};
//--------------------------------------------
// 初期化
//--------------------------------------------
void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.

//--------------------------------------------
//メイン処理
//--------------------------------------------
// This is the Arduino main loop function.
void loop() {
M5.update(); // ボタン状態などの更新(必要に応じて)

  if (connected) {
    // 値が変わったとき、または一定時間ごとに画面を更新
    // ここでは単純化のため毎回描画しますが、fillScreenを毎回しない工夫
    M5.Lcd.setCursor(0, 50);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setTextColor(RED, BLACK); // 第2引数に背景色を入れると、上書き時にチラつきません
    M5.Lcd.printf("%d  ", val); // 桁数が変わった時のために空白を入れる
  } else {
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 10);
    M5.Lcd.setTextSize(2);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.println("Scanning...");
  }
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }
  if (connected) {

  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
  }

 delay(1000); // Delay a second between loops.
} // End of loop

コメント