こんにちは、LINE Beaconを担当している、エンジニアの古田です。
このたび、Messaging APIに機能が追加となり、先日公開したLINE Simple Beaconに対応するデバイスが生成したデータ(device message)をLINE Botが受け取れるようになりました。
本blogでは告知を兼ねて、それを利用した実装例を紹介したと思います。
さっそく試してみましょう
自分のLINE Botに対して、ビーコンを使ってfaceb00c
というdevice messageを送ってみます。
ここでは、先日のblogで紹介をしたnode.js製のビーコンプログラムを使ってみたいと思います。事前に 先日のblogの内容にそって、LINE Botの準備とmacOS(またはRaspberry Pi)のセットアップをしてください。
終わりましたら、次のように、--device-message
という引数に faceb00c
というデータを付けて、コマンドを実行してみましょう。(これはhwidが01deadbeef
の例です)
./simplebeacon.js --hwid=01deadbeef --device-message=faceb00c
皆さんのBotに対して、次のようなWebhookが届くはずです。
{
"events":[
{
"type":"beacon",
"replyToken": (略) ,
"source":{ (略) },
"beacon":{
"hwid":"01deadbeef",
"dm":"faceb00c",
"type":"enter"
}
}
]
}
dm
というプロパティに、device messageに指定したデータが入っているのがわかると思います。(ちなみに、dmというのは device messageの略です。そのままですね)
なお、simplebeacon.js
の --device-message
に指定できるのはバイナリデータの16進数表記だけとなっています。いわゆる「任意の文字列」をそのまま送ることはできないのでご注意ください!
任意の文字列を送りたい場合は、以下のように、指定の文字コードでバイトシーケンスに符号化したあとにHexdumpして取得した文字列を使ってください。
./simplebeacon.js --hwid=01deadbeef --device-message=`python3 -c 'print("ライン".encode("shift_jis").hex())'`
ちなみになのですが、BLEの仕様との兼ね合いで、Device Messageでは13byteまでしかデータを載せられません。UTF-8で日本語を送ると不利なので(ひらがな・カタカナなどが3byteに符号化されます。Shift_JISなら2byte)ご注意ください。
Webシステムの富豪的プログラミングに慣れた身としてはツライところです。
ESP32で電源電圧を送ってみよう!
せっかくなので何か役に立つデータを送ってみたいですよね。
ここでは、電池の消耗/交換時期の目安につかうことを意図して、電源電圧をビーコンデバイス自身が測定し、device messageに入れてadvertisingしてみます。
今回はターゲットデバイスとして、最近話題のIoTチップ、ESP32を使います。
- 回路を実装
- ESP32のAnalog/Digical コンバータを使って電池の電圧を測定し、Device Messageで測定値を含めた状態でSimple Beaconの電波をadvertisingする
- webhook内のデータをデコードするという流れで実装をしてみます!

そもそも:ESP32とは
ESP32とは、Espressif Systemsが開発した、WiFiおよびBluetoothを備えた低コストかつ低消費電力のチップです。
- Arduino用のSDKが提供されていて、Arduino IDEをつかった開発が可能
- MacOSで開発環境を整えることが簡単にできます!
- 日本の技適マークを取得済みなものが容易に入手可能
- 開発用ボードであるESP32-DevKitCが1500円程度で購入可能
- もう少し素の状態に近いESP-WROOM32であれば、数百円で購入可能
といった特徴をもつため、ちょっとしたIoTデバイスをプロトタイピングするのにとても使いやすい製品です。
回路図
回路図および実体写真は以下のとおりです。


