nRF52からアドバタイズしてesp32でスキャンする

isp1807(nRF52)とESP32

概要

  • ISP1807(nRF52840) にはチップ温度を測るためのセンサーTEMPが載っているので、その測定結果をBLEでアドバタイズするスケッチを作成。
  • ESP32 Arduino coreに付属のいくつかのサンプルをもとに、スキャンしたデータをシリアルモニタにダラダラと表示したり、nRF52からの温度を表示したりするスケッチを作成。
  • 当初、nRF52からアドバタイズした温度データの経時変化をPCやスマホ(Android)で観測しようとしたが、適当なアプリが見当たらないので、しばらくお蔵入りしていたESP32 DevKit-C互換品を使いシリアルモニタに表示することにした。

ビルドはArduino IDE (1.8.15)で行い、nRF52用には Adafruit nRF52 BSP (1.1.0)スイッチサイエンス nRF52 BSP (0.1.9)を、ESP32用にはArduino core ESP32 (2.0.0) をおのおの導入した。

ISP1807(nRF52840)用Arduino BSPについて

BSPというのは、Arduino Board Support Package のことで、Arduino IDEの ツール/ボードマネージャ でインストールするターゲットボード用のサポートファイルである。

ISP1807用のBSPの導入方法については、こちらの投稿を参照のこと。Adafruit  nRF52 BSPは比較的頻繁に更新されていて、現時点での最新バージョンは1.1.0 になっていたので、これをインストールして利用した (スイッチサイエンスのISP1807 Microボード用BSPは0.19版)。

ESP32用BSP2.0.0を導入

ESP32に触るのは3年ぶりになる。Arduino IDEにBSPを組み込むための、環境設定/追加のボードマネージャのURL  も以下のように変わっていたので設定し直した。

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json

以前は、https://dl.espressif.com/dl/package_esp32_index.json だった。

esp32 arduino 2.0.0

最新版の2.0.0を導入して ターゲットボードを選択しようとすると、ESP32C3やS2のDev Moduleが追加されていた。うちにあるのは Not recommend for new design のクラシックモジュール なので、以前と同じくESP32 Dev Module を選択した。

esp32側スケッチの動作

ESP32をつないだシリアルモニタには、こんな感じでスキャン結果を表示し続ける(リセットから開始)。

ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:1240
load:0x40078000,len:13012
load:0x40080400,len:3648
entry 0x400805f8
ESP32_BLE_scan2.ino
connect to WiFi.....
 connecting ntp.nict.jp.
now =  2021/10/02 13:55:19
Start scanning...
13:55:20 fb:d1:6e:41:xx:xx (RSSI:-39) : ffff123440004701
13:55:20 7d:9a:89:67:xx:xx (RSSI:-90) : e0003fd4ca83ef20
13:55:22 fb:d1:6e:41:xx:xx (RSSI:-47) : ffff123441004a01
13:55:23 7d:9a:89:67:xx:xx (RSSI:-92) : e0003fd4ca83ef20
13:55:24 7d:9a:89:67:xx:xx (RSSI:-90) : e0003fd4ca83ef20
13:55:25 fb:d1:6e:41:xx:xx (RSSI:-38) : ffff123441004a01
13:55:27 7d:9a:89:67:xx:xx (RSSI:-89) : e0003fd4ca83ef20
13:55:27 fb:d1:6e:41:xx:xx (RSSI:-47) : ffff123442004701
13:55:29 7d:9a:89:67:xx:xx (RSSI:-88) : e0003fd4ca83ef20
13:55:30 fb:d1:6e:41:xx:xx (RSSI:-38) : ffff123442004701
13:55:32 7d:9a:89:67:xx:xx (RSSI:-88) : e0003fd4ca83ef20
13:55:32 fb:d1:6e:41:xx:xx (RSSI:-46) : ffff123443004701
13:55:34 7d:9a:89:67:xx:xx (RSSI:-91) : e0003fd4ca83ef20
13:55:35 fb:d1:6e:41:xx:xx (RSSI:-38) : ffff123443004701
13:55:37 fb:d1:6e:41:xx:xx (RSSI:-47) : ffff123444004701
13:55:37 7d:9a:89:67:xx:xx (RSSI:-90) : e0003fd4ca83ef20

fb:d1:6e:41:xx:xx が ISP1807からのもので、ご近所のGoogle製BLE機器からのデータも混じっている。また、以下のようにnRF52温度データ用にあわせた表示も可能(シリアルモニタに”2″を送信)。

14:51:07 29.2	(RSSI:-33)
14:51:13 29.7	(RSSI:-34)
14:51:17 30.0	(RSSI:-34)
14:51:23 30.0	(RSSI:-34)
14:51:27 30.2	(RSSI:-33)
14:51:33 30.5	(RSSI:-33)
14:51:37 30.5	(RSSI:-34)
14:51:43 30.7	(RSSI:-34)
14:51:47 31.0	(RSSI:-46)
14:51:53 31.0	(RSSI:-46)
14:51:57 31.0	(RSSI:-46)
14:52:03 31.0	(RSSI:-46)
14:52:08 31.2	(RSSI:-46)
14:52:14 31.2	(RSSI:-48)
14:52:18 31.2	(RSSI:-46)
14:52:24 31.2	(RSSI:-47)
...

nRF52のTEMPセンサーはデバイスダイの温度を測るために載っているものらしく、気温を測るための環境センサーではない。だが、だいたい室温+3~4℃で安定しているようだった。

このやり方で温度データがセントラル側にうまく伝わるのなら、ちゃんとした環境センサーをI2Cなどでいくつかぶら下げて温度湿度その他の測定に使えるだろう。

esp32側スケッチ

以下のようなスケッチを用意した。

/*
   Based on Arduino ESP32 examples below.
   "BLE\examples\BLE_scan.ino", 
   "ESP32\examples\Time\simpeTime.ino"  
*/
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

static const int MF_DATA_SIZE = 30;
typedef struct _ADV_DEVICE {
  time_t tm; 
  int rssi;
  uint8_t addr[ESP_BD_ADDR_LEN];
  uint8_t mf_length;
  uint8_t mf_data[MF_DATA_SIZE];
  _ADV_DEVICE* next;
} adv_device_data_t;

typedef struct {  // nRF52から
  uint8_t id[2];  // 0xff 0xff
  uint16_t pid; //
  uint16_t seqno;
  int16_t temp;   // temp * 100 
} adv_temp_data_t;

bool have_ntp_time = false; // ntp時刻を取得済かどうか

std::string time_short_string(time_t* p) {
  char tmp[16];
  struct tm* pt = localtime(p);
  sprintf(tmp, "%02d:%02d:%02d", pt->tm_hour, pt->tm_min, pt->tm_sec);
  return std::string((const char*)tmp);
}  

#include <WiFi.h>
static const char* ssid = "ssid";
static const char* pass = "password";
static const char* ntp_server = "ntp.nict.jp";
void get_ntp_time() {
  int count = 1;
  struct tm _tm;
  Serial.printf("connect to WiFi");
  WiFi.begin(ssid, pass);
  while (!WiFi.isConnected()) {
    delay(2000);
    Serial.print(".");
    if (count > 60) {
      WiFi.disconnect(true);
      WiFi.mode(WIFI_OFF);
      Serial.println("Connect timeout.");
      return;
    } else if (count % 20 == 0)
      Serial.println();
    count++;
  }
  Serial.printf("\n connecting %s", ntp_server);
  for(int i = 0; i < 10; i++) {
    Serial.print(".");
    configTime(9 * 3600, 0, ntp_server);
    if (getLocalTime(&_tm)) {
      have_ntp_time = true;
      break;
    }
  }
  Serial.println();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  if (have_ntp_time)
    Serial.printf("now =  %04d/%02d/%02d %02d:%02d:%02d\n",
      _tm.tm_year + 1900, _tm.tm_mon + 1, _tm.tm_mday,
      _tm.tm_hour, _tm.tm_min, _tm.tm_sec);
  else
    Serial.println("get_ntp_time() failed."); 
}

std::string bin_hex(uint8_t* bin, int sz) {
  char tmp[101];
  memset(tmp, sizeof(tmp), 0);
  BLEUtils::buildHexData((uint8_t*)tmp, bin, sz);
  return std::string((const char*)tmp);
}

bool disp_result = true;  // 受信都度ごとに表示
bool disp_temp = false; // trueのときは生データ,falseでTEMP

adv_device_data_t* first = 0;
adv_device_data_t* current = 0;
int items = 0;
static const int max_items = 500;

void list_clear() {
  adv_device_data_t* p = first;
  while (p) {
    adv_device_data_t* next = p->next;
    free(p);
    p = next;
  }        
  first = current = 0;
  items = 0;   
}

void show_item(adv_device_data_t* p) {
  if (!p)
    return;
  BLEAddress badr(p->addr);
  if (!disp_temp) {  
    Serial.printf("%s", time_short_string(&p->tm).c_str());
    Serial.printf(" %s (RSSI:%d)", badr.toString().c_str(), p->rssi);
    Serial.printf(" : %s",  bin_hex(p->mf_data, p->mf_length).c_str());
    Serial.println();
  } else {
    if (p->mf_length > 5) {
      adv_temp_data_t* pTemp = (adv_temp_data_t*)p->mf_data;
      if (pTemp->id[0] == 0xff && pTemp->id[1] == 0xff && pTemp->pid == 0x3412) {
        static int last_seqno = 0;
        if (last_seqno != pTemp->seqno) {
          last_seqno = pTemp->seqno;
          float temp = (float)(pTemp->temp / 100.0);
          Serial.printf("%s", time_short_string(&p->tm).c_str());
          Serial.printf(" %2.2f", temp);
          Serial.printf("\t(RSSI:%d)", p->rssi);
          Serial.println();
        }
      }
    }
  }
}

void list_add(BLEAdvertisedDevice* padvdev) {
  adv_device_data_t* padv = (adv_device_data_t*)malloc(sizeof(adv_device_data_t));
  if (!padv)
    return;
  memset(padv, 0, sizeof(adv_device_data_t));
  padv->tm = time(NULL);
  memcpy(padv->addr, padvdev->getAddress().getNative(), ESP_BD_ADDR_LEN);
  padv->rssi =  padvdev->getRSSI();
  if (padvdev->haveManufacturerData()) {
    padv->mf_length = min((int)padvdev->getManufacturerData().length(), MF_DATA_SIZE);
    uint8_t* p = (uint8_t*) padvdev->getManufacturerData().data();
    memcpy(padv->mf_data, p, padv->mf_length);
  }
  if (disp_result)
    show_item(padv);

  if (items > max_items) {
    if (first) {
      if (current == first)
        current = first->next;
      adv_device_data_t* p = first;        
      first = first->next;
      free(p);
      items--;
    }
  }
  if (!first)
    first = current = padv;
  else {
    current->next = padv;
    current = padv;    
  }
  items++;
}

void show_log() {
  adv_device_data_t* p = first;
  while (p) {
    show_item(p);
    p = p->next;
  }        
}

class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advdev) {
      list_add(&advdev);  
    }
};

BLEScan* pBLEScan;
void setup() {
  Serial.begin(115200);
  Serial.println("ESP32_BLE_scan2.ino");
  get_ntp_time();
  Serial.println("Start scanning...");
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(500);
  pBLEScan->setWindow(500);  // less or equal setInterval value
}

void loop() {
  BLEScanResults foundDevices = pBLEScan->start(2, false);  // 2秒
  pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory
    
  delay(1000);
  char cmd = 0;
  while (Serial.available()) {
    char c = Serial.read();
    switch(c) {
      bool save_flag;
    case '1':
      disp_result = true;
      disp_temp = false;
      Serial.println("\n\n1. Show advertised data.");
      break;
    case '2':          
      disp_result = true;
      disp_temp = true;
      Serial.println("\n\n2. Show nrf52 TEMP value.");
      break;    
    case '3':
      Serial.println("\n\n3. Show data log.");
      disp_result = false;
      show_log();
      break;
    case '9':
      save_flag = disp_result;
      disp_result = false;
      Serial.println("\n\n9. Get ntp time.");      
      get_ntp_time();
      disp_result = save_flag;
      break;
    default:
      break;      
    }
  }  
}

ESP32用スケッチの概略

BLEScan() によってアドバタイジングデータのスキャンを開始する。見つかると BLEAdvertisedDevice クラスのインスタンスに内容が格納されてコールバックされる。それを構造体 (adv_device_data_t) に格納してリンクリストに保存すると共にシリアルモニタに表示する。

