方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【ESP32】OTAアップデート:MQTTとAWS IoTデバイスシャドウを利用したファームウェア更新

この記事は約40分で読めます。

はじめに

製品をユーザーに提供後に新機能をリリースした際に、開発者がリモートでファームウェアの更新を行えるようにするために、OTAアップデート機能を実装中。

こちらの記事で「デバイス単体でファームウェア更新機能」を実装。

また、こちらの記事で、「新しいFirmwareを開発者がアップロードした時に、ユーザーに対してアプリ通知行う機能」を記載した。

今回は、OTAアップデート機能の最後の実装として、「新しいFirmwareをユーザーがインストールリクエストしたら、新しいFirmwareをダウンロードする」部分を記載する。

これにて、OTAアップデート機能の実装が完了する予定。

実装手順

  1. サーバー側(Go)の手順
    • データベースのユーザーテーブルを更新して、新しいFirmwareバージョンを反映させる。
    • AWS IoTのDevice Shadowのdesiredを、新しいバージョンに更新する。AWS IoTのDevice Shadowで使用される通信形式は、JSON。
  2. デバイス側(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 カラムの値をマッピングして読み込んでいる。
Go
// 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ステートを更新する。
Go
// 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)を通じて送信される
C++
// 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 の最後の更新タイムスタンプが含まれている。
JSON
// MQTTメッセージのペイロード例
{
  "version": 1859,
  "timestamp": 1713327942,
  "state": {
    "config": {
      "phrase_type": "hakata"
    }
  },
  "metadata": {
    "config": {
      "phrase_type": {
        "timestamp": 1713250306
      }
    }
  }
}

JSON形式データ(payload)のデシリアライゼーション

  • onMessage コールバックの中で、受信したトピックに基づき、対応するハンドラ関数(handleShadowGetAcceptedhandleShadowUpdateDelta)が選択されて実行される。今回の場合は、更新なので handleShadowUpdateDelta 関数。
C++
// 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 構造体として、下記の結果が得られる。

ShellScript
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 オブジェクトを返す。

C++
// 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 設定がすでに更新されていると想定して動作し、その設定に基づいて実際のデバイス動作を調整する。
C++
// 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 オブジェクトが返すように変更する。こうすることで、成功時の値を後続のメソッドに受け継ぐことができるようになるので、デバイスのシャドウステートに変更を報告することが可能になる。

C++
// 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

C++

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

アップデート成功の報告

  1. 設定の更新: updateDeviceConfig(*this->reported.config, config) 関数を使って、内部に保持している reported.config(レポートされた設定)を新しい設定で更新する。デバイスの現在の設定を最新の変更で上書きする。
  2. デバイスシャドウの更新: this->report() メソッドを呼び出して、更新された設定をデバイスシャドウに反映させる。このメソッドは内部で AWS IoT の Device Shadow サービスに MQTT を使って通信を行い、reported 状態をクラウドのシャドウに同期する。
  3. レポート済み設定の取得と保存: syncShadow.getReported() で現在のレポート済み設定を取得し、PersistentShadow::getInstance().save(reported) でこの設定をローカルの永続ストレージに保存します。これにより、デバイスの再起動や電源が切れた後も、最新の設定が保持されるようになる。
C++
// 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;
  }
}
C++
// 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リクエストが飛ぶ。

ShellScript
flutter: Request updateUser: {"firmware_version":"1.0.5"}

デバイスシャドウ更新: AWS IoTのデバイスシャドウに新しいファームウェアバージョンをdesiredステートとして設定。

desiredのfirmware_versionが1.0.5に変更されている。

ShellScript
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}
JSON
// device shadow
{
  "state": {
    "desired": {
      "config": {
        "firmware_version": "1.0.5"
      }
    },
    "reported": {
      "config": {
        "firmware_version": "1.0.4"
      }
    },
  }
}

MQTT通信: ESP32がデバイスシャドウのdesiredステートの更新を検知し、MQTTトピックから変更を受け取る。

ShellScript
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]}

ファームウェアダウンロードとインストール: 新しいファームウェアをダウンロードしてインストール

ShellScript
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にはまだ反映されていない。

JSON
{
  "state": {
    "desired": {
      "config": {
        "firmware_version": "1.0.5"
      }
    },
    "reported": {
      "config": {
        "firmware_version": "1.0.4"
      }
    },
    "delta": {
      "config": {
        "firmware_version": "1.0.5"
      }
    }
  }
}

デバイスの再起動: ファームウェアのインストール完了後、デバイスが自動的に再起動。

成功通知: アプリおよびサーバーにアップデート成功を通知。

AWS CloudWatchに送信されたログ

ShellScript
{
    "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を通じてこの変更を検知し、ファームウェアのダウンロードとアップグレードを実行する部分を記載した。

後は、ファームウェアダウンロードに数分程度かかるので、ダウンロード中は進行状況をアプリに表示したい。別記事で記載。

コメント

タイトルとURLをコピーしました