Ⅳ-2. ESP32による赤外線情報のハンドリング

 ESP32用のライブラリーは頻繁に更新されています。赤外線制御については、つい最近まで送信用のライブラリーが未完でしたが、やっと対応するバージョンが公開されました。
 まずそのライブラリーをインストールして、前回作成した赤外線モニターで家電用リモコンデータを解析します。以降の実験では、別の部屋に設置された2台のエアコンを制御することにします。それぞれのリモコンからON/OFF信号を取り込み、そのデータを元に、エアコンの電源ON/OFF制御データをパブリッシュする赤外線リモコンソフトを作成します。


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



1.ESP32用の赤外線送受信ライブラリー

 ESP32で赤外線通信をするためには、専用のライブラリーをインストールする必要があります。冒頭のとおりライブラリーは頻繁に更新されていて、現時点で最新のIRremote ESP8266Library v2.6.4(2019/07/26)において、ESP32による赤外線送信が可能になっています。
 まず、次の手順で開発用のPCにライブラリーをインストールしてください。

  1) https://github.com/crankyoldgit/IRremoteESP8266 からライブラリーをダウンロードします。
    ・[Clone or download]を開きます。
    ・[Download ZIP]ボタンをクリックします。

    ⇒ IRremoteESP8266-master.zip がダウンロードされます。
  2) IRremoteESP8266-master.zipを解凍してフォルダーをIRremoteESP8266にリネームします。
  3) IRremoteESP8266フォルダーを、ESP32用のArduino IDEインストールライブラリフォルダに移動してください。

