1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5StickCPlus2でGitHub Privateリポジトリを使ったOTAを試した

Last updated at Posted at 2026-01-07

M5StickCPlus2で、GitHubのPrivateリポジトリに置いたファームウェアを使ってOTA (Over-The-Air) アップデートを行う仕組みを構築しました。
備忘録として、手順とコードを共有します。

概要

  1. M5StickCPlus2 が定期的にGitHubのリポジトリにある version.txt を見に行きます。
  2. 自分のバージョンより新しいバージョンが書かれていれば、 .bin ファイル(ファームウェア)をダウンロードします。
  3. 自動的に書き換えて再起動します。

これにより、遠隔に設置したM5StickCPlus2に対しても、GitHubにファイルをアップロードするだけでファームウェアの更新が可能になります。

image.png

手順

1. GitHubでの準備

Private Repositoryの作成

GitHubで新しいリポジトリを作成します。公開したくない場合は Private を選択します。
(例: MyOTARepo

Personal Access Token (PAT) の取得

ESP32からPrivateリポジトリにアクセスするために、トークンが必要です。

  1. GitHubの右上のアイコン -> Settings -> 左下のDeveloper settings -> Personal access tokens -> Tokens (classic) を選択。
  2. 右上のGenerate new token (classic) をクリック。
  3. Note に適当な名前(例: M5StickOTA)を入力。
  4. Select scopesrepo (Full control of private repositories) にチェックを入れます。これが必須です。
  5. Generate token をクリックし、表示されたトークン(ghp_から始まる文字列)をコピーして保存しておきます。(この画面を閉じると二度と見れません!)

2. コードの準備

以下の2つのファイルを作成します。

secrets.h

WiFi設定やトークンなどの機密情報を記述します。

secrets.h
#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. バイナリファイルの作成とアップロード

  1. 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
  1. Arduino IDEのメニューの スケッチ -> コンパイル済バイナリをエクスポート を選択します。
  2. スケッチのフォルダの中に build\m5stack.esp32.m5stack_stickc_plus2 フォルダができ、その中に .bin ファイルが生成されます(例: M5StickCPlus2_OTA.ino.bin)。M5StickCPlus2_OTA.ino.binのファイルサイズは1200KBより少し小さいサイズになりました。
  3. この .bin ファイルをGitHubリポジトリにアップロードします。

4. バージョンファイルの作成とアップロード

  1. テキストエディタで version.txt というファイルを作成します。
  2. 中身に新しいバージョン番号(例: 1.0.1)だけを書きます。
  3. GitHubリポジトリにアップロードします。
version.txt
1.0.1

5. スケッチファイルをM5StickCPlus2に書き込み

  1. USBケーブルを接続して、Arduino IDEでスケッチを、M5StickCPlus2に書き込みます。このとき、スケッチ内の currentFirmwareVersion は、上記3のbinファイルを作成したものより古いバージョンにしておきます。(例)binファイル内の currentFirmwareVersion1.0.1かつGitHubの version.txt 内の記述が 1.0.1 の場合には、今回USBケーブルを接続して書き込むスケッチのcurrentFirmwareVersion1.0.0
  2. M5StickC Plus2が起動または定期チェック時に更新を検知。
  3. version.txt内の記述のバージョンが新しい場合、github内のM5StickCPlus2_OTA.ino.bin をダウンロード。
  4. アップデート完了後、再起動してバージョン 1.0.1 になります。ディスプレイに表示されています。遠隔からバージョンを確認する時はAmbientで確認します。

注意点

  • Privateリポジトリの制限:
    • raw.githubusercontent.com のレート制限は、通常 1時間あたり5,000リクエスト(IPアドレスごと) です。
    • このコードでは5分間隔(1時間に12回)でチェックしているため、個人で数台運用する分には制限に引っかかる心配はほぼありません。ただし、短時間に大量のアクセスを行うと(例えば秒単位の連打など)、一時的にブロックされる可能性があるため注意してください。
  • パーティションスキーマ: OTAを行うには十分なフラッシュ領域が必要です。Arduino IDEのツールメニューで Partition Scheme8MB 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: Mastering ESP32(English)/2. ESP32 OTA Firmware Update from Github Private Repo/OTA_update_with_Github_Private_Repo

参照にした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)

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;
}


1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?