iOSでESP32のファームウェアをBLE経由で更新する

## はじめに
この記事は[N・S高等学校 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/n-s-highschool)の3日目です。
N高生です。今回はiOSでESP32のファームウェアをBLE経由で更新してみたいと思います。

リポジトリ: /* TODO */

## 環境
- MacOS12.5.1（ﾓﾝﾄﾚｰ）
- Xcode13.3
- ArduinoIDE2.0
    - ボードマネージャー: esp32 v1.0.6
- iPhoneX (iOS15.5)
- ESP32 DevKitC_V4

※CoreBluetoothを扱うのでSimulatorでは動きません。

## 本題

以下の手順でファームウェアを送信、更新をしていきたいと思います。
![ble](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437056/a9b880ac-90e0-a7f6-32bf-d2fed22e9a33.png)

とりあえずファームウェアを更新する最小コードを見てみます。
```main.ino.cpp
#include <Update.h>

// beginで開始
Update.begin();
// ファームを書き込む。dataはファームの中身（uint8_t）でlengthはdataのlength。
Update.write(data, length);
// 書き込み終わったらendする
Update.end(true);
// ESPをrestart
ESP.restart();
```
思ってたより簡単にできますね....

ファームの書き込み方がわかったので次はBLEの処理をくっつけてみます。
今回は`begin`が送られてきたらbeginして`end`が送られてきたらendするようにしてみます。[^1]
```main.ino.cpp
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <Update.h>

#define SERVICE_UUID        "12ab5cbc-6c8c-4344-b9b6-8f996d2d5ed2"
#define CHARACTERISTIC_UUID "43b81cc8-1dd5-46cc-8b62-cc3499dad2db"

class MyCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* pCharacteristic) {
    std::string value = pCharacteristic->getValue();
    String value_str = value.c_str();
    uint8_t* data = pCharacteristic->getData();

    if (value_str == "begin") {
      Serial.println("BEGIN");
      Update.begin(UPDATE_SIZE_UNKNOWN);
    } else if (value_str == "end") {
      Update.end(true);
      Serial.printf("Update Success: Rebooting...\n");
      ESP.restart();
    } else {
      Serial.print(value.length());
      Serial.print("->");
      Serial.println(value_str);
      Update.write(data, value.length());
    }
  }
};

void setup() {
    Serial.begin(115200);
    std::string deviceName = "BLE_FirmwareUpdater";
    BLEDevice::init(deviceName);
 
    BLEServer* pServer = BLEDevice::createServer();
    BLEService* pService = pServer->createService(SERVICE_UUID);
    BLECharacteristic* pCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_WRITE
    );

    pCharacteristic->setCallbacks(new MyCallbacks());
    pService->start();
    BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    BLEDevice::startAdvertising();

    Serial.println("Finish setup.");
}

void loop() { }
```

### iOS側

今回はプロジェクトルートにファームウェアのデータ(`firmware.bin`)がある想定で実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437056/6d89f208-e467-f498-e4e7-a74825b4133d.png)

iOSでBLEを扱うには`Info.plist`に`Privacy - Bluetooth Always Usage Description`を追加して適当になんか打ちます。[^2]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437056/c837866a-8ce8-c72c-60e3-c6d882d09937.png)


次にiOSのコードを書いていきます。BLEの部分は雑に実装しています。

```BLEManager.swift
import Foundation
import CoreBluetooth

final class BLEManager: NSObject, ObservableObject {
    
    private var centralManager: CBCentralManager!
    private var peripheral: CBPeripheral!
    private var characteristic: CBCharacteristic?
    static let serviceUUID = CBUUID(string: "12ab5cbc-6c8c-4344-b9b6-8f996d2d5ed2")
    static let charcteristicUUID = CBUUID(string: "43b81cc8-1dd5-46cc-8b62-cc3499dad2db")
    
    // 雑に進捗率を計算する用
    private var sendDataLength = 0
    private var currentSendedCount = 0
    @Published var currentParsent = 0
    
    override init() {
        print("init")
    }
    
    func setupBLE() {
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    func sendFile(url: URL) {
        guard let characteristic = characteristic else { return }
        guard let file = try? Data(contentsOf: url) else { return }
        
        let chunckedData = [UInt8](file).chunked(into: 1436) // 今回は1436byteずつ送信する
        // 書き込み開始するために`begin`を送信
        peripheral.writeValue("begin".data(using: .utf8)!, for: characteristic, type: .withResponse)
        sendDataLength = chunckedData.count
        currentSendedCount = 0
        for data in chunckedData {
            // 分割されたdataを送信
            peripheral.writeValue(Data(data), for: characteristic, type: .withResponse)
        }
        // 書き込み終了するために`end`を送信
        peripheral.writeValue("end".data(using: .utf8)!, for: characteristic, type: .withResponse)
    }
}

extension BLEManager: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOn:
            centralManager.scanForPeripherals(withServices: [Self.serviceUUID])
        default:
            print(central)
        }
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print(#function)
        print(peripheral)
        self.peripheral = peripheral
        centralManager.stopScan()
        
        // 接続開始
        central.connect(peripheral)
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print(#function)
        peripheral.delegate = self
        peripheral.discoverServices([Self.serviceUUID])
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        guard let error = error else { return }
        print(error)
        setupBLE()
    }
}


extension BLEManager: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        peripheral.services?.forEach({
            if $0.uuid == Self.serviceUUID {
                return
            }
            print($0.uuid)
        })
        if error != nil {
            print(error!)
            return
        }
        if let service = peripheral.services?.first {
            peripheral.discoverCharacteristics([Self.charcteristicUUID], for: service)
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        print(#function, error, service.characteristics)
        if error != nil {
            print(error!)
            return
        }
        
        guard let characteristics = service.characteristics else { return }
        
        for characteristic in characteristics {
            print(characteristic)
            if characteristic.uuid == Self.charcteristicUUID {
                self.characteristic = characteristic
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        currentSendedCount += 1
        // 雑に進捗率を計算する
        currentParsent = Int(Float(currentSendedCount) / Float(sendDataLength) * 100)
        print("\(currentParsent)% \(currentSendedCount)/\(sendDataLength)")
    }
}

extension Array {
    func chunked(into size: Int) -> [[Element]] {
        return stride(from: 0, to: count, by: size).map {
            Array(self[$0 ..< Swift.min($0 + size, count)])
        }
    }
}

```

```ContentView.swift
import SwiftUI

struct ContentView: View {
    @StateObject var bleManager = BLEManager()
    
    var body: some View {
        VStack {
            Text("\(bleManager.currentParsent)%")
            .padding()
            Button {
                guard let url = Bundle.main.url(forResource: "firmware", withExtension: "bin") else { return }
                bleManager.sendFile(url: url)
            } label: {
                Text("Upload")
            }

        }
        .onAppear() {
            bleManager.setupBLE()
        }
    }
}
```

## リファレンス
https://github.com/JAndrassy/ArduinoOTA

https://github.com/espressif/arduino-esp32/blob/master/libraries/Update/src/Update.h

https://garretlab.web.fc2.com/arduino/esp32/examples/ArduinoOTA/OTAWebUpdater.html

[^1]: 送られてくるデータ毎 beginかendか判断してると遅くなるのでなんとかしてください
[^2]: AppStoreなどにリリースする際はなんかいい感じに何に使うかを書いてください。レビューで弾かれます。