Ⅳ-3. ESP32による部屋別のエアコン制御

 今回の赤外線送信器ソフトの作成で、一連の赤外線制御の仕組みはすべて完成です。すでに赤外線モニターで家電製品用リモコンの制御情報を解析し、その情報をパブリッシュする赤外線リモコンを作っています。MQTT通信ネットワークに接続した赤外線送信器は、これらの赤外線制御情報をサブスクライブして付近の家電製品を制御することが可能になります。
 当面の実験は、異なる部屋に設置されたエアコンの電源をON/OFFするだけですが、これに派生したさまざまな応用が考えられます。高性能低価格のESP32と融通性に富んだMQTTは、IoT分野で大活躍できそうです。


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



1.赤外線送信器ソフトの作成

 サブスクライバーである赤外線送信器へのメッセージは、onMessageイベント発生時に、あらかじめonMessage()イベントハンドラーによって登録されたコールバック関数が呼び出されることで届けられます。後の「2.コードの解説」で記述されているように、コールバック関数は次のような書式です。
  void onMqttMessage(char* topic, char* payload,
      AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
   ・topic: トピック
   ・payload: ペイロード
   ・properties: プロパティー
   ・len: ペイロード長
   ・index: インデックス
   ・total: 受信データ長
 (ペイロードの内容)
     3420,1724,416,1298,416,1298,414,442,414,・・・・・・,416

 以上を頭において、赤外線送信器の基本的な仕組みを眺めてみましょう。大きな流れは右図のようになっています。
 一つ目のポイントは、長大なデータがパブリッシュされた場合の対応です。コールバック関数onMqttMessage()によってデータを受信しますが、この時、引数においてペイロード(データ本文)と共にペイロード長と受信データ長が通知されます。ペイロード長は概ね1,440Byteより少し小さく、その中に有効なペイロードが格納されています。受信データ全体のサイズがそれより大きい場合は分割されて届き、データ全体のサイズは受信データ長に記録されています。
 ペイロードはカンマの位置に関係なく分割されるので、後で正しい位置でカンマ分割をするためには、ペイロードを文字列バッファに結合する必要があります。その都度ペイロード長の合計を求めておくと、その値が受信データ長と等しくなった時に完了です。

 二つ目のポイントは、結合が完了した文字列をカンマで分割しながら、取り出した文字列を数値変換して整数配列に積み上げることです。
 これが終わると、sendRawメソッドで配列と要素数を指定して赤外線送信させることができます。この時、第3引数でIR変調周波数をkHzで指定します。通常は数値定数38、つまり38kHzを割り当てます。

 これらの処理と平行して、送信器とブローカー間のネットワーク切断を診断するために、定期的(ここでは3,600秒毎)にアライブ情報をパブリッシュしています。通信が途絶えると、ブローカー接続時に設定したLWT(Last Will and Testament)が赤外線リモコンに届くようにしています。


2.コードの解説

 メッセージ受信に伴うペイロードの結合処理、文字列バッファから整数シグナル配列の作成処理、赤外線送信処理を除く部分は、今までの典型的なMQTT通信プログラムそのものです。
 2つの部屋のエアコンを制御するため、ここでは1つの送信器を使い回しています。部屋によってサブスクライブするトピックとLWTの内容が異なるので、ソフトを書き換える必要があります。コード上では両方を並記しておき、不要な方をコメントアウトしてESP32に書き込みます。

①ヘッダーファイルとデータ等の定義

・47~48、51行 環境に合った情報を設定してください!
・60行目、64行目 それぞれ部屋1、2用のトピックスです。不要な方をコメントアウトしてコンパイルします。
・70行目、72行目 それぞれ部屋1、2用のLWTです。不要な方をコメントアウトしてコンパイルします。
・76行目 タイムアウト間隔です。テスト用に適宜変更してください。
・77行目 アライブ情報です。
・84行目 以降は「irdend.メソッド」の形で操作できます。
#include <Arduino.h>
#include <WiFi.h>
extern "C" {
  #include "freertos/FreeRTOS.h"
  #include "freertos/timers.h"
}
#include <AsyncMqttClient.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>

/* Function Prototype */
void doInitialize();
void doPrepare();
void connectToWifi();
void connectToMqtt();
void WiFiEvent(WiFiEvent_t);
void onMqttConnect(bool);
void onMqttDisconnect(AsyncMqttClientDisconnectReason);
void onMqttSubscribe(uint16_t, uint8_t);
void onMqttMessage(char*, char*, AsyncMqttClientMessageProperties, size_t, size_t, size_t);
void displayValues(char*, char*, char*);
bool isPushbuttonClicked();
int toRawData(char*);
void sendSignal(int);

/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度
#define MAXSTRBUF   5000            // payloadバッファサイズ
#define MAXSIGNALS  800             // 最大シグナル数
#define irKhz       38              // IR変調周波数(kHz)

// ルーター接続情報
#define WIFI_SSID "your_ssid"
#define WIFI_PASSWORD "your_password"

// Mosquitto MQTT(Raspberry Pi)ブローカー接続情報
#define MQTT_HOST IPAddress(192, 168, ?, ??)
#define MQTT_PORT 1883

// MQTTクライアント操作用オブジェクト
AsyncMqttClient mqttClient;
TimerHandle_t mqttReconnectTimer;
TimerHandle_t wifiReconnectTimer;

// トピックス
//(ROOM1用)
const char* subscribeAirconOn  = "Aircon/room1_on";
const char* subscribeAirconOff = "Aircon/room1_off";
const char* publishAliveSignal = "Aircon/alive";
//(ROOM2用)
//const char* subscribeAirconOn  = "Aircon/room2_on";
//const char* subscribeAirconOff = "Aircon/room2_off";
//const char* publishAliveSignal = "Aircon/alive";

// Error Message(LWT)
//(ROOM1用)
const char* lwtMessage = "*Error room1*";
//(ROOM2用)
//const char* lwtMessage = "*Error room2*";

unsigned long LastPubTime = 0;          // 最新パブリッシュ時刻
const long iInterval = 3600000;         // 通信間隔(3600秒ごとに設定)
const char* aliveSignal = "OK";         // 定期送信メッセージ

char strBuffer[MAXSTRBUF];          // 受信バッファ
int strBufferLen;                   // 受信バッファのカレントサイズ
uint16_t intValues[MAXSIGNALS];     // 送信シグナル配列

const int irLedPin = 19;            // IrLED接続GPIO
IRsend irsend(irLedPin);            // IRsendの初期設定


②setup()初期化処理

 IRsendオブジェクトの開始と再接続用タイマーの作成、コールバック関数の割り当て、ブローカーの設定などの「準備処理」を行い、Wi-Fiルーターに接続します。
void setup() {
    doInitialize();            // 初期化処理をする
    doPrepare();               // 準備処理をして
    connectToWifi();           // Wi-Fiルーターに接続する
}


③loop()反復処理

 所定の間隔でアライブ情報をパブリッシュします。
void loop() {
    unsigned long now = millis();
 
    // 所定の間隔でAlive signalをパブリッシュして
    if (now - LastPubTime >= iInterval) {
        // 最新時刻を控える
        LastPubTime = now;
        mqttClient.publish(publishAliveSignal, 0, false, aliveSignal);
    }
}


④初期化処理

・109行目 IRsendオブジェクトを開始します。
void doInitialize() {
    Serial.begin(SPI_SPEED);
    irsend.begin();
    strBuffer[0] = '\0';
}


⑤準備処理

 再接続用タイマーの作成、すべてのコールバック関数の割り当てとブローカーの設定をします。
void doPrepare() {
    // 再接続タイマーを作成する
    mqttReconnectTimer = xTimerCreate("mqttTimer", pdMS_TO_TICKS(2000), pdFALSE,
            (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToMqtt));
    wifiReconnectTimer = xTimerCreate("wifiTimer", pdMS_TO_TICKS(2000), pdFALSE,
            (void*)0, reinterpret_cast<TimerCallbackFunction_t>(connectToWifi));

    // WiFiイベントのコールバック関数を割り当てて
    WiFi.onEvent(WiFiEvent);

    // MQTTコールバック関数を割り当てる
    mqttClient.onConnect(onMqttConnect);
    mqttClient.onDisconnect(onMqttDisconnect);
    mqttClient.onSubscribe(onMqttSubscribe);
    mqttClient.onMessage(onMqttMessage);

    // ブローカーを設定する
    mqttClient.setServer(MQTT_HOST, MQTT_PORT);
}


⑥コネクション確立用

・145行目 Keep alive timerの設定。
・146行目 LWTの設定です。
/* ESP32をWi-Fiルーターに接続する */
void connectToWifi() {
  Serial.println("Connecting to Wi-Fi...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}

/* ESP32をMQTTブローカーに接続する */
void connectToMqtt() {
    Serial.println("Connecting to MQTT...");
    // 必要であれば、KeepAliveとラーストウィル・メッセージを設定して
    mqttClient.setKeepAlive(iInterval);
    mqttClient.setWill(publishAliveSignal, 1, true, lwtMessage);
    // ブローカーに接続する
    mqttClient.connect();
}


⑦コールバック関数

・175、179行 赤外線制御情報をQoS2でサブスクライブすることを宣言しています。
・201~204行 ペイロードの続きがあれば結合する。
・215~219行 最終ペイロードを判定して、整数配列への変換と赤外線送信を実行する。
/* WiFiEvent関数 */
void WiFiEvent(WiFiEvent_t event) {
    switch(event) {
        case SYSTEM_EVENT_STA_GOT_IP:
            Serial.println("WiFi connected!");
            Serial.print("IP address: ");
            Serial.println(WiFi.localIP());
            connectToMqtt();
            break;
        case SYSTEM_EVENT_STA_DISCONNECTED:
            Serial.println("WiFi disconnected!");
            xTimerStop(mqttReconnectTimer, 0);  // WiFi再接続中はMQTTをストップさせる
            xTimerStart(wifiReconnectTimer, 0);
            break;
        default:
            break;
    }
}

/* MQTT割込: MQTT接続 */
void onMqttConnect(bool sessionPresent) {
    Serial.println("MQTT connected!");
    // subscribeAirconOnトピックをQoS2でサブスクライブする
    uint16_t packetId1 = mqttClient.subscribe(subscribeAirconOn, 2);
    Serial.print("Prepare 'subscribeAirconOn' QoS2, packetId:");
    Serial.println(packetId1);
    // subscribeAirconOffトピックをQoS2でサブスクライブする
    uint16_t packetId2 = mqttClient.subscribe(subscribeAirconOff, 2);
    Serial.print("Prepare 'subscribeAirconOff' QoS2, packetId:");
    Serial.println(packetId2);
}

/* MQTT割込: MQTT切断 */
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
    Serial.println("MQTT disconnected!");
    if (WiFi.isConnected()) {
        xTimerStart(mqttReconnectTimer, 0);
    }
}

/* MQTT割込: サブスクライブ */
void onMqttSubscribe(uint16_t packetId, uint8_t qos) {
    Serial.print("Subscribe now.  packetId:");
    Serial.println(packetId);
}

/* MQTT割込: メッセージ受信 */
void onMqttMessage(char* topic, char* payload,
    AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
    if ((strBufferLen + len) < MAXSTRBUF) {   // ペイロードを受信バッファへ積み上げる
        strncat(strBuffer, payload, len);
        strBufferLen += len;
    }

    Serial.println("Received message.");
    Serial.print("  ・Topic: ");
    Serial.println(topic);
    Serial.print("  ・Message: ");
    Serial.print("  ・QoS: "); Serial.println(properties.qos);
    Serial.print("  ・Retain: "); Serial.println(properties.retain);
    Serial.print("  ・Len: "); Serial.println(len);
    Serial.print("  ・Index: "); Serial.println(index);
    Serial.print("  ・Total: "); Serial.println(total);
    if (strBufferLen >= total) {            // すべてのペイロードを積み上げたら
        Serial.println(strBuffer);
        sendSignal(toRawData(strBuffer));   // シグナル配列を作成して送信して
        strBufferLen = 0;                   // 受信バッファをクリアする
    }
}


⑧その他

 受信バッファから送信用整数配列への変換処理と、赤外線送信処理を関数として独立させています。
/* 受信バッファからシグナル配列を作成する
    返却値: シグナルの個数
*/
int toRawData(char* str){
    char buf[10];
    int pos = 0;

    strcpy(buf, strtok(str, ","));  // 受信バッファからシグナル配列を作成して
    intValues[pos++] = atoi(buf);
    for (int i = 0; i < MAXSIGNALS; i++) {
        char* p = strtok(NULL, ",");
        if (p == NULL)
            break;
        strcpy(buf, p);
        intValues[pos++] = atoi(buf);
    }
    strBuffer[0] = '\0';            // 受信バッファをクリアする
    return pos;
}

/* 赤外線LEDでシグナルを送信する */
void sendSignal(int signals) {
    Serial.print("** Send Signals **   >>  size = "); Serial.println(signals);
    irsend.sendRaw(intValues, signals, irKhz);
    delay(2000);
}


3.実行結果

 最初の部屋にUSB給電した赤外線送信器を設置し、開発用PCに赤外線リモコンをUSB接続します。そして、赤外線リモコンの動作をモニタリングするためにシリアルモニターを開きます。赤外線送信器の赤外線LEDは指向性があるので、エアコンの受光部に届きやすい向きで設置します。
 赤外線リモコンの青色プッシュボタンを押すと青色LEDが点灯して、エアコンの電源が入ります。もし電源がONにならないようであれば、送信器の位置を変えてみてください。もう一度青色プッシュボタンを押すと、青色LEDは消灯してエアコンの電源がOFFになります。

 なお、実験に当たってアライブ情報を監視しやすいように、通信間隔を30秒に変更しています。
 プッシュボタンを押すと、シリアルモニターには「Publish now.」と共に、その時点のパケットIDが更新表示されています。これに連動してLEDの点滅とエアコンのON/OFF動作が起きています。同時に、30秒間隔で「Receive message.」が表示され、アライブ情報の「OK」を受信していることがわかります。
 この状態で赤外線送信器のUSB給電を停止すると、しばらく待ってLWTの「*Error room1*」が表示され、異常が発生したことがわかります。

 今回は1台の赤外線送信器を使い回すので、開発用PCに再接続して先のスケッチを再ロードします。ROOM2用にトピックとlwtMessageのコメントを切り替えてコンパイルし、ボードに書き込みます。赤外線送信器を別の部屋に移動して、赤外線リモコンの黄色プッシュボタンで同様にエアコンの動作を確認します。


4.今後の課題

 MQTTネットワークと赤外線通信を組み合わせたエアコン制御を、とてもシンプルなプログラムで試してみました。MQTT通信の柔軟性と使いやすさ、赤外線通信の簡便さが理解できたと思います。他にもテレビや音響製品、照明など、リモコンで操作できるものは簡単に制御することができます。
 今回の赤外線リモコンは実験用の限定的なもので、多種類の対象を細かなレベルで制御・監視するにはもっと使いやすいものが必要になります。ボタンやスイッチ、選択ボックスやレバーなどを配したWebアプリケーションが適していると思われます。これについては、時間が許せば今後のシリーズで取り組んでみたいと思います。
 また、パスワード認証や暗号化など、セキュリティ面での課題もあります。ローカルでの実験ならルーターの管理をしっかりすることで対応できますが、インターネットを介して遠隔地から操作しようとすれば、ネットワークの選択や暗号化が重要になります。そんなテーマで、後日に取り組めるかも知れません。
 また制御対象の家電製品や機器類によっては、火災の発生などへの安全対策も考慮しなければなりません。遠隔操作であれば、所定の動作の完了レスポンスや、温度変化など異常発生のフィードバックが必要になるかも知れません。LWTが届いた場合にも、今回のようなシリアルモニターへの表示でなく、エラー内容のディスプレイ表示やブザーなどによる警告通知が適しているかも知れません。
 さらに、多種類の対象を制御するには多数の赤外線制御情報をハンドリングしなければならず、制御情報のコンパクトなデータベースかそれに類したものが必要になるでしょう。
 このように、用途の広がりと実際の運用への拡張、あるいは安全で実用に耐える物を考えると、様々な条件をクリアしなければならないことがわかります。そして多くの課題は、新たな面白いテーマを投げかけてもくれそうです。



 今回の実験が何らかのヒントにつながり、皆様の取り組みの一助になれば幸いです。
 いっそうのご健闘を楽しみにしています!
 長らくお付き合いいただきまして、ありがとうございました。


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