Ⅲ-3. ESP32によるBLEペリフェラルとセントラルの作成

 これまでに作成したBLEサーバーとBLEクライアントに機能を付加して、双方向のコネクション通信ができるペリフェラルとセントラルに仕上げます。
 計測データの送受信に加えて、受信応答の送信や、両デバイスのプッシュボタンによるシグナルデータの送受信とそれに対応した動作などを組み込みます。少しロジックが複雑になりますが、BLEコネクション方式によるポイント・ツー・ポイント通信の仕組みと動作を試すことができます。

 ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    


1.双方向通信への機能拡張

 機能拡張のポイントは、ペリフェラルに対する受信用キャラクタリスティックとUUIDの追加、そして、セントラルに対する送信用リモートキャラクタリスティックとUUIDの追加です。ペリフェラルからは今まで通りNotifyで送信しますが、セントラルからの送信にはWriteを使用します。これに伴い、送受信のためのシグナルデータの定義やコールバック処理を追加します。
 まず、ペリフェラルとセントラルそれぞれについて所要機能を列挙し、それを実現するために必要となるデータ定義をとりまとめます。データ定義部分はBLEサーバーとBLEクライアントのコードをベースにするので、追加部分だけを掲載します。
(1)ペリフェラルの所要機能

○セントラルからの接続を待ち、接続されるとセントラルとの通信を開始する。
○タイマーに設定した間隔に従って、温度と湿度の計測情報をNotifyで送信する。
○赤色プッシュボタンを押すと異常(Error)シグナルを送信する。ただし、この時点では特別の動作はしない。
○セントラルの書き出し開始(Write)を監視して受信内容をシリアルモニターに表示し、次のような動作をする。
 ・2文字以上のメッセージは受信文字列を表示する(今回はこのケースは発生しない)。
 ・'A'(Acknowledgment): 肯定応答なので何もしない。
 ・'S'(Stop): すべての動作を停止して黄色LEDを500mSec間隔で点滅させる。これはリセットで消灯される。
 ・'B'(Blink): 黄色LEDが消えていれば点灯し、点灯中なら消灯する。

 BLEサーバーのコードに変更を加える前に、まず異常状態に関連する2行を削除します。
 最初は126行目のコードです。
        /* プッシュボタンが押されたら異常状態を設定し、アラーム信号を送信する */
        if (isPushbuttonClicked()) {
            bAbnormal = true;
            signaldata.signalCode = SIGNAL_ERROR;
            pCharacteristicTX->setValue((uint8_t*)&signaldata, sizeof(tmpSignal));
            pCharacteristicTX->notify();
            Serial.println("Send signal: [Error!]");
        }
 続いて38行目のbAbnormalを削除します。
/* 通信制御用 */
BLECharacteristic *pCharacteristicTX;   // 送信用キャラクタリスティック
bool  deviceConnected = false;          // デバイスの接続状態
bool  bAbnormal  = false;               // デバイス異常判定
 次に修正後のペリフェラルのコードを示します。BLEサーバーのコードに追加されるのは次の網掛けの部分です。
/* シグナル種別 */
#define SIGNAL_ERROR    'E'         // (異常発生:Error)
#define SIGNAL_ACK      'A'         // (了解:Acknoledge)
#define SIGNAL_STOP     'S'         // (停止:Stop)
#define SIGNAL_BLINK    'B'         // (点滅:Blink)

/* UUID定義 */
#define SERVICE_UUID           "28b0883b-7ec3-4b46-8f64-8559ae036e4e"  // サービスのUUID
#define CHARACTERISTIC_UUID_TX "2049779d-88a9-403a-9c59-c7df79e1dd7c"  // 送信用のUUID
#define CHARACTERISTIC_UUID_RX "9348db8a-7c61-4c6e-b12d-643625993b84"  // 受信用のUUID

/* 通信制御用 */
BLECharacteristic *pCharacteristicTX;   // 送信用キャラクタリスティック
BLECharacteristic *pCharacteristicRX;   // 受信用キャラクタリスティック
bool deviceConnected = false;           // デバイスの接続状態
bool bInAlarm  = false;                 // デバイス異常判定
 シグナル種別は、双方向通信で使用するものをペリフェラルにもセントラルにも同じように追加します。また受信用のUUIDの値が必要になりますが、これは従来と同様にこのリンク https://www.uuidgenerator.net/ から作成することができます。


(2)セントラルの所要機能

○ペリフェラルからのアドバタイジングを受信して、サービスUUIDが一致すれば通信を開始する。
○ペリフェラルからNotifyで送信される計測情報を常時受信して、シリアルモニターとOLEDディスプレイに表示する。
○ペリフェラルから計測情報を受信すると了解(Acknowledgment)シグナルを返信する。
○ペリフェラルからシグナルデータで異常(Error)シグナルが届くと赤色LEDを点灯し、内部の異常状態を設定する。
 同時に、ペリフェラルに停止(Stop)シグナルを送信する。
○黄色プッシュボタンを押すと、状況に応じて次の動作を行う。
 ・異常状態なら赤色LEDを消灯し、内部の異常状態を解除する。
 ・異常状態でなければ、ペリフェラルに点滅(Blink)シグナルを送信する。
○ネットワーク切断時には再スキャン処理に移行する。

 セントラルのデータ定義部のコードを示します。BLEクライアントのコードに追加されるのは次の網掛けの部分です。
/* シグナル種別 */
#define SIGNAL_ERROR    'E'         // (異常発生:Error)
#define SIGNAL_ACK      'A'         // (了解:Acknoledge)
#define SIGNAL_STOP     'S'         // (停止:Stop)
#define SIGNAL_BLINK    'B'         // (点滅:Blink)

/* OLEDディスプレイの設定 */
#define SCREEN_WIDTH 128            // 幅 (単位:ピクセル)
#define SCREEN_HEIGHT 64            // 高さ(単位:ピクセル)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire);

/* UUID定義 */
static BLEUUID serviceUUID("28b0883b-7ec3-4b46-8f64-8559ae036e4e");   // サービスのUUID
static BLEUUID CHARA_UUID_RX("2049779d-88a9-403a-9c59-c7df79e1dd7c"); // 受信用のUUID
static BLEUUID CHARA_UUID_TX("9348db8a-7c61-4c6e-b12d-643625993b84"); // 送信用のUUID

/* 通信制御用 */
BLERemoteCharacteristic* pRemoteCharacteristicRX;   // 受信用キャラクタリスティック
BLERemoteCharacteristic* pRemoteCharacteristicTX;   // 送信用キャラクタリスティック
BLEAdvertisedDevice* targetDevice;      // 目的のBLEデバイス
static boolean doConnect = false;
static boolean doScan = false;
bool deviceConnected = false;           // デバイスの接続状態
bool bInAlarm  = false;                 // デバイス異常判定
bool enableMeasurement = false;         // 計測情報が有効
bool bSkipAlarmCheck = false;           // 異常判定スキップ
 送信用のUUIDは、ペリフェラルの受信用UUIDと同じ値を指定します。44行目は、アドバタイジング受信で見つけた目的のペリフェラルデバイスへのポインターです。これは、準備処理においてペリフェラルと接続を確立するために使用します。また50行目のbSkipAlarmCheckはloop()反復処理で参照して、ペリフェラル宛にStopシグナルを1回だけ送信するためのものです。


2.ペリフェラルの作成

①受信コールバックの追加

 ペリフェラルでの最も大きな機能追加は、セントラルからのWriteによるシグナルを受信するコールバック部分です。class funcServerCallbacksの続きに次のコードを挿入します。

92行目: BLECharacteristicCallbacksクラスのonWrite()関数をオーバーライドします。
     この関数は、セントラルがWrite書き出しを開始した時に送信用のディスクリプタを伴って呼び出される
     ので、それを使ってセントラルの送信データを取得することができます。
97行目: ここでそれを行っていて、getValue()でstd::string型のデータを取得できます。
98行目: 2文字以上なら受信文字列を表示させていますが、今回はその対象になるケースはありません。
107行目: 先頭1文字をchar型で評価して、それぞれのcaseに対応した処理を行います。先の「ペリフェラルの
     所要機能」に掲げた受信時の処理がここに記述されています。
