M5StickCPlus2で、GitHubのPrivateリポジトリに置いたファームウェアを使ってOTA (Over-The-Air) アップデートを行う仕組みを構築しました。
備忘録として、手順とコードを共有します。
概要
- M5StickCPlus2 が定期的にGitHubのリポジトリにある
version.txtを見に行きます。 - 自分のバージョンより新しいバージョンが書かれていれば、
.binファイル(ファームウェア)をダウンロードします。 - 自動的に書き換えて再起動します。
これにより、遠隔に設置したM5StickCPlus2に対しても、GitHubにファイルをアップロードするだけでファームウェアの更新が可能になります。
手順
1. GitHubでの準備
Private Repositoryの作成
GitHubで新しいリポジトリを作成します。公開したくない場合は Private を選択します。
(例: MyOTARepo)
Personal Access Token (PAT) の取得
ESP32からPrivateリポジトリにアクセスするために、トークンが必要です。
- GitHubの右上のアイコン -> Settings -> 左下のDeveloper settings -> Personal access tokens -> Tokens (classic) を選択。
- 右上のGenerate new token (classic) をクリック。
-
Note に適当な名前(例:
M5StickOTA)を入力。 - Select scopes で repo (Full control of private repositories) にチェックを入れます。これが必須です。
-
Generate token をクリックし、表示されたトークン(
ghp_から始まる文字列)をコピーして保存しておきます。(この画面を閉じると二度と見れません!)
2. コードの準備
以下の2つのファイルを作成します。
secrets.h
WiFi設定やトークンなどの機密情報を記述します。
#ifndef SECRETS_H
#define SECRETS_H
// WiFi設定
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Ambient設定 (動作確認用)
unsigned int channelID = 12345;
const char* writeKey = "YOUR_AMBIENT_WRITE_KEY";
// GitHub設定
const char* githubToken = "ghp_xxxxxxxxxxxxxxxxxxxx"; // 先ほど取得したトークン
const char* githubRepo = "YourUserName/MyOTARepo"; // ユーザー名/リポジトリ名
const char* githubBranch = "main"; // ブランチ名
// ファイル設定
const char* binFileName = "M5StickCPlus2_OTA.ino.bin";
const char* versionFileName = "version.txt";
#endif
M5StickCPlus2_OTA.ino
メインのスケッチです。
ソースコード(M5StickCPlus2_OTA.ino)は長いので、最後の項に記載します。
- 定期的にGitHubのバージョンを確認
- 画面中央に現在のバージョンを表示
- 動作確認としてAmbientにバージョン情報を送信
今回Ambient自体の設定方法は説明は省略します。
3. バイナリファイルの作成とアップロード
- Arduino IDEでスケッチを開き、メニューからツール -> ボード:M5Stack -> M5StickCPlus2 を選び、次にツールから以下の設定をします。
CPU Frequency:240MHz
Core Debug:None
Erase All Flash:Enable
Event Run On Core1
Flash Frequency:80MHz
Flash Mode: QIO
Flash Size: 8MB
Arduino Run On Core1
Partition Scheme: 8MB with spiffs(3MB APP/1.5MB SPIFFS)
PSRAM: Disable
Upload Speed: 1500000
- Arduino IDEのメニューの スケッチ -> コンパイル済バイナリをエクスポート を選択します。
- スケッチのフォルダの中に
build\m5stack.esp32.m5stack_stickc_plus2フォルダができ、その中に.binファイルが生成されます(例:M5StickCPlus2_OTA.ino.bin)。M5StickCPlus2_OTA.ino.binのファイルサイズは1200KBより少し小さいサイズになりました。 - この
.binファイルをGitHubリポジトリにアップロードします。
4. バージョンファイルの作成とアップロード
- テキストエディタで
version.txtというファイルを作成します。 - 中身に新しいバージョン番号(例:
1.0.1)だけを書きます。 - GitHubリポジトリにアップロードします。
1.0.1
5. スケッチファイルをM5StickCPlus2に書き込み
- USBケーブルを接続して、Arduino IDEでスケッチを、M5StickCPlus2に書き込みます。このとき、スケッチ内の
currentFirmwareVersionは、上記3のbinファイルを作成したものより古いバージョンにしておきます。(例)binファイル内のcurrentFirmwareVersionが1.0.1かつGitHubのversion.txt内の記述が1.0.1の場合には、今回USBケーブルを接続して書き込むスケッチのcurrentFirmwareVersionは1.0.0。 - M5StickC Plus2が起動または定期チェック時に更新を検知。
-
version.txt内の記述のバージョンが新しい場合、github内のM5StickCPlus2_OTA.ino.binをダウンロード。 - アップデート完了後、再起動してバージョン
1.0.1になります。ディスプレイに表示されています。遠隔からバージョンを確認する時はAmbientで確認します。
注意点
-
Privateリポジトリの制限:
-
raw.githubusercontent.comのレート制限は、通常 1時間あたり5,000リクエスト(IPアドレスごと) です。 - このコードでは5分間隔(1時間に12回)でチェックしているため、個人で数台運用する分には制限に引っかかる心配はほぼありません。ただし、短時間に大量のアクセスを行うと(例えば秒単位の連打など)、一時的にブロックされる可能性があるため注意してください。
-
-
パーティションスキーマ: OTAを行うには十分なフラッシュ領域が必要です。Arduino IDEのツールメニューで
Partition Schemeを8MB with spiffs(3MB APP/1.5MB SPIFFS)またはDefault 4M with spiffs(1.2MB APP/1.5MB SPIFFS)など、OTA可能なものに設定してください(今回、8MB Flashを持つESP32を使っているM5StickCPlus2を使っています。4MB Flashを持つESP32を使っても小さいプログラムなら動くかもしれませんが、安全のため、8MB Flashを持つESP32を使っているデバイスを使った方が、プログラムが大きくなっても安心です。)
以上です。これで遠隔地のデバイスも最新コードに保つことができます!
参考にしたスケッチ(共通点と相違点)
参照にしたIoT BhaiのスケッチはGitHubの正式なRelease機能を使った「APIベース」の本格的なアプローチに対して、今回のスケッチはシンプルさを重視した「ファイルベース」のアプローチです。
共通点
- 認証: どちらも GitHub Token (PAT) をAuthorizationヘッダー (token ...) に入れてPrivateリポジトリにアクセスしています。
- 書き込み: どちらも標準の Update ライブラリを使用してフラッシュメモリへの書き込みと再起動を行っています。
- HTTPS: どちらも WiFiClientSecure を使い、setInsecure() または setFollowRedirects で安全な通信を行っています。
相違点の比較
| 比較項目 | 今回のスケッチ | IoT Bhaiのスケッチ |
|---|---|---|
| 更新検知の方法 | Rawファイル確認: リポジトリ内の version.txt というテキストファイルの中身を直接見に行きます。 | GitHub API (Releases)GitHubのRelease機能: (api.github.com/.../releases/latest)を使って、最新のリリースタグ (tag_name) をJSONで取得します。 |
| バイナリ取得 | Rawファイル直指定: src フォルダなどにある .bin ファイルをURL直打ちでダウンロードします。 | Release Asset検索: JSON内の assets リストから、指定したファイル名 (.bin) を探し出し、そのダウンロードURLを取得します。 |
| ライブラリ依存 | 最小限HTTPClient と標準ライブラリのみで動作します。 | JSONパースが必要: ArduinoJson ライブラリを使用して、APIからのJSONレスポンスを解析しています。 |
| メリット | 構成が非常に楽テキストファイルとbinファイルを置くだけで動くため、GitタグやRelease機能を知らなくても使えます。 | 管理がしっかりしているGitHubの「Release」機能でバージョン履歴を管理でき、古いバージョンに戻す運用などがしやすいです。 |
ソースコード (M5StickCPlus2_OTA.ino)
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Ambient.h>
#include <Update.h>
// 設定ファイル (WiFiパスワードやトークンはこちらに記述)
#include "secrets.h"
// M5StickCPlus2用設定
int cycle = 0; // 点滅インジケーター用
// 現在のファームウェアバージョン (書き換えて更新を確認します)
const char* currentFirmwareVersion = "1.0.1";
float currentFirmwareVersion_for_ambient = 1.01; // Ambient送信用の数値型バージョン
WiFiClient client;
Ambient ambient;
unsigned long previous_time_ambient = 0;
unsigned long previous_time_blink = 0;
// 動作間隔設定
const unsigned long ambient_interval = 1 * 60 * 60 * 1000; // 1hごとのAmbient送信
// firmware更新チェック
static unsigned long firmware_lastCheckTime = 0;
const unsigned long firmware_check_interval = 5 * 60 * 1000; // 5minごとのfirmware更新チェック
// プロトタイプ宣言
String fetchLatestVersion();
void downloadAndApplyFirmware(String newVersion);
void checkForFirmwareUpdate();
String getGitHubRawUrl(String fileName);
void checkWiFiConnection();
// === WiFi接続をチェック ===
void checkWiFiConnection() {
// WiFi接続を確認
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected. Attempting to reconnect...");
WiFi.disconnect(true);
delay(100);
// WiFi_Connect.inoのパターンを採用: WiFi.begin()を1回呼び出し、接続が確立するまで待機
Serial.print("Attempting to connect to SSID: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
unsigned long reconnect_start_time = millis();
int attempt_count = 0;
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
// 60秒でタイムアウト
if (millis() - reconnect_start_time >= 60000UL) {
Serial.println("Failed to reconnect to WiFi after 60 seconds. Restarting ESP.");
delay(2000);
ESP.restart();
}
}
}
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
Serial.println("Setup starting...");
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(0, 0);
M5.Lcd.println("Connecting WiFi...");
// 1. WiFi接続
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
unsigned long wifi_start_time = millis();
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
// 60秒でタイムアウト
if (millis() - wifi_start_time >= 60000UL) {
Serial.println("Initial WiFi connection failed after 60 seconds. Restarting.");
delay(2000);
ESP.restart();
}
}
Serial.print("WiFi connected\n\nSSID:");
Serial.println(WiFi.SSID()); //Output Network name.
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI()); //Output signal strength.
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
Serial.println("Initial WiFi setup complete.");
ambient.begin(channelID, writeKey, &client);
// 初回Ambient送信
if (WiFi.status() == WL_CONNECTED) {
ambient.set(1, currentFirmwareVersion_for_ambient);
ambient.send();
Serial.println("Initial Ambient sent: Version");
}
// 2. 起動時の更新チェック
if (WiFi.status() == WL_CONNECTED) {
checkForFirmwareUpdate();
}
Serial.println("--- Current Ver ---");
Serial.println(currentFirmwareVersion);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE);
// 画面中央付近に表示
M5.Lcd.setCursor(10, 50);
M5.Lcd.printf("Ver: %s", currentFirmwareVersion);
}
void loop() {
// M5StickCPlus2の更新が必要な場合(ボタンなど)
M5.update();
checkWiFiConnection();
unsigned long now = millis();
//firmware更新チェック
if (millis() - firmware_lastCheckTime >= firmware_check_interval) {
Serial.print(F("--loop-- Current Version ---"));
Serial.println(currentFirmwareVersion);
if (WiFi.status() == WL_CONNECTED) {
checkForFirmwareUpdate();
}
firmware_lastCheckTime = millis();
}
// 定期的にFirmware VersionをAmbientに送信
if (now - previous_time_ambient >= ambient_interval) {
if (WiFi.status() == WL_CONNECTED) {
ambient.set(1, currentFirmwareVersion_for_ambient);
ambient.send();
Serial.println("Periodic Ambient sent: Version");
previous_time_ambient = millis();
}
}
// 点滅インジケーター (1秒ごとに点滅)
if (now - previous_time_blink >= 1000) {
if(cycle == 0){
M5.Lcd.fillRect(220, 110, 20, 25, BLACK);
M5.Lcd.setCursor(220,110,1);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("*");
}else{
M5.Lcd.fillRect(220, 110, 20, 25, BLACK);
}
cycle=(cycle + 1) % 2;
previous_time_blink = millis();
}
//テストボタン
if (M5.BtnA.wasPressed()) {
Serial.println("Test button was pressed.");
if (WiFi.status() == WL_CONNECTED) {
ambient.set(1, currentFirmwareVersion_for_ambient);
ambient.send();
Serial.println("Button Ambient sent: Version");
}
}
}
// ============================================
// OTA 関連関数
// ============================================
// GitHubのRawファイルへのURLを作成するヘルパー関数
String getGitHubRawUrl(String fileName) {
// 形式: https://raw.githubusercontent.com/<user>/<repo>/<branch>/<file>
return String("https://raw.githubusercontent.com/") + githubRepo + "/" + githubBranch + "/" + fileName;
}
void checkForFirmwareUpdate() {
Serial.println("Check Update...");
// RAMチェック
if (ESP.getFreeHeap() < 30000) {
Serial.println("Low RAM. Skip.");
return;
}
String latestVersion = fetchLatestVersion();
if (latestVersion == "") {
Serial.println("Fetch err.");
return;
}
// バージョン文字列のクリーニング
String cleanVersion = "";
for (int i = 0; i < latestVersion.length(); i++) {
if (isdigit(latestVersion[i]) || latestVersion[i] == '.') {
cleanVersion += latestVersion[i];
}
}
latestVersion = cleanVersion;
if (latestVersion != currentFirmwareVersion) {
Serial.println("New Ver: " + latestVersion);
downloadAndApplyFirmware(latestVersion);
} else {
String msg = "Up to date: " + String(currentFirmwareVersion);
Serial.println(msg);
}
}
String fetchLatestVersion() {
String latestVersion = "";
WiFiClientSecure* client = new WiFiClientSecure;
if (!client) return "";
client->setInsecure(); // 証明書検証スキップ
client->setTimeout(10000);
HTTPClient http;
String url = getGitHubRawUrl(versionFileName);
Serial.println("URL: " + url);
if (http.begin(*client, url)) {
// Privateリポジトリへのアクセスにはトークンヘッダーが必須
String authHeader = "token " + String(githubToken);
http.addHeader("Authorization", authHeader);
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
latestVersion = http.getString();
latestVersion.trim();
} else {
Serial.printf("HTTP Err: %d\n", httpCode);
// トークンエラーの場合は403/404が返ることが多い
if (httpCode == 403 || httpCode == 404) {
Serial.println("Check Token/Repo");
}
}
http.end();
} else {
Serial.println("Connect Fail");
}
delete client;
return latestVersion;
}
void downloadAndApplyFirmware(String newVersion) {
Serial.println("Downloading...");
WiFiClientSecure* client = new WiFiClientSecure;
if (!client) return;
client->setInsecure();
client->setTimeout(30000);
HTTPClient http;
String url = getGitHubRawUrl(binFileName);
Serial.println("DL URL: " + url);
if (http.begin(*client, url)) {
// BINファイルのダウンロードにもトークンが必要
String authHeader = "token " + String(githubToken);
http.addHeader("Authorization", authHeader);
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
int contentLength = http.getSize();
Serial.printf("Size: %d\n", contentLength); Serial.println("");
if (contentLength > 0) {
if (Update.begin(contentLength)) {
Serial.println("Writing Flash...");
WiFiClient* stream = http.getStreamPtr();
uint8_t buff[1280];
size_t written = 0;
while (http.connected() && (written < contentLength)) {
size_t size = stream->available();
if (size) {
int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
Update.write(buff, c);
written += c;
// 進捗表示
if (written % 10000 == 0) {
Serial.print(".");
}
// パーセント表示(シリアルのみ適度に)
int percent = written * 100 / contentLength;
static int lastPercent = 0;
if (percent - lastPercent >= 10) {
lastPercent = percent;
Serial.printf("Progress: %d%%\n", percent);
}
}
yield();
}
Serial.println("");
if (written == contentLength) {
if (Update.end() && Update.isFinished()) {
Serial.println("Success! Reboot");
delete client;
delay(1000);
ESP.restart();
}
} else {
Serial.printf("Err: %d/%d\n", written, contentLength);
}
} else {
Serial.println("No Space for OTA");
}
}
} else {
Serial.printf("HTTP Fail: %d\n", httpCode);
}
http.end();
}
delete client;
}
