Edited at

回路設計なしで省電力なBluetooth LE Beaconを開発する


Beaconとは

 Wikipediaによると、Beaconとは


ビーコンは、地上にある無線局などから発射される電波(あるいはIR(赤外線)のような高周波の電磁波)を航空機・船舶・自動車などの移動体に搭載された機器で受信することにより、位置をはじめとした各種情報を取得するための設備である。


とあります。

 BluetoothのBeacon機器の場合、温度や気圧などのセンサを搭載したモジュールを任意の場所に設置して、その場所の環境を観測するために使います。

また、Bluetoothの電波は数十メートルほどしか届かないため、単純にその電波を受信したらBluetooth機器のIDから自分の位置を把握したり、電波強度から位置を調べることもできます。

BluetoothのBeacon機能は非常に省電力で、ボダン電池でも1年以上動作するものも存在します。


概要

 今回はLeafonyというArduino互換の小型な開発基板を使って、定期的に温度を測定する非常に省電力なBluetooth LE Beacon (以下 BLE Beacon)を開発してみました。ボタン電池駆動で半年程度持つことを目標とします。

Leafonyにはセンサ基板やBluetooth基板などを接続するだけで簡単にBluetoothセンサ機器が実現できるため、ハードウェアの設計の設計をせず非常に簡単にオリジナルのBeacon機器を作ることができます。

こんな感じで家の中に置いて部屋の温度を測定するイメージです。

Leafonyであれば、縦横高さ2cmなので非常に小さく、どこに設置しても余り邪魔にはならなさそうです。

ファイル名


開発環境


ハードウェアの準備

Bluetooth機器として使用するハードウェアの開発はLeafonyで行います。

使用する基板はBasic Kitに含まれる下記の5枚です。


  • 4-Sensorsリーフ (温度・湿度・照度・加速度)

  • AVR MCUリーフ (ATmega328P)

  • USBリーフ

  • BLE Sugarリーフ

  • CR2032コイン電池リーフ

ファイル名


Beaconの実装


データ送信の仕組み

Beaconでのデータのやり取りはAdvertising dataというパケットを使って行われます。

Advertising dataはブロードキャストされるので、周辺のBluetooth機器がすべてこのデータをスキャンすることが可能です。

受信する側は常にスキャンを行っていればBeacon機器側は好きなタイミングでデータを送信して、送信していないときはスリープを行うことができるため、電力を抑えられます。

このAdvertising dataの中にデバイス名や送信時の電力、温度センサのデータなどを入れて送ることでBeaconとして動作させることができます。

Bluetooth 4.0 の仕様書を読むと、Advertising dataのパケットについて下図のように書かれています。

image.png

(引用: BLUETOOTH SPECIFICATION Version 4.0 [Vol. 3] page 375 Part C Generic Access Profile 11 Advertising and Scan Response Data Format)

Advertising dataは、いくつかのAD Structureというデータに分けられます。

このAD StructureにはLengthAD TypeAD Dataというデータで構成されています。

AD TypeによってこのAD Structureは何のデータを送るためのものかが決められていて、

例えば

AD Type = 0x09なら、AD Dataはデバイス名 (Complete Local Name) であるとか、

AD Type = 0x0Aなら、AD Dataは送信電力 (TX Power Level) のように、

様々なデータをのせることができます。

どのようなデータがのせられるのかは、下記のリンクが参考になります。

今回は温度センサとコイン電池の電圧をManufacturer Specific Data (AD Type = 0xFF) に入れてデータを送信します。


ソースコード

実際にLeafony側に書き込むプログラムを見ていきましょう。

adv_data[]が送信するAdvertising dataです。

ここではAD Structure 1FlagAD Structure 2Complete Local NameAD Structure 3Manufacturer Specific Dataで、合計25 octetsのデータを作っています。

AD Structure 1Flag = 0x06なのは下記のリンクが参考になります。

Complete Local NameManufacturer Specific Dataには、

ASCIIコードの文字を入れていきます。

最後に、ble112.ble_cmd_le_gap_set_adv_dataでAdvertising dataをBLEモジュールに書き込んで、

ble112.ble_cmd_le_gap_start_advertisingでブロードキャストを開始しています。


