スキップしてメイン コンテンツに移動

ESP32のOTA(Over The Air)をスマホからやる:ESP32側の話

開発環境

開発環境はVS code + platformIOです。ESP32はこの環境が一番楽かと。
あとはプラグインとしてC/C++ Extension PackやDoxygen Documentation Generator
便利小道具系でしょうか
C/C++ Extension Packだけは入れて置いた方がいいかと

ESP32側の話

色々やり方はあるかと思いますが、Wi-Fiでパケットを受け取って、そのパケットを2面あるプログラム用のフラッシュに書いていく。という方法をとります。
いろんな人のサンプルを見てると、httpを使う人が多いのだけど・・・スマホからファームのアップデートをするのにはちょっと面倒なので。

1.プロジェクトの作成

つーてもプロジェクトウィザードで普通のESP32用のプロジェクトを作るだけ

2.パーティション用にiniファイルを書き換える

出来上がったプロジェクトにplatform.iniというのがあるので、それを自分の使いやすいように編集する
んで、ここで重要なのはパーティションをどう切り分けるのかと言うこと

[env:esp32doit-devkit-v1]
platform = espressif32
board = esp32doit-devkit-v1
framework = arduino

monitor_speed = 115200
board_build.partitions = partitions.csv

なのでパーティションを切り分けてあげるのだけど、その設定はcsvファイルで出来ているのでその切り分けファイルを読み込ませる必要がある。
今回はpartisions.csvというファイルに書いてあるますよと宣言するのにboard_build.partitions = partitions.csvと書いて上げる。
あとは、シリアルが遅いといやなのでmonitor_speed=115200と。

3.パーティション用のcsvを書く

自分で書くのもいいけど・・・デフォルトのパーティション設定ファイルが
{$Windowshome}.platformio\packages\framework-arduinoespressif32\tools\partitions

というところにあるので、それっぽいのを持ってきます

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
spiffs,   data, spiffs,  0x290000,0x160000,
coredump, data, coredump,0x3F0000,0x10000,

今回使ったのは上のCSV。上のota_0,ota_1というのがプログラムを書き込む領域。otaというのがどの面でブートするのかという情報。

coredumpというのがダンプファイルを吐き出す領域みたいだけど、コアダンプなんか見てもわかんないしな・・・

spiffsはファイルの領域。 fatを選ぶことも出来る(そういうサンプルも同じフォルダに入っている)
ファイル領域そんなにいるか?
まあ、フォントファイルとか入れるのに使うかね。まあこれはサンプルなので、自分で適当に気に入ったサイズに直せばいいし、今回はOTAやりたいだけだし無視。

4.コーディング(アップデート用のクラス)

次のソースを見た人の中には「何だそれだけかよ」って人も多いと思う。
要は受け取ったパケットをESP32のAPIを使って書き込んでいくだけなので。

ヘッダファイル(UpdateClass.h)

#pragma once

#include <Arduino.h>
#include "esp_ota_ops.h"

/// @brief ファームウェアアップデートクラス
class UpdateClass
{
private:
    esp_ota_handle_t _update_handle;        ///< アップデートハンドル
    bool _image_header_was_checked = false; ///< イメージヘッダがチェックされたか

public:
    UpdateClass(/* args */);
    ~UpdateClass();

    esp_err_t setData(uint8_t *data, size_t size);  // データをセット
    esp_err_t end();                                //  終了
};

ソースファイル(UpdateClass.cpp)

#include "UpdateClass.h"

/// @brief コンストラクタ
UpdateClass::UpdateClass() {
   
}

/// @brief デストラクタ
UpdateClass::~UpdateClass() {
   
}

