マスター(サーバー)側の全体動作は、以下の手順となる。

  1. 一定時間(現状1秒)のスキャンにより周辺Bluetoothデバイスの情報を取得
  2. 該当するサービス情報を発見(複数可)すれば、そのデバイスに接続
  3. 接続デバイスからデバイス番号とセンサ情報を受信
  4. 接続デバイスへスリープ時間(実験では30秒)を送信
  5. 接続デバイスを切断
  6. 複数のデバイスを発見した場合は、2から5を繰り返す
  7. 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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です