Smart HomeをDIYする

Apple HomeKit は対応製品が少なくて高価なのでHomebridgeでがんばります

Arduino IDEからESP32をOTAアップデート

追記:こちらの改訂版をご覧ください:

diysmarthome.hatenablog.com


 

(以下は古い記事です)

 

OTA (Over The Air)を使ってESP32のスケッチをアップデートするようにしました。通常はESP32とArduino IDEをUSB/シリアル接続するところを、WiFi経由で接続します。ESP32ボードマネージャに入っているOTAスケッチ例よりも、わかりやすい方法が紹介されていましたので試してみました。

OTA

OTAというと、無線経由でファームウェアを入れ替えるようなニュアンスがありました。でもOTAの元々の意味は、無線経由でデータをやり取りすることらしいのです。今や当たり前ですね。やっぱり、OTAを動かしているソフトウェア自体を、OTAで入れ替えてしまう、アクロバティックな使い方が醍醐味な気がします。

ESP32の開発キットをArduino IDEでプログラムする際には、通常はUSB/シリアル接続します。開発段階ではそれで良いのですが、家のどこかに組み込んでしまった後にプログラムを更新する際に厄介です。またUSB/シリアル変換チップを取り外して、ESP32本体だけを組み込んだ場合は、もはやシリアル接続できなくなってます。そこでWiFi経由のOTAでプログラムを更新する方法が使われます。ESPHomeではOTAが用意されていて、簡単に使えました。でも、Home Assistantを使わないとあまりメリットがないです。一方で、Arduino IDEのESP32ボードマネージャにもOTAのスケッチ例が用意されてました。まずはこれを使ってみました。

ArduinoのOTAスケッチ例

ArduinoにESP32のボードマネージャをインストールすると、メニューに、ファイル/スケッチ例/ESP32Dev Module用のスケッチ例/ArduinoOTAが現れ、この中に、BasicOTAとOTAWebUpdaterのサンプルが用意されています。

  • BasicOTAArduino IDEのシリアルの代わりにWiFiで接続してプログラムをダウンロードする方式です。
  • OTAWebUpdaterはES32をwebサーバにして、そこに接続してバイナリをアップロードする方式です。

スケッチ例を試してみました。どちらもすぐに動きました。ただ、OTAWebUpdaterは、webページを作るためにプログラムが長くて、本筋のプログラムがわかりにくくなりそうです。また、BasicOTAの方も長いです。コメントを外しても以下のような感じです。長いです。シリアル経由で進捗状況を報告するようになっていることもあり、そこそこの量があります。またloop()でOTA機能を呼び出し続けなければいけないところも、厄介な気がしました。本来やりたい仕事と、OTAのために実行する仕事が混在しているので混乱しそうです。

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

const char* ssid = "..........";
const char* password = "..........";

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}

なので、ちょっと面倒かなと思ってました。

見た目が簡単なOTA

YouTubeを見ていたら、このESP32のOTAを簡単に見せる方法が紹介されていました。

youtu.be

基本的には上記のサンプルプログラムと同じことをやっているのですが、

  1. 設定の部分を別のヘッダファイルにして、本体プログラムから隠し
  2. freeRTOSの機能を使って、OTAをloop()からではなく、OSスケジューラから呼ぶ

ように変更されています。ソースコードこちらにあります。この結果、全体が以下のように簡単になってます。

#define ESP32_RTOS 
#include "OTA.h"
void setup() { Serial.begin(115200); Serial.println("Booting"); setupOTA("TemplateSketch", "mySSID", "myPASSWORD"); // Your setup code } void loop() { // Your code here }

デバッグのためにシリアルポートを使ってますが、それ以外を見ると、 ヘッダファイルをインクルードして、setup()に1行追加しているだけです。これならOTAを使ってみようという気になります。そこで試してみました。

OTA対応Lチカ

まずはOTAではない、普通のLチカです。GPIOの14番に取り付けたLEDを1秒ごとに点滅します。

#define LED 14

void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
}