4本の単三電池を使ったモバイルバッテリーを使ってESP32を駆動する、単純な回路となっています。
また、電池ケースに電圧センシング用の線を追加し、抵抗を使って1/3に分圧したものをA/Dコンバータ端子(D35)につないでいます。
ESP32では3.3V以上の電圧をA/Dコンバータ端子につなげられないので、少し余裕を見て前記の分圧比を決めました。R1は2000Ωの抵抗2個で実現できるので、2000Ωの抵抗を一種類用意すればOKです。
電圧測定ピンとしてD35を使っていますが、D32-39であればどれでも大丈夫です(ソースの修正は必要です)。ESP32には他にもADCに対応するピンがありますが、現状では前記の8pinだけがAPI経由で値を取得できます。
Arduinoスケッチ
以下は、上記の回路でSimple Beaconを構成するためのArduinoスケッチです。5秒ごとに、GPIO 35ピン経由で読み出した電圧をもとにSimple Beaconのアドバタイズパケットを更新しています。
電圧測定にはArduino関数のanalogRead
を使っていますが、実装/挙動を確認する限りESP32のものは以下のような違いがあり、注意が必要です
- デフォルトの分解能が10bitではなく12bit
- 電圧範囲が0-5Vではない。デフォルトで11dbのアッテネータが使われるため、その場合は0-3.9V。
現状、analogReadに関する上記の挙動はドキュメント化されてはいないようです。今後仕様が変更になると最終的な結果が変わってしまって困るので、setup関数内で明示的にデフォルト値と同じ仕様となるように上書きしています。
Bluetoothまわりは、基本的にHCI Commandを直接つかって制御を行っています。公開されているコードを見るとBluetooth関係の様々なAPIが用意されているのですが、まだ十分なドキュメントがそろっておらず今回の利用は断念しました。
#include "bt.h"
#include "esp_gap_ble_api.h"
static const uint8_t HWID[5] = {0x01, 0xde, 0xad, 0xbe, 0xef}; // 発行されたHWID
static const uint8_t ADC_PIN = 35; // 電圧入力ピン
static const float R1 = 4000.0;
static const float R2 = 2000.0;
static const uint16_t UUID_FOR_LINECORP = 0xFE6F;
static const uint8_t MAX_SIMPLEBEACON_DEVICEMESSAGE_SIZE = 13;
static uint8_t deviceMessageSize = 1;
static uint8_t deviceMessage[MAX_SIMPLEBEACON_DEVICEMESSAGE_SIZE];
static const uint8_t MAX_BLE_ADVERTISING_DATA_SIZE = 31;
static const uint16_t HCI_LE_Set_Advertising_Data = (0x08 << 10) | 0x0008;
static const uint16_t HCI_LE_Set_Advertising_Enable = (0x08 << 10) | 0x000A;
static void _dump(const char * title, uint8_t *data, size_t dataSize) {
Serial.printf("%s [%d]:", title, dataSize);
for (size_t i = 0; i < dataSize; i++) {
Serial.printf(" %02x", data[i]);
}
Serial.println();
}
static void putUint8(uint8_t** bufferPtr, uint8_t data) {
*(*bufferPtr)++ = data;
}
static void putUint16LE(uint8_t** bufferPtr, uint16_t data) {
*(*bufferPtr)++ = lowByte(data);
*(*bufferPtr)++ = highByte(data);
}
static void putArray(uint8_t** bufferPtr, const void* data, size_t dataSize) {
memcpy(*bufferPtr, data, dataSize);
(*bufferPtr) += dataSize;
}
// HCIを使い、指定されたopCode、dataのコマンドをBluetooth Controllerに書き込みます
static void executeBluetoothHCICommand(uint16_t opCode, const uint8_t * hciData, uint8_t hciDataSize) {
uint8_t buf[4 + MAX_BLE_ADVERTISING_DATA_SIZE];
uint8_t* bufPtr = buf;
putUint8(&bufPtr, 1);// 1 = H4_TYPE_COMMAND
putUint16LE(&bufPtr, opCode);
putUint8(&bufPtr, hciDataSize);
putArray(&bufPtr, hciData, hciDataSize);
uint8_t bufSize = bufPtr - buf;
while (!esp_vhci_host_check_send_available());
esp_vhci_host_send_packet(buf, bufSize);
}
static void updateSimpleBeaconDeviceMessage(float voltage) {
memset(deviceMessage, 0x00, MAX_SIMPLEBEACON_DEVICEMESSAGE_SIZE);
uint8_t* deviceMessagePtr = deviceMessage;
// float値を含む文字列をdevice messageに格納します
const uint8_t voltageStringSize = 8;
char voltageString[voltageStringSize];
snprintf(voltageString, voltageStringSize, "V=%f", voltage);
putArray(&deviceMessagePtr, voltageString, voltageStringSize);
deviceMessageSize = deviceMessagePtr - deviceMessage;
}
static void updateAdvertisingData() {
uint8_t data[MAX_BLE_ADVERTISING_DATA_SIZE];
uint8_t* dataPtr = data;
{
// flag
putUint8(&dataPtr, 1 + 1);
putUint8(&dataPtr, ESP_BLE_AD_TYPE_FLAG);
putUint8(&dataPtr, ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
}
{
// complete list of service uuid
putUint8(&dataPtr, 1 + 2);
putUint8(&dataPtr, ESP_BLE_AD_TYPE_16SRV_CMPL);
putUint16LE(&dataPtr, UUID_FOR_LINECORP);
}
{
// simple beacon
putUint8(&dataPtr, 1 + 9 + deviceMessageSize);
putUint8(&dataPtr, ESP_BLE_AD_TYPE_SERVICE_DATA);
putUint16LE(&dataPtr, UUID_FOR_LINECORP);
putUint8(&dataPtr, 0x02); // frame type
putArray(&dataPtr, HWID, 5);
putUint8(&dataPtr, 0x7F); // measured txpower
putArray(&dataPtr, deviceMessage, deviceMessageSize);
}
uint8_t dataSize = dataPtr - data;
_dump("simple beacon", data, dataSize);
uint8_t hciDataSize = 1 + MAX_BLE_ADVERTISING_DATA_SIZE;
uint8_t hciData[hciDataSize];
hciData[0] = dataSize;
memcpy(hciData + 1, data, dataSize);
executeBluetoothHCICommand(HCI_LE_Set_Advertising_Data, hciData, hciDataSize);
}
static void enableBluetoothAdvertising() {
uint8_t enable = 1;
executeBluetoothHCICommand(HCI_LE_Set_Advertising_Enable, &enable, 1);
}
static void notifyHostSendAvailableHandler() {
// _dump("notify_host_send_available", NULL, 0);
}
static int notifyHostRecvHandler(uint8_t *data, uint16_t len) {
// _dump("notify_host_recv", data, len);
return 0;
}
static esp_vhci_host_callback_t vhciHostCallback = {
notifyHostSendAvailableHandler,
notifyHostRecvHandler
};
void setup() {
Serial.begin(115200, SERIAL_8N1);
// 互換性を考慮し、ESP32のデフォルト値で上書きする
analogSetAttenuation(ADC_11db);
analogReadResolution(12);
// bluetoothを初期化
btStart();
esp_vhci_host_register_callback(&vhciHostCallback);
enableBluetoothAdvertising();
}
void loop() {
// 12bit幅(0...4095)で電圧を取得。4095が最大電圧(ADC_11dbなので3.9V)を表します
uint16_t analog12bit = analogRead(ADC_PIN);
// 分圧前の電源電圧を計算
float voltage = analog12bit / 4095.0 * 3.9 * (R1 + R2) / R2;
Serial.printf("analogRead: %d, voltage: %f", analog12bit, voltage);
Serial.println();
updateSimpleBeaconDeviceMessage(voltage);
updateAdvertisingData();
delay(5000);
}
webhook内のデータをデコードする
デバイスの電源をONにすると、以下のように、 events[].beacon.dm
にdevice messageが含まれたwebhookがBotサーバに届くはずです。
{
"events":[
{
"type":"beacon",
"replyToken": (略) ,
"source":{ (略) },
"beacon":{
"hwid":"01deadbeef",
"dm":"563d352e31313700",
"type":"enter"
}
}
]
}
このデータは前記の通りバイト列なので、電源電圧のデータを取得するにはデコード処理が必要です。
といっても、今回の例では単一のASCII文字列しか格納していないため、次のような簡単なデコードプログラムでデータを取り出すことが可能です。
$ python3 -c 'print(bytes.fromhex("563d352e31313700").decode("ascii"))'V=5.117
終わりに
いかがでしたでしょうか。
IoTサービスではデバイスの管理/メンテナンスをどうするかがよく課題としてあげられるのですが、Simple Beaconを使うとAPI経由で簡単にデバイスの状態を取得できるため、管理がやりやすくなるとおもいます。
また、今回の例では文字列だけをadvertisingしましたが、エンコード/デコードを工夫すれば、もっといろいろな種類のデータをDevice Messageに載せることが可能となるので、様々な用途に使うことが出来るのではないでしょうか。
すでにLINE Bot SDKでもサポートされており、すぐに利用可能です!
ということで、みなさまぜひ気軽にお試していただけたらと思います。