開発環境
開発環境は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のページ)
おしまい
コメント
コメントを投稿