esp_err_t UpdateClass::setData(uint8_t *data, size_t size)
{  
    // データを受信
    if(_image_header_was_checked == false)
    {
        Serial.printf("ヘッダチェック\n");

        // esp_image_header_t 24バイト + esp_image_segment_header_t 16バイト + esp_app_desc_t 16バイト
        if (size > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) // ヘッダサイズ以上ならば(いらないかもね)
        {
            auto ret = esp_ota_begin(esp_ota_get_next_update_partition(NULL), OTA_SIZE_UNKNOWN, &_update_handle); // アップデート開始の合図
            if (ret != ESP_OK)
            {
                Serial.printf("esp_ota_begin failed [%d]\n", ret);
                return ret;
            }
            _image_header_was_checked = true; // ヘッダチェック済み
        }
        else
        {
            Serial.printf("ヘッダサイズ不足\n");
            return ESP_FAIL;
        }
    }

    if (_image_header_was_checked == true)
    {
        Serial.printf("esp_ota_write 実行\n");
        auto ret = esp_ota_write(_update_handle, (const void *)data, size);
        if (ret != ESP_OK)
        {
            esp_ota_abort(_update_handle);
            Serial.printf("esp_ota_write failed [%d]\n", ret);
            return ret;
        }
    }
    return ESP_OK;
}

esp_err_t UpdateClass::end()
{
    auto ret = esp_ota_end(_update_handle);
    if (ret != ESP_OK)
    {
        Serial.printf("esp_ota_end failed [%d]!\n", ret);
        return ret;
    }
    else
    {
        if (ESP_OK == esp_ota_set_boot_partition(esp_ota_get_next_update_partition(NULL)))
        {
            Serial.println("リブート!");
            esp_restart();
        }
        else
        {
            Serial.println("Upload Error");
        }
        return ESP_OK;
    }
}

#include "esp_ota_ops.h"

このヘッダがespressifがOTAのために用意してくれたヘッダファイル。
この中にある関数を使ってOTA出来るようになる。

setData()が、外部から受け取ったパケットをフラッシュにちまちま書いていくメソッド。
end()が、全部のパケットを書き込んだら「終わりました」とAPIに伝えるメソッド。

4.1  esp_err_t UpdateClass::setData(uint8_t *data, size_t size)

パケットをフラッシュに書込をするメソッド。

まず、ヘッダデータが全部存在するかをチェック。
このデータが揃うまでは書込をスタートしてはならない。
まあそんなデカいサイズではないのでそんなチェックしないでもいいかもだけど。
厳密には、そのデータが取得出来るまでバッファに溜めておく必要がある。

esp_ota_begin()に第1引数にどの面に書き込むか、第2引数にイメージサイズ、第3引数でハンドラの受取をする・・・のだけど
第1引数にesp_ota_get_next_update_partition(NULL)、第2引数にOTA_SIZE_UNKNOWNを指定しておけばあいてるところを勝手に選んで書いてくれる。
ただ、フラッシュの領域よりも書き込むサイズが大きくても、チェックしてくれなくなるからイメージサイズは指定した方がいいかも。

ドキュメント読んでるとotaを何面も持てるみたいだから、そのためにこういう引数を用意しているのだろう。

第3引数にハンドラと呼ばれるデータが格納される。
これは、ヘッダのprivate部で宣言している(追っかけていくとuint32_tをtypedefしてるだけだったりする)。

で、無事ハンドラが取れたら
esp_ota_write()関数を使用してハンドラ、データ、サイズを渡すとフラッシュに指定サイズを書き込む。

4.2  esp_err_t UpdateClass::end()

ファームウェアの書込を完了するメソッド

setDataメソッドで全データの書込が終わったらesp_ota_end(_update_handle)で書込を完了(ハンドルを渡してやると今やってることがおしまいと判定してくれる)。
esp_ota_set_boot_partition()が、次のブート領域が変更になったことをotadataに書き込んでくれる。

そして、esp_restart()でリブート!
すると、新しいファームでリブートしてくれるという寸法。

4.3 UpdateClassについて

こんな感じで書込クラスを作りました。クラスを見ただけで後は全部自分で出来るわって人も多いと思います。
そんなわけでこれ以降は蛇足

5.main.cpp

メインロジックですが・・・いるかな?

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiAP.h>
#include <UpdateClass.h>

/// @name WiFi設定
/// @{
const char *ssid = "ssidTest";          //!< SSID
const char *password = "passwordTest";  //!< パスワード
/// @}

WiFiServer _wifiserver(80);             //!< WiFiサーバー ポート80(80だけどwebサーバじゃないよ)
UpdateClass _update;                    //!< ファームウェアアップデートクラス

/// @brief セットアップ
void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println("Configuring access point...");

    if (!WiFi.softAP(ssid, password))           // アクセスポイントモードでWiFi接続
    {
        log_e("Soft AP creation failed.");
        while (1)
            ;
    }
    IPAddress myIP = WiFi.softAPIP();           // IPアドレスの自動取得
    Serial.print("AP IP address: ");
    Serial.println(myIP);

    _wifiserver.begin();                        // wifiサーバー開始

    Serial.println("Server started");
}

#define DATA_SIZE (1024 * 8)        //!< データバッファサイズ
const int DATA_MAX_SIZE = 1118240;  //!< ファームウェアのサイズ(本来はファームウェアのサイズを取得する)

bool _isClientConnected = false;     //!< クライアント接続フラグ
WiFiClient _client;                  //!< クライアント
static uint8_t _buffer[DATA_SIZE];   //!< バッファ
static uint32_t _receieved_size = 0; //!< 受信サイズ

void loop() {
    if (!_isClientConnected) {          // 未接続ならば
        _client = _wifiserver.accept();     // 接続受付
        if (_client) {                      // クライアントが接続された場合
            Serial.println("New Client.");  //
            _isClientConnected = true;  // クライアント接続フラグを立てる
        }
    }
    if(_isClientConnected) {                // 接続されている場合
        if (_client.connected())
        {
            if (_client.available())        // クライアントがデータを送信している場合
            {
                auto readsize = _client.read(_buffer, DATA_SIZE);   // データを読み込む
                if (readsize > 0)                                           // データがあったら
                {
                    _receieved_size += readsize;                            // 送受信サイズ
                    _update.setData(_buffer, readsize);                     // データをセット
                    Serial.printf("受信サイズ : %d\n", _receieved_size);
                    if (_receieved_size >= DATA_MAX_SIZE)                   // 全て受信しきったら
                    {
                        _update.end();                                      // アップデート終了
                        _isClientConnected = false;
                        _client.stop();
                        _receieved_size = 0;
                    }
                }
            }
        }
        else
        {
            _isClientConnected = false;
            _client.stop();             // クライアント停止
        }      
    }
    delay(1000);                       // printfが見やすいようデバッグ用にウェイト(本来は不要)
}


5.1 設定部

このサンプルアプリはESP32がアクセスポイントになる前提で作成されています。

  • ssid、passwordを固定値で作成ししてますが・・・まあテストなので
  • WiFiServer:サーバにとして動作します(80)はport番号。そこは適当に自分の都合のいいポートにして下さい(通常は1000以上)
  • UpdateClass : さっき作ったアップデート用のクラス

5.2 void setup()

arduinoのおまじない関数。
WiFi.softAPでアクセスポイントモードでWifi開始。
_wifiserver.begin()でサーバスタート。

5.3 再び設定部

setupとloopの間。
こんなところにマクロ書いたり設定バリバリ書いたりして最低だな、俺。

const int DATA_MAX_SIZE = 1118240;  //!< ファームウェアのサイズ(本来はファームウェアのサイズを取得する)

これ、本当はダメな奴。スマホから送りつけてくるデータがこのサイズだと決め打ちしてるわけです。
まあテストなので。本来は電文を用意してサイズをスマホから通知するようにするなどしましょう。

static uint8_t _buffer[DATA_SIZE];   //!< バッファ

これがデータの受信バッファ
まあ8Kもあれば十分。

5.4 void loop()

