ESP32を使って、周辺のBLEデバイスのRSSIを監視します。
また、せっかくなので、各BLEデバイスのRSSIをMySQLのデータベースに記録してIoTっぽくしたり、新しいBLEデバイスを発見したらMQTTで通知をしたりします。
Obnizを使うことも考えたのですが、ESP32で継続的にBLEスキャンさせるたびにリクエストを投げるためのサーバを立てるのはあまりカッコよくないと思ったので、ESP32単独で「BLEスキャン」「MQTT通知」「MySQLへのInsert」をしています。
そうすることで、監視側はいつでもMySQLのDB上の最新値を取得すれば、各デバイスのRSSIが確認できますし、リアルタイムにBLE処理したければ、MQTTをトリガにBLEセントラルを立ち上げれば良いことになります。
今回は、ESP32として、「M5StickC」を使っていますが、純粋なESP32でも大丈夫です。
ArduinoIDEのライブラリの準備
以下のライブラリを使いました。
・ChuckBell/MySQL_Connector_Arduino
https://github.com/ChuckBell/MySQL_Connector_Arduino
・knolleary/pubsubclient
https://pubsubclient.knolleary.net/
いずれも、ライブラリマネージャのリストに登録されていましたので、それをインストールして使っています。
※一つ大事なことがありました。
ArduinoでESP32を扱うとき、WiFiとBLE両方を有効にすると、以下のようにROMサイズが足りないというエラーが表示されることがあります。
最大1310720バイトのフラッシュメモリのうち、スケッチが1375982バイト(104%)を使っています。
最大327680バイトのRAMのうち、グローバル変数が62116バイト(18%)を使っていて、ローカル変数で265564バイト使うことができます。
スケッチが大きすぎます。http://www.arduino.cc/en/Guide/Troubleshooting#size には、小さくするコツが書いてあります。
ボードM5Stick-Cに対するコンパイル時にエラーが発生しました。
その場合の対応策としていくつかあるのですが、一番簡単なのは、OTAの領域を無効にする方法です。
(ツール)→(Partition Scheme)で、「No OTA(Large APP)」を選択するだけです。
サーバ側の準備
MySQLサーバと、MQTTブローカを使います。
事前に立ち上げておきます。
MQTTブローカについては、ぜひ以下もご参考にしてください。
AWS IoTにMosquittoをブリッジにしてつなぐ
以下のメッセージを送信します。
{
"action": action,
"address": BLEアドレス,
"name": BLEアドバタイズのLocal Name,
"rssi": RSSI値
}
MySQLサーバには、データベースとテーブルを作成し、権限が付与されたアカウントを作成しておきます。
こんな感じです。
※今回は、created_atは使っていません。
actionには以下のいずれかが入ります。
・"enter" : 初めてのBLEデバイス発見の場合
・"stay" : 継続してBLEデバイス発見できている場合
・"leave" : BLEデバイスをロストした場合
Arduino側のソースコード
ちょっと長いですが、ソースコード丸ごと記載します。
#include <M5StickC.h>
#include <BLEDevice.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>
// 編集はここから
#define BLESCAN_DURATION 30 // BLEスキャン時間(秒)
#define BLESCAN_INTARVAL (3 * 60) // BLEスキャン間隔(秒)
const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";
const char* mqttHost = "【MQTTブローカのIPアドレス】";
const int mqttPort = 【MQTTブローカのポート番号】;
const char* mqtt_topic = "【MQTTのトピック名】"; //
IPAddress mysqlHost(XXX, XXX, XXX, XXX); // MySQLのIPアドレス
const int mysqlPort = 【MySQLのポート番号】; // MySQLのポート番号
char* mysqlUserid = "【MySQLのユーザ名】"; // MySQLアカウントのユーザ名
char* mysqlPassword = "【MySQLのパスワード】"; // MySQLアカウントのパスワード
const char* mysqlDatabaseName = "【MySQLのデータベース名】";
const char* mysqlTableName = "【MySQLのテーブル名】";
// 編集はここまで
WiFiClient mqtt_wifiClient;
PubSubClient mqttClient(mqtt_wifiClient);
WiFiClient mysql_wifiClient;
MySQL_Connection conn(&mysql_wifiClient);
MySQL_Cursor* cursor = NULL;
struct BleDeviceInfo {
int rssi;
std::string name;
std::string address;
bool flag;
};
#define NUM_OF_DEVICE 40 // 保持するBLEデバイス情報の数
BleDeviceInfo deviceList[NUM_OF_DEVICE];
#define BUFFER_SIZE 200
char str_buffer[BUFFER_SIZE];
BLEScan* pBLEScan;
unsigned long start_tim = 0;
class BLEAdvertisedDevice_cb: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
std::string address = advertisedDevice.getAddress().toString();
std::string name = advertisedDevice.getName();
int rssi = advertisedDevice.getRSSI();
Serial.print("BLE Device found -> Address: ");
Serial.print(address.c_str());
Serial.print(", Name: ");
Serial.print(name.c_str());
Serial.print(", RSSI: ");
Serial.println(rssi);
int i;
for( i = 0 ; i < NUM_OF_DEVICE; i++ ){
if( deviceList[i].address == address ){
deviceList[i].rssi = rssi;
deviceList[i].flag = true;
break;
}
}
if( i >= NUM_OF_DEVICE ){
Serial.println("New BLE Device found");
if( mqttClient.connected() ){
sprintf(str_buffer, "{\"action\": \"enter\", \"address\": \"%s\", \"name\": \"%s\"}",
address.c_str(), name.c_str());
mqttClient.publish(mqtt_topic, str_buffer);
}
int j;
for( j = 0 ; j < NUM_OF_DEVICE; j++ ){
if( deviceList[j].address == "" ){
// Serial.println("New BLE Device registered");
deviceList[j].address = address;
deviceList[j].name = name;
deviceList[j].rssi = advertisedDevice.getRSSI();
deviceList[j].flag = true;
break;
}
}
if( j >= NUM_OF_DEVICE )
Serial.println("NUM_OF_DEVICE over");
}
}
};
void checkDeviceList(){
Serial.println("checkDeviceList()");
for( int i = 0 ; i < NUM_OF_DEVICE ; i++ ){
if( deviceList[i].address == "" )
continue;
if( !deviceList[i].flag ){
Serial.print("BLE Device lost -> Address: ");
Serial.print(deviceList[i].address.c_str());
Serial.print(", Name: ");
Serial.println(deviceList[i].name.c_str());
if( mqttClient.connected() ){
sprintf(str_buffer, "{\"action\": \"leave\", \"address\": \"%s\", \"name\": \"%s\"}",
deviceList[i].address.c_str(), deviceList[i].name.c_str());
mqttClient.publish(mqtt_topic, str_buffer);
}
if (conn.connected()){
sprintf(str_buffer, "INSERT INTO %s.%s (action, address, rssi, name) VALUES ('leave', '%s', %d, '%s')",
mysqlDatabaseName, mysqlTableName, deviceList[i].address.c_str(), 0, deviceList[i].name.c_str());
cursor->execute(str_buffer);
}
deviceList[i].address = "";
}else{
if (conn.connected()){
sprintf(str_buffer, "INSERT INTO %s.%s (action, address, rssi, name) VALUES ('stay', '%s', %d, '%s')",
mysqlDatabaseName, mysqlTableName, deviceList[i].address.c_str(), deviceList[i].rssi, deviceList[i].name.c_str());
cursor->execute(str_buffer);
deviceList[i].flag = false;
}
}
}
}
void setup() {
M5.begin();
Serial.begin(9600);
M5.IMU.Init();
M5.Axp.ScreenBreath(9);
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.println("[M5StickC]");
delay(1000);
WiFi.begin(wifi_ssid, wifi_password);
Serial.println("Connecting to Wifi AP...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println(WiFi.localIP());
M5.Lcd.println(WiFi.localIP());
mqttClient.setServer(mqttHost, mqttPort);
Serial.println("Connecting to MQTT...");
while( !mqttClient.connected() ) {
String clientId = "ESP32-" + String(random(0xffff), HEX);
if ( mqttClient.connect(clientId.c_str()) ) {
Serial.println("connected");
M5.Lcd.println("MQTT OK");
break;
}
delay(1000);
Serial.print(".");
}
BLEDevice::init("");
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new BLEAdvertisedDevice_cb());
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(true);
pBLEScan->start(BLESCAN_DURATION, false);
}
void loop(){
if( !mqttClient.connected() ){
Serial.println("Reconnecting to MQTT");
String clientId = "ESP32-" + String(random(0xffff), HEX);
mqttClient.connect(clientId.c_str());
}
mqttClient.loop();
unsigned long now_tim = millis();
if( (now_tim - start_tim) > (BLESCAN_INTARVAL * 1000 + 1000) ){
Serial.println("Connecting to MySQL...");
if( !conn.connect(mysqlHost, mysqlPort, mysqlUserid, mysqlPassword) ){
Serial.println("Failed");
}else{
Serial.println("connected");
if( cursor == NULL )
cursor = new MySQL_Cursor(&conn);
}
checkDeviceList();
// cursor.close();
// cursor = NULL;
conn.close();
Serial.println("BLEデバイス検索開始...");
start_tim = now_tim;
pBLEScan->start(BLESCAN_DURATION, false);
}
}
解説
「編集はここから」から「編集はここまで」の間を、各自の環境に合わせて編集してください。
BLESCAN_DURATION
常にBLEスキャンしているのではなく、ここで指定した秒だけスキャンします。
BLESCAN_INTARVAL
BLESCAN_DURATIONの間のBLEスキャンが完了したらそれで終わりではなく、BLESCAN_INTARVALが経過したら、またBLEスキャンを開始します。
wifi_ssid
wifi_password
接続するWiFiアクセスポイントとパスワードです。
mqttHost
mqttPort
MQTTブローカのホスト名とポート番号です。
mqtt_topic
Publishする先のMQTTのトピック名です。
mysqlHost
mysqlPort
MySQLサーバのIPアドレスとポート番号です
mysqlUserid
mysqlPassword
MySQLサーバに接続するためのユーザ名とパスワードです。
mysqlDatabaseName
mysqlTableName
MySQLサーバに保持するデータベース名とテーブル名です。
class BLEAdvertisedDevice_cb : public BLEAdvertisedDeviceCallbacks
BLEスキャン中にBLEデバイスを発見すると呼ばれることを期待したクラスです。
発見したデバイスのBLEアドレスをキーにして検索し、初めての発見であれば内部でBLEデバイス情報を保持し、すでに発見済みの場合には、RSSIを更新します。初めての発見の際には、MQTT通知を行います。
setup()のBLEスキャンの準備時にこのインスタンスをコールバックとして登録しています。
void checkDeviceList()
BLESCAN_INTARVAL 経過後に、保持しておいたBLEデバイス情報からBLEデバイスのロストを検出します。以前に保持していたBLEデバイスがBLEスキャンに反応しなかったことで、ロストと判定します。同時に、MQTT通知およびMySQLデータ登録を行います。
void setup()
以下の処理をしています。
・WiFiアクセスポイントへの接続
・MQTTブローカへの接続
・BLEスキャンの準備と開始
void loop()
BLESCAN_INTARVAL が経過するのを待ち受け、経過したら checkDeviceList() を呼び出した後、BLEスキャンを再度開始します。
補足
長時間利用していると、MySQL接続が不安定な気がする。。。
→ MySQLのコネクションが切れてしまうので、checkdeviceList()の時だけ接続するようにしました(2020/1/6)
以上