ESP32のArduinoでBLEデバイスの出入りをモニタリングする

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)」を選択するだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/848c3332-39c8-48cf-e420-2fd4155177a6.png)

# サーバ側の準備

MySQLサーバと、MQTTブローカを使います。
事前に立ち上げておきます。

MQTTブローカについては、ぜひ以下もご参考にしてください。
　[AWS IoTにMosquittoをブリッジにしてつなぐ](https://qiita.com/poruruba/items/084317e01b9fababc02e)

以下のメッセージを送信します。

```
{
　"action": action,
　"address": BLEアドレス,
　"name": BLEアドバタイズのLocal Name,
　"rssi": RSSI値
}
```

MySQLサーバには、データベースとテーブルを作成し、権限が付与されたアカウントを作成しておきます。
こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5ea9067f-bcee-7069-1fb1-8e154e0670e4.png)
※今回は、created_atは使っていません。

actionには以下のいずれかが入ります。
・"enter" ： 初めてのBLEデバイス発見の場合
・"stay" ： 継続してBLEデバイス発見できている場合
・"leave" ： BLEデバイスをロストした場合

# Arduino側のソースコード

ちょっと長いですが、ソースコード丸ごと記載します。

```c:BLE_scanner
#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)

以上