これまたarduinoのおまじない関数。
OSから自動的にダイレクトループとして呼び出される・・・でいいんだよな。

_client = _wifiserver.accept();   // 接続受付

これでクライアントが接続してきたことを検知。

auto readsize = _client.read(_buffer, DATA_SIZE);   // データを読み込む

受信したデータを自分のバッファに取り込む。バッファのうちのどのサイズまでが受信データなのかはreadsizeに戻ってくる。

_receieved_size += readsize;                            // 送受信サイズ

送受信サイズを保存しておく

_update.setData(_buffer, readsize);                        // データをセット

さっき作ったUpdateClassのパケットをフラッシュに書き込む関数

んで・・・

if (_receieved_size >= DATA_MAX_SIZE)                   // 全て受信しきったら
{
    _update.end();                                      // アップデート終了
    _isClientConnected = false;
    _client.stop();
    _receieved_size = 0;
}

予定したサイズを全部受信したと判定したら_update.end()をコールするとフラッシュの書き込みを完了し、次にブートする領域を変更し、リブート!

なので実は(成功したら)_client.stop()なんていらないわけです(失敗したらちゃんと止めてください)

6.最後に

ちょっと長くなってしまったのです。

けど、実際にコーディングするとあまりたいしたことをやって無くて受け取ったパケットをフラッシュに書き込む、リブートの領域を切り替える・・・というのを全部APIが面倒を見てくれるのでそんな大変じゃ無いことがわかると思います。

また、プログラムを見て貰ったとおり要はパケットさえ受け取れればいいので、WifiだろうがBLEだろうが、シリアルだろうが、それこそSDカードから読み込んだバイナリファイルだろうが何でもいいことがわかります。

サンプルなのでまあ色々と固定値だったりエラーのハンドリングが何だこりゃっていいたくなるようないい加減さですけどね。

暇が出来たら送信側のスマホ編も作る予定
スマホ編は当然.NET MAUIだよ

続きはこちら

パクリ元はこちら1(espressifのOTAのページ)

パクリ元はこちら2(sparkFANのBLEでOTAのページ)

おしまい

 


コメント

このブログの人気の投稿

ImDiskの後継アプリのAIM toolkitを試してみた

ImDiskの開発辞めちゃったらしい というわけで、じゃあ後継ソフトはあるのかと思ったらあった 遅いと言われているけど・・・ とインストールしてみた ImDiskのこの間の結果はこちら 再びRAM DISK(ImDisk)   blog.mazepin-led.com    そして今回のAIM Toolkit なんか、無茶苦茶遅くなってるな 下手するとSSDの方が速いじゃん CPUの使用状態はこんな感じ PIO転送なところは変わって無さそう まあ、遅いからと言ってその速度が体感できるのかというと出来ないんだけどね と思ったところで、なんか設定変えてみたらどうなるのだろう Allocate Memory Dynamicallyというのは、メモリを必要に応じてってことだからと思って以前試したことがあったけど、なんかImDiskのときは不安定だったんだよな  AdvancedのとこにあるUse AWE Physical Memoryというのが良くわからないけど、チェックしてみたら速くなった おお、大分速くなった。なったけど・・・・なんで? これだとImDiskよりちょっと遅いくらいになるのか もしやQuickFormatととかでも変わるのか?と思ったけどそこまでやる気も無かったので放置。速くなるわけないよね。 後は圧縮とか、取り外しメディアとかだし。   最近の性能の良いSSDのおかげで RAM DISKというものの存在意義が薄れちゃったね まあそれでとにかく速度を稼ぎたいぜって人は SoftPerfect RamDiskでも使ってください 昔はPrimo Ramdisk使ってました。当時使ってたけどなかなか良かった。 当時SSD無茶苦茶高かったし。 今はほぼ無用になってしまったが・・・ 未だにスタンダード版は8GBまでなんだな キャッシュ領域にしたいなら大人しく余ってるSSDを使った方がいいんじゃ無いかって気がするけど  おしまい

