Ⅲ-2. ESP32によるBLEクライアントの作成

 コネクション通信を理解するために、単方向通信のシンプルなモデルを作成して動作を確認します。送信と受信のそれぞれのデバイスは、便宜上BLEサーバー、BLEクライアントと呼ぶことにします。この仕組みをベースにして、最終的に双方向通信ができるように機能を拡張する予定です。
 ここでは、前回作成したBLEサーバーからのデータを受信して、有機ディスプレイやシリアルモニターに受信内容を表示すると共に、デバイス異常情報によるアラーム処理を行います。

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


1.BLEクライアントの基本要件

 コネクション通信では、セントラル(クライアント)はアクティブスキャンを行って目的のサーバーを見つけて接続を確立します。以降は、そのサーバーからの送信にタイミングを合わせて受信を行います。
 したがって、クライアント(監視用デバイス)としては次のような機能が必要になります。

○デバイスの検索と受信
 所定の時間だけアクティブスキャンを行って、サーバーからのアドバタイジング応答を待つ。
 目的とするサーバーから応答があると接続を行い受信する。
○受信データの処理
 受信データを中継して外部ネットワークに送信したり、データベースに記録したりという処理部分であるが、
 ここではシリアルモニターとOLEDディスプレイへの表示にとどめる。
○OLEDディスプレイへの表示
 超小型のOLED(SSD1306)に受信した温度と湿度を表示する。
○デバイス異常の処理
 異常発生のシグナルデータを受信すると、LEDを点灯して異常の発生を知らせる。
 LEDの点灯は、プッシュボタンを押すことで消灯し異常状態を解除する。
 異常状態の解除後は、アドバタイジング待ちのスキャンを行う。


2.コールバック関数

 アドバタイジング受信と目的デバイスの判定、接続応答処理、Notify受信など、受信の主要な部分はコールバック関数の中で処理します。そこで、まずBLEクライアントで使用するコールバッククラスと関数の内容を見ておきましょう。
 ①接続・切断コールバック

 BLEClientCallbacksの派生クラスで2つの関数をオーバーライドしています。接続確立時には何もしませんが、切断されたら、デバイスとの接続状態を保持するグローバル変数 bool deviceConnectedをfalseに設定します。この変数は、アドバタイジング受信時に一度だけ実行される準備処理が完了するとtrueになります。
class funcClientCallbacks: public BLEClientCallbacks {
    void onConnect(BLEClient* pClient) {
    };
    void onDisconnect(BLEClient* pClient) {
        deviceConnected = false;
    }
};
 このクラスは、Clientオブジェクト作成時に次のようにコールバックを設定します。     
BLEClient* pClient = BLEDevice::createClient(); pClient->setClientCallbacks(new funcClientCallbacks());


 ②アドバタイジング受信コールバック

 BLEAdvertisedDeviceCallbacksの派生クラスを作成します。サーバーからのアドバタイジングを受信すると、BLEAdvertisedDeviceを伴ってコールされるonResultメソッド(関数)をオーバーライドします。
 84~85行: アドバタイジング・パケットにサービスUUIDがあることを確認して、その値をservice変数に取得して
      います。
 87行目: 取得した値が目的のサービスUUIDであるかどうかを調べ、一致していればスキャニングを停止します。
 89行目: グローバル変数targetDeviceに目的のBLEデバイスへのポインターを取得します。後に参照されること
      になるdoConnectとdoScanはここでtrueに設定されます。
class advertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
        Serial.print("Advertised Device found: ");
        Serial.println(advertisedDevice.toString().c_str());

        // 目的のBLEデバイスならスキャンを停止して接続準備をする
        if (advertisedDevice.haveServiceUUID()) {
            BLEUUID service = advertisedDevice.getServiceUUID();
            Serial.print("Have ServiceUUI: "); Serial.println(service.toString().c_str());
            if (service.equals(serviceUUID)) {
                BLEDevice::getScan()->stop();
                targetDevice = new BLEAdvertisedDevice(advertisedDevice);
                doConnect = doScan = true;
            }
        }
    }
};
 このクラスは、Scanオブジェクト作成時に次のようにコールバックを設定します。     
