@mentra/bluetooth-sdk 0.1.21-beta.0 → 0.1.21-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -11
- package/android/build.gradle +3 -0
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +34 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +82 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +32 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +17 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +53 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +161 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +41 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +45 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/build/BluetoothSdk.types.d.ts +58 -25
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +2 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +1 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +8 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +6 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +6 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +20 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +28 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -1
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +41 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +15 -3
- package/ios/Source/DeviceManager.swift +67 -17
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +37 -8
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/camera/CameraModels.swift +20 -5
- package/ios/Source/internal/BleTraceLogger.swift +1 -1
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +39 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/MentraLive.swift +174 -38
- package/ios/Source/sgcs/SGCManager.swift +8 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/streaming/StreamModels.swift +71 -7
- package/ios/Source/types/DeviceModels.swift +13 -0
- package/ios/Source/utils/BleJsonCompact.swift +8 -0
- package/ios/Source/utils/Constants.swift +2 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +4 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +61 -28
- package/src/_internal.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +20 -6
- package/src/_private/cameraRequestPayload.ts +8 -2
- package/src/_private/photoRequestPayload.ts +12 -2
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +9 -1
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@MainActor
|
|
4
|
+
protocol Ar99OtaGattTransport: AnyObject {
|
|
5
|
+
func enableOtaNotification() -> Bool
|
|
6
|
+
func sendOtaData(_ data: Data)
|
|
7
|
+
func requestMtu(_ mtu: Int)
|
|
8
|
+
func isBleConnected() -> Bool
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@MainActor
|
|
12
|
+
struct Ar99OtaCallbacks {
|
|
13
|
+
var onProgress: (_ offset: Int, _ total: Int, _ progress: Int) -> Void
|
|
14
|
+
var onCompleted: (_ needReboot: Bool) -> Void
|
|
15
|
+
var onError: (_ errorCode: Int, _ message: String) -> Void
|
|
16
|
+
var onCancelled: () -> Void
|
|
17
|
+
var onPausedWaitingReconnect: () -> Void = {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private enum Ar99OtaCommand {
|
|
21
|
+
static let serviceId: UInt8 = 0x09
|
|
22
|
+
static let requestUpgrade: UInt8 = 0x01
|
|
23
|
+
static let connectNegotiation: UInt8 = 0x02
|
|
24
|
+
static let requireImageData: UInt8 = 0x03
|
|
25
|
+
static let validateImage: UInt8 = 0x06
|
|
26
|
+
static let negotiationResult: UInt8 = 0x09
|
|
27
|
+
static let sendImageData: UInt8 = 0x0B
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private enum Ar99OtaErrorCode {
|
|
31
|
+
static let protocolError = 0x04
|
|
32
|
+
static let packageSizeError = 0x06
|
|
33
|
+
static let firmwareValidationFailed = 0x0A
|
|
34
|
+
static let unknownError = 0xFF
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private enum Ar99OtaState {
|
|
38
|
+
static let idle = 0
|
|
39
|
+
static let requesting = 1
|
|
40
|
+
static let negotiating = 2
|
|
41
|
+
static let waitingReady = 3
|
|
42
|
+
static let transferring = 4
|
|
43
|
+
static let completed = 6
|
|
44
|
+
static let failed = 7
|
|
45
|
+
static let pausedDisconnected = 8
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private struct Ar99OtaFrameHeader {
|
|
49
|
+
let serviceId: UInt8
|
|
50
|
+
let command: UInt8
|
|
51
|
+
let parameterLength: Int
|
|
52
|
+
let payloadOffset: Int
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private enum Ar99OtaByteUtils {
|
|
56
|
+
static func readUInt16LE(_ data: Data, offset: Int) -> Int {
|
|
57
|
+
guard offset >= 0, offset + 1 < data.count else { return 0 }
|
|
58
|
+
let bytes = Array(data)
|
|
59
|
+
return Int(bytes[offset]) | (Int(bytes[offset + 1]) << 8)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static func readInt32LE(_ data: Data, offset: Int) -> Int {
|
|
63
|
+
guard offset >= 0, offset + 3 < data.count else { return 0 }
|
|
64
|
+
let bytes = Array(data)
|
|
65
|
+
let value = UInt32(bytes[offset])
|
|
66
|
+
| (UInt32(bytes[offset + 1]) << 8)
|
|
67
|
+
| (UInt32(bytes[offset + 2]) << 16)
|
|
68
|
+
| (UInt32(bytes[offset + 3]) << 24)
|
|
69
|
+
return Int(Int32(bitPattern: value))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private enum Ar99OtaCrc32 {
|
|
74
|
+
private static let table: [UInt32] = {
|
|
75
|
+
(0 ..< 256).map { index in
|
|
76
|
+
var crc = UInt32(index)
|
|
77
|
+
for _ in 0 ..< 8 {
|
|
78
|
+
crc = (crc & 1) == 1 ? (0xEDB88320 ^ (crc >> 1)) : (crc >> 1)
|
|
79
|
+
}
|
|
80
|
+
return crc
|
|
81
|
+
}
|
|
82
|
+
}()
|
|
83
|
+
|
|
84
|
+
static func calculate(_ data: Data?) -> UInt32 {
|
|
85
|
+
var crc: UInt32 = 0xFFFF_FFFF
|
|
86
|
+
for byte in data ?? Data() {
|
|
87
|
+
let index = Int((crc ^ UInt32(byte)) & 0xFF)
|
|
88
|
+
crc = table[index] ^ (crc >> 8)
|
|
89
|
+
}
|
|
90
|
+
return crc ^ 0xFFFF_FFFF
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private enum Ar99OtaProtocol {
|
|
95
|
+
static func buildFrame(command: UInt8, parameterType: UInt8, payload: Data?) -> Data {
|
|
96
|
+
var frame = Data()
|
|
97
|
+
frame.append(Ar99OtaCommand.serviceId)
|
|
98
|
+
frame.append(command)
|
|
99
|
+
frame.append(parameterType)
|
|
100
|
+
let length = payload?.count ?? 0
|
|
101
|
+
frame.append(UInt8(length & 0xFF))
|
|
102
|
+
frame.append(UInt8((length >> 8) & 0xFF))
|
|
103
|
+
if let payload, !payload.isEmpty {
|
|
104
|
+
frame.append(payload)
|
|
105
|
+
}
|
|
106
|
+
return frame
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static func buildRequestUpgrade(packageSize: Int, blockSize _: Int, crc32 _: UInt32) -> Data {
|
|
110
|
+
// Wire layout matches the vendor bring-up frame: type marker, then TLV
|
|
111
|
+
// 0x0A/len4 = package size (LE). CRC is validated per-block in sendImageData;
|
|
112
|
+
// blockSize is negotiated separately in connectNegotiation.
|
|
113
|
+
var payload = Data([0x09, 0x01, 0x00, 0x01, 0x0A, 0x04])
|
|
114
|
+
payload.append(UInt8(packageSize & 0xFF))
|
|
115
|
+
payload.append(UInt8((packageSize >> 8) & 0xFF))
|
|
116
|
+
payload.append(UInt8((packageSize >> 16) & 0xFF))
|
|
117
|
+
payload.append(UInt8((packageSize >> 24) & 0xFF))
|
|
118
|
+
payload.append(0x00)
|
|
119
|
+
return buildFrame(command: Ar99OtaCommand.requestUpgrade, parameterType: 0x80, payload: payload)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static func buildConnectNegotiation(blockSize _: Int) -> Data {
|
|
123
|
+
buildFrame(command: Ar99OtaCommand.connectNegotiation, parameterType: 0x80, payload: nil)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static func buildNegotiationResult() -> Data {
|
|
127
|
+
buildFrame(command: Ar99OtaCommand.negotiationResult, parameterType: 0x80, payload: Data([0x01, 0x01, 0x00, 0x01]))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static func buildSendImageData(packetSequenceNumber: Int, block: Data) -> Data {
|
|
131
|
+
var payload = Data()
|
|
132
|
+
payload.append(UInt8(packetSequenceNumber & 0xFF))
|
|
133
|
+
let crc32 = Ar99OtaCrc32.calculate(block)
|
|
134
|
+
payload.append(UInt8(crc32 & 0xFF))
|
|
135
|
+
payload.append(UInt8((crc32 >> 8) & 0xFF))
|
|
136
|
+
payload.append(UInt8((crc32 >> 16) & 0xFF))
|
|
137
|
+
payload.append(UInt8((crc32 >> 24) & 0xFF))
|
|
138
|
+
payload.append(block)
|
|
139
|
+
return buildFrame(command: Ar99OtaCommand.sendImageData, parameterType: 0x80, payload: payload)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static func parseFrameHeader(_ frame: Data) -> Ar99OtaFrameHeader? {
|
|
143
|
+
guard frame.count >= 5 else { return nil }
|
|
144
|
+
let bytes = Array(frame)
|
|
145
|
+
return Ar99OtaFrameHeader(
|
|
146
|
+
serviceId: bytes[0],
|
|
147
|
+
command: bytes[1],
|
|
148
|
+
parameterLength: Ar99OtaByteUtils.readUInt16LE(frame, offset: 3),
|
|
149
|
+
payloadOffset: 5
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@MainActor
|
|
155
|
+
final class Ar99OtaManager {
|
|
156
|
+
static let shared = Ar99OtaManager()
|
|
157
|
+
|
|
158
|
+
private static let defaultBlockSize = 200
|
|
159
|
+
private static let timeoutMs = 5_000
|
|
160
|
+
private static let reconnectTimeoutMs = 90_000
|
|
161
|
+
|
|
162
|
+
private weak var transport: Ar99OtaGattTransport?
|
|
163
|
+
private var callbacks: Ar99OtaCallbacks?
|
|
164
|
+
private var timeoutItem: DispatchWorkItem?
|
|
165
|
+
private var reconnectTimeoutItem: DispatchWorkItem?
|
|
166
|
+
|
|
167
|
+
private var state = Ar99OtaState.idle
|
|
168
|
+
private var firmwareData: Data?
|
|
169
|
+
private var firmwareSize = 0
|
|
170
|
+
private var firmwareCrc32: UInt32 = 0
|
|
171
|
+
private var blockSize = defaultBlockSize
|
|
172
|
+
private var currentOffset = 0
|
|
173
|
+
private var otaNotifyEnabled = false
|
|
174
|
+
|
|
175
|
+
private init() {}
|
|
176
|
+
|
|
177
|
+
func setTransport(_ transport: Ar99OtaGattTransport?) {
|
|
178
|
+
self.transport = transport
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func setCallback(_ callbacks: Ar99OtaCallbacks?) {
|
|
182
|
+
self.callbacks = callbacks
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func isOTAInProgress() -> Bool {
|
|
186
|
+
state != Ar99OtaState.idle && state != Ar99OtaState.completed && state != Ar99OtaState.failed
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func isPausedWaitingReconnect() -> Bool {
|
|
190
|
+
state == Ar99OtaState.pausedDisconnected
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func getProgress() -> Int {
|
|
194
|
+
guard firmwareSize > 0 else { return 0 }
|
|
195
|
+
return (currentOffset * 100) / firmwareSize
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func startOTA(filePath: String) -> Bool {
|
|
199
|
+
let trimmed = filePath.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
200
|
+
guard !trimmed.isEmpty else {
|
|
201
|
+
notifyError(Ar99OtaErrorCode.unknownError, "Firmware file path is empty")
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let url: URL
|
|
206
|
+
if trimmed.hasPrefix("file://"), let fileUrl = URL(string: trimmed) {
|
|
207
|
+
url = fileUrl
|
|
208
|
+
} else {
|
|
209
|
+
url = URL(fileURLWithPath: trimmed)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
do {
|
|
213
|
+
return startOTA(data: try Data(contentsOf: url))
|
|
214
|
+
} catch {
|
|
215
|
+
notifyError(Ar99OtaErrorCode.unknownError, "Failed to read firmware file")
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func startOTA(data: Data) -> Bool {
|
|
221
|
+
if isOTAInProgress() {
|
|
222
|
+
Bridge.log("Ar99OtaManager: OTA is already in progress")
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
guard !data.isEmpty else {
|
|
226
|
+
notifyError(Ar99OtaErrorCode.packageSizeError, "Firmware data is empty")
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
state = Ar99OtaState.idle
|
|
231
|
+
otaNotifyEnabled = false
|
|
232
|
+
cancelTimeout()
|
|
233
|
+
cancelReconnectTimeout()
|
|
234
|
+
firmwareData = data
|
|
235
|
+
firmwareSize = data.count
|
|
236
|
+
firmwareCrc32 = Ar99OtaCrc32.calculate(data)
|
|
237
|
+
blockSize = Self.defaultBlockSize
|
|
238
|
+
currentOffset = 0
|
|
239
|
+
|
|
240
|
+
guard let transport else {
|
|
241
|
+
notifyError(Ar99OtaErrorCode.unknownError, "OTA transport is not initialized")
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
guard transport.enableOtaNotification() else {
|
|
245
|
+
notifyError(Ar99OtaErrorCode.unknownError, "OTA service was not found")
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func cancelOTA() {
|
|
252
|
+
state = Ar99OtaState.idle
|
|
253
|
+
otaNotifyEnabled = false
|
|
254
|
+
firmwareData = nil
|
|
255
|
+
firmwareSize = 0
|
|
256
|
+
currentOffset = 0
|
|
257
|
+
cancelTimeout()
|
|
258
|
+
cancelReconnectTimeout()
|
|
259
|
+
callbacks?.onCancelled()
|
|
260
|
+
cleanupFirmware()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func onBleDisconnected() {
|
|
264
|
+
guard isOTAInProgress(), firmwareData != nil, firmwareSize > 0 else { return }
|
|
265
|
+
guard state != Ar99OtaState.pausedDisconnected else { return }
|
|
266
|
+
state = Ar99OtaState.pausedDisconnected
|
|
267
|
+
otaNotifyEnabled = false
|
|
268
|
+
cancelTimeout()
|
|
269
|
+
startReconnectTimeout()
|
|
270
|
+
callbacks?.onPausedWaitingReconnect()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
func tryResumeOtaAfterGattReady() {
|
|
274
|
+
let shouldResume = state == Ar99OtaState.pausedDisconnected && firmwareData != nil && firmwareSize > 0
|
|
275
|
+
if shouldResume {
|
|
276
|
+
_ = transport?.enableOtaNotification()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func onOtaNotifyEnabled() {
|
|
281
|
+
otaNotifyEnabled = true
|
|
282
|
+
guard firmwareData != nil, firmwareSize > 0 else { return }
|
|
283
|
+
guard state == Ar99OtaState.idle || state == Ar99OtaState.pausedDisconnected else { return }
|
|
284
|
+
|
|
285
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1_000)) { [weak self] in
|
|
286
|
+
Task { @MainActor in
|
|
287
|
+
guard let self else { return }
|
|
288
|
+
guard self.state == Ar99OtaState.idle || self.state == Ar99OtaState.pausedDisconnected else { return }
|
|
289
|
+
_ = self.sendUpgradeRequest()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func handleMtuNegotiatedForOta(success: Bool) -> Bool {
|
|
295
|
+
guard state == Ar99OtaState.requesting else { return false }
|
|
296
|
+
guard success else {
|
|
297
|
+
handleOTAFailure(Ar99OtaErrorCode.unknownError, "MTU negotiation failed")
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
onMtuNegotiated()
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func handleOTAResponse(_ data: Data) {
|
|
305
|
+
guard data.count >= 5 else { return }
|
|
306
|
+
guard let header = Ar99OtaProtocol.parseFrameHeader(data),
|
|
307
|
+
header.serviceId == Ar99OtaCommand.serviceId
|
|
308
|
+
else {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
cancelTimeout()
|
|
312
|
+
|
|
313
|
+
let payload: Data?
|
|
314
|
+
if header.parameterLength > 0, data.count >= header.payloadOffset + header.parameterLength {
|
|
315
|
+
payload = data.subdata(in: header.payloadOffset ..< header.payloadOffset + header.parameterLength)
|
|
316
|
+
} else {
|
|
317
|
+
payload = nil
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
switch header.command {
|
|
321
|
+
case Ar99OtaCommand.requestUpgrade:
|
|
322
|
+
handleUpgradeRequestResponse(payload)
|
|
323
|
+
case Ar99OtaCommand.connectNegotiation:
|
|
324
|
+
handleNegotiationResponse(payload)
|
|
325
|
+
case Ar99OtaCommand.negotiationResult:
|
|
326
|
+
handleNegotiationResultResponse()
|
|
327
|
+
case Ar99OtaCommand.requireImageData:
|
|
328
|
+
handleRequireImageData(payload)
|
|
329
|
+
case Ar99OtaCommand.sendImageData:
|
|
330
|
+
handleImageDataResponse(payload)
|
|
331
|
+
case Ar99OtaCommand.validateImage:
|
|
332
|
+
handleValidateImageResponse(payload)
|
|
333
|
+
default:
|
|
334
|
+
startTimeout()
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private func sendUpgradeRequest() -> Bool {
|
|
340
|
+
guard isBluetoothConnected() else {
|
|
341
|
+
notifyError(Ar99OtaErrorCode.unknownError, "Bluetooth is not connected")
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
guard firmwareData != nil, firmwareSize > 0 else {
|
|
345
|
+
state = Ar99OtaState.idle
|
|
346
|
+
return false
|
|
347
|
+
}
|
|
348
|
+
cancelReconnectTimeout()
|
|
349
|
+
state = Ar99OtaState.requesting
|
|
350
|
+
guard let transport else {
|
|
351
|
+
notifyError(Ar99OtaErrorCode.unknownError, "OTA transport is not initialized")
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
transport.requestMtu(247)
|
|
355
|
+
startTimeout()
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private func onMtuNegotiated() {
|
|
360
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { [weak self] in
|
|
361
|
+
Task { @MainActor in
|
|
362
|
+
guard let self, self.state == Ar99OtaState.requesting else { return }
|
|
363
|
+
self.sendOtaData(
|
|
364
|
+
Ar99OtaProtocol.buildRequestUpgrade(
|
|
365
|
+
packageSize: self.firmwareSize,
|
|
366
|
+
blockSize: self.blockSize,
|
|
367
|
+
crc32: self.firmwareCrc32
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private func handleUpgradeRequestResponse(_ payload: Data?) {
|
|
375
|
+
guard let payload, payload.count >= 3 else {
|
|
376
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid upgrade response")
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
var offset = 0
|
|
381
|
+
var success = false
|
|
382
|
+
while offset + 3 <= payload.count {
|
|
383
|
+
let bytes = Array(payload)
|
|
384
|
+
let type = bytes[offset]
|
|
385
|
+
offset += 1
|
|
386
|
+
let length = Ar99OtaByteUtils.readUInt16LE(payload, offset: offset)
|
|
387
|
+
offset += 2
|
|
388
|
+
guard offset + length <= payload.count else {
|
|
389
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid upgrade TLV")
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if type == 0x7F, length >= 4 {
|
|
393
|
+
let errorCode = Ar99OtaByteUtils.readInt32LE(payload, offset: offset)
|
|
394
|
+
if errorCode == 100_000 {
|
|
395
|
+
success = true
|
|
396
|
+
} else {
|
|
397
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Upgrade request failed: \(errorCode)")
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
offset += length
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
guard success else {
|
|
405
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Upgrade request was rejected")
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
state = Ar99OtaState.negotiating
|
|
409
|
+
sendOtaData(Ar99OtaProtocol.buildConnectNegotiation(blockSize: blockSize))
|
|
410
|
+
startTimeout()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private func handleNegotiationResponse(_ payload: Data?) {
|
|
414
|
+
guard let payload, payload.count >= 3 else {
|
|
415
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid negotiation response")
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
var offset = 0
|
|
420
|
+
while offset + 3 <= payload.count {
|
|
421
|
+
let bytes = Array(payload)
|
|
422
|
+
let type = bytes[offset]
|
|
423
|
+
offset += 1
|
|
424
|
+
let length = Ar99OtaByteUtils.readUInt16LE(payload, offset: offset)
|
|
425
|
+
offset += 2
|
|
426
|
+
guard offset + length <= payload.count else {
|
|
427
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid negotiation TLV")
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
if type == 0x03, length >= 2 {
|
|
431
|
+
let suggestedBlockSize = Ar99OtaByteUtils.readUInt16LE(payload, offset: offset)
|
|
432
|
+
if suggestedBlockSize > 0 {
|
|
433
|
+
blockSize = suggestedBlockSize
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
offset += length
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
state = Ar99OtaState.waitingReady
|
|
440
|
+
sendOtaData(Ar99OtaProtocol.buildNegotiationResult())
|
|
441
|
+
startTimeout()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private func handleNegotiationResultResponse() {
|
|
445
|
+
state = Ar99OtaState.transferring
|
|
446
|
+
startTimeout()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func handleRequireImageData(_ payload: Data?) {
|
|
450
|
+
guard let payload, payload.count >= 3 else {
|
|
451
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid image data request")
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var offset = 0
|
|
456
|
+
var fileOffset = 0
|
|
457
|
+
var fileLength = 0
|
|
458
|
+
while offset + 3 <= payload.count {
|
|
459
|
+
let bytes = Array(payload)
|
|
460
|
+
let type = bytes[offset]
|
|
461
|
+
offset += 1
|
|
462
|
+
let length = Ar99OtaByteUtils.readUInt16LE(payload, offset: offset)
|
|
463
|
+
offset += 2
|
|
464
|
+
guard offset + length <= payload.count else {
|
|
465
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid image data TLV")
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
if type == 0x01, length >= 4 {
|
|
469
|
+
fileOffset = Ar99OtaByteUtils.readInt32LE(payload, offset: offset)
|
|
470
|
+
} else if type == 0x02, length >= 4 {
|
|
471
|
+
fileLength = Ar99OtaByteUtils.readInt32LE(payload, offset: offset)
|
|
472
|
+
}
|
|
473
|
+
offset += length
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
guard let image = firmwareData,
|
|
477
|
+
firmwareSize > 0,
|
|
478
|
+
fileOffset >= 0,
|
|
479
|
+
fileOffset <= firmwareSize
|
|
480
|
+
else {
|
|
481
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid firmware offset")
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
guard fileOffset < firmwareSize else {
|
|
485
|
+
startTimeout()
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let remaining = firmwareSize - fileOffset
|
|
490
|
+
let totalSendLength = fileLength > 0 ? min(remaining, fileLength) : remaining
|
|
491
|
+
var sentLength = 0
|
|
492
|
+
var packetSequenceNumber = 0
|
|
493
|
+
while sentLength < totalSendLength {
|
|
494
|
+
let size = min(totalSendLength - sentLength, blockSize)
|
|
495
|
+
let current = fileOffset + sentLength
|
|
496
|
+
let block = image.subdata(in: current ..< current + size)
|
|
497
|
+
sendOtaData(Ar99OtaProtocol.buildSendImageData(packetSequenceNumber: packetSequenceNumber, block: block))
|
|
498
|
+
sentLength += size
|
|
499
|
+
packetSequenceNumber += 1
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
currentOffset = fileOffset + sentLength
|
|
503
|
+
notifyProgress(offset: currentOffset, progress: getProgress())
|
|
504
|
+
startTimeout()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private func handleValidateImageResponse(_ payload: Data?) {
|
|
508
|
+
var validFlag: Int?
|
|
509
|
+
if let payload, payload.count >= 4 {
|
|
510
|
+
var offset = 0
|
|
511
|
+
while offset + 3 <= payload.count {
|
|
512
|
+
let bytes = Array(payload)
|
|
513
|
+
let type = bytes[offset]
|
|
514
|
+
offset += 1
|
|
515
|
+
let length = Ar99OtaByteUtils.readUInt16LE(payload, offset: offset)
|
|
516
|
+
offset += 2
|
|
517
|
+
guard offset + length <= payload.count else { break }
|
|
518
|
+
if type == 0x01, length >= 1 {
|
|
519
|
+
validFlag = Int(bytes[offset])
|
|
520
|
+
break
|
|
521
|
+
}
|
|
522
|
+
offset += length
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if validFlag == nil, let payload, let first = payload.first {
|
|
526
|
+
validFlag = Int(first)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if validFlag == 1 {
|
|
530
|
+
handleOTASuccess()
|
|
531
|
+
} else {
|
|
532
|
+
handleOTAFailure(Ar99OtaErrorCode.firmwareValidationFailed, "Firmware validation failed")
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private func handleImageDataResponse(_ payload: Data?) {
|
|
537
|
+
guard let payload, payload.count >= 5 else {
|
|
538
|
+
handleOTAFailure(Ar99OtaErrorCode.protocolError, "Invalid image data response")
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
let receivedOffset = Ar99OtaByteUtils.readInt32LE(payload, offset: 0)
|
|
542
|
+
let status = Int(Array(payload)[4])
|
|
543
|
+
guard status == 0 else {
|
|
544
|
+
handleOTAFailure(status, "Firmware data write failed")
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
if receivedOffset != currentOffset {
|
|
548
|
+
currentOffset = receivedOffset
|
|
549
|
+
}
|
|
550
|
+
startTimeout()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private func sendOtaData(_ frame: Data) {
|
|
554
|
+
guard let transport else {
|
|
555
|
+
handleOTAFailure(Ar99OtaErrorCode.unknownError, "OTA transport is not initialized")
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
transport.sendOtaData(frame)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private func isBluetoothConnected() -> Bool {
|
|
562
|
+
transport?.isBleConnected() == true && otaNotifyEnabled
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private func handleOTASuccess() {
|
|
566
|
+
state = Ar99OtaState.completed
|
|
567
|
+
currentOffset = firmwareSize
|
|
568
|
+
cancelTimeout()
|
|
569
|
+
cancelReconnectTimeout()
|
|
570
|
+
notifyProgress(offset: firmwareSize, progress: 100)
|
|
571
|
+
callbacks?.onCompleted(false)
|
|
572
|
+
cleanupFirmware()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private func handleOTAFailure(_ code: Int, _ message: String) {
|
|
576
|
+
state = Ar99OtaState.failed
|
|
577
|
+
cancelTimeout()
|
|
578
|
+
cancelReconnectTimeout()
|
|
579
|
+
notifyError(code, message)
|
|
580
|
+
cleanupFirmware()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private func startTimeout() {
|
|
584
|
+
cancelTimeout()
|
|
585
|
+
let item = DispatchWorkItem { [weak self] in
|
|
586
|
+
Task { @MainActor in
|
|
587
|
+
self?.handleOTAFailure(Ar99OtaErrorCode.unknownError, "OTA timed out")
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
timeoutItem = item
|
|
591
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Self.timeoutMs), execute: item)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private func cancelTimeout() {
|
|
595
|
+
timeoutItem?.cancel()
|
|
596
|
+
timeoutItem = nil
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private func startReconnectTimeout() {
|
|
600
|
+
cancelReconnectTimeout()
|
|
601
|
+
let item = DispatchWorkItem { [weak self] in
|
|
602
|
+
Task { @MainActor in
|
|
603
|
+
guard let self, self.state == Ar99OtaState.pausedDisconnected else { return }
|
|
604
|
+
self.handleOTAFailure(
|
|
605
|
+
Ar99OtaErrorCode.unknownError,
|
|
606
|
+
"Bluetooth disconnected during AR99 OTA. Reconnect timed out; please try the update again."
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
reconnectTimeoutItem = item
|
|
611
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Self.reconnectTimeoutMs), execute: item)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private func cancelReconnectTimeout() {
|
|
615
|
+
reconnectTimeoutItem?.cancel()
|
|
616
|
+
reconnectTimeoutItem = nil
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private func notifyProgress(offset: Int, progress: Int) {
|
|
620
|
+
callbacks?.onProgress(offset, firmwareSize, progress)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private func notifyError(_ errorCode: Int, _ message: String) {
|
|
624
|
+
callbacks?.onError(errorCode, message)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private func cleanupFirmware() {
|
|
628
|
+
firmwareData = nil
|
|
629
|
+
firmwareSize = 0
|
|
630
|
+
firmwareCrc32 = 0
|
|
631
|
+
currentOffset = 0
|
|
632
|
+
blockSize = Self.defaultBlockSize
|
|
633
|
+
otaNotifyEnabled = false
|
|
634
|
+
cancelTimeout()
|
|
635
|
+
cancelReconnectTimeout()
|
|
636
|
+
}
|
|
637
|
+
}
|