14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

N/S高等学校Advent Calendar 2022

Day 3

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

Last updated at Posted at 2022-12-02

はじめに

この記事はN・S高等学校 Advent Calendar 2022の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

とりあえずファームウェアを更新する最小コードを見てみます。

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

iOSでBLEを扱うにはInfo.plistPrivacy - Bluetooth Always Usage Descriptionを追加して適当になんか打ちます。2
image.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()
        }
    }
}

リファレンス

  1. 送られてくるデータ毎 beginかendか判断してると遅くなるのでなんとかしてください

  2. AppStoreなどにリリースする際はなんかいい感じに何に使うかを書いてください。レビューで弾かれます。

14
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?