Ⅴ-1. ESP32によるIoTゲートウェイの開発①

 新たに導入したPlatformIOとVSCodeの組み合わせは実に快適で、最高の開発環境であると感じています。今回のゲートウェイの開発では、それがさらに威力を発揮することになります。
 今回はまず、ホスト側データベースの仕様を決めて、ホスト間インターフェイスを検討します。それに基づいてゲートウェイ用のプロジェクトを作成し、メモリー不足の問題を解決してゲートウェイを完成させます。

 ※BLEGatewayのダウンロードは右側の[Download]ボタンをクリックしてください。     


1.ホスト側データベース

 ゲートウェイの機能・構造を考えるにあたって、入出力データを明確にしておきましょう。
 開発するゲートウェイBLEGatewayは、ブロードキャスターのアドバタイジング・データから目的の計測情報を見つけると、構造体tmpDataに受信データを取り出します。この時、整数型の温度と湿度は100.00で除算して、元の計測値に戻します。


 最終的なデータは、データベース上のテーブルdatatableに記録することにして、その構造を決めておきましょう。
 先頭に自動連番のid_noを確保して、これを主キーにします。また最後部にtimestamp型のdaytimeを配置して、ここには書き込みの都度の時刻を自動記録させます。計測から記録までにわずかな時間経過はありますが、実質的に計測時刻と見なして差し支えないでしょう。
 主キーの続きに、複数のゲートウェイ接続を識別できるように、ゲートウェイ識別子gwt_idを設置しています。この値は、それぞれのゲートウェイプログラムにおいて、1から始まる重複しない番号を割り付けることになります。図のように、#defineで設定したSTATION_IDの値を記録させることにします。
 他のフィールドは、並びに変更はありますが、構造体の内容をそのまま記録しています。

2.ホスト間インターフェイス

 ホスト間通信にはWi-Fiを使用します。上記のように、BLEGatewayで保持するデータと出力データの対応が決まれば、これをどのようにWi-Fiに乗せるかを考えればよいことになります。
 ホストをデータベースサーバーとして動作させれば、特に機密性を必要としない少量データの通信には、HTMLリクエストのGETメソッドを使えばよいことがわかります。
 HTTP1.1クライアントからGETメソッドで発行するリクエストは次の形式に従います({}は個別指定)。

GET /{request_URI}?{query_string} HTTP/1.1 Host: {host_ip_address} Connection: close
  ・request_URI: リクエスト対象のホスト側のリソースURI
  ・query_string:「&名前1=値1&名前2=値2&・・・・・・」の形式で記述した文字列
  ・host_ip_address: ホストのIPアドレス

 ホスト側に設定するデータベースへのデータ追加スクリプトを「insert_db.php」とし、ホストのIPアドレスを「192.168.0.34」として上図のデータベースを対象にすると、リクエストは次のようになります。

GET /insert_db.php?&gtw_id=**&dev_id=**&seq_no=**&state=**&temp=**&humd=** HTTP/1.1 Host: 192.168.0.34 Connection: close

 ここで、**の部分には構造体tmpDataに読み込んだデータを編集します。また最終行のConnection: closeは、リクエスト実行後のセッションを切断することを指示しています。

 これから分かるように、有効なデータを受信した都度、受信データを上記の形式に編集したスクリプトをホスト宛に送信すればよいことになります。
 ホストとのコネクションの確立や実際の送信方法はとても簡単なので、4節の「プログラム構造」で説明します。

3.プロジェクトの作成

 まず、プロジェクト名を設定します。開発済みのBLEObserver.inoをベースにして作成しますが、機能にふさわしくBLEGatewayとします。そして、前回のBLEBroadcasterと同じように開発ボードを選択します。


 プロジェクトが作成されたら、ExplorerのUNTITLED(WORKSPACE)に現れたBLEGatewayを展開して、src/main.cppをクリックしてソースコードを表示します。次に、Arduino IDEの開発環境で開発済みのBLEObserver.inoをコピーして、ソースコードを上書きします。この時、include <Arduino.h>を消さないように注意します。
 25,26行目にエラーの赤波線。これは前回同様にライブラリーの未登録です。今回はSSD136関連のライブラリーが未登録なので、すでにインストール済みのArduino IDE環境のlibraryからAdafruit_SSD1306とAdafruit-GFX-Libraryフォルダーを見つけて、開発中のBLEGatewayのlibにコピーします。
 リロードして少し待つと、25,26行目の赤波線が消えます。


 Explorerからplatformio.iniを開いて、モニタースピードとCOMポートを定義しておきます。

 これで準備が整ったのでビルドします。が、残念ながらエラーが発生。1,310,720バイト中の114.4%(1,499,854バイト)を使用するメモリー不足エラーです。

 とりあえずこのエラーは保留して、次に進むことにします。


