はじめに
製品をユーザーに提供後に新機能をリリースした際に、開発者がリモートでファームウェアの更新を行えるようにするために、OTAアップデート機能を実装中。
こちらの記事で「デバイス単体でファームウェア更新機能」を実装。
また、こちらの記事で、「新しいFirmwareを開発者がアップロードした時に、ユーザーに対してアプリ通知行う機能」を記載した。
今回は、OTAアップデート機能の最後の実装として、「新しいFirmwareをユーザーがインストールリクエストしたら、新しいFirmwareをダウンロードする」部分を記載する。
これにて、OTAアップデート機能の実装が完了する予定。
実装手順
- サーバー側(Go)の手順:
- データベースのユーザーテーブルを更新して、新しいFirmwareバージョンを反映させる。
- AWS IoTのDevice Shadowのdesiredを、新しいバージョンに更新する。AWS IoTのDevice Shadowで使用される通信形式は、JSON。
- デバイス側(ESP32)の操作:
- AWS IoTからdesired状態の更新をMQTT経由で受信する。
- 受信データ(JSON形式)をデシリアライズし、現在の状態(repoted)との差分を検出する。
- 差分がある場合(=Firmwareの更新が必要な場合)、OTAで更新を実行する。
- クラウドに成功した更新を報告するために、Device Shadowのreported状態を更新する。
サーバー側(Go)
DBとDevice Shadow Desiredを更新
データベースの更新
UpdateUser(h.db, user)
関数を呼び出して、データベースのユーザー情報を更新する。具体的には、DatabaseにアクセスしてUPDATE文でUserテーブルを更新。
Go構造体 → Protocol Buffers
ToDeviceConfig
関数は、Go言語で定義されたUser
構造体のインスタンスからpb.DeviceConfig
構造体へとデータを変換するためのメソッド。pb.DeviceConfig
構造体はプロトコルバッファ(Protocol Buffers)定義に基づいたデータ構造で、AWS IoTのデバイスシャドウのdesired設定のために利用される。User
構造体のFirmwareVersion
フィールドは、データベースのuser
テーブルにあるfirmware_version
カラムの値をマッピングして読み込んでいる。
// user.go
type User struct {
// 他のフィールド...
FirmwareVersion string `db:"firmware_version" json:"firmware_version"`
}
func (u *User) ToDeviceConfig() pb.DeviceConfig {
return pb.DeviceConfig{
FirmwareVersion: &u.FirmwareVersion,
}
}
デバイスシャドウの更新:
- AWS IoTのDevice Shadowで使用される通信形式は、JSONなので
h.sm.UpdateShadow()
関数で、Protocol Buffers形式のデータをJSONに変換し、AWS IoTのデバイスシャドウのdesiredステートを更新する。
// user_handler.go
type UserHandler struct {
db *sqlx.DB
sm ShadowManager
}
func (h *UserHandler) HandleUpdateUser(c echo.Context) error {
uid := c.Get("uid").(string)
user := &User{}
if err := c.Bind(user); err != nil {
return err
}
user.UID = uid
// DB更新
updatedUser, err := UpdateUser(h.db, user)
if err != nil {
c.Logger().Errorf("failed to update user: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to update user")
}
// デバイスIDが存在するとき
if updatedUser.DeviceID.Valid {
dc := updatedUser.ToDeviceConfig()
desired := &pb.PublishShadowDesired{
State: &pb.ShadowDesiredState{
Desired: &pb.ShadowDesired{
Config: &dc,
},
},
}
// DeviceShadowを反映
if err = h.sm.UpdateShadow(updatedUser.DeviceID.String, desired); err != nil {
c.Logger().Errorf("failed to update device shadow: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to update device shadow")
}
}
return c.JSON(http.StatusOK, updatedUser)
}
デバイス側(ESP32)
全体の流れ
まず、全体の流れから。個人的に、Device Shadowのdesiredステートと、デバイス内部のdesired設定が混乱してたので整理含めて記載。
AWS IoT Device Shadow の desired
ステート
AWS IoT Device Shadow の desired
ステートは、デバイスが達成すべき目標状態を示す。これはサーバー側(クラウド)で設定され、デバイスへと送信される。このステートは、デバイスがオフラインのときでもクラウド側で管理され、デバイスがオンラインに戻った時に同期される。
デバイス内部での desired
設定
デバイス内部での desired
設定は、デバイスが実際に操作を行うための内部パラメータや設定。これは、受け取った Device Shadow の desired
ステートに基づいて更新されるが、直接的にはデバイスの動作に影響を与える設定値。デバイスはこの内部 desired
設定に基づいて実際に操作を行い、その結果を reported
ステートに反映させる。
同期と更新のプロセス
同期:
- デバイスはクラウドから、Device Shadowの
desired
ステートを受信する。 - 受信した
desired
ステートをもとに、デバイス内部のdesired
設定を更新する。
実行:
- デバイスは更新された内部
desired
設定に基づいて動作を調整する。
報告:
- 調整後の実際の状態をデバイスの
reported
ステートとしてクラウドに報告する。
MQTT通信経由でのデバイスシャドウの desired ステート更新
- MQTTクライアントが新しいメッセージを受信し、設定された
onMessage
コールバック関数が呼び出される。 - デバイスは、MQTTプロトコルを使用してAWS IoTから
desired
ステートの更新を購読する。新しいdesired
ステートのデータがトピック(例$aws/things/THING_NAME/shadow/update/delta
)を通じて送信される
// src/mqtt_client.cpp
void MqttClient::onMessage(const std::function<void(const String &topic, const String &payload)> &callback) {
client->onMessage([callback](const String &topic, const String &payload) {
Serial.println("MQTTPubSubClient::onMessage: " + topic + " " + payload);
callback(topic, payload);
});
}
MQTTメッセージの解析(トピックとPayload)
- MQTTメッセージはデバイスシャドウの
delta
トピックから送信され、デバイスの現在のreported
ステートと異なるdesired
ステートが含まれている。 - Payloadは通常、JSON 形式のデータで、デバイスの状態に関する情報を含み、特定のデバイスシャドウの
desired
ステートやreported
ステートを表す。
トピック: $aws/things/XXXXXXXXXXXXXXXXXXXXXXXX/shadow/update/delta
- これは、特定のデバイス(Thing)のDevice Shadowのdeltaトピック。ここで、
XXXXXXXXXXXXXXXXXXXXXXXX
はデバイスのThing名(またはThing ID)
Payload
version
: デバイスシャドウのバージョン番号。シャドウの更新ごとに増加する。timestamp
: デバイスシャドウが最後に更新されたUNIXタイムスタンプ。state
: デバイスの新しいdesired
ステートを示す。この例では、config
オブジェクトの中にphrase_type
というキーがあり、その値が"hakata"
。これは、デバイスの設定や動作に関連するパラメータを示しており、デバイスはこの新しい値に従って動作を更新する必要がある。metadata
: 各desired
ステートのフィールドに関する追加情報を含みむ。ここでは、phrase_type
の最後の更新タイムスタンプが含まれている。
// MQTTメッセージのペイロード例
{
"version": 1859,
"timestamp": 1713327942,
"state": {
"config": {
"phrase_type": "hakata"
}
},
"metadata": {
"config": {
"phrase_type": {
"timestamp": 1713250306
}
}
}
}
JSON形式データ(payload)のデシリアライゼーション
onMessage
コールバックの中で、受信したトピックに基づき、対応するハンドラ関数(handleShadowGetAccepted
やhandleShadowUpdateDelta
)が選択されて実行される。今回の場合は、更新なのでhandleShadowUpdateDelta
関数。
// src/sync_shadow.cpp
tl::expected<void, String> SyncShadow::start() {
// MQTTクライアント初期化
client->init();
// TopicごとにHandlerを呼び出す
this->client->onMessage([this](const String &topic, const String &payload) {
Logger &logger = Logger::getInstance();
if (topic == topicGetAccepted) {
if (auto result = handleShadowGetAccepted(payload); !result) {
logger.error(ErrorCode::SHADOW_FAILED, result.error());
}
} else if (topic == topicUpdateDelta) {
if (auto result = handleShadowUpdateDelta(payload); !result) {
logger.error(ErrorCode::SHADOW_FAILED, result.error());
}
}
});
}
// デバイスシャドウの更新があった場合に呼ばれる
tl::expected<void, String> SyncShadow::handleShadowUpdateDelta(const String &payload) {
auto resultDelta = deserializeSubscribeShadowUpdateDelta(payload);
if (!resultDelta) {
return tl::make_unexpected("can not deserialize delta shadow: " + resultDelta.error());
}
// 現在の設定
const auto currentConfig = this->reported.config;
// 更新された設定
const auto deltaConfig = resultDelta->state->config;
// configに変更がない場合は終了
if (!isDeviceConfigChanged(deltaConfig, currentConfig)) {
Serial.println("no change: skip");
return {};
}
// 更新された設定を反映
updateDeviceConfig(*this->desired.config, *resultDelta->state->config);
return {};
}
受信したデータ(JSON形式)は、デバイス側でデシリアライズされる。これには、deserializeDeviceConfig
関数が使われ、JSONデータからDeviceConfig
オブジェクトを作成する。先ほどのMQTTメッセージのペイロードの場合は、resultDelta は、SubscribeShadowUpdateDelta
構造体として、下記の結果が得られる。
Phrase Type: hakata
updateDeviceConfig(*this->desired.config, *resultDelta->state->config)
: この関数は、現在のデバイスの desired
設定(this->desired.config
)を、新しく受け取った差分(resultDelta->state->config
)に基づいて更新している。
この時点で、受信したAWS IoT Device Shadowの desired
ステートをもとに、デバイス内部の desired
設定が更新された。
次は、デバイス内部のdesired設定に合わせて、現在の設定を変更していく。
デバイスのdesired設定と現在の設定の差分検出
getDeviceConfigDiff
関数を使用して、デバイスの現在の設定(currentConfig
)と所望の設定(desiredConfig
)の間に差異があるかを検出し、その差異を表す DeviceConfig
オブジェクトを返す。
// src/json/device_config.cpp
// 現在の設定とdesiredの設定の差分を取得する
std::optional<DeviceConfig> getDeviceConfigDiff(const std::optional<DeviceConfig> &desiredConfig, const std::optional<DeviceConfig> ¤tConfig) {
DeviceConfig diff;
bool hasDiff = false;
// desiredの値がcurrentと異なる場合にdiffに設定する
auto checkAndAssign = [&hasDiff](const auto &desired, const auto ¤t, auto &diffField) {
if (desired.has_value()) {
if (!current.has_value() || *desired != *current) {
diffField = desired;
hasDiff = true;
}
}
};
checkAndAssign(desiredConfig->firmware_version, currentConfig->firmware_version, diff.firmware_version);
if (hasDiff) {
return diff;
} else {
return std::nullopt;
}
}
bool isDeviceConfigChanged(const std::optional<DeviceConfig> &desiredConfig, const std::optional<DeviceConfig> ¤tConfig) {
if (desiredConfig->firmware_version.has_value() && desiredConfig->firmware_version != currentConfig->firmware_version) {
return true;
}
// その他のフィールド
// ...
// すべてのフィールドが同じ場合
return false;
}
ファームウェアの更新判定とOTA Update実行
Wi-Fi接続の設定
- システムが起動すると最初にWi-Fi接続が確立される。
SyncShadow の start メソッド内でのプロセス
SyncShadow::start()
で設定されるonMessage
コールバックにより、デバイスは AWS IoT からのdesired
ステートの更新を自動的に受け取り、それに応じてデバイス内部のdesired設定を更新する。
applyAndReportConfigUpdates 関数の役割
applyAndReportConfigUpdates()
関数は、定期的に呼び出され(loop()
関数内で定期的に呼び出され、デバイス内部でのdesired
設定とreported
設定の差分を検出し、必要に応じてデバイス設定を更新し、その結果を AWS IoT Device Shadow のreported
状態に反映する- この関数は、
SyncShadow
クラスが管理する内部desired
設定がすでに更新されていると想定して動作し、その設定に基づいて実際のデバイス動作を調整する。
// src/main.cpp
void setup() {
// Wi-Fi接続の設定
setupWiFi(ssid, password);
// システムの起動ログ
Serial.println("System initialization...");
// SyncShadowインスタンスの取得
SyncShadow &syncShadow = SyncShadow::getInstance();
// SyncShadowの開始
if (auto result = syncShadow.start(); !result) {
Serial.println("Failed to start SyncShadow: " + result.error());
} else {
Serial.println("SyncShadow started successfully.");
}
// デバイス設定の変更を適用し、デバイスシャドウに通知する
void applyAndReportConfigUpdates() {
Logger &logger = Logger::getInstance();
SyncShadow &syncShadow = SyncShadow::getInstance();
auto desiredConfig = syncShadow.getDesiredConfig();
auto reportedConfig = syncShadow.getReportedConfig();
auto optConfigDiff = getDeviceConfigDiff(desiredConfig, reportedConfig);
// 変更がない場合はスキップ
if (!optConfigDiff) {
return;
}
logger.debug("device config changed");
// 変更のあった設定
auto configDiff = optConfigDiff.value();
logger.debug("config diff: " + serializeDeviceConfig(configDiff));
// ファームウェアバージョンの変更があった場合
if (configDiff.firmware_version.has_value()) {
logger.debug("Firmware version changed: " + desiredConfig->firmware_version.value());
// ファームウェアアップグレード開始
if (auto result = upgradeFirmware(desiredConfig->firmware_version.value()); !result) {
logger.error(ErrorCode::OTA_UPDATE_FAILED, "failed to upgrade firmware: " + result.error());
delay(100);
ESP.restart(); // ダウンロードに失敗した場合は再起動
return;
} else {
logger.info("Firmware upgrade successful");
}
}
}
}
void loop() {
// デバイスシャドウが更新されていた場合の処理
applyAndReportConfigUpdates();
}
先ほどのgetDeviceConfigDiff
関数を使用して、戻り値として差分リストが返された場合の中で、ファームウェアバージョンが異なる場合、do_firmware_upgrade関数を呼び出して、desiredのファームウェアバージョンのインストールを開始する。
ダウンロードに失敗した場合は、メモリリークなどでメモリが足りなくなっている可能性があるので、再起動するようにする。
upgradeFirmware関数は、新しいファームウェアバージョン(device shadowのdesiredに記載されたfirmware version)を引数にとり、firmwareダウンロードの完全なURLを生成し、ダウンロードを実行する。こちらに関する、詳しい説明は下記。
ただ、簡易的にOTAを実行するesp_https_otaはイメージが大きい場合は機能しないという記載があったため、個別のOTAの関数を呼び出すように修正。
https://github.com/espressif/esp-idf/issues/4582#issuecomment-772264969
関数をvoid型から tl::expected<void, String>
型に変更し、成功時には空の tl::expected
オブジェクトが返すように変更する。こうすることで、成功時の値を後続のメソッドに受け継ぐことができるようになるので、デバイスのシャドウステートに変更を報告することが可能になる。
// src/ota_update.cpp
tl::expected<void, String> upgradeFirmware(const String &firmwareVersion) {
Serial.println("OTA firmware upgrade start");
// ルートCA証明書を読み込む
String cert = readRootCA();
if (cert.length() == 0) {
return tl::make_unexpected("Failed to load root CA certificate");
}
// Firmware Update URLを組み立てる
String firmwareUpdateUrl = String(FIRMWARE_UPGRADE_URL) + firmwareVersion + ".bin";
Serial.println("Firmware update URL: " + firmwareUpdateUrl);
esp_http_client_config_t config = {
.url = firmwareUpdateUrl.c_str(),
.cert_pem = cert.c_str(),
.timeout_ms = 600000, // 念の為に通信タイムアウトを10分に設定。
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
// OTAのエラーを格納する
String otaError;
int last_progress = -1;
// OTA開始
esp_https_ota_handle_t https_ota_handle = nullptr;
esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle);
if (err != ESP_OK) {
otaError = "OTA begin failed: " + String(esp_err_to_name(err));
goto ota_end;
}
// イメージの存在チェック(descriptionを取得)
esp_app_desc_t app_desc;
err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc);
if (err != ESP_OK) {
otaError = "failed to get image description: " + String(esp_err_to_name(err));
goto ota_end;
}
// OTAのダウンロードを実行
while (true) {
err = esp_https_ota_perform(https_ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
break;
}
int32_t dl = esp_https_ota_get_image_len_read(https_ota_handle);
int32_t size = esp_https_ota_get_image_size(https_ota_handle);
int progress = 100 * ((float)dl / (float)size);
if (progress != last_progress) {
Serial.printf("Firmware update progress: %d%%\n", progress);
last_progress = progress;
}
}
// OTAが正常に完了したか確認
if (err != ESP_OK) {
otaError = "OTA finish failed: " + String(esp_err_to_name(err));
goto ota_end;
}
// OTAで完全なデータが受信されたか確認
if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) {
otaError = "OTA image was not completely received";
goto ota_end;
}
// OTAの終了処理を実行
err = esp_https_ota_finish(https_ota_handle);
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
otaError = "OTA image validation failed";
goto ota_end;
}
Serial.println("Firmware upgrade succeeded");
return {};
ota_end:
esp_https_ota_abort(https_ota_handle);
return tl::make_unexpected("OTA process ended unexpectedly: " + otaError);
}
OTAアップデート時は、MQTT接続を切るように変更
MQTT通信経由に切り替えたところ、OTAアップデートが途中で失敗するようになった。大きな要因が、おそらくMQTTの接続を行いながらOTAを行なっていること。
AWSのSDKを使っている場合によく発生するとの記載があったので、接続を切るようにしたところ、エラーの発生は大幅に改善された。
参考: espressif/esp-aws-iot#160
tl::expected<void, String> performFirmwareUpdate(const String &firmwareVersion) {
Logger &logger = Logger::getInstance();
SyncShadow &syncShadow = SyncShadow::getInstance();
// ダウンロードステータスを開始状態に更新
if (auto result = startDownloadStatus(syncShadow, "firmware"); !result) {
return tl::make_unexpected("failed to start download status: " + result.error());
}
delay(100);
// ファームウェアアップデートの前にデバイスシャドウの同期を停止する
if (auto result = syncShadow.stop(); !result) {
return tl::make_unexpected("failed to stop mqtt: " + result.error());
}
// ファームウェアアップデートを実行
if (auto result = upgradeFirmware(firmwareVersion); !result) {
return tl::make_unexpected("failed to upgrade firmware: " + result.error());
}
// ファームウェアアップデート後にデバイスシャドウの同期を再開する
if (auto result = syncShadow.start(); !result) {
return tl::make_unexpected("failed to start mqtt: " + result.error());
}
// ダウンロードステータスを終了状態に更新
if (auto result = updateDownloadStatus(syncShadow, 100, "firmware"); !result) {
return tl::make_unexpected("failed to update download status: " + result.error());
}
delay(100);
// 初期値に戻す
if (auto result = finishDownloadStatus(syncShadow, "firmware"); !result) {
return tl::make_unexpected("failed to init download status" + result.error());
}
return {};
}
アップデート成功の報告
- 設定の更新:
updateDeviceConfig(*this->reported.config, config)
関数を使って、内部に保持しているreported.config
(レポートされた設定)を新しい設定で更新する。デバイスの現在の設定を最新の変更で上書きする。 - デバイスシャドウの更新:
this->report()
メソッドを呼び出して、更新された設定をデバイスシャドウに反映させる。このメソッドは内部で AWS IoT の Device Shadow サービスに MQTT を使って通信を行い、reported
状態をクラウドのシャドウに同期する。 - レポート済み設定の取得と保存:
syncShadow.getReported()
で現在のレポート済み設定を取得し、PersistentShadow::getInstance().save(reported)
でこの設定をローカルの永続ストレージに保存します。これにより、デバイスの再起動や電源が切れた後も、最新の設定が保持されるようになる。
// src/main.cpp
// デバイス設定の変更を適用し、デバイスシャドウに通知する
void setup() {
// 既存のコード
// ...
// デバイス設定をデバイスシャドウに通知
if (auto result = syncShadow.reportConfig(configDiff); !result) {
logger.error(ErrorCode::CONFIG_REPORT_FAILED, "failed to report config: " + result.error());
return;
}
const auto reported = syncShadow.getReported();
if (auto result = PersistentShadow::getInstance().save(reported); !result) {
logger.error(ErrorCode::IO_FAILED, "failed to save app state: " + result.error());
return;
}
}
// src/sync_shadow.cpp
// デバイスシャドウの状態を報告
tl::expected<void, String> SyncShadow::report() const {
if (!isWiFiConnected()) {
return {};
}
const auto publishShadowUpdate = PublishShadowUpdate{.state = reported};
const String payload = serializePublishShadowUpdate(publishShadowUpdate);
if (client->publish(topicUpdate, payload)) {
return {};
} else {
return tl::make_unexpected("Failed to publish shadow update");
}
}
tl::expected<void, String> SyncShadow::reportConfig(const DeviceConfig &config) {
updateDeviceConfig(*this->reported.config, config);
if (auto result = this->report(); !result) {
return tl::make_unexpected("failed to report config: " + result.error());
}
return {};
}
動作確認
ファームウェアのバージョンを1.0.4から1.0.5にアップデートする場合で動作確認。
まず、ファームウェアのアップデートがある場合には、アプリにメッセージが表示され、ユーザーはメッセージをクリックすると、アップデートの受け入れを選択できる。
インストールをクリックすると、まず、アプリからサーバーに対してfirmwareアップデートのAPIリクエストが飛ぶ。
flutter: Request updateUser: {"firmware_version":"1.0.5"}
デバイスシャドウ更新: AWS IoTのデバイスシャドウに新しいファームウェアバージョンをdesiredステートとして設定。
desiredのfirmware_versionが1.0.5に変更されている。
clocky_api_local | 2024/04/17 11:50:00 received listen request from user: [USER_ID]
clocky_api_local | 2024/04/17 11:50:00 received listen firmware update request from user: [USER_ID]
clocky_api_local | 2024/04/17 11:50:00 received listen request from deviceId: [DEVICE_ID]
clocky_api_local | 2024/04/17 11:50:00 sent message to user: 1
clocky_api_local | subscribing to topic: $aws/things/[DEVICE_ID]/shadow/update/accepted
clocky_api_local | {"time":"2024-04-17T11:50:00.754083024Z","id":"","remote_ip":"[IP_ADDRESS]","host":"[HOST_IP]:8080","method":"GET","uri":"/app/weather","user_agent":"Dart/3.3 (dart:io)","status":200,"error":"","latency":166399359,"latency_human":"166.399359ms","bytes_in":0,"bytes_out":233}
// device shadow
{
"state": {
"desired": {
"config": {
"firmware_version": "1.0.5"
}
},
"reported": {
"config": {
"firmware_version": "1.0.4"
}
},
}
}
MQTT通信: ESP32がデバイスシャドウのdesiredステートの更新を検知し、MQTTトピックから変更を受け取る。
MQTTPubSubClient::onMessage: $aws/things/[DEVICE_ID]/shadow/update/delta {"version":1242,"timestamp":[TIMESTAMP],"state":{"config":{"firmware_version":"1.0.5"}},"metadata":{"config":{"firmware_version":{"timestamp":[TIMESTAMP]}}}}
{"timestamp":[TIMESTAMP],"message":"device config changed","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
MQTTPubSubClient::publish: mia/things/[DEVICE_ID]/logs {"timestamp":[TIMESTAMP],"message":"device config changed","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
{"timestamp":[TIMESTAMP],"message":"config diff: {"config":{"firmware_version":"1.0.5"}}","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
MQTTPubSubClient::publish: mia/things/[DEVICE_ID]/logs {"timestamp":[TIMESTAMP],"message":"config diff: {"config":{"firmware_version":"1.0.5"}}","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
{"timestamp":[TIMESTAMP],"message":"Firmware version changed: 1.0.5","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
MQTTPubSubClient::publish: mia/things/[DEVICE_ID]/logs {"timestamp":[TIMESTAMP],"message":"Firmware version changed: 1.0.5","level":"DEBUG","thingName":"[DEVICE_ID]","freeHeap":[FREE_HEAP],"usedBytes":[USED_BYTES]}
ファームウェアダウンロードとインストール: 新しいファームウェアをダウンロードしてインストール
OTA firmware upgrade start
Starting OTA task
HTTP_EVENT_ON_CONNECTED
HTTP_EVENT_HEADER_SENT
HTTP_EVENT_ON_HEADER, key=x-amz-id-2, value=[AMAZON_ID]
HTTP_EVENT_ON_HEADER, key=x-amz-request-id, value=[REQUEST_ID]
HTTP_EVENT_ON_HEADER, key=Date, value=[DATE]
HTTP_EVENT_ON_HEADER, key=Last-Modified, value=[MODIFIED_DATE]
HTTP_EVENT_ON_HEADER, key=ETag, value="[ETAG_VALUE]"
HTTP_EVENT_ON_HEADER, key=x-amz-server-side-encryption, value=AES256
HTTP_EVENT_ON_HEADER, key=Accept-Ranges, value=bytes
HTTP_EVENT_ON_HEADER, key=Content-Type, value=application/octet-stream
HTTP_EVENT_ON_HEADER, key=Server, value=AmazonS3
HTTP_EVENT_ON_HEADER, key=Content-Length, value=[CONTENT_LENGTH]
HTTP_EVENT_ON_DATA, len=[DATA_LENGTH]
HTTP_EVENT_ON_DATA, len=[DATA_LENGTH]
HTTP_EVENT_ON_DATA, len=[DATA_LENGTH]
....
HTTP_EVENT_ON_DATA, len=[DATA_LENGTH]
HTTP_EVENT_DISCONNECTED
Total data received before disconnect: [TOTAL_BYTES] bytes
HTTP_EVENT_DISCONNECTED
Total data received before disconnect: [TOTAL_BYTES] bytes
Firmware upgrade succeeded
ファームウェアインストール中は、deltaに差分情報として、新しいfirmware_versionに記載されるが、reportedにはまだ反映されていない。
{
"state": {
"desired": {
"config": {
"firmware_version": "1.0.5"
}
},
"reported": {
"config": {
"firmware_version": "1.0.4"
}
},
"delta": {
"config": {
"firmware_version": "1.0.5"
}
}
}
}
デバイスの再起動: ファームウェアのインストール完了後、デバイスが自動的に再起動。
成功通知: アプリおよびサーバーにアップデート成功を通知。
AWS CloudWatchに送信されたログ
{
"timestamp": [TIMESTAMP],
"message": "Firmware version changed: 1.0.5",
"level": "DEBUG",
"thingName": "[DEVICE_ID]",
"freeHeap": [FREE_HEAP],
"usedBytes": [USED_BYTES]
}
Device Shadowのreportedも無事1.0.5に変更されたことを確認。
無事完了した。
おわりに
ユーザーがアップデートを受け入れると、アプリがサーバーにAPIリクエストを送信し、サーバーがAWS IoTのデバイスシャドウに変更を反映させる。デバイスはMQTTを通じてこの変更を検知し、ファームウェアのダウンロードとアップグレードを実行する部分を記載した。
後は、ファームウェアダウンロードに数分程度かかるので、ダウンロード中は進行状況をアプリに表示したい。別記事で記載。
コメント