BLEScan* pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new advertisedDeviceCallbacks());


 ③Notifyコールバック関数

 Notify送信データを受信した時にコールバックさせる関数です。
 関数名は何でもいいのですが、BLEライブラリーExamplesのBLE_client.inoに従いました。引数の数と型式は厳密に指定しなければなりませんが、これもBLE_client.iniに合わせています。これは以前に紹介した「BLE C++ Guide.pdf」にも記載されています。
 ついでながら、これらの情報がない状態で開発しなければならない場合は、Notifyコールバックを設定するためのメソッド名とおぼしきregisterForNotifyとか、notify_callbackなどでライブラリーソースを検索すると(例えばgrepなどで)、BLERemoteCharacteristic.hにそれらしきものを見つけることができます。そして、同ファイル内にtypedef void (*notify_callback)として引数リストが定義されているのを知ることができます。

100行目: pDataで通知される受信データの先頭が、0xFFFFつまりシグナルデータであるかどうかを調べて、そうで
     あればLEDを点灯してシリアルモニターにその旨を表示し、異常状態をセットします。
108~110行: 受信データをtmpData構造体にサイズ分だけ取り出し、温湿度を100.00で除して実数に戻します。
     後続行ではそれを表示用に編集してシリアルモニターに表示、enableMeasurementをtrueに設定して
     います。
static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
                uint8_t* pData, size_t length, bool isNotify) {
    // アラーム信号ならLEDを点灯してアラーム状態を設定する
    if (*pData == *(pData+1) && *pData == 0xff && *(pData+2) == SIGNAL_ERROR) {
        digitalWrite(ledPin, HIGH);
        Serial.println("Received signal [Error!]");
        bInAlarm = true;
        return;
    }

    // 受信メッセージから温度と湿度を切り出して表示用に編集する
    memcpy(&data, pData, length);
    float t = data.temperature / 100.00;
    float h = data.humidity / 100.00;
    static char temp[10];
    static char humd[10];
    sprintf(temp, "%5.2fC", t);
    sprintf(humd, "%5.2f%%", h);
    temperature = (char*)temp;
    humidity = (char*)humd;
    enableMeasurement = true;
    Serial.print("Received data: ");  Serial.print(temperature);
    Serial.print(", ");  Serial.println(humidity);
}


3.コードの解説

(1)必要なライブラリー

 「準備作業編」で述べたように、事前にOLEDディスプレイ(SSD1306)用のライブラリーをインストールしておく必要があります。
 BLEクライアントに必要なヘッダーファイルは次の通りです。
#include <BLEDevice.h>
#include <Wire.h>                   // For I2C interface
#include <Adafruit_SSD1306.h>       // For SSD1306 display
#include <Adafruit_GFX.h>           // For SSD1306 display


(2)属性定義とデータ定義

34行目: サービスUUIDの指定です。BLEServerの定義と同じ値を指定します。
35行目: 受信用のキャラクタリスティックUUIDです。BLEServerのCHARACTERISTIC_UUID_TXと同じ値を
     指定します。
/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度

/* シグナル種別 */
#define SIGNAL_ERROR   'E'          // (Error:異常発生)

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

/* UUID定義 */
BLEUUID serviceUUID("28b0883b-7ec3-4b46-8f64-8559ae036e4e");   // サービスのUUID
BLEUUID CHARA_UUID_RX("2049779d-88a9-403a-9c59-c7df79e1dd7c"); // RXのUUID

/* 通信制御用 */
BLERemoteCharacteristic* pRemoteCharacteristicRX;  // 受信用キャラクタリスティック
BLEAdvertisedDevice* targetDevice;      // 目的のBLEデバイス
bool  doConnect = false;                // 接続指示
bool  doScan = false;                   // スキャン指示
bool  deviceConnected = false;          // デバイスの接続状態
bool  bInAlarm  = false;                // デバイス異常
bool  enableMeasurement = false;        // 計測情報が有効