4.プログラム構造

 BLEObserverをベースに機能を追加して組み立てます。ゲートウェイとして付加すべき機能は次の通りです。
  ○ホストとのWi-Fi通信機能。
  ○例外データ(異常値のデータ)のフィルタリング。
  ○データ受信と同期したクエリーストリングの生成とWi-Fi送信。
 なお、ブロードキャスターから異常発生機能を削除したので、BLEObserverのデバイス異常対応処理は不要になりますが、今後のことを考慮してそのまま残すことにします。

 以下では、BLEObserverから変わった部分を中心にコードを見てみましょう。
①ヘッダーファイルとデータ等の定義

・22、24行 開発環境固有の<Arduino.h>とWi-Fi用ヘッダーファイルをインクルードしています。
・30~36行 前方宣言として関数プロトタイプを定義します。
・40行目 デバッグ時のシリアルモニター表示を制御します。
・42~46行 運用環境に合った値を設定してください。
・47行目 リクエスト対象のホスト側のリソースURIです。ホスト側でこの名前のPHPスクリプトを作成します。
・51行目 複数のゲートウェイで重複しない識別番号を設定します(1~255の範囲)。
・58~65行 受信データを格納する構造体です。
#include <Arduino.h>

#include <WiFi.h>
#include <BLEDevice.h>
#include <Wire.h>                   // I2C interface
#include <Adafruit_SSD1306.h>       // SSD1306 display
#include <Adafruit_GFX.h>           // SSD1306 display

/* Function Prototype */
void doInitialize(void);
String makeQueryString(struct tmpData*);
void sendData(struct tmpData*);
void displayData(struct tmpData*);
void displayAlarm(struct tmpData*);
void displayValues(const char*, const char*, const char*);

/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度
#define DEBUG_MODE  (true)         // デバッグモード

/* WiFiホスト制御用 */
const char* ssid = "your_ssid";
const char* password = "your_password";
const char* hostIP = "your_host_address";
const int   httpPort = 80;
const char* targetFile = "insert_db.php";

/* BLEスキャン制御用 */
#define DEVICE_NAME "ESP32"         // 対象デバイス名
#define STATION_ID   1              // ステーション(Gateway)識別番号
#define MAX_DEVICES  8              // 最大デバイス数
#define ManufacturerId 0xffff       // 既定のManufacturer ID
const int scanning_time = 3;        // スキャン時間(秒)
BLEScan* pBLEScan;                  // Scanオブジェクトへのポインター
int prev_seq[MAX_DEVICES] = { -1, -1, -1, -1, -1, -1, -1, -1 };

/* 受信データ構造体 */
struct tmpData {
    int     device_id;              // デバイス識別番号
    bool    abnormal;               // デバイス異常
    int     seq_number;             // シーケンス番号
    float   temperature;            // 温度
    float   humidity;               // 湿度
};


②setup()初期化処理

・84行目 Wi-Fiの接続条件を指定して、
・86~89行 ホストとの接続を確立します。
・93行目 接続したIPアドレスを表示します。
void setup() {
    doInitialize();                             // 初期化処理をして
    
    WiFi.begin(ssid, password);                 // WiFiに接続する
    Serial.print("Connectiong");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected.");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    
    BLEDevice::init("");                        // BLEデバイスを作成し
    Serial.println("Client application start...");
    pBLEScan = BLEDevice::getScan();            // Scanオブジェクトを取得して、
    pBLEScan->setActiveScan(false);             // パッシブスキャンに設定する
}


③loop()反復処理

・102行目 ここで受信データ構造体の実態を定義(確保)します。
・129~130行 ブロードキャスターの不意の電源操作によるノイズを無視(除去)します。
・131行目 ここでホスト送信関数を呼びます。
void loop() {
    struct tmpData td;

    // 所定時間だけスキャンして、見つかったデバイス数を取得する
    BLEScanResults foundDevices = pBLEScan->start(scanning_time);
    int count = foundDevices.getCount();

    for (int i = 0; i < count; i++) {     // 受信したアドバタイズデータを調べて
        BLEAdvertisedDevice dev = foundDevices.getDevice(i);
        std::string device_name = dev.getName();
        if (device_name == DEVICE_NAME 
                && dev.haveManufacturerData()) {   // デバイス名が一致しManufacturer dataがあって
            std::string data = dev.getManufacturerData();
            int device_id = data[2];
            int seq_number = data[4];
            //Serial.print("DEV: ");  Serial.println(device_id);

            // デバイス識別番号が有効かつシーケンス番号が更新されていたら
            if (device_id >= 1 && device_id <= MAX_DEVICES
                      && seq_number != prev_seq[device_id - 1]) {
                // 受信データを取り出して
                prev_seq[device_id - 1] = seq_number;
                td.device_id = device_id;
                td.abnormal = (bool)data[3];
                td.seq_number = seq_number;
                td.temperature = (float)(data[6] << 8 | data[5]) / 100.00;
                td.humidity = (float)(data[8] << 8 | data[7]) / 100.00;
                // 有効データならホストへ送信する
                if (!(td.seq_number == 0
                            && (td.temperature > 100 || td.humidity > 100))) {
                    sendData(&td);
                }
                if (!td.abnormal) {
                    displayData(&td);
                } else {
                    displayAlarm(&td);
                }
            }
        }
    }
    // プッシュボタン押下でLEDを消灯してディスプレイ表示を消去する
    int buttonState = digitalRead(buttonPin);
    if (buttonState == HIGH) {
        displayValues(" ", " ", " ");
        digitalWrite(ledPin, LOW);
    }
}