2.赤外線モニターとリモコンデータの解析

 開発用PCのUSBポートに赤外線モニターボードを接続してください。赤外線モニター用のソフトは特に開発せず、赤外線ライブラリーに付属しているサンプルスケッチを使用します。
 Arduino IDEで、[ファイル]-[スケッチ例]-[IRremoteESP8266]の順に選ぶとサンプルプログラム一覧が表示されるので、IRrecvDumpV2を選択してください。


 選択したサンプルのIRrecvDumpV2は書き込み禁止なので、いつもスケッチを保存しているフォルダーにいったん保存してください。
 ボードの赤外線受光モジュールはGPIO18に接続されているので、スケッチのkRecvPinの値(下図の赤矢印)を14から18に修正します。シリアルモニターを開いて、所定の手続きでコンパイルしてマイコンボードに書き込みます。
 シリアルモニターに「IRrecvDumpV2 is now running and waiting for IR input on Pin 18」の表示を確認したら、リモコンからの赤外線信号受信準備が完了です。


 まず試しに、テレビ(ここでは東芝 REZGA)のリモコンを赤外線受光モジュールに向けて、電源ボタンをON/OFFしてみます。シリアルモニターには大量の情報が表示されます。

 文字が小さくて読みにくいですが、次のように表示されているのがわかります。
   Encoding : NEC
   Code : 2FD48B7 (32 bits)
   Raw Timing(67):
     + 9028, - 4426, + 592, - 538, + 592, - 554, + 574, - 558,
       <以下省略>

   uint16_t rawData[67] = {9028, 4426, 592, 538, 592, 554, 574, 556, 572, 556, <省略>
   uint32_t address = 0x40;
   uint32_t command = 0x12;
   uint64_t data = 0x2FD48B7;

 Encodingで示されるのはベンダー名で、NECフォーマットが使われています。これとCode:行の値がわかれば、「irsend.sendNEC(2FD48B7, 32);」のような短縮形式で赤外線を送信することができます。しかし、場合によって(次に試したエアコンの一台で)は、
   Encoding : UNKNOWN
となりベンダーがわかりません。
 このような場合に対処するため、ここではすべて生の信号を送信することにします。上記の
   uint16_t rawData[67] = {9028, 4426, 592, 538, ....... };
に表示されている配列の値すべてを送信します。この数字の並びは、「赤外線がONの時間, OFFの時間, ONの時間, OFFの時間, ,...」を表しています。

 続いて、今回の実験で使用するエアコンのリモコンでやってみましょう。

 エアコンの赤外線情報はとても長大で、このケースのrawDataの配列数は583個で3,300字に上ります。2種類のエアコンのON/OFFが終わったら、4つのrawDataの値をエディターなどにコピー&ペーストして保存しておきましょう。

【余 談】
 このようなサイズが大きい情報の送信は、データ長の制約が少ないMQTTではまったく問題なく扱えます。では実際の送信はどのように行われるのか、リモコンソフトの開発に先立って動作を確認してみました。ざっくりとしたテストですが、特に設定に手を加えていない状況で、1回のパブリッシュでは約5,700Byteまで問題なく送れることが判明しました。

○最長のトピック文字列が19Byteのケース
  ・1回の送信データ長は1440byte。
     受信側で見た受信データ長で、これには制御ヘッダー(4Byte)が含まれている。
  ・初回は、これにトピック文字列が送信される(この例では 19Byte)。
   したがって、初回のメッセージサイズは
      伝送サイズ - Fixed Header長 - Topic文字列長 - PacketID長 =
            1,440 - 4 - 19 - 2 = 1,415Byte
  ・2回目以降の分割メッセージサイズは
      伝送サイズ - FixedHeader長 =
            1,440 - 4 = 1,436Byte
  ・データ長を増加させながら試してみると、1回のパブリッシュで最大4分割までの送信が可能と判明。
   したがって、最大伝送長は
       (1,440 - 4) x 4 - 2 = 5,742となり、
   最大メッセージ長は、5,742 - 最大トピック文字列長 = 5,742 - 19 = 5,723Byte

 もう一度、4つのrawDataの値がエディターに取り込まれていることを確認して、適当なファイル名で保管してください。
 赤外線モニターで行うことはこれがすべてです。ボードは別の用途に配線し直してかまいません。


3.赤外線リモコンソフトの作成

 2台のエアコンの電源をON/OFFするだけの簡単なリモコンです。
 ボード上には青色と黄色のプッシュボタンがあり、同じ色のLEDが配置されています。青色のプッシュボタンは1台目のエアコン、黄色のプッシュボタンは別室にある2台目のエアコンを操作します。
 初期状態で青色ボタンを押すと、1台目のエアコンの電源ONシグナルをパブリッシュして青色LEDを点灯します。もう一度押すと電源OFFシグナルをパブリッシュして、青色LEDを消灯します。黄色ボタンは2台目のエアコンに対して同じようなパブリッシュとLEDの点滅を行います。
 次表はこれらの関係を整理したものです。ここで「メッセージ」は、先の赤外線モニターで取得した赤外線制御情報rawDataの値です。

プッシュボタンLEDトピックメッセージ動 作
 青色ボタン押下  青色LED・点灯  Aircon/room1_on  *Aircon1_on 部屋1のエアコンの電源を入れる 
 青色LED・消灯 Aircon/room1_off *Aircon1_off  部屋1のエアコンの電源を切る
 黄色ボタン押下  黄色LED・点灯  Aircon/room2_on  *Aircon2_on 部屋2のエアコンの電源を入れる 
 黄色LED・消灯 Aircon/room2_off *Aircon2_off 部屋2のエアコンの電源を切る


4.コードの解説

 コードにはかなり細かくコメントを記述しているので、以下ではポイントを絞って解説します。
 基本的にはプッシュボタン操作によるパブリッシュだけなのですが、赤外線送信器が回線断などで動作しなくなるケースを考えて、一定の周期でアライブ情報をサブスクライブしています。実験用なので、そのような事態が発生した場合はシリアルモニターへ表示するにとどめています。

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

・45~46、49行 環境に合った情報を設定してください!
・58~62行 上の4つはパブリッシュ用です。
      subscribeAliveSignalは、赤外線送信器が応答しなくなった場合のサブスクライブ用です。
・66~69行 ""の間に赤外線モニターで取得した4種類の赤外線制御情報(rawDataの値)を複写してください。
#include <Arduino.h>
#include <WiFi.h>
extern "C" {
  #include "freertos/FreeRTOS.h"
  #include "freertos/timers.h"
}
#include <AsyncMqttClient.h>

/* Function Prototype */
void doInitialize();
void doPrepare();
void doProcess();
void connectToWifi();
void connectToMqtt();
void WiFiEvent(WiFiEvent_t);
void onMqttConnect(bool);
void onMqttDisconnect(AsyncMqttClientDisconnectReason);
void onMqttSubscribe(uint16_t, uint8_t);
void onMqttPublish(uint16_t);
void onMqttMessage(char*, char*, AsyncMqttClientMessageProperties, size_t, size_t, size_t);
bool isPushbuttonClicked();
char* compress(const char*);

/* 基本属性定義  */
#define SPI_SPEED   115200          // SPI通信速度

// ルーター接続情報
#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;

// トピックス
const char* publishAircon1On  = "Aircon/room1_on";
const char* publishAircon1Off = "Aircon/room1_off";
const char* publishAircon2On  = "Aircon/room2_on";
const char* publishAircon2Off = "Aircon/room2_off";
const char* subscribeAliveSignal = "Aircon/alive";

// 赤外線制御データ
//Aircon1: Mitsubishi Kirigamine, Aircon2: Hitachi RAS-E36Z
const char* Aircon1_on  = "3420, 1724,  416, 1298,  416, 1298,  414, 442,  414,(省略)";
const char* Aircon1_off = "3446, 1698,  440, 1274,  438, 1274,  440, 416,  440,(省略)";
const char* Aircon2_on  = "3294, 1692,  404, 1256,  406, 494,  406, 494,  406, (省略)";
const char* Aircon2_off = "3296, 1688,  408, 1256,  406, 494,  406, 492,  408, (省略)";

// ボタン接続GPIO
const int btnBluePin = 18;
const int btnYellowPin = 19;
const int ledBluePin = 25;
const int ledYellowPin = 26;

// ボタンON/OFF状態
bool enabledBlue = false;
bool enabledYellow = false;

/* プッシュボタン */
int buttonState1;                       // 状態
int lastButtonState1 = LOW;             // 直前の状態
unsigned long lastDebounceTime1 = 0;    // 直前の切替時刻
int buttonState2;                       // 状態
int lastButtonState2 = LOW;             // 直前の状態
unsigned long lastDebounceTime2 = 0;    // 直前の切替時刻
unsigned long debounceDelay = 50;       // デバウンスタイム(mSec.)


②setup()初期化処理

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


③loop()反復処理

 青色・黄色プッシュボタンのクリックを確認し、クリックされていたらボタンのON/OFF状態を反転して、それに応じたエアコン制御信号をパブリッシュします。ボタンのON/OFF状態にしたがって対応するLEDを点灯または消灯します。
・104、108行 ボタンのON/OFF状態を反転する。
・115、120、126、131行 確実な伝達が必要なのでQoS2を指定。送信データは空白を除去してパブリッシュ。
void loop() {
    bool bClickedBlue = false;
    bool bClickedYellow = false;

    if (isPushbuttonClicked1(btnBluePin)) {
        enabledBlue = !enabledBlue;
        bClickedBlue = true;
    }
    if (isPushbuttonClicked2(btnYellowPin)) {
        enabledYellow = !enabledYellow;
        bClickedYellow = true;
    }

    if (bClickedBlue) {
        if (enabledBlue) {
            // トピック'Aircon/room1_on'、QoS2で部屋1のエアコンの電源ON操作文字列をパブリッシュする
            mqttClient.publish(publishAircon1On, 2, false, compress(Aircon1_on));
            digitalWrite(ledBluePin, HIGH);
        }
        else {
            // トピック'Aircon/room1_off'、QoS2で部屋1のエアコンの電源OFF操作文字列をパブリッシュする
            mqttClient.publish(publishAircon1Off, 2, false, compress(Aircon1_off));
            digitalWrite(ledBluePin, LOW);
        }
    } else if (bClickedYellow) {
        if (enabledYellow) {
            // トピック'Aircon/room2_on'、QoS2で部屋2のエアコンの電源ON操作文字列をパブリッシュする
            mqttClient.publish(publishAircon2On, 2, false, compress(Aircon2_on));
            digitalWrite(ledYellowPin, HIGH);
        }
        else {
            // トピック'Aircon/room2_off'、QoS2で部屋2のエアコンの電源OFF操作文字列をパブリッシュする
            mqttClient.publish(publishAircon2Off, 2, false, compress(Aircon2_off));
            digitalWrite(ledYellowPin, LOW);
        }
    }
}


④初期化処理

void doInitialize() {
    Serial.begin(SPI_SPEED);
    // GPIOを設定する
    pinMode(btnBluePin, INPUT);
    pinMode(btnYellowPin, INPUT);
    pinMode(ledBluePin, OUTPUT);
    pinMode(ledYellowPin, OUTPUT);
}


⑤準備処理

 再接続用タイマーの作成、すべてのコールバック関数の割り当てとブローカーの設定をします。
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.onPublish(onMqttPublish);

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


⑥コネクション確立用

/* 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...");
    // ブローカーに接続する
    mqttClient.connect();
}


⑦コールバック関数

・207行目 アライブ情報のトピックsubscribeAliveSignalを指定してサブスクライブを宣言しています。
・233行目 アライブ情報はここで受信します(正常時も異常時も)。
/* WiFiEvent関数 */
void WiFiEvent(WiFiEvent_t event) {
    switch(event) {
        case SYSTEM_EVENT_STA_GOT_IP:   // WiFi接続が完了したらMQTTブローカーに接続する
            Serial.println("WiFi connected!");
            Serial.print("IP address: ");
            Serial.println(WiFi.localIP());
            connectToMqtt();
            break;
        case SYSTEM_EVENT_STA_DISCONNECTED: // WiFiが切断されたらタイマーを起動して再接続を試行
            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!");
    // subscribeAliveSignalトピックをQoS0でサブスクライブする
    uint16_t packetId = mqttClient.subscribe(subscribeAliveSignal, 0);
    Serial.print("Prepare 'subscribeAliveSignal' QoS0, packetId:");
    Serial.println(packetId);
}

/* 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 onMqttPublish(uint16_t packetId) {
    Serial.print("Publish now.  packetId:");
    Serial.println(packetId);
}

/* MQTT割込: メッセージ受信 */
void onMqttMessage(char* topic, char* payload, 
    AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
    String messageTemp;
    for (int i = 0; i < len; i++) {
        messageTemp += (char)payload[i];
    }
    Serial.println("Received message.");
    Serial.print("  ・Topic: ");
    Serial.println(topic);
    Serial.print("  ・Message: ");
    Serial.println(messageTemp);
}


5.動作検証

 動作検証といってもまだ赤外線送信器のソフトが未完なので、送信データの確認だけにとどまります。MQTTブローカーにどのようにデーターが届くかを、Raspberry Piのコンソールで確認してみましょう。
 MQTTがメッセージを受け取ったらすぐに表示できるよう、サブスクライブ要求を指示します。以下のように、Raspberry Piのコンソールからmosquitto_subコマンドを実行させます。-hでブローカーアドレスを、-tで対象トピックを指定します。ここではトピック「Aircon/room1_on」を対象にしていますが、「#」を指定すればすべてのトピックをサブスクライブします。

$ mosquitto_sub -h localhost -t Aircon/room1_on

 トピックAircon/room1_onに対応した赤外線リモコンの青色プッシュボタンをクリックすると、Raspberry Piコンソールに受信データが表示されます。下はWindows上のSSHクライアントPoderosaの画面です。


 次回は赤外線送信器ソフトを作成して、今回の赤外線リモコンと連動させてみましょう。
 お楽しみに!


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