RAM DISKを使ってみた(使ったのはImDisk)

GWだし、まあちょっとラムディスクを入れてみました うちのPCはWindows11 使ったのはImDiskというRAM Disk。 まあ、この辺のインストールとかはあちこちで解説してる人がいるので適当にぐぐってくださいな で、とりあえずベンチマーク なかなかいいスピードだ で、大抵の人はブラウザのキャッシュをRAMディスクにするといいよ・・・と言うけど そもそもメインドライブがNVMeのSSDを使っている状態で、体感速度なんか上がらない(使い終わったキャッシュを再起動したら綺麗さっぱり捨て去ってくれるという利点はある)  うちで一番効果があるのは Adobe Audition というアプリ これが結構高速化する(キャッシュをちゃんとRAMディスクにしたら・・・だけど) ハイレゾ音源だと、1時間の音源が何かする度に4GBのファイルを作られてしまう なので、RAM DISKにすると、結構編集時間を短縮できる Premiere Rushも出力先をRam Diskにしておいて終わったら、SSDにコピーすると言う事をやるとかなりスピードアップになる 実はうちのPCは普段は99%のパワーで動作していて、CPUのターボブーストが掛からないようになっている 大体3.6GHz当たりで安定してるのだけど、これを100%にするとターボブースト機能がONになって一部のコアが4.5とか4.8GHzまで上がる まあ、毎回電源オプションをいじる事になるのだけどさ・・・ そうしてベンチを取ると こんな感じ とは言え、ブーストしてるからと行ってRam Diskのスピードの差を体感する事はさすがに無理 ・・・と言うかフォトショでもRAM DISKにしてよかった・・・と言うほど変わらない SSDの性能が上がってきたしもし次にPCを買い換えたらRAM DISKよりSSDの方が速いかもね  

QCC dongle proを買った→Windows11で接続までのみちのり

何があった  買ったけど最初の接続に手間取ったので そのメモ QuestyleのQCC dongle proを買ったのだけど、使っているイヤホンと接続できない なんでー? ペアリングしてるのに 説明書 説明書には外装のQRコードを読むように書いてあるのだけど そもそもpage not found 404になる   Windowsには・・・ ちゃんと  ヘッドホンのところには QCC Dongle Pro って出てるのよね しかし音が出ない つーか、新しいペアリングはどうやるのだ  スマホが必要でした 何だそりゃっていいたくなるがどうやらまず、スマホにQCC dongle Pro接続して専用アプリで ドングルとイヤホンをペアリングする必要があった 何だそりゃー Google PlayでQuestyleで検索かけるか こちら でまずはインストール そしてスマホにドングルを刺して、このアプリ内からイヤホンをペアリング そしてそのドングルをWindows11の空いてるUSB TYPE-Cポートに突き刺す   音が出た! やったね  結局音が良くなった?  どうなんだろうね 正直言ってよくわからん ただ、マイケルジャクソンのThe Jamのオープニングのガラスの割れる音は 今までよりも細かくパリパリ聞こえるようになったから効果はあったんだと思う それと遅延が少なくなった  finalのZE3000 SVというイヤホンを使っていたのだけど今までのただのUSBドングルで YouTubeを見てると明らかに音と画面のタイミングがずれてて、 Bluetoothイヤホンってこんなもんかと表他のだけどそれが亡くなったのはとても快適 LDACは遅延が酷いとか書いてあったからビビってたんだけどね(でも有線イヤホン使うと、あ、これが合ってる状態なんだってなるので遅延はあるみたい)   あと、たまにイヤホンと繋がらなくなるねぇ 相性問題というやつなのだろうか    今までのUSBドングルが捨てられない 最期に罠が一つ そりゃ無いぜって言いたくなるのが今までのUSBドングルが捨てられないこと このQCC dongle pro はWindowsからはイヤホンとして認識されるので他のBluetoo...