④送信関連の関数

・167行目○ホストへの送信関数です。
・174行目 WiFiClientクラスのインスタンスを作成して、
・175行目 connectメソッドで指定したホストの指定したポートに接続する。
・181行目 ユーザー関数makeQueryString()でクエリーストリングを作成する。
・185行目 WiFiClientのprintlnメソッドでクエリーストリングを送信する。
・191~193行目 クエリーに対するレスポンスを受信してシリアルモニターに表示する。
・202行目 接続を解除する。
・206行目○クエリーストリングの生成関数です。
・209~213行 ホストに対するリクエスト・クエリーストリングを組み立てる。
/* 受信データをホストへ送信する */
void sendData(struct tmpData* td) {
    if (DEBUG_MODE) {
        Serial.print("Connect to ");
        Serial.println(hostIP);
    }

    // WiFiClientクラスでコネクションを確立する
    WiFiClient client;
    if (!client.connect(hostIP, httpPort)) {
        Serial.println(" - Can't connect to the host!");
        return;
    }
    
    // クエリーストリングを作成して送信して
    String request = makeQueryString(td);
    if (DEBUG_MODE) {
        Serial.println(">> Send string");  Serial.println(request);  Serial.println(">>");
    }
    client.println(request);
    delay(20);

    // レスポンスを受信し
    if (DEBUG_MODE) {
        Serial.println("<< Response");
        while (client.available()) {
            String line = client.readStringUntil('\n');
            Serial.println(line);
        }
        Serial.println("<<");
    }

    // コネクションを解放する
    if (DEBUG_MODE) {
        Serial.println(" - Disconnect.");
    }
    client.stop();
}

/* クエリーストリングを作成する */
String makeQueryString(struct tmpData* td) {
    int state;
    td->abnormal ? state = 1 : state = 0;
    String request = "GET /" + String(targetFile) + "?>w_id=" + String(STATION_ID)
          + "&dev_id=" + String(td->device_id) + "&seq_no=" + String(td->seq_number)
          + "&state=" + String(state) + "&temp=" + String(td->temperature)
          + "&humd=" + String(td->humidity)
          + " HTTP/1.1\r\nHost: " + String(hostIP) + "\r\nConnection: close\r\n\r\n";
  return request;
}


5.メモリーパティションの設定

 以上でゲートウェイのコードが出来上がり、残すはメモリー容量不足への対応です。
 メモリー・パーティションの割り当ては、下図左
    C:\User\~~\Arduino15\packages\esp32\hardware\esp32\1.0.2\tools\partitions
というフォルダーのdefault.csvというファイルに定義されています。図右がその内容で、app0が通常使用するアプリケーションのプログラム書き込み領域サイズです。0x140000ですから、1,310,720バイトの領域を割り当てていることが分かります。


 このapp0の値を0x250000に変更して、2,424,832バイトまで使えるように拡大します。そして、その分だけapp1の値が縮小するよう0x30000に変更します。
 default.csvの内容を変更すると、すべてのArduino IDE配下のスケッチが影響されてしまいます。ところが、PlatformIOではプロジェクト単位で環境設定ができるので、BLEGatewayに限定して割り当て変更することが可能です。次のように、割り当て変更をファイルcustom.csvに入力して、プロジェクト直下に配置します。


 続いて、platformio.iniの内容を次のように変更します。board_build.partition=でcustom.csvを指定することにより、既定の割り当てを変更することができるわけです。


 これでメモリー不足の問題は解消しました。メモーリー占有率は61.9%で、まだ十分に余裕があります。こんな具合で、platformIOの仕組みは素晴らしいですね!



 次回は最終回です。ホストのデータベースを構築し、データベース更新系のスクリプトを作成してゲートウェイを接続。全体の動作検証をすることにしましょう。
 お楽しみに!


 
Copyright (C) 2011-2024 Marchan, All rights reserved.