小ロットの量産でOTAに挑戦した話

# はじめに
マイコンでLチカができて、サーバとの通信も一通り実現できたけれど、意外と手が届かないのが**OTA（Over the Air）**。
今回は、30台程度の小規模ロットのIoT端末開発・運用を通じて得た、OTAの実装方法を紹介したいと思います。

※初投稿のため、至らない点があるかもしれませんが、よろしくお願いいたします。orz
# 前提条件
- **作りたいモノ** 
    複数のIoT機器を監視・制御・メンテナンスする、よくある（？）構成を想定しています
    - **通常時**：サーバとマイコンでいろいろ送受信(MQTT)したい
        1. （定期）マイコンから**センサデータ**をサーバに送信
        2. （オンライン）サーバからマイコンにつながる**駆動部**を制御
    - **メンテナンス時(今回のメイン)**：**OTA**してマイコンのアップデートがしたい
        1.（定期）マイコンからファームウェアの**バージョン確認**
        2.（1.でアップデートあれば）マイコンがサーバ上のファームウェアの**ダウンロードと更新**
- **使用機材と用途**
    - クラウドサーバ：AWS
        1. IoT Core経由で通信(MQTT)
        2. ファームウェアのバージョン情報やファームウェアはS3に格納
    - 通信機器：WiFiルータを使って、サーバとマイコンを中継
    - マイコン： [Raspberry Pi Pico W](https://akizukidenshi.com/catalog/g/g117947/ "Raspberry Pi Pico W")をArduino化して利用　
    - その他：各種センサおよび駆動部をマイコンと接続
# OTAでやりたい要件
1. **マイコン全台をまとめてOTAしたい**
    - 小ロットとはいえ、一台ずつOTAするのは非現実的
    - OTAするファームウェアは全台共通だが、各マイコン固有の設定情報は保持したい
2. **万が一に備え、個別アップデートも可能にしたい**
    - その際、個別の設定情報も書き換えられるようにしたい
3. **OTA失敗時にはリトライやアラートが欲しい**
    - OTAが失敗すると現地対応が必要になり非常に辛い。可能な限りリトライして欲しい
# 解決した仕組み概要

1. **マイコン上のファイル構成**
    - マイコンのメモリ内にFileSystem領域を用意し、マイコンの識別ID等の個体情報に書き込み、ファームウェア領域とは分離します
        ```cpp
        void setup() {
          mountFlashMemory();//FileSystemへのアクセス確認
          #ifdef FILE_WRITE_ENABLED
            setupInitialConfig(deviceConfig);//deviceConfigを設定
            saveConfigToFS(deviceConfig);  // deviceConfigを書き込み
          #elif defined(FILE_WRITE_DISABLED)
            loadConfigFromFS(deviceConfig);  //deviceConfigを読み込み 
          #else
            #error "No FILE_WRITE selected! Please define FILE_WRITE setup."
          #endif
          setupMQTT();//通信関連の初期設定
          checkForUpdates(ota_univ_version_url);//全体のアップデート確認
          checkForUpdates(ota_device_version_url);//個別のアップデート確認
          healthCheckPolling();//サーバへ諸々送信
        }

        void loop(){          
          checkForUpdates(ota_univ_version_url);//全体のアップデート確認
          checkForUpdates(ota_device_version_url);//個別のアップデート確認
          healthCheckPolling();//サーバへ諸々送信
          delay(10000);//定期的にOTAチェック
        
        }
        ```
     - 個別のファームウェアを書き込む場合
        ```cpp
        #define FILE_WRITE_ENABLED //FileSystemへ以下を書き込み保存
        inline void setupInitialConfig(DeviceConfigData &deviceConfig) {
        deviceConfig.deviceId = "device_dev_00001";
        deviceConfig.deviceVersion = "ver1.0.0";
        deviceConfig.wifiSSID = "hogehoge";
        deviceConfig.wifiPassword = "piyopiyo";
        }
        ```
     - 全体のファームウェアを書き込む場合
        ```cpp
        #define FILE_WRITE_DISABLED ////FileSystemから読み込むのみ
        inline void setupInitialConfig(DeviceConfigData &deviceConfig) {
        deviceConfig.deviceId = "";
        deviceConfig.deviceVersion = "";
        deviceConfig.wifiSSID = "";
        deviceConfig.wifiPassword = "";
        }
        ```
2. **サーバ上のファームウェアとバージョン情報のファイル構成**
    - 個別書き込み用と全体書き込み用のファームウェアおよびバージョン情報を用意し、以下のようにS3に格納します
        ```
        ##ファイル構成
        s3-bucket/
        ├── firmware/
        │   ├── device_dev_00001/                       
        │   │   └── firmware.bin        // device_dev_00001向けの個別ファームウェア    
        │   ├── device_dev_00002/                       
        │   │   └── firmware.bin        // device_dev_00002向けの個別ファームウェア 
        │   ├── device_dev_00003/                       
        │   │   └── firmware.bin        // device_dev_00003向けの個別ファームウェア 
        │   ├── device_dev_00004/                       
        │   │   └── firmware.bin        // device_dev_00004向けの個別ファームウェア 
        │   └── universal/                           
        │       └── firmware.bin        // 全体向けファームウェア
        └── version/
            ├── device_dev_00001.txt    // device_dev_00001向けの個別ver情報    
            ├── device_dev_00002.txt    // device_dev_00002向けの個別ver情報    
            ├── device_dev_00003.txt    // device_dev_00003向けの個別ver情報    
            ├── device_dev_00004.txt    // device_dev_00004向けの個別ver情報    
            └── universal.txt           // 全体向けver情報
        ```
3. **OTAの流れ**
マイコンからの起動時や定期で下記を確認します
    1. 個別用・全体用バージョン情報を取得
    2. マイコン自身のバージョンと比較
        1. 新しいverがなければ終了
        2. 新しいバージョンがあればファームウェアをダウンロード・書き込み
            - ※全体向けファームウェアを書き込んでもFileSystem側に保存された個体情報は影響を受けません。 
 
# おわりに
一旦上記の設計で作りましたが、まだ改善点はあります。
特に現状の仕組みでは、個別ファームウェアを書き込む際のバージョン管理が煩雑になりがちです。
例えば、全体のファームウェアverが`1.2.0`の時に個別のファームウェアでOTAしたい際には、個別のverを`1.2.1`等に値を上げて書き込む必要があります。
次に全体のアップデートしたい場合、個別のverの最新が`1.2.1`のために全体のファームウェアverが`1.2.2`以上にする必要があります。
現状の3桁管理ではなく、個別の書き込み用に4桁管理へと増設をしておく方が良いかもしれません。
他にもS3のテキストでバージョン管理するよりはDBで管理した方が良いなぁとか、まだまだOTAは奥が深いです。

今回の記事が皆さんのIoT開発の参考になれば幸いです。