From be5647f9a7a2379336936c11599698f875a7e5ca Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 17 Jan 2024 17:01:09 +0100 Subject: [PATCH 1/4] :truck: (LekaUpdater): Add UpdateProcessV150 Copied from UpdateProcessV130 --- .../UpdateProcessController.swift | 3 + .../Version/UpdateProcessV150.swift | 611 ++++++++++++++++++ 2 files changed, 614 insertions(+) create mode 100644 Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift index 29ddfa6e85..445b172f94 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/UpdateProcessController.swift @@ -45,6 +45,8 @@ class UpdateProcessController { case Version(1, 3, 0), Version(1, 4, 0): self.currentUpdateProcess = UpdateProcessV130() + case Version(1, 5, 0): + self.currentUpdateProcess = UpdateProcessV150() default: self.currentUpdateProcess = UpdateProcessTemplate() } @@ -63,6 +65,7 @@ class UpdateProcessController { Version(1, 2, 0), Version(1, 3, 0), Version(1, 4, 0), + Version(1, 5, 0), ] public var currentStage = CurrentValueSubject(.initial) diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift new file mode 100644 index 0000000000..2be599420b --- /dev/null +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift @@ -0,0 +1,611 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import BLEKit +import Combine +import Foundation +import GameplayKit +import RobotKit +import Version + +// MARK: - UpdateEvent + +// swiftlint:disable file_length + +private enum UpdateEvent { + case startUpdateRequested + + case fileLoaded + case failedToLoadFile + case fileExchangeStateSet + case destinationPathSet + case fileCleared + case fileSent + case fileVerificationReceived + case robotDisconnected + case robotDetected +} + +// MARK: - StateEventProcessor + +private protocol StateEventProcessor { + func process(event: UpdateEvent) +} + +// MARK: - StateInitial + +private class StateInitial: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateLoadingUpdateFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + func process(event: UpdateEvent) { + switch event { + case .startUpdateRequested: + stateMachine?.enter(StateLoadingUpdateFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateLoadingUpdateFile + +private class StateLoadingUpdateFile: GKState, StateEventProcessor { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateErrorFailedToLoadFile.Type || stateClass is StateSettingFileExchangeState.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + let isLoaded = globalFirmwareManager.load() + + if isLoaded { + self.process(event: .fileLoaded) + } else { + self.process(event: .failedToLoadFile) + } + } + + func process(event: UpdateEvent) { + switch event { + case .fileLoaded: + stateMachine?.enter(StateSettingFileExchangeState.self) + case .failedToLoadFile: + stateMachine?.enter(StateErrorFailedToLoadFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } +} + +// MARK: - StateSettingFileExchangeState + +private class StateSettingFileExchangeState: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSettingDestinationPath.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setFileExchangeState) + } + + func process(event: UpdateEvent) { + switch event { + case .fileExchangeStateSet: + stateMachine?.enter(StateSettingDestinationPath.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setFileExchangeState() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.setState, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .fileExchangeStateSet) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSettingDestinationPath + +private class StateSettingDestinationPath: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateClearingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: self.setDestinationPath) + } + + func process(event: UpdateEvent) { + switch event { + case .destinationPathSet: + stateMachine?.enter(StateClearingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setDestinationPath() { + let osVersion = globalFirmwareManager.currentVersion + + let directory = "/fs/usr/os" + let filename = "LekaOS-\(osVersion).bin" + let destinationPath = directory + "/" + filename + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.filePath, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .destinationPathSet) + } + ) + + Robot.shared.connectedPeripheral?.send(destinationPath.data(using: .utf8)!, forCharacteristic: characteristic) + } +} + +// MARK: - StateClearingFile + +private class StateClearingFile: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateSendingFile.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setClearPath) + } + + func process(event: UpdateEvent) { + switch event { + case .fileCleared: + stateMachine?.enter(StateSendingFile.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private func setClearPath() { + let data = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.clearFile, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.process(event: .fileCleared) + } + ) + + Robot.shared.connectedPeripheral?.send(data, forCharacteristic: characteristic) + } +} + +// MARK: - StateSendingFile + +private class StateSendingFile: GKState, StateEventProcessor { + // MARK: Lifecycle + + override init() { + let dataSize = globalFirmwareManager.data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + + self.currentPacket = 0 + + super.init() + + self.subscribeToFirmwareDataUpdates() + } + + // MARK: Public + + public var progression = CurrentValueSubject(0.0) + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateApplyingUpdate.Type || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.sendFile) + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + self.characteristic = nil + } + + func process(event: UpdateEvent) { + switch event { + case .fileSent: + stateMachine?.enter(StateApplyingUpdate.self) + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private let maximumPacketSize: Int = 61 + + private var currentPacket: Int = 0 + private var expectedCompletePackets: Int + private var expectedRemainingBytes: Int + private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, + serviceUUID: BLESpecs.FileExchange.service, + onWrite: { + self.currentPacket += 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.04, execute: self.tryToSendNextPacket) + } + ) + + private var expectedPackets: Int { + self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 + } + + private var _progression: Float { + Float(self.currentPacket) / Float(self.expectedPackets) + } + + private func subscribeToFirmwareDataUpdates() { + globalFirmwareManager.$data + .receive(on: DispatchQueue.main) + .sink { data in + let dataSize = data.count + + self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) + self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + } + .store(in: &self.cancellables) + } + + private func sendFile() { + self.tryToSendNextPacket() + } + + private func tryToSendNextPacket() { + self.progression.send(self._progression) + if self._progression < 1.0 { + self.sendNextPacket() + } else { + self.process(event: .fileSent) + } + } + + private func sendNextPacket() { + let startIndex = self.currentPacket * self.maximumPacketSize + let endIndex = + self.currentPacket < self.expectedCompletePackets + ? startIndex + self.maximumPacketSize - 1 : startIndex + self.expectedRemainingBytes - 1 + + let dataToSend = globalFirmwareManager.data[startIndex...endIndex] + + if let characteristic { + Robot.shared.connectedPeripheral?.send(dataToSend, forCharacteristic: characteristic) + } + } +} + +// MARK: - StateApplyingUpdate + +private class StateApplyingUpdate: GKState, StateEventProcessor { + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateWaitingForRobotToReboot.Type + } + + override func didEnter(from _: GKState?) { + self.setMajor() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDisconnected: + stateMachine?.enter(StateWaitingForRobotToReboot.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private func setMajor() { + let majorData = Data([globalFirmwareManager.major]) + + let majorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMajor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setMinor + ) + + Robot.shared.connectedPeripheral?.send(majorData, forCharacteristic: majorCharacteristic) + } + + private func setMinor() { + let minorData = Data([globalFirmwareManager.minor]) + + let minorCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionMinor, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.setRevision + ) + + Robot.shared.connectedPeripheral?.send(minorData, forCharacteristic: minorCharacteristic) + } + + private func setRevision() { + let revisionData = globalFirmwareManager.revision.data + + let revisionCharacteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.versionRevision, + serviceUUID: BLESpecs.FirmwareUpdate.service, + onWrite: self.applyUpdate + ) + + Robot.shared.connectedPeripheral?.send(revisionData, forCharacteristic: revisionCharacteristic) + } + + private func applyUpdate() { + let applyValue = Data([1]) + + let characteristic = CharacteristicModelWriteOnly( + characteristicUUID: BLESpecs.FirmwareUpdate.Characteristics.requestUpdate, + serviceUUID: BLESpecs.FirmwareUpdate.service + ) + + Robot.shared.connectedPeripheral?.send(applyValue, forCharacteristic: characteristic) + } +} + +// MARK: - StateWaitingForRobotToReboot + +private class StateWaitingForRobotToReboot: GKState, StateEventProcessor { + // MARK: Lifecycle + + init(expectedRobot: RobotPeripheral?) { + self.expectedRobot = expectedRobot + } + + // MARK: Internal + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + stateClass is StateFinal.Type || stateClass is StateErrorRobotNotUpToDate.Type + || stateClass is StateErrorRobotUnexpectedDisconnection.Type + } + + override func didEnter(from _: GKState?) { + self.registerScanForRobot() + } + + override func willExit(to _: GKState) { + self.cancellables.removeAll() + } + + func process(event: UpdateEvent) { + switch event { + case .robotDetected: + if self.isRobotUpToDate { + stateMachine?.enter(StateFinal.self) + } else { + stateMachine?.enter(StateErrorRobotNotUpToDate.self) + } + case .robotDisconnected: + stateMachine?.enter(StateErrorRobotUnexpectedDisconnection.self) + default: + return + } + } + + // MARK: Private + + private var cancellables: Set = [] + + private var expectedRobot: RobotPeripheral? + private var isRobotUpToDate: Bool = false + + private func registerScanForRobot() { + BLEManager.shared.scanForRobots() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in + // nothing to do + }, + receiveValue: { robotDiscoveryList in + let robotDetected = robotDiscoveryList.first { robotDiscovery in + robotDiscovery.robotPeripheral == self.expectedRobot + } + if let robotDetected { + self.isRobotUpToDate = + Version(robotDetected.osVersion) == globalFirmwareManager.currentVersion + + self.process(event: .robotDetected) + } + } + ) + .store(in: &self.cancellables) + } +} + +// MARK: - StateFinal + +private class StateFinal: GKState {} + +// MARK: - StateError + +private protocol StateError {} + +// MARK: - StateErrorFailedToLoadFile + +private class StateErrorFailedToLoadFile: GKState, StateError {} + +// MARK: - StateErrorRobotNotUpToDate + +private class StateErrorRobotNotUpToDate: GKState, StateError {} + +// MARK: - StateErrorRobotUnexpectedDisconnection + +private class StateErrorRobotUnexpectedDisconnection: GKState, StateError {} + +// MARK: - UpdateProcessV150 + +class UpdateProcessV150: UpdateProcessProtocol { + // MARK: Lifecycle + + init() { + self.stateMachine = GKStateMachine(states: [ + StateInitial(), + + StateLoadingUpdateFile(), + StateSettingFileExchangeState(), + StateSettingDestinationPath(), + StateClearingFile(), + self.stateSendingFile, + StateApplyingUpdate(), + StateWaitingForRobotToReboot(expectedRobot: Robot.shared.connectedPeripheral), + + StateFinal(), + + StateErrorFailedToLoadFile(), + StateErrorRobotNotUpToDate(), + StateErrorRobotUnexpectedDisconnection(), + ]) + self.stateMachine?.enter(StateInitial.self) + + self.startRoutineToUpdateCurrentState() + self.registerDidDisconnect() + self.sendingFileProgression = self.stateSendingFile.progression + } + + // MARK: Public + + // MARK: - Public variables + + public var currentStage = CurrentValueSubject(.initial) + public var sendingFileProgression = CurrentValueSubject(0.0) + + public func startProcess() { + self.process(event: .startUpdateRequested) + } + + // MARK: Private + + // MARK: - Private variables + + private var stateMachine: GKStateMachine? + private var stateSendingFile = StateSendingFile() + + private var cancellables: Set = [] + + private func process(event: UpdateEvent) { + guard let state = stateMachine?.currentState as? any StateEventProcessor else { + return + } + + state.process(event: event) + + self.updateCurrentState() + } + + private func registerDidDisconnect() { + BLEManager.shared.didDisconnect + .receive(on: DispatchQueue.main) + .sink { + self.process(event: .robotDisconnected) + } + .store(in: &self.cancellables) + } + + private func startRoutineToUpdateCurrentState() { + Timer.publish(every: 1, on: .main, in: .default) + .autoconnect() + .sink { _ in + self.updateCurrentState() + } + .store(in: &self.cancellables) + } + + private func updateCurrentState() { + guard let state = stateMachine?.currentState else { return } + + switch state { + case is StateInitial: + self.currentStage.send(.initial) + case is StateLoadingUpdateFile, + is StateSettingFileExchangeState, + is StateSettingDestinationPath, + is StateClearingFile, + is StateSendingFile: + self.currentStage.send(.sendingUpdate) + case is StateApplyingUpdate, + is StateWaitingForRobotToReboot: + self.currentStage.send(.installingUpdate) + case is StateFinal: + self.currentStage.send(completion: .finished) + case is any StateError: + self.sendError(state: state) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } + + private func sendError(state: GKState) { + switch state { + case is StateErrorFailedToLoadFile: + self.currentStage.send(completion: .failure(.failedToLoadFile)) + case is StateErrorRobotNotUpToDate: + self.currentStage.send(completion: .failure(.robotNotUpToDate)) + case is StateErrorRobotUnexpectedDisconnection: + self.currentStage.send(completion: .failure(.robotUnexpectedDisconnection)) + default: + self.currentStage.send(completion: .failure(.unknown)) + } + } +} + +// swiftlint:enable file_length From 00cd0230b583f1c90bf2111acacf53b1b683dfc3 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 17 Jan 2024 18:03:37 +0100 Subject: [PATCH 2/4] :zap: (LekaUpdater): Remove/Reduce waiting time before writing in BLE --- .../UpdateProcess/Version/UpdateProcessV150.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift index 2be599420b..a18a73b340 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift @@ -95,7 +95,7 @@ private class StateSettingFileExchangeState: GKState, StateEventProcessor { } override func didEnter(from _: GKState?) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setFileExchangeState) + self.setFileExchangeState() } func process(event: UpdateEvent) { @@ -118,6 +118,7 @@ private class StateSettingFileExchangeState: GKState, StateEventProcessor { characteristicUUID: BLESpecs.FileExchange.Characteristics.setState, serviceUUID: BLESpecs.FileExchange.service, onWrite: { + sleep(1) self.process(event: .fileExchangeStateSet) } ) @@ -136,7 +137,7 @@ private class StateSettingDestinationPath: GKState, StateEventProcessor { } override func didEnter(from _: GKState?) { - DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: self.setDestinationPath) + self.setDestinationPath() } func process(event: UpdateEvent) { @@ -181,7 +182,7 @@ private class StateClearingFile: GKState, StateEventProcessor { } override func didEnter(from _: GKState?) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.setClearPath) + self.setClearPath() } func process(event: UpdateEvent) { @@ -241,7 +242,7 @@ private class StateSendingFile: GKState, StateEventProcessor { } override func didEnter(from _: GKState?) { - DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: self.sendFile) + self.sendFile() } override func willExit(to _: GKState) { @@ -274,7 +275,7 @@ private class StateSendingFile: GKState, StateEventProcessor { serviceUUID: BLESpecs.FileExchange.service, onWrite: { self.currentPacket += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.04, execute: self.tryToSendNextPacket) + self.tryToSendNextPacket() } ) From 2c5ac38133ab97ba26181db4865b694b648d7435 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Mon, 17 Jun 2024 11:37:07 +0200 Subject: [PATCH 3/4] :sparkles: (BLEKit): Add negotiated MTU characteristic --- Modules/BLEKit/Sources/BLESpecs.swift | 1 + .../RobotKit/Sources/Robot+Information.swift | 30 +++++++++++++++++++ Modules/RobotKit/Sources/Robot.swift | 3 ++ 3 files changed, 34 insertions(+) diff --git a/Modules/BLEKit/Sources/BLESpecs.swift b/Modules/BLEKit/Sources/BLESpecs.swift index bdef0f0f75..b8d6371c60 100644 --- a/Modules/BLEKit/Sources/BLESpecs.swift +++ b/Modules/BLEKit/Sources/BLESpecs.swift @@ -37,6 +37,7 @@ public enum BLESpecs { public static let screensaverEnable = CBUUID(string: "0x8369") public static let softReboot = CBUUID(string: "0x8382") public static let hardReboot = CBUUID(string: "0x7282") + public static let negotiatedMTU = CBUUID(data: Data("NEGOTIATED_MTU".utf8) + Data([0, 0])) } public static let service = CBUUID(string: "0x7779") diff --git a/Modules/RobotKit/Sources/Robot+Information.swift b/Modules/RobotKit/Sources/Robot+Information.swift index af2065343f..117ac60858 100644 --- a/Modules/RobotKit/Sources/Robot+Information.swift +++ b/Modules/RobotKit/Sources/Robot+Information.swift @@ -37,6 +37,21 @@ extension Robot { connectedPeripheral?.notifyingCharacteristics.insert(characteristic) } + func registerNegotiatedMTUNotificationCallback() { + let characteristic = CharacteristicModelNotifying( + characteristicUUID: BLESpecs.Monitoring.Characteristics.negotiatedMTU, + serviceUUID: BLESpecs.Monitoring.service, + onNotification: { data in + if let data { + self.negotiatedMTU.send(Int(data.map { UInt16($0) }.reduce(0) { $0 << 8 + $1 })) + log.trace("🤖 negotiated MTU: \(self.negotiatedMTU.value)") + } + } + ) + + connectedPeripheral?.notifyingCharacteristics.insert(characteristic) + } + func registerOSVersionReadCallback() { let characteristic = CharacteristicModelReadOnly( characteristicUUID: BLESpecs.DeviceInformation.Characteristics.osVersion, @@ -86,4 +101,19 @@ extension Robot { connectedPeripheral?.readOnlyCharacteristics.insert(characteristic) } + + func registerNegotiatedMTUReadCallback() { + let characteristic = CharacteristicModelReadOnly( + characteristicUUID: BLESpecs.Monitoring.Characteristics.negotiatedMTU, + serviceUUID: BLESpecs.Monitoring.service, + onRead: { data in + if let data { + self.negotiatedMTU.send(Int(data.map { UInt16($0) }.reduce(0) { $0 << 8 + $1 })) + log.trace("🤖 negotiated MTU: \(self.negotiatedMTU.value)") + } + } + ) + + connectedPeripheral?.readOnlyCharacteristics.insert(characteristic) + } } diff --git a/Modules/RobotKit/Sources/Robot.swift b/Modules/RobotKit/Sources/Robot.swift index 5f95778ee6..ab38e51d01 100644 --- a/Modules/RobotKit/Sources/Robot.swift +++ b/Modules/RobotKit/Sources/Robot.swift @@ -32,6 +32,7 @@ public class Robot { public var serialNumber: CurrentValueSubject = CurrentValueSubject("(n/a)") public var isCharging: CurrentValueSubject = CurrentValueSubject(false) public var battery: CurrentValueSubject = CurrentValueSubject(0) + public var negotiatedMTU: CurrentValueSubject = CurrentValueSubject(0) // MARK: - Internal properties @@ -39,10 +40,12 @@ public class Robot { didSet { registerBatteryCharacteristicNotificationCallback() registerChargingStatusNotificationCallback() + registerNegotiatedMTUNotificationCallback() registerOSVersionReadCallback() registerSerialNumberReadCallback() registerChargingStatusReadCallback() + registerNegotiatedMTUReadCallback() self.connectedPeripheral?.discoverAndListenForUpdates() self.connectedPeripheral?.readReadOnlyCharacteristics() From 7d7b7e07fb73ccd41a6c0ba80311084330823778 Mon Sep 17 00:00:00 2001 From: Yann Locatelli Date: Wed, 17 Jan 2024 17:20:43 +0100 Subject: [PATCH 4/4] :zap: (LekaUpdater): Maximum packet size related to negotiated MTU 182 is the max value possible on iOS device, indeed 185 is the max MTU and the header is always 3 bytes long, so each packet has a limit of 182 bytes --- .../Version/UpdateProcessV150.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift index a18a73b340..feb47e449d 100644 --- a/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift +++ b/Apps/LekaUpdater/Sources/Libs/UpdateProcess/Version/UpdateProcessV150.swift @@ -219,10 +219,7 @@ private class StateSendingFile: GKState, StateEventProcessor { // MARK: Lifecycle override init() { - let dataSize = globalFirmwareManager.data.count - - self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) - self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + self.firmwareDataSize = globalFirmwareManager.data.count self.currentPacket = 0 @@ -242,6 +239,7 @@ private class StateSendingFile: GKState, StateEventProcessor { } override func didEnter(from _: GKState?) { + self.maximumPacketSize = Robot.shared.negotiatedMTU.value - self.l2capOverhead self.sendFile() } @@ -265,11 +263,6 @@ private class StateSendingFile: GKState, StateEventProcessor { private var cancellables: Set = [] - private let maximumPacketSize: Int = 61 - - private var currentPacket: Int = 0 - private var expectedCompletePackets: Int - private var expectedRemainingBytes: Int private lazy var characteristic: CharacteristicModelWriteOnly? = CharacteristicModelWriteOnly( characteristicUUID: BLESpecs.FileExchange.Characteristics.fileReceptionBuffer, serviceUUID: BLESpecs.FileExchange.service, @@ -279,6 +272,20 @@ private class StateSendingFile: GKState, StateEventProcessor { } ) + private var firmwareDataSize: Int = 0 + private let l2capOverhead: Int = 3 + private var maximumPacketSize: Int = 182 // 185 (iOS max MTU) - 3 (header size) + + private var currentPacket: Int = 0 + + private var expectedCompletePackets: Int { + Int(floor(Double(self.firmwareDataSize / self.maximumPacketSize))) + } + + private var expectedRemainingBytes: Int { + self.firmwareDataSize % self.maximumPacketSize + } + private var expectedPackets: Int { self.expectedRemainingBytes == 0 ? self.expectedCompletePackets : self.expectedCompletePackets + 1 } @@ -291,10 +298,7 @@ private class StateSendingFile: GKState, StateEventProcessor { globalFirmwareManager.$data .receive(on: DispatchQueue.main) .sink { data in - let dataSize = data.count - - self.expectedCompletePackets = Int(floor(Double(dataSize / self.maximumPacketSize))) - self.expectedRemainingBytes = Int(dataSize % self.maximumPacketSize) + self.firmwareDataSize = data.count } .store(in: &self.cancellables) }