@mentra/bluetooth-sdk 0.1.20 → 0.1.21-beta.1
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 +27 -12
- 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 +169 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +72 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +127 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +400 -99
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +333 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +43 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +18 -2
- 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/G1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +16 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +342 -42
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +7 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +1 -1
- 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/status/WifiHotspotStatus.kt +19 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +52 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +115 -47
- 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/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -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 +45 -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 +100 -22
- 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 +3 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +3 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +31 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +26 -3
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +37 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -0
- package/build/_private/MentraLocalNetworkModule.js +3 -0
- package/build/_private/MentraLocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +1 -1
- package/build/_private/cameraRequestPayload.d.ts.map +1 -1
- package/build/_private/cameraRequestPayload.js +7 -0
- package/build/_private/cameraRequestPayload.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +10 -0
- 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 +11 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -0
- package/ios/BluetoothSdkModule.swift +113 -2
- package/ios/Source/BluetoothSdkDefaults.swift +2 -1
- package/ios/Source/Bridge.swift +87 -21
- package/ios/Source/DeviceManager.swift +118 -20
- package/ios/Source/DeviceStore.swift +4 -0
- package/ios/Source/MentraBluetoothSDK.swift +326 -33
- package/ios/Source/OtaManifest.swift +47 -4
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +26 -6
- 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/G1.swift +1 -1
- package/ios/Source/sgcs/G2.swift +19 -7
- package/ios/Source/sgcs/Mach1.swift +1 -1
- package/ios/Source/sgcs/MentraLive.swift +478 -61
- package/ios/Source/sgcs/MentraNex.swift +1 -1
- package/ios/Source/sgcs/Nimo.swift +1 -1
- package/ios/Source/sgcs/SGCManager.swift +13 -2
- package/ios/Source/sgcs/Simulated.swift +1 -1
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/status/WifiHotspotStatus.swift +21 -2
- package/ios/Source/streaming/StreamModels.swift +68 -2
- package/ios/Source/tts/TTSTools.swift +1 -1
- 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 -2
- package/src/BluetoothSdk.types.ts +106 -24
- package/src/_internal.ts +3 -0
- package/src/_private/BluetoothSdkModule.ts +83 -18
- package/src/_private/MentraLocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +9 -2
- package/src/_private/photoRequestPayload.ts +13 -4
- package/src/_private/sdkOtaManifest.ts +31 -0
- package/src/index.ts +13 -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
|
+
}
|
|
@@ -133,20 +133,39 @@ public enum WifiStatus: CustomStringConvertible, Equatable {
|
|
|
133
133
|
public struct WifiStatusEvent: CustomStringConvertible {
|
|
134
134
|
public let status: WifiStatus
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
/// Glasses-reported provisioning failure reason when THIS event is the verdict of a
|
|
137
|
+
/// failed connect attempt; nil for routine link-state updates. An attempt property,
|
|
138
|
+
/// not a link property — which is why it lives on the event and not on `WifiStatus`:
|
|
139
|
+
/// "connect_timeout" arrives with a disconnected status (never associated), while
|
|
140
|
+
/// "connected_to_other_network" arrives with a *connected* status — the attempt
|
|
141
|
+
/// failed and Android's auto-join left the glasses on (or returned them to) a
|
|
142
|
+
/// different SSID than requested, so the link is genuinely up while the request
|
|
143
|
+
/// genuinely failed. Sent by ASG client builds that include the WiFi error
|
|
144
|
+
/// surfacing (v40+); older glasses never set it.
|
|
145
|
+
public let error: String?
|
|
146
|
+
|
|
147
|
+
public init(status: WifiStatus, error: String? = nil) {
|
|
137
148
|
self.status = status
|
|
149
|
+
self.error = error
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
init(connected: Bool, ssid: String?, localIp: String?) {
|
|
141
153
|
status = WifiStatus.fromStoreFields(connected: connected, ssid: ssid, localIp: localIp) ?? .disconnected
|
|
154
|
+
error = nil
|
|
142
155
|
}
|
|
143
156
|
|
|
144
157
|
init(values: [String: Any]) {
|
|
145
158
|
status = WifiStatus(values: values) ?? .disconnected
|
|
159
|
+
let rawError = stringValue(values, "error")
|
|
160
|
+
error = rawError?.isEmpty == false ? rawError : nil
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
public var values: [String: Any] {
|
|
149
|
-
status.values.merging(["type": "wifi_status_change"]) { _, new in new }
|
|
164
|
+
var body = status.values.merging(["type": "wifi_status_change"]) { _, new in new }
|
|
165
|
+
if let error {
|
|
166
|
+
body["error"] = error
|
|
167
|
+
}
|
|
168
|
+
return body
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
public var description: String {
|