リンクリストには最大500件まで格納するようにし、シリアルモニタへの操作(文字の送信)で、リスト内の全データを再表示できるようにした。

BLEAdvertisedDeviceには、なぜか getADType() というメンバがないのでアドバタイジングデータのタイプは格納していないが、BLEビーコン相当(アドレス、メーカーID 、マニュファクチャラーデータおよびrssi)のデータは記録/表示するようにした。 複数ブロックのデータには対応していない。

過去データを保存するにあたり、WiFiを使ってntpサーバーに接続して現在時刻を取得するようにした。Arduino IDEのシリアルモニタには、タイムスタンプを表示 というオプションがあるので表示するだけなら不要なのだが、受信ログをフラッシュに保存しておく場合を考えて時刻も記録するようにした。

WiFiはすぐ切るようにしているので、長期間使っていると誤差がでてくる。そのため、シリアルモニタへの操作(“9″の送信)で時刻の再取得を行うようにした。

ビルド結果およびパーティションスキーマ

ビルドすると以下のようになった。

最大2097152バイトのフラッシュメモリのうち、スケッチが1375449バイト(65%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が38996バイト(11%)を使っていて、ローカル変数で288684バイト使うことができます。

スケッチが1,375,449バイト使っている。なんでこんなに大きいんだろう。OTA可能なパーティションスキーマではダメで、2MB APP/2MB SPIFFS という構成でビルドする必要があった。

ISP1807 (nRF52)側スケッチ

定期的にアドバタイズを行うスケッチとして、AdafruitのnRF52ライブラリに含まれている、beacon.inoやadv_advanced.ino といったサンプルスケッチを参考に以下を用意した。

// temp_adv2.ino
#include "bluefruit.h"

typedef struct {
  uint8_t id[2];  // maker_id は 0xffff に固定
  uint16_t pid;   // 送信側の識別用
  uint16_t seqno; // 同じ測定機会かどうかの判別用
  int16_t temp;   // 温度データ
} adv_data_t;

adv_data_t adv_data = {
  .id = {0xff, 0xff},
  .pid = 0x3412,
  .seqno = 0,
 };

void adv_temp() {
  adv_data.temp = (int16_t)(readCPUTemperature() * 100.0);
  adv_data.seqno++;
  if (adv_data.seqno > 9999)
    adv_data.seqno = 0;
  Bluefruit.Advertising.clearData();
  Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &adv_data, sizeof(adv_data));
  Bluefruit.Advertising.start(5); // 
}

void setup() {
  Bluefruit.begin();
  Bluefruit.autoConnLed(false);
  Bluefruit.setTxPower(-40);    // Check bluefruit.h for supported values
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.setType(BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED);
  Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &adv_data, sizeof(adv_data));
  Bluefruit.Advertising.setIntervalMS(400, 2500); // 
  Bluefruit.Advertising.setFastTimeout(1);  
}

void loop() { 
  adv_temp();
  delay(10000);
}

nRF52用スケッチの概略

setup() でアドバタイジングのためのいろいろなフラグや種別の設定を行い、loop() 内で10秒ごとに adv_temp() を呼ぶことで温度データの送信を行っている。

セントラル側(esp32)がスキャンしている期間に送信しないとデータが失われてしまうので、Bluefruit.Advertising.start()  に指定するタイムアウト時間(秒)と、Bluefruit.Advertising.setIntervalMS()  や Bluefruit.Advertising.setFastTimeout() で指定する送信間隔などは実際のスキャン結果を見ながら、頻繁になりすぎず欠報も増えすぎずといった値に設定した。

このスケッチを動かしているときの消費電流はテスター読みで約4.6mA (PCに接続したUSBケーブルのVBUSの電流) だった。もうちょっと少なくてもいいんじゃないの、と思って以下のようなスケッチも用意してみた。

suspendLoop を使ったスケッチ

// temp_adv3.ino
#include "bluefruit.h"

typedef struct {
  uint8_t id[2];  // maker_id は 0xffff に固定
  uint16_t pid;   // 送信側の識別用
  uint16_t seqno; // 同じ測定機会かどうかの判別用
  int16_t temp;   // 温度データ
} adv_data_t;