leafony_beacon.ino

void StartAdvData()

{
(中略)

/* [set Advertising Data] 25byte MAX*/
uint8 adv_data[] = {
(2), //0: field length
BGLIB_GAP_AD_TYPE_FLAGS, //1: field type (0x01)
(6), //2: data
(1), //3: field length (1は仮の初期値) BGLIB_GAP_AD_TYPE_LOCALNAME_COMPLETE(LOCAL DEVICE NAMEのデータ長
BGLIB_GAP_AD_TYPE_LOCALNAME_COMPLETE, //4: field type (0x09)
(0), //5: L 1
(0), //6: e 2
(0), //7: a 3
(0), //8: f 4
(0), //9: _ 5
(0), //10: A 6
(0), //11: field length
(0xff), //12: field type (0xff)
(0), //13: 温度 1 T 1
(0), //14: 温度 2 X 2
(0), //15: 温度 3 X 3
(0), //16: 温度 4 . 4
(0), //17: 温度 5 X 5
(0), //18: 温度 6 X 6
(0), //19: 電圧 S V 7
(0), //20: 電圧 1 X 8
(0), //21: 電圧 2 . 9
(0), //22: 電圧 3 X 10
(0), //23: 電圧 4 X 11
(0), //24:
};

(中略)

//アドバタイズデータを登録
stLen = (5 + lenStr2 + 13);
ble112.ble_cmd_le_gap_set_adv_data(SCAN_RSP_ADVERTISING_PACKETS, stLen, adv_data); //SCAN_RSP_ADVERTISING_PACKETS
while (ble112.checkActivity(1000)); /* 受信チェック */

/* start */
// index = 0 LE_GAP_SCANNABLE_NON_CONNECTABLE / LE_GAP_UNDIRECTED_CONNECTABLE
ble112.ble_cmd_le_gap_start_advertising(0, LE_GAP_USER_DATA, LE_GAP_SCANNABLE_NON_CONNECTABLE);
while (ble112.checkActivity(1000)); /* 受信チェック */
}



省電力化のための実装

Beaconのいいところは、Beacon機器側が好きなタイミングでデータをブロードキャストして、あとはスリープできるところです。

下図のようにAdvertising中はBLEモジュールやマイコン、センサ等が起動しているので電力は大きいですが、

一定期間送信したらスリープモードに突入し、マイコンとBLEをスリープモードに入れたり、

センサの電源端子に接続されたスイッチをマイコンで切ってあげることで、電力を大幅に削減することが可能です。

image.png


データの受信

Beacon信号の受信はPCで行います。

今回はNode.jsのnobleというBluetoothライブラリを使ってBeaconスキャナを実装しました。

Windows 10へのnobleのインストールには苦戦したので、インストール方法に関して別に記事を書きましたのでそちらを参考にしてください。

(このあたりはRaspberry Piでやったほうが圧倒的に楽でした。nobleのインストールも簡単ですし、Bluetoothのモジュールも入っているので、何も難しいことはなくすんなりテストできます。)

スキャナのソースコードは下記の


scan.js

'use strict';

const noble = require('noble');

const textEncoding = require('text-encoding');
const TextDecoder = textEncoding.TextDecoder;

require('date-utils');

//discovered BLE device
const discovered = (peripheral) => {
const device = {
name: peripheral.advertisement.localName,
uuid: peripheral.uuid,
rssi: peripheral.rssi,
data: peripheral.advertisement.manufacturerData
};

// Leaf_A~Leaf_Fという名前のデバイスだけを表示
if (String(device.name).match(/^Leaf_[A-F]$/) != null){
let dt = new Date();
let dt_s = dt.toFormat('YYYY/MM/DD,HH24:MI:SS');
let data_s = (new TextDecoder('utf-8')).decode(device.data);
console.log(`${device.name},${dt_s},${data_s},${device.rssi}`);
}
}

//BLE scan start
const scanStart = () => {
noble.startScanning([], true);
noble.on('discover', discovered);
}

if(noble.state === 'poweredOn'){
scanStart();
}else{
noble.on('stateChange', scanStart);
}


Leafonyの発するAdvertisementパケットを特定する方法として今回はデバイス名を使用しました。他にももっといい方法はあるのかもしれませんが。。。