/* 通信データ */
struct tmpData {                        // 計測データ
    int   temperature;
    int   humidity;
};
struct tmpData  data;

char* temperature;                      // 温度
char* humidity;                         // 湿度

/* LEDピン */
const int ledPin = 25;                  // 接続ピン
int ledState = LOW;                     // 状態

/* プッシュボタン */
const int buttonPin = 32;               // 接続ピン
int buttonState;                        // 状態
int lastButtonState = LOW;              // 直前の状態
unsigned long lastDebounceTime = 0;     // 直前の切替時刻
unsigned long debounceDelay = 50;       // デバウンスタイム(mSec.)


(3)setup初期化処理

 まず初期化処理関数を実行します。GPIOピンを設定しOLEDディスプレイを初期化します。
 初期化したBLEDeviceでScanオブジェクトを取得して、アドバタイズド・コールバック関数を設定します。
 BLEServerからのアドバタイジング・パケットを受信するために、アクティブスキャンで10秒間スキャンを実行します。アドバタイズド・コールバック関数で目的のデバイスが見つかると直ちにスキャンを停止するので、実際はさほど時間を要さずに処理が完了します。

setup関数のコードを以下に示します。
void setup() {
    // 初期化処理を行ってBLEデバイスを初期化する
    doInitialize();
    BLEDevice::init("");
    Serial.println("Client application start...");

    // Scanオブジェクトを取得してコールバックを設定する
    BLEScan* pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new advertisedDeviceCallbacks());
    // アクティブスキャンで10秒間スキャンする
    pBLEScan->setActiveScan(true);
    pBLEScan->start(10);
}


(4)loop反復処理

 スキャンで目的のデバイスが見つかり接続準備ができていれば、一度だけ準備処理doPrepare()を実行します。この準備処理の中でBLEServerに接続されます。
 次にデバイスに接続されていることを確認して、測定値が正常に受信されていれば、編集してOLEDディスプレイとシリアルモニターに表示します。さらにプッシュボタンがクリックされているかを調べ、押されていればLEDを消灯して異常状態を解除します。
 これをloop()内で反復処理しますが、BLEサーバーの異常発生で通信が中断された場合は、スキャン可能であれば再スキャンをして復旧を待ちます。この際の再スキャンの実行
    BLEDevice::getScan()->start(0);
は、BLEライブラリーExamplesのBLE_client.inoのコードをそのまま使っています。
   // this is just example to start scan after disconnect,
       most likely there is better way to do it in arduino.
とのコメントが付されているのですが、ベターな方法を思いつかないのでこのようにしました。BLEサーバー復帰時に妙な動きが起きるのですが、この点は「実行結果」でレビューします。
loop関数のコードを以下に示します。

147行目: doPrepare()は一度だけ実行させます。
163~164行: 前述の再スキャンのコードです!
void loop() {
    // アドバタイジング受信時に一回だけサーバーに接続する
    if (doConnect == true) {
        if (doPrepare()) {
            Serial.println("Connected to the BLE Server.");
        } else {
            Serial.println("Failed to connect to the BLE server.");
        }
        doConnect = false;
    }
    // 接続状態なら
    if (deviceConnected) {
        // 測定値が有効かつ異常でなければOLEDに表示する
        if (enableMeasurement && !bInAlarm) {
            displayValues("DHT SENSOR", temperature, humidity);
            enableMeasurement = false;
        }
        // プッシュボタンが押されるとLEDを消灯して異常状態を解除する
        if (isPushbuttonClicked()) {
            digitalWrite(ledPin, LOW);
            displayValues("", "", "");
            bInAlarm = false;
            Serial.println("Pushbutton clicked!");
        }
    } else if (doScan) {
        BLEDevice::getScan()->start(0);
    }
}


(5)主要ロジック

 その他の主要ロジックは次の通りです。

 GPIOピンの設定とOLEDディスプレイの初期化をします。
/*  初期化処理  */
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(buttonPin, INPUT);
    pinMode(ledPin, OUTPUT);
    digitalWrite(ledPin, LOW);

    // OLEDディスプレイを初期化する
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println(F("** SSD1306 allocation failed **"));
        for(;;); // 失敗すると処理を進ませない!
    }
    displayValues("DHT SENSOR", "", "");
    Serial.println("BLE Client start ...");
}


188行目: クライアントコールバックを設定する。
192行目: BLEサーバーと接続する。
196行目: サービスへの参照を取得する。
206行目: キャラクタリスティックへの参照を取得する。
216行目: Notifyコールバック関数を設定する。
/*  準備処理  */
bool doPrepare() {
    // クライアントオブジェクトを作成してコールバックを設定する
    BLEClient* pClient = BLEDevice::createClient();
    pClient->setClientCallbacks(new funcClientCallbacks());
    Serial.println(" - Created client.");

    // リモートBLEサーバーと接続して
    pClient->connect(targetDevice);
    Serial.println(" - Connected to server.");

    // サービスへの参照を取得する
    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.");
    }

    deviceConnected = true;
    return true;
}

 その他のコードは省略します。スケッチをダウンロードしてください。


4.実行結果

 シリアルモニターでは同時に2つのデバイスの状況を見ることができません。そこで、まずはBLEサーバーのシリアルモニターを表示して起動します。モニターには「Waiting to connect ...」が表示されて待ち状態になります。続いてBLEクライアントを起動すると、計測と送信が始まります。
 しばらく送信を繰り返させてから、BLEサーバーの赤色プッシュボタンを押して異常を発生させます。モニターには「Send signal: [Error!]」と表示されて計測・送信が停止し、黄色LEDがブリンクします。同時に、BLEクライアントの赤色LEDが点灯します。


 シリアルモニターをBLEクライアント側に切り替えて、BLEクライアントをリセット(ENボタンを押下)します。モニターにはスタートメッセージ「BLE Client start ...」に続き、目的のアドバタイジング受信と準備処理の実行状態が表示され接続が完了します。そこから約10秒間隔で受信計測データが表示されます。
 BLEクライアントのプッシュボタンを押すと黄色LEDが点滅、同時にシリアルモニターには「Received signal [Error!]」が表示され、BLEクライアントの赤色LEDが点灯します。


 BLEクライアントのプッシュボタンを押して異常状態を解除すると、モニターに「Push button clicked!」と表示され、赤色LEDが消えてOLEDディスプレイの表示がクリアされます。まだ黄色LEDが点滅を続けているBLEサーバーをリセットすると、LEDは消灯して再起動します。
 BLEクライアントは再スキャンの状態にあり、少し待つとBLEサーバーを発見して「Advertised Device found: ~」とサービスUUIDを表示します。ところが、またすぐに再スキャンして同じ処理を反復します。下にモニターの内容を掲げますが、なぜこのようになるのか原因は分かりません。これは、BLEクライアントをリセットして最初に一連の異常発生と解除をした場合だけに起き、2回目以降の同じ操作で二重スキャンは発生しません。
 ただ、いずれの場合もBLEサーバーとの再接続は正しく行われ、計測データの受信も正常なので、これはこれで良しとしておきましょう。


 今回は単方向のコネクション通信を試してみました。ブロードキャスト通信と比較すると、コールバック関数の扱いや受信開始までの準備処理が複雑になりますが、受信・表示処理はとてもシンプルです。またクライアント・サーバー間で接続を確立してポイント・ツー・ポイントの通信を行うことから、セキュリティ面でもより安心な方法であることが分かります。


 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 次回は、BLEクライアントとサーバーの機能を強化して、双方向通信ができるコネクション方式を試す予定です。
 お楽しみに!


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