Help us understand the problem. What is going on with this article?

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

サーバ側の準備

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

MQTTブローカについては、ぜひ以下もご参考にしてください。
 AWS IoTにMosquittoをブリッジにしてつなぐ

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

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

MySQLサーバには、データベースとテーブルを作成し、権限が付与されたアカウントを作成しておきます。
こんな感じです。
image.png
※今回は、created_atは使っていません。

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

Arduino側のソースコード

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

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)

以上

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした