バッファローの「探せるキーホルダー」や、Logitecの「ぶるタグ」、懐かしい「Stick-N-Find」やNTTドコモのProject Linkingの「Pochiru」や「Tomoru」など、あるいは、LINE Beaconなど、常に電源On状態でアドバタイズし続けるBLEデバイスの、稼働状態や出入りを監視します。
BLEアドバタイズやレスポンスのスキャンデータには、デバイスがサポートするサービスUUIDが含まれています。
そこに、以下のUUIDが含まれていたら、生存していることとしてサーバに通知します。
Stick-N-Find:bec26202-a8d8-4a94-80fc-9ac1de37daa6
Project Linking:b3b36901-50d3-4044-808d-50835b13a6cd
Immediate Alert Service:0x1802
LINE Things:0xfe6f
スキャンするBLE CentralデバイスとしてESP32を使います。
M5StampC3が小さくてちょうど良い感じです。とはいっても、どのESP32でも動作します。
ソースコードもろもろは以下にあります。
poruruba/NearbyBleDeviceScan
BLEデバイスの検出(@ESP32)
BLEデバイスを検出したら、HTTP Postで通知するか、MQTTで通知するか、2種類を用意しておきました。
スキャンと通知の流れは以下のようにしています。
① 30秒BLEアドバタイズをスキャンします。スキャンしたデータにさきほどのUUIDが含まれていた場合にはそのデバイスの情報を記憶しておきます。
② スキャン完了後、検出したBLEデバイスの情報一覧をサーバにプッシュします。
③ 60分経過したら、①に戻ってスキャンを再実施します。
BLEスキャン
BLEDevice::init("NearbyBleDevice");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
// pBLEScan->setInterval(1349);
// pBLEScan->setWindow(449);
pBLEScan->setActiveScan(true);
pBLEScan->start(SCAN_DURATION_SEC, myScanCompleteCallback);
Serial.println("setup finished\n");
MyAdvertisedDeviceCallbacksのコールバック関数に、検出したすべてのBLEデバイスが引数に渡されて呼び出されます。
myScanCompleteCallbackのコールバック関数は、スキャン終了時に呼び出され、サーバにBLEデバイス情報一覧をプッシュするようにします。
検出したデバイスは以下の部分で、サービスUUIDによりフィルタリングしています。
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks
{
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.println(advertisedDevice.toString().c_str());
if (advertisedDevice.haveServiceUUID() ){
uint8_t device_type = DEVICE_TYPE_UNKNOWN;
if(advertisedDevice.isAdvertisingService(sticknfind_discoverServiceUUID))
device_type = DEVICE_TYPE_STICKNFIND;
else if(advertisedDevice.isAdvertisingService(immediate_alert_discoverServiceUUID))
device_type = DEVICE_TYPE_IMMEDIATE_ALERT;
else if(advertisedDevice.isAdvertisingService(linking_discoverServiceUUID))
device_type = DEVICE_TYPE_PROJECT_LINKING;
else if(advertisedDevice.isAdvertisingService(line_things_discoverServiceUUID))
device_type = DEVICE_TYPE_LINE_THINGS;
else
return;
BLE_DEVICE_ENTRY entry = { getCurrentTime(), true, device_type,advertisedDevice.getAddressType(),
advertisedDevice.haveRSSI() ? advertisedDevice.getRSSI(): 0, advertisedDevice.haveName() ? advertisedDevice.getName() : "" };
device_list[advertisedDevice.getAddress().toString()] = entry;
Serial.print("*** BLE Advertised Device found: ");
Serial.println(advertisedDevice.toString().c_str());
}
}
};
サーバへプッシュ
サーバにプッシュする際に、BLEデバイス情報一覧はJSON形式にします。
void myScanCompleteCallback(BLEScanResults results)
{
Serial.println("counting start");
long ret;
time_t current_time = getCurrentTime();
char address[18];
json_request.clear();
sprintf(address, "%02x:%02x:%02x:%02x:%02x:%02x", ble_mac_address[0], ble_mac_address[1], ble_mac_address[2], ble_mac_address[3], ble_mac_address[4], ble_mac_address[5]);
json_request["ble_mac_address"] = address;
sprintf(address, "%02x:%02x:%02x:%02x:%02x:%02x", wifi_mac_address[0], wifi_mac_address[1], wifi_mac_address[2], wifi_mac_address[3], wifi_mac_address[4], wifi_mac_address[5]);
json_request["wifi_mac_address"] = address;
sprintf(address, "%d.%d.%d.%d", wifi_ip_address[0], wifi_ip_address[1], wifi_ip_address[2], wifi_ip_address[3]);
json_request["wifi_ip_address"] = address;
int index = 0;
for (std::unordered_map<std::string, BLE_DEVICE_ENTRY>::iterator iterator = device_list.begin(); iterator != device_list.end(); iterator++){
std::pair<std::string, BLE_DEVICE_ENTRY> element = *iterator;
std::string mac_address = element.first;
if( device_list[mac_address].connected && device_list[mac_address].updated_at < (current_time - EXPIRE_DURATION_SEC) ){
device_list[mac_address].updated_at = getCurrentTime();
device_list[mac_address].connected = false;
}
BLE_DEVICE_ENTRY entry = element.second;
json_request["device_list"][index]["mac_address"] = (char*)mac_address.c_str(),
json_request["device_list"][index]["name"] = (char*)entry.name.c_str(),
json_request["device_list"][index]["connected"] = entry.connected;
json_request["device_list"][index]["device_type"] = entry.device_type;
json_request["device_list"][index]["address_type"] = entry.address_type;
json_request["device_list"][index]["rssi"] = entry.rssi;
json_request["device_list"][index]["updated_at"] = entry.updated_at;
index++;
}
if( serializeJson(json_request, json_buffer, sizeof(json_buffer)) <= 0 ){
Serial.println("serializeJson error");
return;
}
#ifdef _MQTT_ENABLE_
ret = mqttPublish(json_buffer);
if( ret != 0 )
Serial.println("mqtt publish failed");
#endif
#ifdef _HTTP_ENABLE_
ret = httpPost(HTTP_PUT_URL, json_buffer);
if( ret != 0 )
Serial.println("httpPost error");
#endif
}
サーバ側の実装(@Node.js)
Node.jsサーバにて、HTTP Postで送られてきたBLEデバイス情報の一覧を管理します
MQTTで送られてきた場合の処理と、HTTP Post呼び出しで送られてきた場合の両方で動くようにしておきました。
'use strict';
const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');
let device_list = new Map();
exports.mqtt_handler = async (event, context) => {
console.log(event);
for( let device of event.payload.device_list ){
device.host_address = event.payload.ble_mac_address;
device_list.set(device.mac_address, device);
}
};
exports.handler = async (event, context, callback) => {
var body = JSON.parse(event.body);
console.log(body);
if( event.path == "/bledevice-list" ){
return new Response({ list: [...device_list.values()] });
}else if( event.path == "/bledevice-put" ){
for( let device of body.device_list ){
device.host_address = body.ble_mac_address;
device_list.set(device.mac_address, device);
}
return new Response({});
}
};
BLEデバイスの状態表示ページ(@ブラウザ)
Node.jsで管理されているBLEデバイス一覧は、WebAPI呼び出しで取得できるようにし、Webページから可視化するようにしました。
こんな感じです。大した処理ではないので、ソースコードをご参照ください。
参考
obniz-nobleでBLEスキャンをグラフ表示してみた
ESP32のArduinoでBLEデバイスの出入りをモニタリングする
以上