Ⅱ-2. ESP32によるBLEオブザーバーの作成

 ブロードキャスト通信は、あるBLEデバイスから別のBLEデバイスに対して、一方的にデータを発信する通信方式です。アドバタイジング・パケットを送信し続けるデバイスをブロードキャスターと呼び、受信する側はオブザーバーと呼ばれます。ここではオブザーバーのソフトウェアを作成します。
 前回作成した複数のブロードキャスターから温湿度計測情報を受信して、有機ディスプレイやシリアルモニターで受信内容を確認すると共に、デバイス異常情報によるアラーム処理を検証します。

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


1.オブザーバーの基本要件

 オブザーバー(監視用デバイス)としては次のような機能が必要になります。

○デバイスの検索と受信
 周辺のBLEデバイスから目的とするブロードキャスターを検索して、見つかるとデータを受信する。
 複数のデバイスを、デバイス名とデバイス識別番号で判別する。
○受信データの処理
 受信データを中継して外部ネットワークに送信したり、データベースに記録したりという処理部分であるが、
 ここではシリアルモニターへ表示するにとどめる。特定のデバイス(デバイス識別番号が1のもの)だけ、
 受信内容をディスプレイに表示させる。
○OLEDディスプレイへの表示
 超小型のOLED(SSD1306)に受信した温度と湿度を表示する。
○デバイス異常の処理
 受信データのデバイス異常フィールドに非ゼロを受信すると、LEDを点灯して異常の発生を知らせる。同時に、
 OLEDとシリアルモニターにエラーの発生を表示する。LEDの点灯は、プッシュボタンを押すことで消灯させる。
 エラー発生後もブロードキャスターの検索と受信処理は継続させる。

 なお、オブザーバーは常時受信する必要があること、ブロードキャスターと違って頻繁に充電できる電池や安定的な電源が確保されていることなどから、省電力のための特別な対策は行いません。


2.受信データの形式

 ブロードキャスターが発信するパケットから、Manufacturer Specific Dataだけを取り出します。そのために、アドバタイジングデータのAD Typeが0xFFのものだけを選択して、getManufacturerData()関数でstd::string型のデータを取得します。取得したデータにはCompany ID以降が取り出され、バイト配列として内容を参照することができます。
 Company IDが0xFFFF、つまりTest Manufacturer IDになっているものが目的のデータになります。



3.コードの解説

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

 すでに必要なライブラリーはインストールされているはずですが、まだであれば次の2つのライブラリーをインストールしてください。いずれもOLEDディスプレイの表示に必要なものです。
①Adafruit SSD1306ライブラリーのインストール
 ・ここをクリックしてAdafruit SSD1306ライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、Adafruit_SSD1306-masterフォルダが表示されます。
 ・フォルダー名をAdafruit_SSD1306に変更して、Arduino IDEのインストールライブラリフォルダに移動します。
②Adafruit GFXライブラリーのインストール
 ・ここをクリックしてAdafruit GFXライブラリーをダウンロードしてください。
 ・zipフォルダを解凍すると、Adafruit-GFX-Library-masterフォルダが表示されます。
 ・フォルダー名をAdafruit-GFX-Libraryに変更して、Arduino IDEのインストールライブラリフォルダに移動します。

 ここで作成するオブザーバーに必要なヘッダーファイルは次の通りです。
 コメントの通り、Wire.hはOLEDディスプレイをI2C接続するために必要です。
#include <BLEDevice.h>
#include <Wire.h>                   // I2C interface
#include <Adafruit_SSD1306.h>       // SSD1306 display
#include <Adafruit_GFX.h>           // SSD1306 display


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

25行目: 受信対象のデバイス名です。
26行目: スキャン対象のブロードキャスターデバイスの最大数です。
30行目: デバイスごとのシーケンス番号の変化を管理する配列です。最大デバイス数だけ配列を確保します。
     シーケンス番号はゼロから始まるので(-1)で初期化していて、ここにカレントの番号を退避します。
33行目: 受信データはこの構造体形式で取り出します。
44行目: SSD1306(128x64 pixel)のインスタンスを作成しています。
/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度

/* スキャン制御用 */
#define DEVICE_NAME "ESP32"         // 対象デバイス名
#define MAX_DEVICES  8              // 最大デバイス数
#define ManufacturerId 0xffff       // 既定のManufacturer ID
const int scanning_time = 3;        // スキャン時間(秒)
BLEScan* pBLEScan;                  // Scanオブジェクトへのポインター
int prev_seq[MAX_DEVICES] = { -1, -1, -1, -1, -1, -1, -1, -1 };

/* 受信データ構造体 */
struct tmpData {
    int     device_number;          // デバイス識別番号
    bool    abnormal;               // デバイス異常
    int     seq_number;             // シーケンス番号
    float   temperature;            // 温度
    float   humidity;               // 湿度
};

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

/* LEDピン */
const int ledPin = 25;              // LEDの接続ピン

/* プッシュボタン */
const int buttonPin = 32;           // プッシュボタンの接続ピン


(3)ロジックの全体構造

 setup()では、アドバタイズメント・パケットを受信するためにScanオブジェクトを取得して、パッシブスキャンに設定します。BLEには
   ・パッシブスキャン
   ・アクティブスキャン
の2種類があります。パッシブスキャンは、受信状態にして、周りのアドバタイジング・パケットを受信する方法です。アクティブスキャンは、アドバタイジング・パケット受信後に自らSCAN_REQを送信して、さらに追加データを受信する方法です。BLEで単にスキャンといった場合はパッシブスキャンのことです。

 loop()では、まず所定時間のスキャンを実行して、見つかったBLEデバイスの数を取得します。周辺にスマートフォンなどのBLEデバイスがあれば、それも個数に含まれます。
 続いて、目的とするデバイス名のManufacturer Specific Dataであるかどうかを調べます。さらに、デバイス識別番号の妥当性とシーケンス番号の更新を確認して、受信データを取り出します。

setup()、loop()と周辺関数のコードを以下に示します。

56行目: 別関数でGPIOの設定やOLEDディスプレイの初期化をしています。
67~68行目: コメントの通り、所定時間だけスキャンして見つかったデバイス数を取得します。
70行目: 見つかったデバイスそれぞれについて以下の処理を行います。
73~74行目: 受信対象のデバイス名であれば、Manufacturer specific dataであることを確認します。
81~82行目: 最終的に目的のデバイスであることを判断する際に、シーケンス番号が変化しているかを併せて調べて
    います。タイミングによってデータが重複受信される場合があるためで、重複データは無視します。
90行目: 受信したCondition statusがノーマルならdisplayData()を、アブノーマルならdisplayAlarm()を実行
    させます。
void setup() {
    doInitialize();                             // 初期化処理をして
    BLEDevice::init("");                        // BLEデバイスを作成する
    Serial.println("Client application start...");
    pBLEScan = BLEDevice::getScan();            // Scanオブジェクトを取得して、
    pBLEScan->setActiveScan(false);             // パッシブスキャンに設定する
}

void loop() {
    struct tmpData td;

    // 所定時間だけスキャンして、見つかったデバイス数を取得する
    BLEScanResults foundDevices = pBLEScan->start(scanning_time);
    int count = foundDevices.getCount();

    for (int i = 0; i < count; i++) {     // 受信したアドバタイズデータを調べて
        BLEAdvertisedDevice dev = foundDevices.getDevice(i);
        std::string device_name = dev.getName();
        if (device_name == DEVICE_NAME 
                && dev.haveManufacturerData()) {   // デバイス名が一致しManufacturer dataがあって
            std::string data = dev.getManufacturerData();
            int manu_code = data[1] << 8 | data[0];
            int device_number = data[2];
            int seq_number = data[4];

            // デバイス識別番号が有効かつシーケンス番号が更新されていたら
            if (device_number >= 1 && device_number <= MAX_DEVICES
                      && seq_number != prev_seq[device_number - 1]) {
                // 受信データを取り出す
                prev_seq[device_number - 1] = seq_number;
                td.device_number = device_number;
                td.abnormal = (bool)data[3];
                td.seq_number = seq_number;
                td.temperature = (float)(data[6] << 8 | data[5]) / 100.00;
                td.humidity = (float)(data[8] << 8 | data[7]) / 100.00;
                if (!td.abnormal) {
                    displayData(&td);
                } else {
                    displayAlarm(&td);
                }
            }
        }
    }
    // プッシュボタン押下でLEDを消灯してディスプレイ表示を消去する
    int buttonState = digitalRead(buttonPin);
    if (buttonState == HIGH) {
        displayValues("", "", "");
        digitalWrite(ledPin, LOW);
    }
}

/*  初期化処理  */
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(buttonPin, INPUT);        // GPIO設定:プッシュボタン        
    pinMode(ledPin, OUTPUT);          // GPIO設定:LED
    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 ...");
}
 その他のコードは省略します。スケッチをダウンロードしてください。


4.実行結果

 同型の計測用デバイスを3台作成してブロードキャスターにしました。それぞれのデバイスは、デバイス識別番号を1~3としてコンパイルし書き込んでいます。オブザーバーを立ち上げた状態で、3台のブロードキャスターを順次起動します。
 ブロードキャスティング時間の1秒とスキャニング時間の3秒は、それぞれ適当に設定したものですが、下図のように、取りこぼしもなく順調にシリアルモニターに表示されています。
 ブロードキャスター側で、デバイス異常発生のためのプッシュボタンを長押しすると黄色LEDが点滅し、同時にオブザーバー側のOLEDディスプレイにデバイス識別番号とエラーメッセージが表示され、赤色LEDが点灯します。オブザーバーのプッシュボタンを押すとOLEDのエラー表示が消え、赤色LEDも消灯します。ただし、スキャニング待ちの関係でボタンを長押しする必要があります。
 ブロードキャスター側は、リセットボタンをクリックすると黄色LEDが消灯し、計測と送信を再開します。


 今回の実験から明らかなように、BLEブロードキャスト通信は、センサーなどの少量データを単方向で通信する場合に適した方法です。アドバタイジング・パケットだけでデータを受け渡すため、プログラム・ロジックはとてもシンプルで、軽快に通信することができます。またセンサーデバイス側の省電力モードは大きなメリットと言えるでしょう。
 「デバイスの検索方法」については、積極的に検索するのでなく、どこまでも受動的です。受信できるものはすべて受信してから該当するデバイスとデータをピックアップするのが特徴です。
 今回の実験では、デバイス異常のスイッチ操作で長押しが必要でしたが、実装時にはスイッチを使用することはなくセンシングによるはずなので、この問題は発生しません。また、オブザーバーの受信データ処理の部分を機能拡張することで、IoTゲートウェイへ格上げすることも考えられます。今後、さまざまな活用が期待できそうです。

 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 次回は、コネクション方式のもっともシンプルな単方向通信に取り組む予定です。
 お楽しみに!


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