adv_data_t adv_data = {
  .id = {0xff, 0xff},
  .pid = 0x3412,
  .seqno = 0,
 };

void adv_temp() {
  adv_data.temp = (int16_t)(readCPUTemperature() * 100.0);
  adv_data.seqno++;
  if (adv_data.seqno > 9999)
    adv_data.seqno = 0;
  Bluefruit.Advertising.clearData();
  Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &adv_data, sizeof(adv_data));
  Bluefruit.Advertising.start(5); // 
}

void adv_stop_cb() {
  Bluefruit.Advertising.stop();
  delay(10000);
  adv_temp();
}

void setup() {
  Bluefruit.configPrphBandwidth(BANDWIDTH_LOW);
  Bluefruit.begin();
  Bluefruit.autoConnLed(false);
  Bluefruit.setTxPower(-40);    // Check bluefruit.h for supported values
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.setType(BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED);
  Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA, &adv_data, sizeof(adv_data));
  Bluefruit.Advertising.setIntervalMS(400, 2500); // 
  Bluefruit.Advertising.setFastTimeout(1);  
  Bluefruit.Advertising.setStopCallback(adv_stop_cb);
  adv_temp();
  suspendLoop();// loop タスクをサスペンドする。
}
void loop() { }

こちらも約10秒間隔で温度測定とアドバタイジングを行うが、suspendLoop() によってloopタスクをサスペンド状態にしている。その代わり、アドバタイジング期間が終了するごとに  adafruit_ble_taskadv_stop_cb() をコールバックしてくれるので、連続的なアドバタイジングが持続する。ただ、タスクのコールバック内で10秒間もdelay() してしまうのは、好ましくはないだろう。

こちらのスケッチの方がFreeRTOSのタスクが1つ止まる分だけ有効かと思ったが、テスター読みの消費電流はやはり4.6mA程度だった。0.1mA以上の効果があるわけはないか。

アドバタイジングデータの中身

このスケッチは、以下のようなデータを含むペイロードを飛ばしている。

ペイロード

このペイロードの内容は構造体 adv_data_t として定義してあって、測定機会ごとにSeqnoとtempを更新する。

typedef struct {
  uint8_t id[2]; // maker_id は 0xffff に固定
  uint16_t pid;  // 送信側の識別用
  int16_t seqno; // 同じ測定機会かどうかの判別用
  int16_t temp;  // 温度データ
} adv_data_t;

ペイロードは最大31バイトで、アドバタイジングデータの長さ、種別、メーカーIDを除くと27バイトまで任意のデータを格納できるので、この構造体をさらに拡張して複数のセンサーから得た値を飛ばすこともできるだろう。

pid は、複数のnRF52からアドバタイジングするようなとき、測定点の識別用に定義した。

seqno は、測定するごとにインクリメントしている。受信側は、同じタイミングで測定したデータを何度も受け取る可能性があるので、前回のseqnoと比較して一致している場合は重複とみなせるようにした。esp32側スケッチの show_item() にそのための処理を入れた。

ビルド結果

最大815104バイトのフラッシュメモリのうち、スケッチが111212バイト(13%)を使っています。
最大237568バイトのRAMのうち、グローバル変数が13724バイト(5%)を使っていて、ローカル変数で223844バイト使うことができます。

それなりに大きくはなったが、esp32と比べるととてもコンパクトだろう。

きょうのまとめ

当初の目的は、アドバタイジング時の消費電流を測ることだった。そのためには、スケッチが安定してアドバタイジングデータを飛ばしていることを確認する必要があって、esp32側のスケッチも用意することになった。

今回のスケッチ動作中のUSB経由での消費電流はテスター読みで約4.6mA だった。テスター読みなので瞬間的にはもっと流れているのかもしれない。それにしても、ちょっと電流が大きすぎる印象なので、もうちょっと調べてみることにした。

もっとも、PC(ハブ)とUSBケーブルで接続しシリアル通信もできる状態のままで電流の多い少ないを考えてもあまり意味がないのかもしれないが。次回は、ISP1807のいろいろとポイントで消費電流を測定してみた以下の話になります。

ISP1807 Microボードの消費電流を測ってみた