はじめに
この記事は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では動きません。
本題
以下の手順でファームウェアを送信、更新をしていきたいと思います。
とりあえずファームウェアを更新する最小コードを見てみます。
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
)がある想定で実装します。
iOSでBLEを扱うにはInfo.plist
にPrivacy - Bluetooth Always Usage Description
を追加して適当になんか打ちます。2
次に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()
}
}
}
リファレンス