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

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のページ)

おしまい

 


コメント

このブログの人気の投稿

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の方が速いかもね  

.NET MAUI BLE(Bluetooth Low Energy)も上手く行った

2023/10/11更新 ↓こちらの記事で更新しれました。 .NET MAUI PLUGIN BLEがWindows(10/11)に対応してた Windows対応 イヤッッホォォォオオォオウ Bluetooth LE plugin for Xamarin &amp; MAUI がWindowsに対応してた~ nugetでver3.00をみんな早速ゲットだ。   ちなみにこちらはAndroid版のサンプルアプリ   GitHub...   [ブログカード風リンクタグ作成] ------------------------------------------------------------------------------ 昔、スマホとESP32の接続確認用に作ったプログラムをXamarinからMAUIに移植したら動いちゃった   まあ、ESP32からスマホへの一方通行なアプリなんだけど 面倒くさいので github に公開した 下手くそなコード書きやがってとか思われそう 「間違ってるぞこのやろう」というのを見つけたら教えてくれると嬉しいです 要素技術の調査はこれで完了かな

.NET MAUIでスプラッシュスクリーン Android12で地獄を見る

まずは起動するところから・・・・  スプラッシュスクリーンだぬ マイクロソフトのサイト 見てると簡単そう(実際簡単で細かい事を気にしなければsvgファイルを用意して1行だけ書き換えておしまい)   なんかプロジェクトファイルに自動で記述されるらしい よし、プロジェクト作った                     つーか、もうスプラッシュがあるんだけど・・・・ そして自作のsvgファイル くうっ・・・デザインセンスない・・・・   それはともかく、このSVGファイルを Resources\Images にドラッグ&ドラッグ プロジェクトを右クリックして「プロジェクトファイルの編集」 して、編集できるようになったプロジェクトファイルを 自分のプロジェクトファイルに書き換える <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="168,168" /> ↓ <MauiSplashScreen Include="Resources\Splash\ splashtestmaui .svg" Color="#512BD4" BaseSize="168,168" />   とりあえずAndroidで実行 お、おう・・・・ まあ最初はこんなもんよね 最初に紹介したマイクロソフトのサイトでもBaseSizeを書き換えてくださいって言ってるし 言われたとおりに <MauiSplashScreen Include="Resources\Splash\splashtestmaui.svg" Color="#512BD4" BaseSize=" 320,600 " /> 書き換えてみると いい感じじゃーん じゃあアンドロイドのバージョン毎に試してみよう   Android 7 Android 11 Android 12 Android 13