5台のLeafonyを15分おきにデータ送信をするように設定し、

実行するとBeaconの温度、バッテリー残量、電波強度を見ることができました。

> node .\scan.js

Leaf_C,2019/10/28,17:28:20,T23.32V3.02,-64
Leaf_E,2019/10/28,17:42:27,T22.58V2.96,-70
Leaf_A,2019/10/28,17:43:08,T21.40V2.81,-65
Leaf_C,2019/10/28,17:44:32,T23.30V3.02,-65
Leaf_B,2019/10/28,17:49:01,T23.06V2.89,-74
Leaf_D,2019/10/28,17:49:25,T22.92V2.84,-64

TXX.XXが温度、VX.XXがバッテリー残量です。(USBから給電している場合は0.00Vになるようになっています。)

一番最後の数字はRSSIです。RSSIは受信時の電波強度を表しています。

Advertising dataにTxPowerを入れて送信時の電力を知ることができれば、RSSIと比較してBeaconがどのくらいの距離離れた場所にあるかなどもわかります。

詳しくは下記の記事が参考になりました。


電力測定

後日公開予定


実際に部屋の温度を測ってみた (追記 2019/10/30)

 研究室の学生居室に、LeafonyのBeaconを5台設置して、各座席の温度と、Beaconの電池残量のログを取ってみました。

受信機はRaspberry Pi 3 B+を使いました。

(ここまでWindowsでの環境構築を説明したのですが、部屋に設置することを考えると、小型で環境構築なラズパイが便利でした。)

部屋の広さはおおよそ6m × 7mでラズパイは部屋の中央付近に設置されています。

ラズパイの環境を構築して、下記のコマンドでログが取れます。

sudo node ./scan.js | tee -a log.txt

定期的にlog.txtをsftpなどで取ってきて、下記のPythonスクリプトでグラフを描画しています。


graph.py

import numpy as np

import csv
import datetime
import matplotlib.pyplot as plt

d = {}
leaves = []
with open('log.txt', 'r') as f:
reader = csv.reader(f)

for row in reader:
devname = row[0]
dt = datetime.datetime.strptime(row[1] + ' ' + row[2], '%Y/%m/%d %H:%M:%S')
temp = float(row[3][1:6])
batt = float(row[3][7:12])
rssi = float(row[4])

if (d.get(devname) == None):
leaves.append(devname)
d.setdefault(devname, \
{ \
'datetime': [dt], \
'temp': [temp], \
'batt': [batt], \
'rssi': [rssi] \
})
else:
d[devname]['datetime'].append(dt)
d[devname]['temp'].append(temp)
d[devname]['batt'].append(batt)
d[devname]['rssi'].append(rssi)

leaves.sort()

plt.figure()

plt.subplot(1,2,1)
plt.title('Temperature')
plt.xlabel('Datetime')
plt.ylabel('Temp [℃]')
for leaf in leaves:
plt.plot(d[leaf]['datetime'], d[leaf]['temp'], label=leaf)
plt.legend()
plt.grid()

plt.subplot(1,2,2)
plt.title('Battery')
plt.xlabel('Datetime')
plt.ylabel('Voltage [V]')
for leaf in leaves:
plt.plot(d[leaf]['datetime'], d[leaf]['batt'], label=leaf)
plt.legend()
plt.grid()

plt.show()


とれたグラフがこちら

Figure_1.png

青色が私の座席です。窓際なので他の席よりも常に1℃くらい気温が低いですね。

グラフが急激に下がっている部分は窓を開けた時間に一致しています。生活リズムが狂った学生が早朝4時位に窓を開けているのがわかりますね。

緩やかに温度が上がってしばらく一定になる箇所は、窓を締め切ってエアコンをつけていた時間です。

夜中から早朝にかけては人もいないので、測定器等の冷却をするようのエアコンが稼働して一定温度に保たれています。

右側のグラフは電池電圧で、バッテリーはCR2032 225mAhのものを使っています。

最初しばらく下がって2.85V付近になってからほとんど変化していません。

うまく行けば数ヶ月ほどは持つ計算なのですが、もう数ヶ月ほど様子を見てみましょう。