M5StickCでWeb Update OTAをためす

概要

アプリランチャーっぽいものをSDカードのないM5StickCでできるか検討したところ、Web経由のOTAが楽そうと思い、実験してみました。

OTAとは?

Over The Airの略で、無線経由でなにかをアップデートすることをいいますが、無線以外の場合もOTAと表す場合があります。

ESP32の場合には、ESP32からファームウエアをSDカードやHTTP経由でダウンロードすることの他に、ESP32が待ち受けているポートやHTTPサーバーに対して、ファイルをアップロードする方法があります。

上記の記事でもまとめてあります。今回はAWS_S3_OTA_Updateをベースにして、アプリランチャーからアプリを選択して、HTTP経由でダウンロード。利用し終わったらアプリランチャーに戻る検証をしました。

作成物の説明

スケッチ名概要
M5StickC-Update最初にWi-Fiの設定とメニューを読み込む
M5StickC-Update-Menu実際のアプリランチャー
M5StickC-Update-NTPNTPを利用してRTCを設定するサンプルアプリ
M5StickC-Update-MovingIconsLovyanGFXのサンプルアプリ

最初にM5StickC-Updateを実行して、Wi-Fi設定をして実際にアプリランチャーをOTAしています。このアプリが動かないってことはWi-Fi設定がちゃんとできていないってことになります。

直接アプリランチャーを入れてもいいのですが、Wi-Fi設定ができていないと切り分けが難しいので、分離しました。アプリランチャーは単純にアプリ2を選択でき、OTAするだけの作りです。

サンプルアプリは既存のアプリにボタン押したらランチャーアプリにOTAで戻る仕組みを追加しています。

容量で左右すると思いますが、700キロバイトぐらいのファームウエアが15秒弱ぐらいでOTAできました。OTA実行中は画面が止まってしまうのでわかりにくいですが、別タスクなどでアニメーションを入れたほうがいいかもしれません。

書込アプリスケッチ

#include <M5StickC.h>
#include <WiFi.h>
#include <Update.h>

WiFiClient client;

void execOTA(String host, int port, String bin);

void setup() {
  //WiFiに接続したことがない場合には接続してください
  //WiFi.begin("SSID", "KEY");

  String host = "lang-ship.com";
  int port = 80;
  String bin = "/tools/update-esp32/M5StickC-Update-Menu.ino.m5stick_c.bin";
  execOTA(host, port, bin);
}

void loop() {
}

// Utility to extract header value from headers
String getHeaderValue(String header, String headerName) {
  return header.substring(strlen(headerName.c_str()));
}

// OTA Logic
void execOTA(String host, int port, String bin) {
  Serial.println("Connecting to Wi-fi");

  // Connect to provided SSID and PSWD
  WiFi.begin();

  // Wait for connection to establish
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print("."); // Keep the serial monitor lit!
    delay(500);
  }

  // Connection Succeed
  Serial.println("");
  Serial.println("Connected to Wi-Fi");

  long contentLength = 0;
  bool isValidContentType = false;

  Serial.println("Connecting to: " + String(host));
  if (client.connect(host.c_str(), port)) {
    Serial.println("Fetching Bin: " + String(bin));

    client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Cache-Control: no-cache\r\n" +
                 "Connection: close\r\n\r\n");

    unsigned long timeout = millis();
    while (client.available() == 0) {
      if (millis() - timeout > 5000) {
        Serial.println("Client Timeout !");
        client.stop();
        return;
      }
    }

    while (client.available()) {
      String line = client.readStringUntil('\n');
      line.trim();

      if (!line.length()) {
        break;
      }

      if (line.startsWith("HTTP/1.1")) {
        if (line.indexOf("200") < 0) {
          Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
          break;
        }
      }

      if (line.startsWith("Content-Length: ")) {
        contentLength = atol((getHeaderValue(line, "Content-Length: ")).c_str());
        Serial.println("Got " + String(contentLength) + " bytes from server");
      }

      if (line.startsWith("Content-Type: ")) {
        String contentType = getHeaderValue(line, "Content-Type: ");
        Serial.println("Got " + contentType + " payload.");
        if (contentType == "application/octet-stream") {
          isValidContentType = true;
        }
      }
    }
  } else {
    Serial.println("Connection to " + String(host) + " failed. Please check your setup");
  }

  Serial.println("contentLength : " + String(contentLength) + ", isValidContentType : " + String(isValidContentType));

  if (contentLength && isValidContentType) {
    bool canBegin = Update.begin(contentLength);

    if (canBegin) {
      Serial.println("Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!");
      size_t written = Update.writeStream(client);

      if (written == contentLength) {
        Serial.println("Written : " + String(written) + " successfully");
      } else {
        Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" );
      }

      if (Update.end()) {
        Serial.println("OTA done!");
        if (Update.isFinished()) {
          Serial.println("Update successfully completed. Rebooting.");
          ESP.restart();
        } else {
          Serial.println("Update not finished? Something went wrong!");
        }
      } else {
        Serial.println("Error Occurred. Error #: " + String(Update.getError()));
      }
    } else {
      Serial.println("Not enough space to begin OTA");
      client.flush();
    }
  } else {
    Serial.println("There was no content in the response");
    client.flush();
  }
}

ライブラリ化すればスッキリしますが、後半の関数はAWS_S3_OTA_Updateをそのまま持ってきています。

void setup() {
  //WiFiに接続したことがない場合には接続してください
  //WiFi.begin("SSID", "KEY");

ESP32のWi-Fi設定は最後に接続したものを保存していますので、すでにWi-Fi接続済みであれば上記の設定は必要ありません。実用化のためには複数AP情報を登録できたり、SmartConfigに対応したり、M5StickCから登録したり、APモードでブラウザから設定できたりしたほうがいいと思います。

  String host = "lang-ship.com";
  int port = 80;
  String bin = "/tools/update-esp32/M5StickC-Update-Menu.ino.m5stick_c.bin";
  execOTA(host, port, bin);

ベタ書きで、ファームウエアを指定してOTAしています。execOTAの中身はAWS_S3_OTA_Updateのものを持ってきています。

実はこれだけでOTAできてしまいます。ArduinoJsonとかをつかって、サーバーと現在のバージョンやファームウエアのリストを解析して、更新があったらOTAなども実現可能です。

上記のライブラリとかは、バージョンチェックとOTAまでやってくれそうです。

このライブラリはもう一歩進んで、IoTプラットフォームとしてIOTAppStory.comというのがあるみたいです。とはいえ、Googleで検索しても、件数があまりでないからあまり利用されていないのかな?

まとめ

本当はMQTTなどを使って、端末とサーバーでメッセージのやりとりも含めたライブラリにすることで、サーバー上に新しいファームウエアをアップロードして、ポチッとボタン押すことで特定の端末にOTAすることなども可能になります。

MTQQ使わなくても、定期的にサーバーにチェックして、次はこのファームウエアに更新みたいな仕組みができたらいいなと思っています。

Oracle Cloud Free Tierで無料のサーバーが作れたので、時間が取れたらもう少し実験してみたいと思います。

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。