1.ブロードキャスターの基本要件
まず、ブロードキャスター(計測用デバイス)としてどのような機能が必要になるかをまとめておきましょう。
○計測と発信
決められた時間間隔で、温度と湿度を計測してブロードキャストする。
○固有IDの通知
複数のデバイスからブロードキャストするケースを考えて、デバイス名とは別に固有の識別番号を持たせる。
1オブザーバー当たりの最大通信デバイスを8個と想定し、1~8の番号を割り当てる。
○デバイス異常の通知
デバイス異常の発生を通知できるようにする。
異常の検出はセンサー値の判定などで内部的に行うが、ここでは異常状態を発生させるためにプッシュボタンを
使用する。ボタンを押すと、異常発生をブロードキャストしてからLEDを点滅させて、計測とブロードキャストを
停止する。
○ディープスリープ
電力消費量を抑えるため、短時間だけ計測・ブロードキャストを行ってディープスリープ状態に移行する。
所定の時間経過でウェークアップ(再起動)するが、プッシュボタンが押されると直ちにウェークアップする。
これらのシステム要件から、アドバタイジング・データの様式が決まってきます。基本は、第Ⅰ章「3. BLEのパケットフォーマット」の内容に基づいています。
再度掲載すると、Advertising PDUの中のAdvertising Dataの形式は次の通りでした。
下表に送信データの内容を示します。Lengthには、AD Type以降のデータ長をByte(=Octet)で指定します。ここでは10Byteを指定しています。AD Typeによって、続くデータの性格が決められます。自由な形式でデータを格納するためにはManufacturer Specific Dataを指定します。これを0xFFで指定し、次の2Byteではテスト用に許可されているManufacturer IDの0XFFFFを設定します。
ここまでを規定の様式に合わせれば、後続フィールドには何を盛り込んでもかまいません。Server IDは前述のデバイス識別番号で、Condition Statusは異常通知のためのフィールドです。Sequential Numberは、ブロードキャストの都度カウントアップした番号です。TemperatureとHumidityには、計測データを100倍して整数化したものを格納します。
2.ESP32の省電力モード
ブロードキャスターはセンサーデバイスであることが多く、電池での駆動が中心になります。したがって、省電力を徹底して電池寿命を延ばすことが重要な課題です。コーディングに取りかかる前に、ESP32の省電力モードを概観して、ここで採用するモードを決めることにしましょう。
(1)ESP32の省電力モード
ESP32 Espressifデータシートによると、ESP32は、
・アクティブモード
・モデムスリープモード
・ライトスリープモード
・ディープスリープモード
・ハイバネーションモード
・休止モード
の電力モードを切り替えることができます。
次の表は、休止状態を除くそれぞれの電力モードで何が起きているかを示すものです。モードによって、CPU、Wi-Fi・Bluetoothベースバンド、RTCメモリとRTC周辺機器、ULPコプロセッサーの動作状態がわかります。なお、ULP(Ultra Low Power)コプロセッサーは、ESP32がデュアルコアとは別に装備しているもう一つの低消費電力プロセッサーです。
また、ハイバネーションは電源を切る直前の状態から動作を再開できる機能です。
次の表は、それぞれの電力モードでの消費電流です。
以降の検討では、Arduino IDEで簡単に扱えて効果が大きいDeep sleepに絞ることにします。
(2)Deep SleepとWake Up制御
Deep Sleepからもとの状態に戻す、つまりウェークアップさせるには次のような方法があります。
・タイマーによるウェイクアップ
・外部ウェイクアップ
・タッチピンを使ったウェイクアップ
・ULPコプロセッサを使ったウェイクアップ
今回は、所定の時間間隔で計測・ブロードキャスティングをするので、「タイマーによるウェイクアップ」を
使用します。ESP32 RTCコントローラーには、指定時間の経過後にESP32を再起動できるタイマーが内蔵されています。タイマーによるウェイクアップはとても簡単で、次の関数でスリープ時間をマイクロ秒単位で指定するだけです。
esp_sleep_enable_timer_wakeup(time);
指定が終わったら次のようにしてdeep sleepに入らせます。
sp_deep_sleep_start();
次の関数を使うと、上記の2つの関数呼び出しをまとめて指示することができ、直ちにdeep sleepに移行します。
esp_deep_sleep(time);
またこれとあわせて、プッシュボタンで異常情報をブロードキャストさせるために、外部ウェークアップを使います。このシリーズの『準備作業編』で作成した計測用デバイスの回路から分かるように、計測用デバイスのプッシュボタンを押すと、GPIO32がHighになります。これを利用してウェークアップさせることにします。
外部ウェークアップは次のようにext0とext1の2種類があり、ここではext0を使います。
・ext0
GPIOピンを使用してESP32をウェークアップします。
RTCペリフェラルはディープスリープ中もONのままになります。
次の関数を使用します。
esp_sleep_enable_etx0_wakeup(GPIO_NUM_x, level)
(level)
0: Lowでウェイクアップ。
1: Highでウェイクアップ。
・ext1
複数のRTC GPIOを使用してESP32をウェークアップします。
RTC制御コントローラによって実行されるので、RTCメモリーとRTCペリフェラルの電源を切ることが
できます。
次の関数を使用します。
esp_sleep_enable_etx1_wakeup(bitmask, mode)
(mode)
ESP_EXT1_WAKEUP_ALL_LOW: すべてのGPIOがLowになったときにウェイクアップす。
ESP_EXT1_WAKEUP_ANY_HIGH:いずれかのGPIOがHighになるとウェイクアップします。
3.コードの解説
(1)必要なライブラリー
すでに必要なライブラリーはインストールされているはずですが、まだであれば次の2つのライブラリーをインストールしてください。いずれもセンサー使用に必要なものです。
①DHTセンサーライブラリーのインストール
・ここをクリックしてDHTセンサーライブラリーをダウンロードしてください。
・zipフォルダを解凍すると、DHT-sensor-library-masterフォルダが表示されます。
・フォルダー名をDHT-sensor-libraryに変更して、Arduino IDEのインストールライブラリフォルダに移動します。
②Adafruit Unified Sensor Driverのインストール
・ここをクリックしてAdafruit Unified Sensor Driverをダウンロードしてください。
・zipフォルダを解凍すると、Adafruit_Sensor-masterフォルダが表示されます。
・フォルダー名をAdafruit_Sensorに変更して、Arduino IDEのインストールライブラリフォルダに移動します。
まず、ブロードキャスターに必要なヘッダーファイルは次の通りです。
#include <BLEDevice.h>
#include <BLEServer.h>
#include <esp_deep_sleep.h> // Deep sleep用
#include <DHT.h> // DHTセンサー用
(2)属性定義とデータ定義
22行目: この名称でブロードキャスターを作成します。オブザーバーは受信した周辺のBluetooth電波から、
この名前で対象のデバイスグループを選択します。スマートフォンの「nRF Connect for Mobile」や
「BLE Scanner」という無料アプリをインストールすると、ブロードキャスト時にこの名前を見つける
ことができます。
23行目: 同名のデバイスを複数設置する場合に、重複しないように番号を設定します。
オブザーバーはこの番号で個々のデバイスを識別します。
25行目: DHTセンサーにはDHT11, DHT21, DHT22などがあります。一致するものを指定します。
27行目: ESP32はRTCにFASTメモリと呼ばれる8KBのSRAMを持っていて、deep sleep時のデータ保存などに
利用できます。シーケンス番号がdeep sleepで消失しないよう、ここに設定します。
33行目: センサーのインスタンスを作成しています。
41行目: 計測ロジック内での異常判定に備えて、グローバル変数にしています。
その他は、GPIOピンの定義と時間設定です。測定・ブロードキャスティングの間隔は十分長い値(分とか時間単位の)になると思いますが、ここではテスト時の動作が分かりやすいように10秒を設定、またブロードキャスティング(アドバタイジング)時間は1秒に設定しています。
/* 基本属性定義 */
#define DEVICE_NAME "ESP32" // デバイス名
#define DEVICE_NUMBER 1 // デバイス識別番号(1~8)
#define SPI_SPEED 115200 // SPI通信速度
#define DHTTYPE DHT11 // DHTセンサーの型式
RTC_DATA_ATTR static uint8_t seq_number; // RTCメモリー上のシーケンス番号
const int sleeping_time = 10; // ディープスリープ時間(秒)
const int advertising_time = 1; // アドバータイジング時間(秒)
/* DHTセンサー*/
const int DHTPin = 14; // DHTセンサーの接続ピン
DHT dht(DHTPin, DHTTYPE); // DHTクラスの生成
/* LEDピン */
const int ledPin = 25; // LEDの接続ピン
/* プッシュボタン */
const int buttonPin = 32; // プッシュボタンの接続ピン
bool bAbnormal; // デバイス異常判定
(3)ロジックの全体構造
測定とブロードキャスティングを行うたびにdeep sleepに移行するので、loop関数内のコードが実行されることはありません。したがって、すべてのロジックをsetup関数で実行できるように記述する必要があります。
ブロードキャストの手順は次のフローチャートがすべてです。「所定の時間だけアドバタイズする」部分のdelay(advertising_time * 1000)に注目してください。アドバタイジングを開始させて、advertising_timeで指定した秒数だけ待つことで、その間にアドバタイジング、つまりブロードキャスティングを行わせています。この時間が短すぎると、オブザーバー側の検索でこのデバイスを見つけにくくなります。逆に長く設定すると、デバイスの消費電力が増加してしまいます。この時間をどの程度に設定するかは重要です。
さらに重要なのが、送信情報を設定するsetAdvertisementData関数の記述です。これを適確に行えば、ブロードキャスターが完成します。
setup関数と周辺のコードを以下に示します。
47行目: 別関数でGPIOの設定やセンサーの起動をしています。
50行目: プッシュボタンの状態を検知します。
63行目: 後述の関数で計測と送信情報の設定を行います。
75行目: デバイス異常になった場合は、無限ループでLEDを点滅させます。
84行目: 秒で指定されたdeep sleep時間をマイクロ秒に換算しています。
void setup() {
doInitialize(); // 初期化処理をして
BLEDevice::init(DEVICE_NAME); // BLEデバイスを初期化する
int buttonState = digitalRead(buttonPin); // プッシュボタンが押されたら異常状態にする
if (buttonState == HIGH) {
bAbnormal = true;
Serial.println("** Abnormal condition! **");
}
else {
bAbnormal = false;
}
// BLEサーバーを作成してアドバタイズオブジェクトを取得する
BLEServer *pServer = BLEDevice::createServer();
BLEAdvertising *pAdvertising = pServer->getAdvertising();
// 送信情報を設定してシーケンス番号をインクリメントする
setAdvertisementData(pAdvertising);
seq_number++;
// 所定の時間だけアドバタイズする
pAdvertising->start();
Serial.println("Advertising started!");
delay(advertising_time * 1000);
pAdvertising->stop();
if (bAbnormal) { // 異常状態なら
pinMode(ledPin, OUTPUT);
int ledState = HIGH;
for (;;) {
digitalWrite(ledPin, ledState); // LEDを点滅させて処理を停止する
ledState = !ledState;
delay(500);
}
}
// 外部ウェイクアップを設定してディープスリープに移行する
esp_sleep_enable_ext0_wakeup(GPIO_NUM_32, 1);
Serial.println("... in deep sleep!");
esp_deep_sleep(sleeping_time * 1000000LL);
}
void loop() {
}
/* 初期化処理 */
void doInitialize() {
Serial.begin(SPI_SPEED);
pinMode(buttonPin, INPUT); // GPIO設定:プッシュボタン
dht.begin(); // センサーの起動
}
計測と送信情報の設定処理は以下の通りです。
102行目: 読み取った計測値を100倍して2バイトの整数に変換しています。
108行目: stringクラスを使って、バイト単位のデータを連結して送信データを作成します。
内容と順序は前述「1.計測デバイスの基本要件」でまとめた表に従っています。
120行目: 連結されたデータ長を先頭に配置すれば送信データは完成です。
123行目: Advertising Dataを設定するためにBLEAdvertisementDataオブジェクトを作成します。
124行目: オブザーバーが検索できるように、デバイス名をBLEAdvertisementDataオブジェクトへ設定します。
125行目: 「第Ⅰ章3(3)アドバタイジング・データ」で述べたFlagsを設定しています。
0x06は、「LE General Discoverable Mode | BR_EDR_NOT_SUPPORTED」に対応。時間制限付き
で常時デバイスが発見できるモードであり、BR/EDRとは非互換であることを意味します。
FlagsはBLEAdvertisementDataオブジェクトに設定されます。
126行目: 最後に、送信データをBLEAdvertisementDataオブジェクトに設定します。
127行目: 構成されたBLEAdvertisementDataオブジェクトをアドバタイズオブジェクトに設定します。
void setAdvertisementData(BLEAdvertising* pAdvertising) {
// 温度と湿度を読み取る
float t = dht.readTemperature();
float h = dht.readHumidity();
uint16_t temp = (uint16_t)(t * 100);
uint16_t humd = (uint16_t)(h * 100);
// string領域に送信情報を連結する
std::string strData = "";
strData += (char)0xff; // Manufacturer specific data
strData += (char)0xff; // manufacturer ID low byte
strData += (char)0xff; // manufacturer ID high byte
strData += (char)DEVICE_NUMBER; // サーバー識別番号
strData += (char)bAbnormal; // 異常状態判定
strData += (char)seq_number; // シーケンス番号
strData += (char)(temp & 0xff); // 温度の下位バイト
strData += (char)((temp >> 8) & 0xff); // 温度の上位バイト
strData += (char)(humd & 0xff); // 湿度の下位バイト
strData += (char)((humd >> 8) & 0xff); // 湿度の上位バイト
strData = (char)strData.length() + strData; // 先頭にLengthを設定
// デバイス名とフラグをセットし、送信情報を組み込んでアドバタイズオブジェクトに設定する
BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
oAdvertisementData.setName(DEVICE_NAME);
oAdvertisementData.setFlags(0x06); // LE General Discoverable Mode | BR_EDR_NOT_SUPPORTED
oAdvertisementData.addData(strData);
pAdvertising->setAdvertisementData(oAdvertisementData);
}
124~6行の部分で、実際にどのように送信データが準備されているのかわかりにくいので、もう少し細かく見ておきましょう。
Arduino開発環境のBLEライブラリーにあるBLEAdvertising.cppを観察すると、具体的な仕組みが明らかになります。メソッドsetName()、setFlags()、addData()は、いずれもLengthやAD Typeなどの必要項目を整えてから、それらをstd::string m_payloadに追加結合していることがわかります(std::string m_payloadは、BLEAdvertising.hで定義されたBLEAdvertisementDataクラス内のprivate変数です)。
追加結合は、結合後のデータ長がESP_BLE_ADV_DATA_LEN_MAX(定数31が定義されています)以下の場合だけ行われます。したがって、データを構成するすべてのフィールドの合計長が31バイトに収まらなければなりません。それを超えた場合は、超えた部分だけが切り捨てられるのでなく、追加結合対象のデータブロック全体が結合されないので注意が必要です。
最終的に結合された結果を図に示します。合計は20バイトで、まだ11バイト余裕があります。このデータがAdvertising Channel PDUにセットされ、BLEパケットに構成されて送信されるわけです。
4.実行結果
起動すると直ちに計測とBloadcastingが始まり、1秒間ですぐにDeep sleppに入ります。10秒経過すると、再び計測とBloadcastingをしてDeep sleepに移行します。以後はこれを繰り返します。
これからわかるように、計測間隔はスリープ時間に起動時間が加わったものになります。ちょうど10秒間隔で計測する場合は、sleeping_timeとesp_deep_sleep()関数の引数の扱いをミリ秒に変更して、sleeping_timeの値を調整する必要があります。
デバイス異常を発生させるためにプッシュボタンを押します。押した瞬間にウェークアップして計測と1秒間のブロードキャスティングを実行してしまうので、通常の押し方では検知されません。1秒少々の長押しをすると、検知されてLEDが点滅します。
スケッチ全体は、ページ先頭の[Download]ボタンでダウンロードしてください。
次回は、今回のブロードキャスターのデータを受信して動作するオブザーバーを作成します。
お楽しみに!