センサーのデータをBLEで送信する際は、電波の届く範囲に受信機を置く必要があります。しかし屋外に置いた複数のセンサーを屋内で受信するようなケースでは、センサーからの電波が届かない事もあります。そこでアドバタイズのデータを中継できないか実験をしてみました。
先に断ってしまいますが、結論としては、あまり実用的ではなかったです。
構成
今回の実験は4つのシステムで構成されています。
(1) センサーのデータをBLEのアドバタイズで送信
(2) 受信したアドバタイズを再送信(中継)
(3) 受信したアドバタイズをWiFiでサーバーに送信
(4) 受信したデータをファイルに保存
アドバタイズの中身
送信するアドバタイズは以下のような14バイトのフォーマットになっています。
// 送信するデータの構造体
typedef struct {
uint8_t maker[4]; // maker_id 子機(nRF52840)の識別用
uint8_t type; // 子機種別
uint8_t id; // 子機ID
uint8_t ttl; // TTL (time to live)
uint8_t repeater; // リピーターID
uint16_t seq; // シーケンス番号 -- ここまで共通フォーマット
int16_t volt; // 電圧データ
int16_t temp; // 温度データ
} AdvData;
0~3バイト目 端末を区別するための情報
4バイト目 子機種別(センサーの種類別に分けるなどの用途)
5バイト目 子機ID(同じセンサーを複数設置する場合に区別する用途)
6バイト目 TTL(リピーターを経由した回数)
7バイト目 直近の経由したリピーターのID
8~9バイト目 シーケンス番号(測定値を更新したらカウントアップ)
ここまでが共通のフォーマットで、10バイト目以降はセンサーによって任意の値が入ります。今回は実験なので、センサーのデータは内部のバッテリー電圧とCPU温度を測定しています。
サンプルプログラム
サンプルプログラムはGitHubにアップしました。
https://github.com/kaz-mac/ble2wifi_demo
中継のしくみ
おおまかな流れ
中継器はスキャンを行い、全てのアドバタイズを受信します。受信したデータの中から中継するデータを抽出し、一旦キューに保存します。キューに保存されたデータは定期的にチェックされ、一定期間アドバタイズとして送信されます。
スキャン
// BLEスキャンの設定(受信)
Bluefruit.Scanner.setRxCallback(advScanCallback);
Bluefruit.Scanner.restartOnDisconnect(false);
Bluefruit.Scanner.setInterval(160, 160); // n x 0.625ms: 100ms間隔で100ms間受信する
Bluefruit.Scanner.useActiveScan(false); // false=パッシブスキャン
Bluefruit.Scanner.start(0); // 0=永続する
setInterval()で受信する時間の割合を指定できるのですが、こちらは両方同じになっているので、常時受信し続けます。BLEのスキャンは意外と電力を食うようで、測定したら11mA近くありました。電池駆動は現実的でないので、USBから給電した方がよさそうです。
setRxCallback()はアドバタイズを受信するたびに呼ばれるコールバック関数です。最初1件だけ受信して止まる謎現象があり調べていたら、resume()をしないと再開しないようでした。
// アドバタイズ受信時のコールバック
void advScanCallback(ble_gap_evt_adv_report_t* report) {
filterAdvData(report);
Bluefruit.Scanner.resume(); // スキャンを再開
}
// アドバタイズから目的のデータのみ抽出する
void filterAdvData(ble_gap_evt_adv_report_t* report) {
}
一般的なご家庭なら様々なBLEのビーコンが飛び交っていることでしょう。この中から中継すべきデータをフィルタリングしていきます。setRxCallback()のコールバックはデータを受信する度に呼ばれますが、アドバタイズの信号は何度も繰り返し送信されています。そうすると同じデータなのに何度もコールバックされる現象が起こります。これを避けるために、一度受信したデータは無視するようにしています。
// 過去に受信したデータの記憶 (Type+ID+Seqで区別)
const size_t HIST_MAX = 20; // 保存する最大件数
uint32_t tisqHist[HIST_MAX];
uint16_t tisqHistIdx = 0;
// 過去に受信した同じデータは無視する
if (inHistory(nowTisq)) { // 存在した場合
return;
} else { // 存在しなかった場合
addHistory(nowTisq);
}
子機種別(type)、子機ID(id)、シーケンス番号(seq)の組み合わせで1つのデータを区別できるので、この4バイトの値を配列に入れておいて、存在したらスキップするようにします。
一旦キューに入れる理由
// 中継するキューの情報
struct QueueData {
uint8_t stat; // 0=無効 1=実行中
uint8_t data[26];
uint16_t len;
uint32_t expire;
};
std::map<uint16_t, QueueData> queueData;
// 転送するデータをキューに格納する
QueueData item = {
.stat = 1,
.len = len,
.expire = millis() + RELAY_EXPIRE,
};
memcpy(item.data, buff, len);
addQueue(nowTi, item); // キューに入れる
受信したアドバタイズ即座に再送信しないのには理由があります。複数のセンサーから受信したデータを中継器でまとめて送信する必要があるため、その都度送信していると送信が完了する前に新しいデータが来てしまう可能性があります。.expireでいつまでそのデータを送信し続けるかという情報を付加しているので、受信したデータが欠落しにくいようにしています。
時分割で送信する
キューにデータが複数存在する場合は、タイミングを分割して交互に送信していきます。本来アドバタイズというのは同じデータを定間隔で繰り返し送信するものですが、異なるデータを送信することができないので、1回送信して停止、データを変えてまた送信して停止、を繰り返しています。
// キューにあるデータを順番に1回ずつアドバタイズする
for (auto& [ti, item] : queueData) {
uint8_t buff[26] = {0};
if (item.stat == 1) {
memcpy(buff, item.data, item.len);
sendAdv(buff, item.len, interval+100); // アドバタイズする
delay(intervalMs);
Bluefruit.Advertising.stop(); // アドバタイズ中断
delay(40);
}
}
もう完全に本来のアドバタイズの使い方から逸してしまっていますが、こんな風に無理やり異なるデータを送信することができました。
話はそれますが、map型のデータを
for (auto& [ti, item] : queueData) {}
のような記述ができるの初めて知りました。連想配列(辞書型)の変数をforeachで回すような記述がC++でもできるなんて便利ですね。コンパイル時にワーニングがでますが、Arduino IDE 2.3では正常に動作しました。
中継器が複数あるときの不都合
中継器が再送信したアドバタイズを、別の中継器が再送信することは想定した動作です。そのためにttl (time to live)というパラメーターを付けて、一定回数まで中継できるようにしました。これで中継器が何段あってもいけると思ってました。しかし実際にやってみると、近くにある中継器から遠くの中継器に逆方向に戻っていってしまいました。まぁ、当たり前ですよね。これを避けるには、許可する中継器からのデータなのかをフィルタリングしなければなりません。そうすると柔軟に設置場所を変えたりできなくなり、何か変えるたびにプログラムを修正する必要がでてきてしまいそうです。これではシンプルなアドバタイズの仕組みが複雑になってしまう…。
気づいてしまった
ここまで進めてきて、これ、あまり良いやり方じゃないなって気づいてしまいました。アドバタイズのパケットを中継するのは良いアイディアだと思ったのですが、末端のセンサーが複数あると大量のアドバタイズパケットで埋め尽くされてしまいます。本来転送すべきデータが埋もれてしまい、届かなくなる恐れもあります。
それならばいっそアドバタイズを中継するより、受信機(図で言うとM5Stackの部分)を複数設置して、あとはWiFiでサーバーに送信した方がシンプルでよさそうです。もしアドバタイズの中継器を設けるなら、センサーと1対1で対応させるようにした方がいいですね。BLEでペアリングしてデータの送受信をすれば確実なデータの送受信ができますが、消費電力が大きいので、ボタン電池で長期間運用するとなると、やはりどうしてもアドバタイズの仕組みを利用したいところです。
ということで今回の実験結果は今後にあまり活用できそうにはありませんでしたが、せっかくここまで調べて実験までやったので、記念に記録として残しておきたいと思います。