これをOTA対応に書き換えます。追加するのは太字の3行だけです。OTA.hは同じディレクトリに入れておく必要があります。#defineと#include以外の本体プログラムは、setup()にsetupOTA()が追加されただけです。

#define ESP32_RTOS
#include "OTA.h"
#define LED 14

void setup() {
  setupOTA("BlinkLED_OTA", "mySSID", "myPASSWORD");
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
}

これをシリアル経由で書き込みます。書き込む前は、Arduino IDEの接続手段にシリアルポートしか見えていませんでした。

でもこのプログラムが動き始めると、setup()の中のsetupOta()で指定した名前、BlinkLED_OTAで始まるネットワークポートが見えるようになってます。これを選択すると、新しいプログラムが書き込めます。OTAのネットワークポートが現れない場合は、ESP32をリセット(電源をoff/on)すると良いようでした。

もとのサンプルではSerialの設定もされていました。Serial.begin(115200);という記述です。シリアルは使わないので、その部分は削除しました。OTA.hには、Serial経由でメッセージを伝える部分が残っているので、削除しておいた方が良いかもしれません。gitの方には、シリアルではなくて、telnet経由でメッセージを送るバージョンも公開されています。ただ、MQTTを使う機会が多いので、メッセージはMQTT経由で流せば良いと思いました。それでtelnet版は試していません。

MQTTプログラムをOTA対応にする

ということで、MQTTを使ったプログラムもOTA対応にしました。OTA化したのは、こちらで作ったプログラムです。MQTTからのコマンドでLEDをon/offするものです。

diysmarthome.hatenablog.com

MQTTもOTAもどちらもWiFiを使うので、初期化の部分などでコンフリクトするのではないかと心配したのですが、問題ありませんでした。OTA.hを同じディレクトリに入れて、3行(以下の太文字の行)を追加するだけでOTA対応になりました。

#include "EspMQTTClient.h"
#define ESP32_RTOS
#include "OTA.h"

EspMQTTClient *client;

const int LED=14; //pin for LED
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "diysmarthome_cv3a2JTs";//適当な文字列

const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker addr
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/seton"; //sub topic
const char  PUBTOPIC[] = "mqttthing/geton"; //pub topic 
const char DBGTOPIC[] = "mqttthing/debug"; //debug topic  

void setup() {
  pinMode(LED, OUTPUT);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
  setupOTA("MQTT_OTA", SSID, PASS);
}

void onMessageReceived(const String& msg) {
  client->publish(DBGTOPIC, "Message received.");
  if(msg.compareTo("true")==0) {
    digitalWrite(LED, HIGH);
    client->publish(PUBTOPIC,"true");
  }
  else if(msg.compareTo("false")==0) {
    client->publish(PUBTOPIC,"false");
    digitalWrite(LED, LOW);
  }
}

void onConnectionEstablished() {
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback 
  client->publish(DBGTOPIC, "Connection established. !!!");
}

void loop() {
  client->loop();
}

もっと複雑なプログラムも大丈夫でした。以下で紹介したJEM-A HA端子をコントロールするプログラムも、3行追加でokでした。

diysmarthome.hatenablog.com

少しだけ改良

少しだけ改良しました。そのためにOTA.hを少しだけ書き直しました。ただ、この改良はしてもしなくてもあまり関係ないことなので、書き直したOTA.hは添付しないでおきます。

WiFi初期化の無駄を省く

すんなり動いたのですが、無駄なことをしている点が気に掛かりました。一つは、前述のように、WiFiを重複して初期化していることです。MQTTもOTAもどちらも同じWiFiを使ってます。それぞれのライブラリの中で、SSIDとパスワードを使って初期化してます。2回初期化しているので、最初の初期化は無駄になってるはずです。結局は、WiFiという名前の同一のクラスのクラスメソッドにアクセスしているので問題なく使用できてます。でも無駄なことをしている点が気に掛かりました。そこで、OTA.hの中で、WiFiに関係する行を全部コメントアウトしました。WiFi.hなどのWiFi関係のヘッダファイルのインクルードもコメントアウトしました。

