mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
OTA Protocol updates and clean-up
This commit is contained in:
parent
58826f65d2
commit
e4a572a382
6 changed files with 209 additions and 534 deletions
|
|
@ -16,7 +16,7 @@
|
|||
10D109F22E2047D600536CE6 /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F12E2047D600536CE6 /* DatadogSessionReplay */; };
|
||||
10D109F42E2047D600536CE6 /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = 10D109F32E2047D600536CE6 /* DatadogTrace */; };
|
||||
230A98402EF86A44004D87F1 /* AsyncCentral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230A983F2EF86A44004D87F1 /* AsyncCentral.swift */; };
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */; };
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift */; };
|
||||
230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */; };
|
||||
231251382E3BC96400E6ED07 /* BLEAuthorizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */; };
|
||||
23148E302EE1CCE500F0DB2C /* MeshtasticAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */; };
|
||||
|
|
@ -64,7 +64,6 @@
|
|||
237B46962DC8F1C100B22D99 /* RateLimitedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */; };
|
||||
23825FF52EF6CF0B00C25543 /* NWHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FF42EF6CF0B00C25543 /* NWHost.swift */; };
|
||||
23825FF92EF6D9AA00C25543 /* ESP32WifiOTASheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FF82EF6D9AA00C25543 /* ESP32WifiOTASheet.swift */; };
|
||||
23825FFD2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */; };
|
||||
23825FFF2EF6E79C00C25543 /* ESP32BLEOTASheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */; };
|
||||
2388EC382EDF88E900F6F982 /* NordicDFU in Frameworks */ = {isa = PBXBuildFile; productRef = 2388EC372EDF88E900F6F982 /* NordicDFU */; };
|
||||
2388EC3A2EDF8A1400F6F982 /* DFUModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2388EC392EDF8A1400F6F982 /* DFUModel.swift */; };
|
||||
|
|
@ -366,7 +365,7 @@
|
|||
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
|
||||
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
|
||||
230A983F2EF86A44004D87F1 /* AsyncCentral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncCentral.swift; sourceTree = "<group>"; };
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTAViewModel2.swift; sourceTree = "<group>"; };
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTAViewModel.swift; sourceTree = "<group>"; };
|
||||
230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = "<group>"; };
|
||||
231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEAuthorizationHelper.swift; sourceTree = "<group>"; };
|
||||
23148E2F2EE1CCE500F0DB2C /* MeshtasticAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAPI.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -413,7 +412,6 @@
|
|||
237B46952DC8F1C100B22D99 /* RateLimitedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitedButton.swift; sourceTree = "<group>"; };
|
||||
23825FF42EF6CF0B00C25543 /* NWHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWHost.swift; sourceTree = "<group>"; };
|
||||
23825FF82EF6D9AA00C25543 /* ESP32WifiOTASheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32WifiOTASheet.swift; sourceTree = "<group>"; };
|
||||
23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTAViewModel.swift; sourceTree = "<group>"; };
|
||||
23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32BLEOTASheet.swift; sourceTree = "<group>"; };
|
||||
2388EC392EDF8A1400F6F982 /* DFUModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DFUModel.swift; sourceTree = "<group>"; };
|
||||
23A1AFB62E42BD2500E46C96 /* RXTXIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RXTXIndicatorView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -940,10 +938,9 @@
|
|||
23825FFB2EF6E6FB00C25543 /* BLE */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23825FFC2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift */,
|
||||
23825FFE2EF6E79C00C25543 /* ESP32BLEOTASheet.swift */,
|
||||
230A983F2EF86A44004D87F1 /* AsyncCentral.swift */,
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift */,
|
||||
230A98412EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift */,
|
||||
);
|
||||
path = BLE;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1891,7 +1888,6 @@
|
|||
259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */,
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */,
|
||||
DD1BEF522E08E9B80090CE24 /* ChannelLock.swift in Sources */,
|
||||
23825FFD2EF6E70B00C25543 /* ESP32BLEOTAViewModel.swift in Sources */,
|
||||
259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */,
|
||||
259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
|
||||
|
|
@ -1907,7 +1903,7 @@
|
|||
DDD5BB182C2F9C36007E03CA /* OSLogEntryLog.swift in Sources */,
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
|
||||
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel2.swift in Sources */,
|
||||
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift in Sources */,
|
||||
23C2BE2A2EEAF96A00F6A997 /* FirmwareViewModel.swift in Sources */,
|
||||
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct ESP32BLEOTASheet: View {
|
|||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@StateObject var ota = ESP32BLEOTAViewModel2()
|
||||
@StateObject var ota = ESP32BLEOTAViewModel()
|
||||
|
||||
// The stuff were updating, and the place we're updating it to
|
||||
let binFileURL: URL
|
||||
|
|
|
|||
|
|
@ -1,530 +1,176 @@
|
|||
//
|
||||
// ESP32BLEOTAViewModel.swift (previously BLEConnection.swift in the DFU app)
|
||||
// ESP32BLEOTAViewModel2.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/4/22
|
||||
// Created by jake on 12/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
import OSLog
|
||||
import UIKit
|
||||
import CryptoKit
|
||||
|
||||
private let meshtasticOTAServiceId = CBUUID(string: "4FAFC201-1FB5-459E-8FCC-C5C9C331914B")
|
||||
private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") // ESP32 pTxCharacteristic ESP send (notifying)
|
||||
private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005") // ESP32 pOtaCharacteristic ESP write
|
||||
private let statusCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130003") // ESP32 Send (Notify) -> "OK", "ACK", "ERR..."
|
||||
private let otaCharacteristicId = CBUUID(string: "62EC0272-3EC5-11EB-B378-0242AC130005") // ESP32 Receive (Write)
|
||||
|
||||
private let outOfRangeHeuristics: Set<CBError.Code> = [.unknown, .connectionTimeout, .peripheralDisconnected, .connectionFailed]
|
||||
|
||||
class ESP32BLEOTAViewModel: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||
|
||||
var manager: CBCentralManager!
|
||||
var statusCharacteristic: CBCharacteristic?
|
||||
var otaCharacteristic: CBCharacteristic?
|
||||
|
||||
var otaCharacteristicIsNotifying = false
|
||||
var statusCharacteristicIsNotifying = false
|
||||
|
||||
var state = StateBLE.poweredOff
|
||||
|
||||
enum StateBLE {
|
||||
case poweredOff
|
||||
case restoringConnectingPeripheral(CBPeripheral)
|
||||
case restoringConnectedPeripheral(CBPeripheral)
|
||||
case disconnected
|
||||
case scanning
|
||||
case connecting(CBPeripheral, Countdown)
|
||||
case discoveringServices(CBPeripheral, Countdown)
|
||||
case discoveringCharacteristics(CBPeripheral, Countdown)
|
||||
case connected(CBPeripheral)
|
||||
case outOfRange(CBPeripheral)
|
||||
|
||||
var peripheral: CBPeripheral? {
|
||||
switch self {
|
||||
case .poweredOff: return nil
|
||||
case .restoringConnectingPeripheral(let p): return p
|
||||
case .restoringConnectedPeripheral(let p): return p
|
||||
case .disconnected: return nil
|
||||
case .scanning: return nil
|
||||
case .connecting(let p, _): return p
|
||||
case .discoveringServices(let p, _): return p
|
||||
case .discoveringCharacteristics(let p, _): return p
|
||||
case .connected(let p): return p
|
||||
case .outOfRange(let p): return p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used by contentView.swift
|
||||
@MainActor
|
||||
final class ESP32BLEOTAViewModel: ObservableObject {
|
||||
@Published var name = ""
|
||||
@Published var connected = false
|
||||
@Published var transferProgress : Double = 0.0
|
||||
@Published var chunkCount = 1 // number of chunks to be sent before peripheral needs to accknowledge.
|
||||
@Published var elapsedTime = 0.0
|
||||
@Published var kBPerSecond = 0.0
|
||||
@Published var transferProgress: Double = 0
|
||||
@Published var otaStatus: LocalOTAStatusCode = .idle
|
||||
@Published var statusMessage: String = ""
|
||||
|
||||
private let ble = AsyncCentral()
|
||||
|
||||
// OTA file URL
|
||||
var fileUrl: URL?
|
||||
var desiredPeripheral: CBPeripheral?
|
||||
|
||||
// transfer varibles
|
||||
var dataToSend = Data()
|
||||
var dataBuffer = Data()
|
||||
var chunkSize = 0
|
||||
var dataLength = 0
|
||||
var transferOngoing = true
|
||||
var sentBytes = 0
|
||||
var packageCounter = 0
|
||||
var startTime = 0.0
|
||||
var stopTime = 0.0
|
||||
var firstAcknowledgeFromESP32 = false
|
||||
|
||||
// Initiate CentralManager
|
||||
override init() {
|
||||
super.init()
|
||||
manager = CBCentralManager(delegate: self, queue: .none)
|
||||
manager.delegate = self
|
||||
}
|
||||
|
||||
func startOTA(peripheral: CBPeripheral, binFileURL: URL) {
|
||||
self.desiredPeripheral = peripheral
|
||||
self.fileUrl = binFileURL
|
||||
|
||||
self.startScanning()
|
||||
}
|
||||
|
||||
// CentralManager State updates
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
Logger.services.info("Bluetooth State Updated")
|
||||
switch manager.state {
|
||||
case .unknown:
|
||||
Logger.services.info("Unknown")
|
||||
case .resetting:
|
||||
Logger.services.info("Resetting")
|
||||
case .unsupported:
|
||||
Logger.services.info("Unsupported")
|
||||
case .unauthorized:
|
||||
Logger.services.info("Bluetooth is disabled")
|
||||
case .poweredOff:
|
||||
Logger.services.info("Bluetooth is powered off")
|
||||
case .poweredOn:
|
||||
Logger.services.info("Bluetooth is working properly")
|
||||
@unknown default:
|
||||
Logger.services.info("fatal error")
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery (scanning) and handling of BLE devices in range
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
guard case .scanning = state else { return }
|
||||
|
||||
self.name = peripheral.name ?? "Unknown"
|
||||
Logger.services.info("Discovered \(self.name)")
|
||||
|
||||
// Check if this is the desired peripheral
|
||||
if let desiredPeripheral, desiredPeripheral.identifier != peripheral.identifier {
|
||||
Logger.services.info("This peripheral is not the one we're looking for")
|
||||
}
|
||||
|
||||
manager.stopScan()
|
||||
connect(peripheral: peripheral)
|
||||
}
|
||||
|
||||
// Connection established handler
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
Logger.services.info("Connection suceeded")
|
||||
|
||||
transferOngoing = false
|
||||
|
||||
// Clear the data that we may already have
|
||||
dataToSend.removeAll(keepingCapacity: false)
|
||||
|
||||
// Make sure we get the discovery callbacks
|
||||
peripheral.delegate = self
|
||||
|
||||
if peripheral.statusCharacteristic == nil {
|
||||
discoverServices(peripheral: peripheral)
|
||||
} else {
|
||||
setConnected(peripheral: peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
// Connection failed
|
||||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||||
Logger.services.info("\(Date()) CM DidFailToConnect")
|
||||
state = .disconnected
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||||
transferOngoing = false
|
||||
Logger.services.info("\(peripheral.name ?? "unknown") disconnected")
|
||||
// Did our currently-connected peripheral just disconnect?
|
||||
if state.peripheral?.identifier == peripheral.identifier {
|
||||
name = ""
|
||||
connected = false
|
||||
// IME the error codes encountered are:
|
||||
// 0 = rebooting the peripheral.
|
||||
// 6 = out of range.
|
||||
if let error = error, (error as NSError).domain == CBErrorDomain,
|
||||
let code = CBError.Code(rawValue: (error as NSError).code),
|
||||
outOfRangeHeuristics.contains(code) {
|
||||
// Try reconnect without setting a timeout in the state machine.
|
||||
// With CB, it's like saying 'please reconnect me at any point
|
||||
// in the future if this peripheral comes back into range'.
|
||||
Logger.services.info("Connection failure, try and reconnect when the device is back in range")
|
||||
manager.connect(peripheral, options: nil)
|
||||
state = .outOfRange(peripheral)
|
||||
} else {
|
||||
// Likely a deliberate unpairing.
|
||||
state = .disconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// Peripheral callbacks
|
||||
// -----------------------------------------
|
||||
|
||||
// Discover BLE device service(s)
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
Logger.services.info("Discovered Bluetooth Services")
|
||||
// Ignore services discovered late.
|
||||
guard case .discoveringServices = state else {
|
||||
return
|
||||
}
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
guard peripheral.meshtasticOTAService != nil else {
|
||||
Logger.services.info("Meshtastic OTA service missing")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
// All fine so far, go to next step
|
||||
guard let services = peripheral.services else { return }
|
||||
for service in services {
|
||||
peripheral.discoverCharacteristics(nil, for: service)
|
||||
}
|
||||
|
||||
}
|
||||
// Discover BLE device Service charachteristics
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||||
Logger.services.info("Discovering Characteristics For Meshtastic OTA Service")
|
||||
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
guard peripheral.statusCharacteristic != nil else {
|
||||
Logger.services.info("\(Date()) Desired characteristic missing")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
guard let characteristics = service.characteristics else {
|
||||
return
|
||||
}
|
||||
|
||||
for characteristic in characteristics {
|
||||
switch characteristic.uuid {
|
||||
|
||||
case statusCharacteristicId:
|
||||
statusCharacteristic = characteristic
|
||||
Logger.services.info("Discovered Status Characteristic: \(self.statusCharacteristic!.uuid.uuidString)")
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
|
||||
case otaCharacteristicId:
|
||||
otaCharacteristic = characteristic
|
||||
Logger.services.info("Discovered OTA Characteristic: \(self.otaCharacteristic!.uuid.uuidString)")
|
||||
peripheral.setNotifyValue(false, for: characteristic)
|
||||
|
||||
default:
|
||||
Logger.services.info("\(Date()) unknown")
|
||||
}
|
||||
}
|
||||
setConnected(peripheral: peripheral)
|
||||
}
|
||||
// The BLE peripheral device sent some notify data. Deal with it!
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didUpdateValueFor")
|
||||
if let error = error {
|
||||
Logger.services.error("\(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
if let data = characteristic.value {
|
||||
// deal with incoming data
|
||||
// First check if the incoming data is one byte length?
|
||||
// if so it's the peripheral acknowledging and telling
|
||||
// us to send another batch of data
|
||||
if data.count == 1 {
|
||||
if !firstAcknowledgeFromESP32 {
|
||||
firstAcknowledgeFromESP32 = true
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
// Logger.services.info("\(Date()) -X-")
|
||||
if transferOngoing {
|
||||
packageCounter = 0
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when .withResponse is used.
|
||||
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didWriteValueFor")
|
||||
if let error = error {
|
||||
Logger.services.error("\(Date()) Error writing to characteristic: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Callback indicating peripheral notifying state
|
||||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||||
Logger.services.info("\(Date()) PH didUpdateNotificationStateFor")
|
||||
Logger.services.info("\(Date()) PH characteristic: \(characteristic.uuid.uuidString)")
|
||||
if error == nil {
|
||||
Logger.services.info("\(Date()) Notification Set OK, isNotifying: \(characteristic.isNotifying)")
|
||||
if !characteristic.isNotifying {
|
||||
Logger.services.info("\(Date()) isNotifying is false, set to true again!")
|
||||
peripheral.setNotifyValue(true, for: characteristic)
|
||||
} else {
|
||||
if characteristic.uuid == statusCharacteristic?.uuid {
|
||||
statusCharacteristicIsNotifying = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkStartTransfer()
|
||||
}
|
||||
|
||||
func checkStartTransfer() {
|
||||
guard connected else {
|
||||
Logger.services.info("Not connected, cannot start transfer")
|
||||
return
|
||||
}
|
||||
guard statusCharacteristicIsNotifying else {
|
||||
Logger.services.info("Status Characteristic not notifying yet, cannot start transfer")
|
||||
return
|
||||
}
|
||||
guard transferOngoing == false else {
|
||||
Logger.services.info("Transfer already ongoing")
|
||||
return
|
||||
}
|
||||
func startOTA(binURL: URL) {
|
||||
Task {
|
||||
// Prevent screen sleep during update
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
|
||||
// All set, start the transfer
|
||||
self.sendFile()
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
Functions
|
||||
-------------------------------------------------------------------------*/
|
||||
// Scan for a device with the OTA Service UUID (myDesiredServiceId)
|
||||
func startScanning() {
|
||||
Logger.services.info("Scanning for Meshtastic Devices in OTA Mode")
|
||||
guard manager.state == .poweredOn else {
|
||||
Logger.services.info("Cannot scan, Bluetooth is not powered on")
|
||||
return
|
||||
}
|
||||
manager.scanForPeripherals(withServices: [meshtasticOTAServiceId], options: nil)
|
||||
// state = .scanning(Countdown(seconds: 10, closure: {
|
||||
// self.manager.stopScan()
|
||||
// self.state = .disconnected
|
||||
// Logger.services.info("Scan timed out")
|
||||
// }))
|
||||
state = .scanning
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
Logger.services.info("Disconnect")
|
||||
if let peripheral = state.peripheral {
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
}
|
||||
state = .disconnected
|
||||
connected = false
|
||||
transferOngoing = false
|
||||
}
|
||||
|
||||
// Connect to the device from the scanning
|
||||
func connect(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Connect Button Pushed")
|
||||
if connected {
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
} else {
|
||||
// Connect!
|
||||
manager.connect(peripheral, options: nil)
|
||||
name = String(peripheral.name ?? "unknown")
|
||||
Logger.services.info("Attempting connection to \(self.name)")
|
||||
state = .connecting(peripheral, Countdown(seconds: 10, closure: {
|
||||
self.manager.cancelPeripheralConnection(self.state.peripheral!)
|
||||
self.state = .disconnected
|
||||
self.connected = false
|
||||
Logger.services.info("Attempted connection to \(self.name) timed out")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Discover Services of a device
|
||||
func discoverServices(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Discovering Meshtastic OTA service")
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices([meshtasticOTAServiceId])
|
||||
state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: {
|
||||
self.disconnect()
|
||||
Logger.services.info("\(Date()) Could not discover services")
|
||||
}))
|
||||
}
|
||||
|
||||
// Discover Characteristics of a Services
|
||||
func discoverCharacteristics(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Discovering characteristics for Meshtastic OTA service")
|
||||
guard let meshtasticOTAService = peripheral.meshtasticOTAService else {
|
||||
self.disconnect()
|
||||
return
|
||||
}
|
||||
peripheral.discoverCharacteristics([statusCharacteristicId], for: meshtasticOTAService)
|
||||
state = .discoveringCharacteristics(peripheral,
|
||||
Countdown(seconds: 10,
|
||||
closure: {
|
||||
self.disconnect()
|
||||
Logger.services.info("\(Date()) Could not discover characteristics")
|
||||
}))
|
||||
}
|
||||
|
||||
func setConnected(peripheral: CBPeripheral) {
|
||||
Logger.services.info("Max write value with response: \(peripheral.maximumWriteValueLength(for: .withResponse))")
|
||||
Logger.services.info("Max write value without response: \(peripheral.maximumWriteValueLength(for: .withoutResponse))")
|
||||
guard let statusCharacteristic = peripheral.statusCharacteristic
|
||||
else {
|
||||
Logger.services.info("Missing status characteristic")
|
||||
disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
peripheral.setNotifyValue(true, for: statusCharacteristic)
|
||||
state = .connected(peripheral)
|
||||
connected = true
|
||||
name = String(peripheral.name ?? "unknown")
|
||||
do {
|
||||
// --- 1. Connection Phase ---
|
||||
self.statusMessage = "Connecting..."
|
||||
self.otaStatus = .waitingForConnection
|
||||
|
||||
try await ble.waitUntilPoweredOn()
|
||||
let peripheral = try await ble.scan(for: meshtasticOTAServiceId)
|
||||
name = peripheral.name ?? "unknown"
|
||||
try await ble.connect(peripheral)
|
||||
|
||||
otaStatus = .connected
|
||||
self.statusMessage = "Discovering Services..."
|
||||
|
||||
checkStartTransfer()
|
||||
}
|
||||
|
||||
// Peripheral callback when its ready to receive more data without response
|
||||
func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
|
||||
if transferOngoing && packageCounter < chunkCount {
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFile() {
|
||||
Logger.services.info("Start sending .bin file to device")
|
||||
|
||||
// 1. Get the data from the file(name) and copy data to dataBUffer
|
||||
guard let fileUrl, let data: Data = try? Data(contentsOf: fileUrl) else {
|
||||
Logger.services.info("Failed to open .bin file")
|
||||
return
|
||||
}
|
||||
dataBuffer = data
|
||||
dataLength = dataBuffer.count
|
||||
|
||||
// 1. Get the peripheral and its transfer characteristic
|
||||
guard let discoveredPeripheral = state.peripheral else { return }
|
||||
// Send dataLength to the device
|
||||
let sizeMessage = "OTA_SIZE:\(dataLength)"
|
||||
if let sizeData = sizeMessage.data(using: .utf8) {
|
||||
// Send sizeData to the peripheral
|
||||
// Assuming writeCharacteristic is the characteristic for sending messages
|
||||
discoveredPeripheral.writeValue(sizeData, for: otaCharacteristic!, type: .withoutResponse)
|
||||
Logger.services.info("Sent OTA size message: \(sizeMessage)")
|
||||
} else {
|
||||
Logger.services.info("Failed to encode OTA size message")
|
||||
return
|
||||
}
|
||||
|
||||
// Logger.services.info the total size of the data in hexadecimal format
|
||||
Logger.services.info("Total data size (hexadecimal): \(String(format: "%02X", self.dataBuffer.count))")
|
||||
transferOngoing = true
|
||||
|
||||
packageCounter = 0
|
||||
// Send the first chunk
|
||||
elapsedTime = 0.0
|
||||
sentBytes = 0
|
||||
firstAcknowledgeFromESP32 = false
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
writeDataToPeripheral(characteristic: otaCharacteristic!)
|
||||
}
|
||||
|
||||
func writeDataToPeripheral(characteristic: CBCharacteristic) {
|
||||
// 1. Get the peripheral and its transfer characteristic
|
||||
guard let discoveredPeripheral = state.peripheral else { return }
|
||||
// ATT MTU - 3 bytes
|
||||
let maxWriteValueLength = discoveredPeripheral.maximumWriteValueLength(for: .withoutResponse)
|
||||
chunkSize = maxWriteValueLength - 3
|
||||
Logger.services.info("Chunk size: \(self.chunkSize), 0x\(String(format: "%02X", self.chunkSize))")
|
||||
// Get the data range
|
||||
var range: Range<Data.Index>
|
||||
|
||||
// 2. Loop through and send each chunk to the BLE device
|
||||
// check to see if the number of iterations completed and the peripheral can accept more data
|
||||
// package counter allows only "chunkCount" of data to be sent per time.
|
||||
while transferOngoing && packageCounter < chunkCount {
|
||||
// 3. Create a range based on the length of data to return
|
||||
range = (0..<min(chunkSize, dataBuffer.count))
|
||||
// 4. Get a subcopy copy of data
|
||||
let subData = dataBuffer.subdata(in: range)
|
||||
// Logger.services.info the first byte of the subData package as hexadecimal
|
||||
if let firstByte = subData.first {
|
||||
Logger.services.info("First byte of subData package: \(String(format: "%02X", firstByte))")
|
||||
}
|
||||
// 5. Send data chunk to BLE peripheral, send EOF when buffer is empty.
|
||||
if !dataBuffer.isEmpty {
|
||||
discoveredPeripheral.writeValue(subData, for: characteristic, type: .withoutResponse)
|
||||
packageCounter += 1
|
||||
// Logger.services.info(" Packages: \(packageCounter) bytes: \(subData.count)")
|
||||
} else {
|
||||
transferOngoing = false
|
||||
let services = try await ble.discoverServices([meshtasticOTAServiceId], on: peripheral)
|
||||
guard let service = services.first(where: { $0.uuid == meshtasticOTAServiceId }) else { throw BLEError.serviceMissing }
|
||||
|
||||
let chars = try await ble.discoverCharacteristics([statusCharacteristicId, otaCharacteristicId],
|
||||
in: service,
|
||||
on: peripheral)
|
||||
guard
|
||||
let statusChar = chars.first(where: { $0.uuid == statusCharacteristicId }),
|
||||
let otaChar = chars.first(where: { $0.uuid == otaCharacteristicId })
|
||||
else { throw BLEError.characteristicMissing }
|
||||
|
||||
// --- 2. Setup Notification Stream ---
|
||||
try await ble.setNotify(true, for: statusChar, on: peripheral)
|
||||
let stream = ble.notifications(for: statusChar)
|
||||
var iterator = stream.makeAsyncIterator()
|
||||
|
||||
// --- 3. Prepare Firmware & Command ---
|
||||
let data = try Data(contentsOf: binURL)
|
||||
let sha256Digest = SHA256.hash(data: data)
|
||||
let fileHash = sha256Digest.map { String(format: "%02hhx", $0) }.joined()
|
||||
let fileSize = data.count
|
||||
|
||||
Logger.services.info("Firmware Size: \(fileSize), Hash: \(fileHash)")
|
||||
|
||||
// Unified Protocol Command: "OTA <size> <hash>\n"
|
||||
let command = "OTA \(fileSize) \(fileHash)\n"
|
||||
|
||||
// --- 4. Handshake ---
|
||||
self.statusMessage = "Negotiating..."
|
||||
|
||||
// Send command
|
||||
try await ble.writeValue(Data(command.utf8), for: otaChar, type: .withResponse, on: peripheral)
|
||||
|
||||
// Wait for "OK" response from ESP32, handling "ERASING" intermediate state
|
||||
var handshakeComplete = false
|
||||
|
||||
while !handshakeComplete {
|
||||
guard let handshakeData = await iterator.next(),
|
||||
let handshakeStr = String(data: handshakeData, encoding: .utf8) else {
|
||||
throw OTAError.unexpectedResponse("Connection lost during handshake")
|
||||
}
|
||||
|
||||
let trimmed = handshakeStr.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if trimmed == "OK" {
|
||||
handshakeComplete = true
|
||||
} else if trimmed == "ERASING" {
|
||||
// Update UI to let user know the device is busy erasing partition
|
||||
self.statusMessage = "Erasing partition..."
|
||||
Logger.services.info("Device is erasing flash...")
|
||||
// Continue loop to wait for OK
|
||||
} else {
|
||||
// Any other response is an error
|
||||
throw OTAError.unexpectedResponse(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.services.info("Handshake OK. Starting Stream.")
|
||||
|
||||
// --- 5. Upload Stream ---
|
||||
self.otaStatus = .transferring
|
||||
self.statusMessage = "Uploading..."
|
||||
|
||||
var offset = 0
|
||||
// Use MTU - 3 bytes overhead for chunk size.
|
||||
let chunkSize = peripheral.maximumWriteValueLength(for: .withoutResponse)
|
||||
|
||||
while offset < fileSize {
|
||||
let endIndex = min(offset + chunkSize, fileSize)
|
||||
let chunk = data.subdata(in: offset..<endIndex)
|
||||
|
||||
// Send chunk
|
||||
try await ble.writeValue(chunk, for: otaChar, type: .withoutResponse, on: peripheral)
|
||||
|
||||
// Optimistically calculate new offset to determine if this was the last chunk
|
||||
let nextOffset = offset + chunk.count
|
||||
|
||||
// [FLOW CONTROL]
|
||||
// Wait for ACK (or OK if last packet) before proceeding.
|
||||
// This ensures the ESP32 has written the chunk to flash.
|
||||
guard let respData = await iterator.next(),
|
||||
let respStr = String(data: respData, encoding: .utf8) else {
|
||||
throw OTAError.unexpectedResponse("Connection lost waiting for ACK")
|
||||
}
|
||||
|
||||
let trimmed = respStr.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if trimmed == "ACK" {
|
||||
// Normal chunk processed successfully
|
||||
offset = nextOffset
|
||||
|
||||
// Update UI occasionally
|
||||
if offset % (chunkSize * 20) == 0 {
|
||||
self.transferProgress = Double(offset) / Double(fileSize)
|
||||
}
|
||||
|
||||
} else if trimmed == "OK" {
|
||||
// "OK" indicates completion (hash verified, partition set).
|
||||
// This should only happen on the very last chunk.
|
||||
if nextOffset >= fileSize {
|
||||
offset = nextOffset
|
||||
self.transferProgress = 1.0
|
||||
self.otaStatus = .completed
|
||||
self.statusMessage = "Success! Rebooting..."
|
||||
Logger.services.info("OTA Success (OK received on last chunk)")
|
||||
break // Exit loop
|
||||
} else {
|
||||
// OK received before we finished sending? Error.
|
||||
throw OTAError.unexpectedResponse("Premature OK received at offset \(nextOffset)")
|
||||
}
|
||||
|
||||
} else {
|
||||
// Likely ERR or garbage
|
||||
throw OTAError.unexpectedResponse(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
// Double check completion state
|
||||
if self.otaStatus != .completed {
|
||||
throw OTAError.unexpectedResponse("Stream ended without OK")
|
||||
}
|
||||
|
||||
} catch {
|
||||
self.otaStatus = .error
|
||||
self.statusMessage = "Error: \(error.localizedDescription)"
|
||||
Logger.services.error("OTA Failed: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
if discoveredPeripheral.canSendWriteWithoutResponse {
|
||||
Logger.services.info("BLE peripheral ready?: \(discoveredPeripheral.canSendWriteWithoutResponse)")
|
||||
}
|
||||
|
||||
// 6. Remove already sent data from buffer
|
||||
dataBuffer.removeSubrange(range)
|
||||
|
||||
// 7. calculate and Logger.services.info the transfer progress in %
|
||||
transferProgress = (1 - (Double(dataBuffer.count) / Double(dataLength))) * 100
|
||||
Logger.services.info("File transfer progress: \(String(format: "%.02f", self.transferProgress))%")
|
||||
sentBytes += chunkSize
|
||||
elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
|
||||
let kbPs = Double(sentBytes) / elapsedTime
|
||||
kBPerSecond = kbPs / 1000
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBPeripheral {
|
||||
// Helper to find the service we're interested in.
|
||||
var meshtasticOTAService: CBService? {
|
||||
guard let services = services else { return nil }
|
||||
return services.first { $0.uuid == meshtasticOTAServiceId }
|
||||
}
|
||||
// Helper to find the characteristic we're interested in.
|
||||
var statusCharacteristic: CBCharacteristic? {
|
||||
guard let characteristics = meshtasticOTAService?.characteristics else {
|
||||
return nil
|
||||
}
|
||||
return characteristics.first { $0.uuid == statusCharacteristicId }
|
||||
}
|
||||
}
|
||||
|
||||
class Countdown {
|
||||
let timer: Timer
|
||||
init(seconds: TimeInterval, closure: @escaping () -> Void) {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in closure() })
|
||||
}
|
||||
deinit {
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,18 +72,37 @@ final class ESP32BLEOTAViewModel2: ObservableObject {
|
|||
// --- 4. Handshake ---
|
||||
self.statusMessage = "Negotiating..."
|
||||
|
||||
// Send command with response to ensure delivery before waiting for logic response
|
||||
// Send command
|
||||
try await ble.writeValue(Data(command.utf8), for: otaChar, type: .withResponse, on: peripheral)
|
||||
|
||||
// Wait for "OK" response from ESP32
|
||||
guard let handshakeData = await iterator.next(),
|
||||
let handshakeStr = String(data: handshakeData, encoding: .utf8),
|
||||
handshakeStr.trimmingCharacters(in: .whitespacesAndNewlines) == "OK" else {
|
||||
throw OTAError.unexpectedResponse("Handshake failed")
|
||||
// Wait for "OK" response from ESP32, handling "ERASING" intermediate state
|
||||
var handshakeComplete = false
|
||||
|
||||
while !handshakeComplete {
|
||||
guard let handshakeData = await iterator.next(),
|
||||
let handshakeStr = String(data: handshakeData, encoding: .utf8) else {
|
||||
throw OTAError.unexpectedResponse("Connection lost during handshake")
|
||||
}
|
||||
|
||||
let trimmed = handshakeStr.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if trimmed == "OK" {
|
||||
handshakeComplete = true
|
||||
} else if trimmed == "ERASING" {
|
||||
// Update UI to let user know the device is busy erasing partition
|
||||
self.statusMessage = "Erasing partition..."
|
||||
Logger.services.info("Device is erasing flash...")
|
||||
// Continue loop to wait for OK
|
||||
} else {
|
||||
// Any other response is an error
|
||||
throw OTAError.unexpectedResponse(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.services.info("Handshake OK. Starting Stream.")
|
||||
|
||||
Logger.services.info("Handshake OK. Starting Stream.")
|
||||
|
||||
// --- 5. Upload Stream ---
|
||||
self.otaStatus = .transferring
|
||||
self.statusMessage = "Uploading..."
|
||||
|
|
@ -124,9 +124,11 @@ struct ESP32OTAIntroSheet: View {
|
|||
}
|
||||
|
||||
}.sheet(isPresented: $showWifiUpdater) {
|
||||
ESP32WifiOTASheet(binFileURL: binFileURL).environmentObject(accessoryManager)
|
||||
ESP32WifiOTASheet(binFileURL: binFileURL)
|
||||
.environmentObject(accessoryManager)
|
||||
}.sheet(isPresented: $showBLEUpdater) {
|
||||
ESP32BLEOTASheet(binFileURL: binFileURL).environmentObject(accessoryManager)
|
||||
ESP32BLEOTASheet(binFileURL: binFileURL)
|
||||
.environmentObject(accessoryManager)
|
||||
}
|
||||
.navigationTitle("ESP32 Update")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
|
|
|||
|
|
@ -194,12 +194,24 @@ class ESP32WifiOTAViewModel: ObservableObject {
|
|||
// 3. Send Command
|
||||
try await connection.sendAsync(data: command.data(using: .utf8)!)
|
||||
|
||||
// 4. Wait for initial "OK\n"
|
||||
let response = try await readLine(from: connection)
|
||||
if response.trimmingCharacters(in: .whitespacesAndNewlines) != "OK" {
|
||||
throw OTAError.unexpectedResponse(response)
|
||||
// 4. Wait for initial "OK\n", handling "ERASING"
|
||||
var handshakeComplete = false
|
||||
while !handshakeComplete {
|
||||
let response = try await readLine(from: connection)
|
||||
let trimmed = response.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if trimmed == "OK" {
|
||||
handshakeComplete = true
|
||||
} else if trimmed == "ERASING" {
|
||||
await updateUI {
|
||||
self.statusMessage = "Erasing partition..."
|
||||
}
|
||||
Logger.services.info("[ESP OTA] Device is erasing flash...")
|
||||
} else {
|
||||
throw OTAError.unexpectedResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 5. Stream Firmware Data
|
||||
await updateUI {
|
||||
self.otaState = .transferring
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue