マスター(サーバー)側の全体動作は、以下の手順となる。
- 一定時間(現状1秒)のスキャンにより周辺Bluetoothデバイスの情報を取得
- 該当するサービス情報を発見(複数可)すれば、そのデバイスに接続
- 接続デバイスからデバイス番号とセンサ情報を受信
- 接続デバイスへスリープ時間(実験では30秒)を送信
- 接続デバイスを切断
- 複数のデバイスを発見した場合は、2から5を繰り返す
- 1へ戻る
しかしながら、デバイスへの接続・切断を繰り返すと、次のメッセージでリブートする問題が発生した。目安としては、100回の接続で1回程度の割合である。
Guru Meditation Error: Core 0 panic’ed (Unhandled debug exception).
Debug exception reason: Stack canary watchpoint triggered (BTU_TASK)
この問題はネット上でも報告されているが、現状では根本的な解決策は見当たらない。本稿はセンサデバイス側の電力消費に焦点を当てているため、この問題に深入りすることは避けた。ただし、マスターは接続デバイスの状況を保持する必要があるため、接続・切断のプロセス発生ごとにEEPROMへ状況を書き込むことで、問題回避した。
また、次のような2種類の警告メッセージも発生する。これらもネット上で報告されている。前者は、10回の接続で1回程度の割合。後者は、初回のサービス取得時である。
- lld_pdu_get_tx_flush_nb HCI packet count mismatch (0, 1)
- BLERemoteCharacteristic.cpp:287
retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
なお、最大接続デバイス数を4としているが、この数値に根拠はない。実験に使えるデバイスが手元に3から4台あり、仮設定した。
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <EEPROM.h>
// サービスUUID
BLEUUID serviceUUID("19ce426e-6c37-4cd2-b8b4-b7f51b065a3c");
// 送信用UUID
BLEUUID txUUID("4fe2d1f9-c424-4124-8dc5-6f06b73ad9c5");
// 受信用UUID
BLEUUID rxUUID("76ac6030-b243-439f-b96e-b964c45e4180");
#define MAX_DEVICES 4 // 最大接続デバイス数
#define EEPROMPin 19 // EEPROM領域初期化ピン
BLEScan* pBLEScan;
BLEClient* pClient;
BLEAdvertisedDevice* targetDevice;
BLERemoteService* pRemoteService;
BLEAdvertisedDevice advertiseDevice;
BLERemoteCharacteristic* pRemoteCharacteristicRX; // 受信用キャラクタリスティック
BLERemoteCharacteristic* pRemoteCharacteristicTX; // 送信用キャラクタリスティック
// マスターデバイス状態(EEPROM保管)
uint32_t setupCount = 0; // 起動カウンター(毎秒更新, 最大4294967295秒 = 1193時間)
uint32_t rebootCount = 0; // リブート回数
uint32_t timeoutCount = 0; // 接続タイムアウト回数
uint8_t sleepTime = 30; // スリープ時間(接続デバイスへ提示)
// センサデバイス状態(EEPROM保管)
uint32_t deviceSetup[MAX_DEVICES]; // 初回ブート時のマスター起動カウンター値
uint32_t deviceCounter[MAX_DEVICES]; // 初回ブート時からの経過時間(秒単位)
uint32_t deviceBoot[MAX_DEVICES]; // 取得ブート回数
#define EEPROM_SIZE 16 + 12 * MAX_DEVICES // EEPROM使用サイズ64byte
int scanTime = 1; // スキャン時間(秒単位)
int unitTime = 100; // 100msec基本割込み
uint8_t notifyTimeOut = 15; // Notify受信タイムアウト(10msec単位)
uint8_t connectTimeOut = 75; // Notify受信を含む接続切断タイムアウト(100msec単位)
bool notifyFlag = false; // Notify受信フラグ
uint8_t notifyNum = 0; // Notify受信番号
bool connectTryFlag = false; // デバイス接続開始フラグ
void eepromRead();
void eepromWrite();
// タイマー管理用の構造体ポインター
hw_timer_t *timer = NULL;
// 基本割込みサービスルーチン(100msec間隔)
void IRAM_ATTR baseInterrupt() {
static int sec = 0, connectWait = 0;
// デバイス接続状態確認
if (connectTryFlag) {
connectWait++;
if (connectWait == connectTimeOut) {
Serial.print("Timeout Restart\n");
timeoutCount++;
eepromWrite();
ESP.restart();
}
}
else connectWait = 0;
// 1秒経過確認
sec++;
if (sec == 10) {
setupCount++; // マスター起動カウンター更新
sec = 0;
}
}
// 遅延処理(単位:ミリ秒)
void delayMSec(int mSecond) {
vTaskDelay(mSecond / portTICK_RATE_MS);
}
// 経過時間の文字列変換(最大表示 999時間59分59秒 = 3,599,999秒)
void timeConvert(uint32_t counter, char time[10]) {
uint32_t limit = 3600000, n;
uint16_t h;
uint8_t m, s;
if (counter >= limit) n = limit - 1;
else n = counter;
s = n % 60;
m = ((n - s) / 60) % 60;
h = (n - s - m * 60) / 3600;
snprintf_P(&time[0], 10, "%03d:%02d:%02d", h, m, s);
}
// Notify受信イベント
static void notifyEvent(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
uint8_t num = *pData;
uint32_t count;
notifyNum = 0;
if (num >= 1 && num <= MAX_DEVICES) {
count = *(pData + 1) << 24 | *(pData + 2) << 16 | *(pData + 3) << 8 | *(pData + 4);
deviceBoot[num - 1] = count;
// デバイス状態更新
if (count == 0) { // デバイス起動時(ブート回数ゼロ)
deviceCounter[num - 1] = 0;
deviceSetup[num - 1] = setupCount;
}
else deviceCounter[num - 1] = setupCount - deviceSetup[num - 1];
notifyNum = num;
}
notifyFlag = true;
}
// デバイスとの双方向通信
void handshakeDevice(BLEAdvertisedDevice device) {
targetDevice = new BLEAdvertisedDevice(device); // 接続デバイスクラス取得
connectTryFlag = true; // 接続開始(タイムアウト検証開始)
pClient->connect(targetDevice); // デバイス接続
// 受信サービス参照取得
pRemoteService = pClient->getService(serviceUUID);
// 受信キャラクタリスティック参照取得
pRemoteCharacteristicRX = pRemoteService->getCharacteristic(rxUUID);
// Notify受信イベント割り当て
pRemoteCharacteristicRX->registerForNotify(notifyEvent);
// Notifyタイムアウト発生確認
notifyFlag = false;
uint8_t notifyWait = 0;
while (!notifyFlag) {
delayMSec(10);
notifyWait++;
if (notifyWait == notifyTimeOut) {
Serial.print("Notify Timeout\n");
break;
}
}
// Notify受信後スリープ時間送信
if (notifyFlag) {
pRemoteCharacteristicTX = pRemoteService->getCharacteristic(txUUID);
pRemoteCharacteristicTX->writeValue(sleepTime);
}
pClient->disconnect(); // 切断処理
connectTryFlag = false; // タイムアウト検証終了
delete(targetDevice); // 接続デバイスクラス解放
}
// EEPROM領域初期化
void eepromInit() {
for (uint16_t n = 0; n < EEPROM_SIZE; n += 4) EEPROM.put(n, 0);
EEPROM.commit();
}
// EEPROM読み込み
void eepromRead() {
uint32_t data;
for (uint16_t n = 0; n < EEPROM_SIZE; n += 4) {
EEPROM.get(n, data);
if (n == 0) setupCount = data; // 起動カウンター
else if (n == 4) rebootCount = data; // リブート回数
else if (n == 8) timeoutCount = data; // 接続タイムアウト回数
else if (n == 12) sleepTime = (uint8_t)data; // スリープ時間
else { // [16, 20, 24], [28, 32, 36], [40, 44, 48], [52, 56, 60] デバイス情報
uint16_t np = ((n - 16) / 4) % 3; // 0, 1, 2, 0, 1, ...
uint16_t nd = (n - 16) / 12; // 0, 0, 0, 1, 1, 1, 2, ...
if (np == 0) deviceSetup[nd] = data;
else if (np == 1) deviceCounter[nd] = data;
else deviceBoot[nd] = data;
}
}
}
// EEPROM書き込み
void eepromWrite() {
uint16_t n = 0;
EEPROM.put(n, setupCount); // 起動カウンター
n += 4; EEPROM.put(n, rebootCount); // リブート回数
n += 4; EEPROM.put(n, timeoutCount); // 接続タイムアウト回数
n += 4; EEPROM.put(n, (uint32_t)sleepTime); // スリープ時間
for (int d = 0; d < MAX_DEVICES; d++) { // センサデバイス情報
n += 4; EEPROM.put(n, deviceSetup[d]);
n += 4; EEPROM.put(n, deviceCounter[d]);
n += 4; EEPROM.put(n, deviceBoot[d]);
}
EEPROM.commit();
}
// ネットワーク状況表示
void printNetWork() {
char time[10];
timeConvert(setupCount, time);
Serial.printf("\nTime %s, Reboot %04d, Timeout %04d, Sleep %dsec\n",
time, rebootCount, timeoutCount, sleepTime);
Serial.printf("No. Boots ElapsTime\n");
for (uint8_t n = 0; n < MAX_DEVICES; n++) {
timeConvert(deviceCounter[n], time);
Serial.printf(" %1d %05d %s\n", n + 1, deviceBoot[n], time);
}
Serial.printf("\n");
}
// セットアップ
void setup() {
Serial.begin(115200);
while(!Serial) {} // シリアルポート準備待ち
Serial.println("");
pinMode(EEPROMPin, INPUT_PULLUP); // EEPROM領域初期化ピン設定
EEPROM.begin(EEPROM_SIZE); // EEPROM使用サイズ設定
if (digitalRead(EEPROMPin) == 0) {
eepromInit(); // EEPROM領域初期化
eepromWrite(); // スリープ時間書き込み
Serial.printf("Init EEPROM\n");
}
else {
eepromRead(); // EEPROM領域読み込み
rebootCount++; // リブート回数更新
// デバイス情報更新
for (uint8_t n = 0; n < MAX_DEVICES; n++) {
if (deviceBoot[n] == 0) deviceCounter[n] = 0;
}
printNetWork();
}
// BLE初期化
BLEDevice::init("");
// スキャンオブジェクト生成
pBLEScan = BLEDevice::getScan();
// パッシブスキャン設定
pBLEScan->setActiveScan(false);
// クライアント(デバイス)オブジェクト生成
pClient = BLEDevice::createClient();
// タイマー割込み設定
timer = timerBegin(0, 80, true); // timer = 1usec
timerAttachInterrupt(timer, &baseInterrupt, true);
timerAlarmWrite(timer, unitTime * 1000, true);
timerAlarmEnable(timer);
}
// メインループ
void loop() {
static bool scanPrintFlag = true;
bool findBVNET = false;
if (scanPrintFlag) {
Serial.printf("Scan");
scanPrintFlag = false;
}
else Serial.printf(".");
// 一定時間スキャン
BLEScanResults foundDevices = pBLEScan->start(scanTime);
// 周辺デバイス動作数取得
int devices = foundDevices.getCount();
findBVNET = false;
for (int n = 0; n < devices; n++) {
// 各デバイスの情報抽出
advertiseDevice = foundDevices.getDevice(n);
// サービスの提供確認
if (advertiseDevice.haveServiceUUID()) {
BLEUUID service = advertiseDevice.getServiceUUID();
// BVNETサービス所有の確認
if (service.equals(serviceUUID)) {
findBVNET = true;
if (!scanPrintFlag) Serial.printf("\n");
scanPrintFlag = true;
// 双方向通信開始
handshakeDevice(advertiseDevice);
}
}
}
// BVNET発見 ⇒ 表示
if (findBVNET) {
printNetWork();
eepromWrite();
}
}
// End of File