ということで、WiFiの初期化はMQTTライブラリだけで行い、OTAライブラリでは初期化しないようにできました。SSIDとパスワードを渡す必要がなくなったので、setupOTAの引数からそれらを省きました。

ただこの方式では、MQTTライブラリでのWiFi初期化が完全に終了する前にOTAライブラリを起動してしまうと失敗するようです。なので、setupOTAの呼び出しを、setup()の中ではなく、MQTTの接続が完了した時に呼び出されるコールバック関数である、onConnectionEstablished()の中で行うように、場所を移動しました。

Serial.printの無駄を省く

有線では接続しないので、Serialに流れているデバッグメッセージは無用です。とはいえ、無いと寂しいかと思い、MQTTで流すようにOTA.hを変更しました。Serial.print()でフォーマットしているところはsprintf()を使ってテキスト列にしました。進捗報告の場所は、数字を間引いて、表示します。mosquitto_subしておけば、以下のように表示されます。実のところそれほど有用なメッセージは流れていないので、無駄なところに力を使ってしまった気がします。Serialを使用している部分をコメントアウトするか、単に放置しておいても良かったかもしれません。

mqttthing/debug Progress: 0%
mqttthing/debug Progress: 20%
mqttthing/debug Progress: 40%
mqttthing/debug Progress: 60%
mqttthing/debug Progress: 80%
mqttthing/debug Progress: 100%
mqttthing/debug End
mqttthing/debug OTA Initialized
mqttthing/debug IP address: 192:168:XXX:XXX
MQTTにデバッグメッセージを流すためには、MQTT関係の設定が終了した後で、OTA.hをインクルードする必要が生じました。そこで、インクルードする位置を、変数設定の後ろに移動しました。
以上の結果、以下のような場所に追加の3行(太字の行)が入ります。
#include "EspMQTTClient.h"
#define ESP32_RTOS

EspMQTTClient *client;

const int LED=14; //pin for LED
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "diysmarthome_cv3a2JTs";//適当な文字列

const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker addr
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/seton"; //sub topic
const char  PUBTOPIC[] = "mqttthing/geton"; //pub topic 
const char DBGTOPIC[] = "mqttthing/debug"; //debug topic  

#include "OTA.h"

void setup() {
  pinMode(LED, OUTPUT);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
}

void onMessageReceived(const String& msg) {
  client->publish(DBGTOPIC, "Message received.");
  if(msg.compareTo("true")==0) {
    digitalWrite(LED, HIGH);
    client->publish(PUBTOPIC,"true");
  }
  else if(msg.compareTo("false")==0) {
    client->publish(PUBTOPIC,"false");
    digitalWrite(LED, LOW);
  }
}

void onConnectionEstablished() {
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback 
  client->publish(DBGTOPIC, "Connection established. !!!");   
  setupOTA("MQTT_OTA");
}

void loop() {
  client->loop();
}

メインプログラムでは微妙な違いですが、OTA.hでは20行くらい手を入れました。

パスワードを設定する

サンプルプログラムだと、OTAのパスワードが省略されているので、WiFiに接続できる人が、Arduino IDEを起動すれば、誰でもコードを書き換えられます。それはちょっと困るので、パスワードを設定しました。OTA.hの中に、以下の行がありますので、

 // No authentication by default
 // ArduinoOTA.setPassword("admin");

コメントを外してパスワードを設定しておきます。すると書き込みの際にパスワードを聞かれるようになります。

まとめ

Arduino OTAの記述を簡単にする秘訣がYoutubeで紹介されていたので、それを使いました。設定を別ファイル(OTA.h)にして、FreeTOSの機能を使ってloop()での記述を避ける方法でした。gitにあるOTA.hで問題なく動きますし、MQTTなどの、同じくWiFiを使うライブラリとのコンフリクトもありませんでした。その部分を書き直したのですが、作業しなくても問題なく動きます。