Ⅲ-1. ESP32によるBLEサーバーの作成

 コネクション通信を理解するために、単方向通信のシンプルなモデルを作成して動作を確認します。送信と受信のそれぞれのデバイスは、便宜上BLEサーバー、BLEクライアントと呼ぶことにします。この仕組みをベースにして、最終的に双方向通信ができるように機能を拡張する予定です。
 ここでは計測用デバイスを、温湿度センサーの計測情報を定期的に送信するサーバーに仕立てます。

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


1.BLEサーバーの基本要件

 コネクション通信では、データを送信するペリフェラル(サーバー)が所定のUUIDでアドバタイジングを行います。データを受信する側のセントラル(クライアント)は、アドバタイジングをスキャンして、目的のUUIDが見つかるとそのデバイスと接続を確立して通信を開始します。
 そこで、まずBLEサーバー(計測用デバイス)としてどのような機能が必要になるかをまとめておきましょう。

○アドバタイジングと接続待ち
 所定のサービスUUIDでアドバタイジングを行って、クライアントからの接続を待つ。
○計測と送信
 タイマーを利用して、決められた間隔で温度と湿度を計測してNotifyで送信する。
○デバイス異常の通知
 デバイス異常の発生を通知できるようにする。
 異常の検出はセンサー値の判定などで内部的に行うが、ここでは異常状態を発生させるためにプッシュボタンを
 使用する。ボタンを押すと、異常発生シグナルを送信してLEDを点滅させて、計測とブロードキャストを停止する。


 アドバタイジング・パケットにデータを乗せて発信するブロードキャスト通信と違って、計測データをデータ・パケットで送信するので、データ形式は自由に決めることができます。
 下表のように、計測データは計測値を100倍して整数型にしたものを構造体(struct型)で送信することにします。またデバイス異常の通知は、0xFFFFを前置した1文字の英字コードから構成されるシグナルデータ構造体で送信します。

 送信にあたって、それぞれのデータは、BLEライブラリーのsetValue()関数を使ってキャラクタリスティックのValueに設定しますが、その関数内でData Channel PDU内のLengthが設定されるので、データ長の設定を個別に行う必要はありません。


2.コールバック関数

 通信処理ではしばしばコールバック関数が使用されます。次にみるように、BLEサーバーのそれはきわめて単純なものですが、BLEクライアントではかなり複雑な処理を担うことになります。
 コールバックは、関数呼び出しにおいて関数やクラスのアドレスを引数として渡すことにより、必要な時点で渡した関数(またはクラス内の関数)を実行させるものです。これには次の2つの方法があります。
  ・既存のクラスをサブクラス化して、関数をオーバーライドする。
  ・コールバック関数を所定の形式に従って記述する。
 ①接続・切断コールバック

 クライアントからの接続時と切断時に呼び出されて内部条件を設定できるように、BLEServerCallbacksクラスをサブクラス化して、2つの関数をオーバーライドしています。     
class funcServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; } void onDisconnect(BLEServer* pServer) { deviceConnected = false; } };
 このクラスは、Serverオブジェクト作成時に次のようにコールバックを設定します。     
BLEServer *pServer = BLEDevice::createServer(); pServer->setCallbacks(new funcServerCallbacks());


 ②タイマー割り込み関数

 タイマー割り込み発生時に呼ばれる関数を記述します。関数内では内部変数を設定できるようにしています。     
static void kickRoutine() { bReadyTicker = true; }
 この関数は、タイマー割り込み開始時に設定します。     
Ticker ticker; const int iIntervalTime = 10; // 計測間隔(10秒) : ticker.attach(iIntervalTime, kickRoutine);


3.タイマーの使い方

 定期的な計測をするために、ESP32ライブラリーのタイマー割り込み関数Tickerを使用します。使い方は上記の通りとても簡単で、Tickerをインスタンス化して
     ticker.attach(iIntervalTime, kickRoutine);
のように起動するだけです。iIntervalTimeに計測間隔を秒で指定し、それに続けてタイマー起動させるコールバック関数kickRoutineを指定します。
 このように起動は簡単なのですが、ひとつだけ制約があります。以前に『超小型格安チップ「ESP-WROOM-02 (ESP8266)」はどこまで使えるか?』の「Ⅴ.定期自動計測とデータ記録」でも取り上げましたが、コールバック関数内ではネットワーク通信、シリアル通信、ファイル読み書きなどのブロッキングI/O処理を行ってはならない点です。これを忘れると確実に誤動作します。
 このため、次のようなグローバル変数
     bool bReadyTicker = false;
を定義して、コールバック関数内では
     bReadyTicker = true;
を行っています。少々わかりにくいロジックになりますが、loop()内でこのbReadyTickerを確認して、trueなら送信やシリアルモニター表示などの主処理を実行し、直後にbReadyTickerをfalseに戻します。


4.コードの解説

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

 「準備作業編」で述べたように、事前にDHTセンサーライブラリーとAdafruit Unified Sensor Driverをインストールする必要があります。
 BLEサーバーに必要なヘッダーファイルは次の通りです。
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include <Ticker.h>                 // タイマー割り込み用
#include <DHT.h>                    // DHTセンサー用


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

28行目: 異常発生時に送信するシグナルコードを定義しています。
32行目: サービスUUIDの定義です。
33行目: キャラクタリスティックUUIDの定義です。
     これらのUUIDはこのリンク https://www.uuidgenerator.net/ から作成できます。
41,47行目: 先に表で示した計測データとシグナルデータのテンプレートと実態の定義です。
64~67行: プッシュボタン押下で発生するチャタリングを除去して、状態を取得するためのものです。
70行目: Tickerオブジェクトを作成します。
71行目: タイマー割り込みの発生状態を保持します。
/* 基本属性定義  */
#define DEVICE_NAME "ESP32"         // デバイス名
#define SPI_SPEED   115200          // SPI通信速度
#define DHTTYPE     DHT11           // DHTセンサーの型式

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

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

/* 通信制御用 */
BLECharacteristic *pCharacteristicTX;   // 送信用キャラクタリスティック
bool  deviceConnected = false;          // デバイスの接続状態
bool  bAbnormal  = false;               // デバイス異常判定

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

struct tmpSignal {                      // シグナル
    char  hdr1;
    char  hdr2;
    char  signalCode;
};
struct tmpSignal signaldata = { 0xff, 0xff, 0x00 };

/* DHTセンサー*/
const int DHTPin = 14;                  // 接続ピン
DHT   dht(DHTPin, DHTTYPE);             // DHTクラスの生成

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

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

/* タイマー制御用 */
Ticker  ticker;
bool  bReadyTicker = false;         
const int iIntervalTime = 10;           // 計測間隔(10秒)


(3)コールバックのクラスと関数

 すでに前出のコードです。
// 接続・切断時コールバック
class funcServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
    }
    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
    }
};

//  タイマー割り込み関数  //
static void kickRoutine() {
    bReadyTicker = true;
}


(4)setup初期化処理

 まず初期化処理関数を実行します。GPIOピンを設定し計測センサーを起動します。
 初期化したBLEDeviceでBLEServerを作成し、BLEServerのポインターを使ってコールバッククラスを設定します。
 サービスUUIDを引数として、BLEServerのポインターからBLEServiceを作成します。
 BLEServiceへのポインターを引数にして準備処理を実行します。準備処理では、キャラクタリスティックUUIDを指定して、Notify送信用のキャラクタリスティックを作成します。
 サービスを開始し、BLEServerのアドバタイジングオブジェクトへのポインターを取得して、サービスUUIDを設定します。このサービスUUIDはアドバタイジング・パケットに組み込まれて、クライアントが目的のデバイスを判別するために使用されます。
 最後にタイマーを起動しています。場合によっては、コネクション確立時に起動した方が良いかも知れません。

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

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

    // Serviceオブジェクトを作成して準備処理を実行する
    BLEService *pService = pServer->createService(SERVICE_UUID);
    doPrepare(pService);

    // サービスを開始して、SERVICE_UUIDでアドバタイジングを開始する
    pService->start();
    BLEAdvertising *pAdvertising = pServer->getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->start();
    // タイマー割り込みを開始する
    ticker.attach(iIntervalTime, kickRoutine);
    Serial.println("Waiting to connect ...");
}


(5)loop反復処理

 接続が確立されていて異常でなければ、タイマー割り込みによって主処理を実行します。主処理では温湿度センサー情報の読み取りと変換を行い、計測データ構造体に変換結果をセットしてNotifyで送信します。
 続いてプッシュボタンのクリックをチェックして、クリックされていれば異常状態を設定し、シグナルデータ構造体にアラームシグナルを設定してNotifyで送信します。
 異常状態になっていれば500mSec間隔でLEDを点滅させます。
loop関数のコードを以下に示します。

128行目: 異常シグナルをセットした構造体の実態signaldataを、テンプレート長だけ一括設定します。
129行目: Notifyによる送信をしています。
void loop() {
    // 接続が確立されていて異常でなければ
    if (deviceConnected && !bAbnormal) {
        // タイマー割り込みによって主処理を実行する
        if (bReadyTicker) {
            doMainProcess();
            bReadyTicker = false;
        }
        /* プッシュボタンが押されたら異常状態を設定し、アラーム信号を送信する */
        if (isPushbuttonClicked()) {
            bAbnormal = true;
            signaldata.signalCode = SIGNAL_ERROR;
            pCharacteristicTX->setValue((uint8_t*)&signaldata, sizeof(tmpSignal));
            pCharacteristicTX->notify();
            Serial.println("Send signal: [Error!]");
        }
    }
    // アラーム中はLEDを500mSecで点滅させる
    if (bAbnormal) {
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
        delay(500);
    }
}


(6)主要ロジック

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

146行目: DHTセンサーを起動します。
/*  初期化処理  */
void doInitialize() {
    Serial.begin(SPI_SPEED);
    pinMode(buttonPin, INPUT);
    pinMode(ledPin, OUTPUT);
    dht.begin();
}


156行目: Notifyで送信するので、キャラクタリスティック・ディスクリプタにCCCD(Client Characteristic
     Configuration Descriptor:0x2902)を設定します。
/*  準備処理  */
void doPrepare(BLEService *pService) {
    // Notify用のキャラクタリスティックを作成する
    pCharacteristicTX = pService->createCharacteristic(
                      CHARACTERISTIC_UUID_TX,
                      BLECharacteristic::PROPERTY_NOTIFY
                    );
    pCharacteristicTX->addDescriptor(new BLE2902());
}


173行目: キャラクタリスティックに送信データを設定します。温度と湿度をセットした構造体dataを、テンプレート
     tmpDataのデータ長だけ一括して設定しています。
174行目: Notifyで送信します。
/*  主処理ロジック  */
void doMainProcess() {
    // 温度と湿度を読み取る
    float t = dht.readTemperature();
    float h = dht.readHumidity();

    // 計測失敗なら再試行させる
    if (isnan(h) || isnan(t)) {
        Serial.println("Failed to read sensor!");
        return;
    }
    // 構造体に値を設定して送信する
    data.temperature = (int)(t * 100);
    data.humidity = (int)(h * 100);
    pCharacteristicTX->setValue((uint8_t*)&data, sizeof(tmpData));
    pCharacteristicTX->notify();
    // シリアルモニターに表示する
    char bufValue[20];
    sprintf(bufValue, "%5.2fC, %5.2f%%", t, h);
    Serial.print("Send data: ");    Serial.println(bufValue);
}
 その他のコードは省略します。スケッチをダウンロードしてください。


5.実行結果

 起動すると、シリアルモニターに「Waiting to connect ...」と表示して待ち状態になります。


 この状態では何も起きませんが、スマートフォンから接続して送信を開始させることができます。例えば、BLE汎用ツールの「nRF Connect」をインストールしてみましょう。iPhoneの場合はここから、Androidの場合はここからインストールすることができます。
 次のスマートフォン画面は、nRF ConnectでESP32とコネクトした状態のものです。コネクトすると、BLEサーバーの待ち状態が解除されて通信が始まり、シリアルモニターに送信データの温度と湿度が表示されます。これから、10秒ごとに送信されている状況を眺めることができます。
 スマートフォン画面のUnknown Serviceを展開すると、CharatceristicとDescriptorの内容が表示されます。 プログラムで定義したサービスUUIDとキャラクタリスティックUUIDを確認することができます。また、CharacteristicのPropertyでNOYIFYが指定されていること、DescriptorsからはCCCDの0x2902とNotification enabledであることが確認でき、予定通りに動作していることが分かります。

 さらに細かく見ると、CharacteristicのValueには計測値が表示されています。
     (0x)56-09-00-00-44-16-00-00
は、0x0956と0x1644なので、10進数に変換すると2390と5700になります。100で割り算してもとの値に戻すと、23.9℃と57.0%であることが分かります(これは先のモニター画面とは別時点で撮影したものなので、値は異なっています)。
 デバイス異常を発生させるためにプッシュボタンを押すと、LEDが点滅してシリアルモニターに「Send signal: [Error!]」と表示されます。スマートフォンのCharacteristic Valueは
     (0x)FF-FF-41
に変わり、先頭に0xFFFFのヘッダーとシグナルコード'A'をもつシグナルデータが送信されているのを知ることができます。

 スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
 次回は、今回のBLEサーバーのデータを受信して動作するBLEクライアントを作成します。
 お楽しみに!


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