📖

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

2024/04/17に公開

はじめに

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

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

https://kazulog.fun/dev/esp32-ota-update-vol1/

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

https://kazulog.fun/dev/esp32-ota-update-app-notification/

今回は、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 カラムの値をマッピングして読み込んでいる。
// 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: デバイスシャドウのバージョン番号。シャドウの、、、

続きは、こちらで記載しています。
https://kazulog.fun/dev/espr32-ota-update-mqtt-device-shadow/

Discussion