// シグナル受信時のコールバック
class funcReceiveCallback: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristicRX) {
        if (bInAlarm) {                       // 異常状態なら何もしない
            return;
        }
        // 受信メッセージならモニターに表示する
        std::string rxValue = pCharacteristicRX->getValue();
        if (rxValue.length() > 1) {
            Serial.print("Received data: ");
            for (int i = 0; i < rxValue.length(); i++) {
                Serial.print(rxValue[i]);
            }
            Serial.println();
        }
        // シグナルなら対応した動作をする
        else {
            switch (rxValue[0]) {
                case SIGNAL_ACK:
                    Serial.println("Receive data: [Ack!]");
                    break;
                case SIGNAL_STOP:   // 切断状態に移行し異常状態をセットする
                    Serial.println("Receive data: [Stop!]");
                    deviceConnected = false;
                    bInAlarm = true;
                    ledState = HIGH;
                    break;
                case SIGNAL_BLINK:  // LEDを点灯または消灯する
                    Serial.println("Receive data: [Blink!]");
                    if (ledState == LOW) {
                        ledState = HIGH;
                    } else {
                        ledState = LOW;
                    }
                    digitalWrite(ledPin, ledState);
                    break;
                default:
                    break;
            }
        }
    }
};


②準備処理への追加

 受信用UUIDとPROPERTY_WRITEで受信用キャラクタリスティックを作成し、それを使って先の受信コールバックを設定します。
//  準備処理  //
void doPrepare(BLEService *pService) {
    // Notify用のキャラクタリスティックを作成する
    pCharacteristicTX = pService->createCharacteristic(
                      CHARACTERISTIC_UUID_TX,
                      BLECharacteristic::PROPERTY_NOTIFY
                    );
    pCharacteristicTX->addDescriptor(new BLE2902());

    // 受信用キャラクタリスティックを作成してシグナル受信時のコールバックを設定する
    pCharacteristicRX = pService->createCharacteristic(
                                           CHARACTERISTIC_UUID_RX,
                                           BLECharacteristic::PROPERTY_WRITE
                                         );
    pCharacteristicRX->setCallbacks(new funcReceiveCallback());
}
 これでペリフェラルは完成です。


3.セントラルの作成

①loop()反復処理

 セントラルでは、ペリフェラルとの接続中に状況判断をしながらシグナルを送信するので、それらのコードをloop()内に追加する必要があります。以下の網掛けが追加部分です。処理内容はコメントの通りなので省略しますが、シグナルの送信にwriteValue()を使用している点に注意してください。
void loop() {
    // アドバタイジング受信時に一回だけ準備処理を実行(ペリフェラルに接続)する
    if (doConnect == true) {
        if (doPrepare()) {
            Serial.println("Connected to the BLE Server.");
        } else {
            Serial.println("Failed to connect BLE server.");
        }
        doConnect = false;
    }
    // 接続状態において
    if (deviceConnected) {
        // 異常状態ならOLED表示を消してSIGNAL_STOPを送信する
        if (bInAlarm) {
            if (!bSkipAlarmCheck) {
                displayValues("", "", "");
                pRemoteCharacteristicTX->writeValue(SIGNAL_STOP);
                Serial.println("Send data: [Stop!]");
                bSkipAlarmCheck = true;
            }
        }
        // そうでなくて測定値が有効ならOLEDに表示してSIGNAL_ACKを送信する
        else if (enableMeasurement)
        {
            displayValues("DHT SENSOR", temperature, humidity);
            pRemoteCharacteristicTX->writeValue(SIGNAL_ACK);
            Serial.println("Send data: [ACK!]");
        }
        enableMeasurement = false;
        // プッシュボタンが押されていれば
        if (isPushbuttonClicked()) {
            // 異常状態でなければSIGNAL_BLINKを送信し、異常状態ならそれを解除する
            if (!bInAlarm) {
                pRemoteCharacteristicTX->writeValue(SIGNAL_BLINK);
                Serial.println("Send data: [Blink!]");
            } else {
                digitalWrite(ledPin, LOW);
                bInAlarm = false;
                bSkipAlarmCheck = false;
                Serial.println("Alarm state cleared.");
            }
        }
    } else if(doScan) {
        BLEDevice::getScan()->start(0);
    }
}


②準備処理

 準備処理doPrepare()関数の終わりに、Send用キャラクタリスティックへの参照取得を追加します。またLEDを消灯して異常状態bInAlarmをリセットするコードを追加しています。
//  準備処理  //
bool doPrepare() {
    // クライアントを作成してコールバック関数を設定する
    BLEClient* pClient = BLEDevice::createClient();
    pClient->setClientCallbacks(new funcClientCallbacks());
    Serial.println(" - Created client.");

    // ペリフェラルと接続して
    pClient->connect(targetDevice);
    Serial.println(" - Connected to peripheral.");

    // サービスへの参照を取得する
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
        Serial.print("Failed to find serviceUUID: ");
        Serial.println(serviceUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }
    Serial.println(" - Found target service.");

    // キャラクタリスティックへの参照を取得して
    pRemoteCharacteristicRX = pRemoteService->getCharacteristic(CHARA_UUID_RX);
    if (pRemoteCharacteristicRX == nullptr) {
      Serial.print("Failed to find characteristicUUID: ");
      Serial.println(CHARA_UUID_RX.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found characteristic CHARA_UUID_RX.");

    // Notifyのコールバック関数を割り当てる
    if(pRemoteCharacteristicRX->canNotify()) {
        pRemoteCharacteristicRX->registerForNotify(notifyCallback);
        Serial.println(" - Registered notify callback function.");
    }

    // Send用キャラクタリスティックの参照を取得する
    pRemoteCharacteristicTX = pRemoteService->getCharacteristic(CHARA_UUID_TX);
    if (pRemoteCharacteristicTX == nullptr) {
      Serial.print("Failed to find CHARA_UUID_TX: ");
      Serial.println(CHARA_UUID_TX.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found characteristic CHARA_UUID_TX.");

    digitalWrite(ledPin, LOW);
    deviceConnected = true;
    bInAlarm = false;
    return true;
}


4.動作の検証

 まずペリフェラルのシリアルモニターを表示して起動します。モニターには「Waiting to connect ...」が表示されて待ち状態になります。続いてセントラルを起動すると、計測と送信が始まります。計測データがセントラルに届くと直ちにACK応答が表示されます。以降はタイマーに設定した間隔でこれが繰り返されます。
 セントラルの黄色プッシュボタンをクリックすると、Blinkシグナルの受信がシリアルモニターに表示されてペリフェラルの黄色LEDが点灯します。もう一度クリックすると消灯します。
 次にペリフェラルの赤色プッシュボタンをクリックすると、異常シグナルErrorが送信されて、セントラルの赤色LEDが点灯します。同時にセントラルから停止シグナルStopが送り返され、これを受信したペリフェラルは、黄色LEDを点滅させて計測と送信を停止します。この状態はペリフェラルがリセットされるまで続きます。


 続いて、シリアルモニターをセントラル側に切り替えて、セントラルをリセットします。モニターにはスタートメッセージ「BLE Client start ...」に続いて、一連の準備処理と接続処理の経過が表示され、計測データの受信が始まります。データを受信すると直ちにACKを送信していることがわかります。
 セントラルの黄色プッシュボタンを押すとBlinkシグナルが送信されて、ペリフェラルの黄色LEDが点灯します。もう一度押すと同じシグナルが送られて消灯します。
 ペリフェラルから異常シグナルErrorを受信するとセントラルの赤色LEDが点灯し、ペリフェラルに停止シグナルStopを送信します。


 異常が発生した状態でセントラルの黄色プッシュボタンを押すと、赤色LEDが消えて異常状態が解除されます。シリアルモニターには「Alarm state cleared.」と表示されて、セントラルは再スキャンに移行します。
 これ以降、セントラルはスキャンを継続して待ち状態になります。やがてペリフェラルがリセットされて(リセットボタンが押されて)再起動すると、交信が再開されます。


 スケッチが少し込み入ってしまいましたが、基本的なコールバックの使い方やキャラクタリスティックの操作手順などを理解すれば、比較的簡単に双方向通信を実現できることが分かりました。双方のデバイスがコミュニケーションを交わしながら処理を進めることができるので、応用範囲がぐっと広がりそうです。


 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 次回はもう一歩進めて、IoTゲートウェイの実験ができればと思っています。
 お楽しみに!


 
Copyright (C) 2011-2024 Marchan, All rights reserved.