@mentra/bluetooth-sdk 0.1.11 → 0.1.13
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 +67 -11
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +182 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +73 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +10 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +83 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +266 -31
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +20 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +153 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +93 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +3 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +3 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +1 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +90 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +3835 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +20 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/{Mach1.java → Mach1.kt} +517 -560
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +8712 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +15 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +7 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +2 -1
- package/build/BluetoothSdk.types.d.ts +72 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +9 -7
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +3 -1
- package/build/_private/photoRequestPayload.d.ts.map +1 -1
- package/build/_private/photoRequestPayload.js +43 -1
- package/build/_private/photoRequestPayload.js.map +1 -1
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -5
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +59 -64
- package/ios/Source/BluetoothSdkDefaults.swift +47 -0
- package/ios/Source/Bridge.swift +6 -0
- package/ios/Source/DeviceManager.swift +46 -32
- package/ios/Source/DeviceStore.swift +5 -1
- package/ios/Source/MentraBluetoothSDK.swift +357 -34
- package/ios/Source/MentraBluetoothSDKDebug.swift +12 -0
- package/ios/Source/ObservableStore.swift +11 -0
- package/ios/Source/OtaManifest.swift +170 -0
- package/ios/Source/camera/CameraModels.swift +267 -9
- package/ios/Source/controllers/ControllerManager.swift +2 -5
- package/ios/Source/controllers/R1.swift +2 -5
- package/ios/Source/events/BluetoothEvents.swift +3 -0
- package/ios/Source/internal/BluetoothAvailability.swift +20 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +160 -0
- package/ios/Source/sgcs/G1.swift +6 -5
- package/ios/Source/sgcs/G2.swift +20 -18
- package/ios/Source/sgcs/Mach1.swift +6 -5
- package/ios/Source/sgcs/MentraLive.swift +158 -38
- package/ios/Source/sgcs/MentraNex.swift +6 -5
- package/ios/Source/sgcs/SGCManager.swift +16 -7
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/status/DeviceStatus.swift +1 -1
- package/ios/Source/types/DeviceModels.swift +5 -1
- package/package.json +6 -1
- package/plugin/build/index.d.ts +4 -0
- package/plugin/build/withAndroid.d.ts +2 -3
- package/plugin/build/withAndroid.js +46 -0
- package/plugin/build/withIos.d.ts +2 -3
- package/plugin/build/withIos.js +32 -0
- package/src/BluetoothSdk.types.ts +80 -10
- package/src/_private/BluetoothSdkModule.ts +13 -10
- package/src/_private/photoRequestPayload.ts +45 -2
- package/src/debug.ts +9 -0
- package/src/index.ts +25 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +0 -3974
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +0 -7405
|
@@ -81,10 +81,14 @@ private final class PendingHotspotStatusRequest {
|
|
|
81
81
|
private final class PendingVideoRecordingRequest {
|
|
82
82
|
let expectedStatus: String
|
|
83
83
|
let pending: PendingResponse<VideoRecordingStatusEvent>
|
|
84
|
+
let waitForUpload: Bool
|
|
85
|
+
var stoppedEvent: VideoRecordingStatusEvent?
|
|
86
|
+
var uploadSucceeded = false
|
|
84
87
|
|
|
85
|
-
init(expectedStatus: String, pending: PendingResponse<VideoRecordingStatusEvent
|
|
88
|
+
init(expectedStatus: String, pending: PendingResponse<VideoRecordingStatusEvent>, waitForUpload: Bool = false) {
|
|
86
89
|
self.expectedStatus = expectedStatus
|
|
87
90
|
self.pending = pending
|
|
91
|
+
self.waitForUpload = waitForUpload
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -143,17 +147,26 @@ private final class PendingResponse<T> {
|
|
|
143
147
|
|
|
144
148
|
@MainActor
|
|
145
149
|
public final class MentraBluetoothSDK {
|
|
150
|
+
private static let otaBesVersionWaitMs = 5_000
|
|
151
|
+
private static let otaMtkVersionWaitMs = 2_000
|
|
152
|
+
private static let otaVersionPollMs = 100
|
|
153
|
+
|
|
146
154
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
147
155
|
|
|
148
156
|
private let configuration: MentraBluetoothSDKConfiguration
|
|
149
157
|
private var discoveredDeviceNames = Set<String>()
|
|
158
|
+
private var bluetoothAvailabilityListenerId: UUID?
|
|
159
|
+
private var shouldRestoreGlassesOnBluetoothRestore = false
|
|
160
|
+
private var shouldRestoreControllerOnBluetoothRestore = false
|
|
150
161
|
private var bridgeEventSinkId: String?
|
|
151
162
|
private var storeListenerId: String?
|
|
152
163
|
private let defaultDeviceKeys: Set<String> = ["default_wearable", "device_name", "device_address"]
|
|
164
|
+
private let videoUploadStopTimeoutMs = 10 * 60 * 1000
|
|
153
165
|
private var suppressDefaultDeviceEvents = false
|
|
154
166
|
private var defaultDeviceApplyGeneration = 0
|
|
155
167
|
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
156
168
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive?
|
|
169
|
+
private let analytics: BluetoothSdkAnalytics
|
|
157
170
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
158
171
|
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
159
172
|
private var pendingRgbLedRequests: [String: PendingResponse<RgbLedControlResponseEvent>] = [:]
|
|
@@ -167,10 +180,16 @@ public final class MentraBluetoothSDK {
|
|
|
167
180
|
private var pendingWifiStatus: PendingWifiStatusRequest?
|
|
168
181
|
private var pendingHotspotStatus: PendingHotspotStatusRequest?
|
|
169
182
|
private var pendingVersionInfo: PendingResponse<VersionInfoResult>?
|
|
183
|
+
private var configuredOtaVersionUrl: String?
|
|
170
184
|
|
|
171
185
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
172
186
|
self.configuration = configuration
|
|
173
|
-
|
|
187
|
+
analytics = BluetoothSdkAnalytics(configuration: configuration.analytics)
|
|
188
|
+
bluetoothAvailabilityListenerId = BluetoothAvailability.shared.addStateListener { [weak self] state in
|
|
189
|
+
Task { @MainActor [weak self] in
|
|
190
|
+
self?.handleBluetoothAvailability(state)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
174
193
|
bridgeEventSinkId = Bridge.addEventSink { [weak self] eventName, data in
|
|
175
194
|
Task { @MainActor [weak self] in
|
|
176
195
|
self?.dispatchBridgeEvent(eventName, data)
|
|
@@ -181,6 +200,8 @@ public final class MentraBluetoothSDK {
|
|
|
181
200
|
self?.dispatchStoreUpdate(category, changes)
|
|
182
201
|
}
|
|
183
202
|
}
|
|
203
|
+
analytics.initializeGlassesStatus(glassesStatus)
|
|
204
|
+
analytics.captureStarted()
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
public var state: MentraBluetoothState {
|
|
@@ -211,6 +232,15 @@ public final class MentraBluetoothSDK {
|
|
|
211
232
|
currentDefaultDevice()
|
|
212
233
|
}
|
|
213
234
|
|
|
235
|
+
private func requireGlassesConnected(operation: String) throws {
|
|
236
|
+
guard glassesStatus.connected else {
|
|
237
|
+
throw BluetoothError(
|
|
238
|
+
code: "glasses_not_connected",
|
|
239
|
+
message: "Cannot \(operation) because glasses are not connected."
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
public func getDefaultDevice() -> Device? {
|
|
215
245
|
currentDefaultDevice()
|
|
216
246
|
}
|
|
@@ -296,6 +326,7 @@ public final class MentraBluetoothSDK {
|
|
|
296
326
|
}
|
|
297
327
|
|
|
298
328
|
public func connect(to device: Device, options: ConnectOptions = ConnectOptions()) throws {
|
|
329
|
+
clearBluetoothRestoreIntent()
|
|
299
330
|
if device.model != .simulated {
|
|
300
331
|
try BluetoothAvailability.shared.requirePoweredOn(operation: "connect to glasses")
|
|
301
332
|
}
|
|
@@ -315,6 +346,7 @@ public final class MentraBluetoothSDK {
|
|
|
315
346
|
}
|
|
316
347
|
|
|
317
348
|
public func connectDefault(options: ConnectOptions = ConnectOptions()) throws {
|
|
349
|
+
clearBluetoothRestoreIntent()
|
|
318
350
|
guard let device = currentDefaultDevice() else {
|
|
319
351
|
throw BluetoothError(
|
|
320
352
|
code: "default_device_missing",
|
|
@@ -331,18 +363,22 @@ public final class MentraBluetoothSDK {
|
|
|
331
363
|
}
|
|
332
364
|
|
|
333
365
|
public func cancelConnectionAttempt() {
|
|
366
|
+
clearBluetoothRestoreIntent()
|
|
334
367
|
DeviceManager.shared.disconnect()
|
|
335
368
|
}
|
|
336
369
|
|
|
337
370
|
func connectSimulated() {
|
|
371
|
+
clearBluetoothRestoreIntent()
|
|
338
372
|
DeviceManager.shared.connectSimulated()
|
|
339
373
|
}
|
|
340
374
|
|
|
341
375
|
public func disconnect() {
|
|
376
|
+
clearBluetoothRestoreIntent()
|
|
342
377
|
DeviceManager.shared.disconnect()
|
|
343
378
|
}
|
|
344
379
|
|
|
345
380
|
public func forget() {
|
|
381
|
+
clearBluetoothRestoreIntent()
|
|
346
382
|
DeviceManager.shared.forget()
|
|
347
383
|
}
|
|
348
384
|
|
|
@@ -447,15 +483,66 @@ public final class MentraBluetoothSDK {
|
|
|
447
483
|
}
|
|
448
484
|
|
|
449
485
|
public func setButtonPhotoSettings(size: ButtonPhotoSize) async throws -> SettingsAckEvent {
|
|
450
|
-
try await
|
|
451
|
-
setting: "button_photo",
|
|
452
|
-
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_size", size.rawValue) },
|
|
453
|
-
send: { requestId in try DeviceManager.shared.sendButtonPhotoSettings(requestId: requestId, size: size.rawValue) }
|
|
454
|
-
)
|
|
486
|
+
try await setButtonPhotoSettings(ButtonPhotoSettings(size: size))
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
public func setButtonPhotoSettings(_ settings: ButtonPhotoSettings) async throws -> SettingsAckEvent {
|
|
458
|
-
try await
|
|
490
|
+
try await performSettingsCommand(
|
|
491
|
+
setting: "button_photo",
|
|
492
|
+
updateStore: { _ in
|
|
493
|
+
if settings.resetCaptureTuning == true {
|
|
494
|
+
// Mirror Android: clear all cached scan-tuning keys so reconnect sync
|
|
495
|
+
// does not replay stale values after a reset.
|
|
496
|
+
let cat = ObservableStore.bluetoothCategory
|
|
497
|
+
for key in ["button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
498
|
+
"button_photo_edge_enhancement", "button_photo_isp_digital_gain",
|
|
499
|
+
"button_photo_isp_analog_gain", "button_photo_ae_exposure_divisor",
|
|
500
|
+
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"] {
|
|
501
|
+
DeviceStore.shared.remove(cat, key)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if let size = settings.size {
|
|
505
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_size", size.rawValue)
|
|
506
|
+
}
|
|
507
|
+
if let mfnr = settings.mfnr {
|
|
508
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_mfnr", mfnr)
|
|
509
|
+
}
|
|
510
|
+
if let zsl = settings.zsl {
|
|
511
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_zsl", zsl)
|
|
512
|
+
}
|
|
513
|
+
if let noiseReduction = settings.noiseReduction {
|
|
514
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_noise_reduction", noiseReduction)
|
|
515
|
+
}
|
|
516
|
+
if let edgeEnhancement = settings.edgeEnhancement {
|
|
517
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_edge_enhancement", edgeEnhancement)
|
|
518
|
+
}
|
|
519
|
+
if let ispDigitalGain = settings.ispDigitalGain {
|
|
520
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_isp_digital_gain", ispDigitalGain)
|
|
521
|
+
}
|
|
522
|
+
if let ispAnalogGain = settings.ispAnalogGain {
|
|
523
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_isp_analog_gain", ispAnalogGain)
|
|
524
|
+
}
|
|
525
|
+
if let aeExposureDivisor = settings.aeExposureDivisor {
|
|
526
|
+
DeviceStore.shared.set(
|
|
527
|
+
ObservableStore.bluetoothCategory,
|
|
528
|
+
"button_photo_ae_exposure_divisor",
|
|
529
|
+
aeExposureDivisor
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
if let isoCap = settings.isoCap {
|
|
533
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_iso_cap", isoCap)
|
|
534
|
+
}
|
|
535
|
+
if let compress = settings.compress {
|
|
536
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_compress", compress)
|
|
537
|
+
}
|
|
538
|
+
if let sound = settings.sound {
|
|
539
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_sound", sound)
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
send: { requestId in
|
|
543
|
+
try DeviceManager.shared.sendButtonPhotoSettings(requestId: requestId, settings: settings)
|
|
544
|
+
}
|
|
545
|
+
)
|
|
459
546
|
}
|
|
460
547
|
|
|
461
548
|
public func setButtonVideoRecordingSettings(width: Int, height: Int, fps: Int) async throws -> SettingsAckEvent {
|
|
@@ -683,19 +770,7 @@ public final class MentraBluetoothSDK {
|
|
|
683
770
|
)
|
|
684
771
|
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(request.requestId)")
|
|
685
772
|
pendingPhotoRequests[request.requestId] = pending
|
|
686
|
-
DeviceManager.shared.requestPhoto(
|
|
687
|
-
request.requestId,
|
|
688
|
-
request.appId,
|
|
689
|
-
request.size.rawValue,
|
|
690
|
-
request.webhookUrl,
|
|
691
|
-
request.authToken,
|
|
692
|
-
request.compress?.rawValue,
|
|
693
|
-
request.flash,
|
|
694
|
-
request.save,
|
|
695
|
-
request.sound,
|
|
696
|
-
exposureTimeNs: request.exposureTimeNs,
|
|
697
|
-
iso: request.iso
|
|
698
|
-
)
|
|
773
|
+
DeviceManager.shared.requestPhoto(request)
|
|
699
774
|
do {
|
|
700
775
|
let event = try await pending.wait()
|
|
701
776
|
pendingPhotoRequests.removeValue(forKey: request.requestId)
|
|
@@ -806,6 +881,7 @@ public final class MentraBluetoothSDK {
|
|
|
806
881
|
guard !request.requestId.isEmpty else {
|
|
807
882
|
throw BluetoothError(code: "missing_request_id", message: "requestId is required to start video recording.")
|
|
808
883
|
}
|
|
884
|
+
try requireGlassesConnected(operation: "start video recording")
|
|
809
885
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
810
886
|
operation: "start video recording \(request.requestId)"
|
|
811
887
|
)
|
|
@@ -825,7 +901,8 @@ public final class MentraBluetoothSDK {
|
|
|
825
901
|
request.sound,
|
|
826
902
|
request.width,
|
|
827
903
|
request.height,
|
|
828
|
-
request.fps
|
|
904
|
+
request.fps,
|
|
905
|
+
request.maxRecordingTimeMinutes
|
|
829
906
|
)
|
|
830
907
|
do {
|
|
831
908
|
let event = try await pending.wait()
|
|
@@ -837,10 +914,13 @@ public final class MentraBluetoothSDK {
|
|
|
837
914
|
}
|
|
838
915
|
}
|
|
839
916
|
|
|
840
|
-
public func stopVideoRecording(
|
|
917
|
+
public func stopVideoRecording(
|
|
918
|
+
requestId: String, webhookUrl: String? = nil, authToken: String? = nil
|
|
919
|
+
) async throws -> VideoRecordingStatusEvent {
|
|
841
920
|
guard !requestId.isEmpty else {
|
|
842
921
|
throw BluetoothError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
843
922
|
}
|
|
923
|
+
try requireGlassesConnected(operation: "stop video recording")
|
|
844
924
|
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
845
925
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
846
926
|
throw BluetoothError(
|
|
@@ -848,13 +928,16 @@ public final class MentraBluetoothSDK {
|
|
|
848
928
|
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
849
929
|
)
|
|
850
930
|
}
|
|
931
|
+
let waitForUpload = !(webhookUrl?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
|
|
851
932
|
pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
|
|
852
933
|
expectedStatus: "recording_stopped",
|
|
853
|
-
pending: pending
|
|
934
|
+
pending: pending,
|
|
935
|
+
waitForUpload: waitForUpload
|
|
854
936
|
)
|
|
855
|
-
DeviceManager.shared.stopVideoRecording(requestId)
|
|
937
|
+
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
856
938
|
do {
|
|
857
|
-
let
|
|
939
|
+
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15_000
|
|
940
|
+
let event = try await pending.wait(timeoutMs: timeoutMs)
|
|
858
941
|
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
859
942
|
return event
|
|
860
943
|
} catch {
|
|
@@ -887,8 +970,43 @@ public final class MentraBluetoothSDK {
|
|
|
887
970
|
}
|
|
888
971
|
}
|
|
889
972
|
|
|
890
|
-
|
|
891
|
-
|
|
973
|
+
func setOtaVersionUrl(_ otaVersionUrl: String) throws {
|
|
974
|
+
configuredOtaVersionUrl = try OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
func getOtaVersionUrl() throws -> String {
|
|
978
|
+
try configuredOtaVersionUrl ?? OtaManifestDefaults.defaultOtaVersionUrl()
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/// Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available.
|
|
982
|
+
public func checkForOtaUpdate() async throws -> Bool {
|
|
983
|
+
let status = await getFreshGlassesStatus()
|
|
984
|
+
guard status.connected else {
|
|
985
|
+
throw BluetoothError(
|
|
986
|
+
code: "glasses_not_connected",
|
|
987
|
+
message: "Cannot check OTA update because glasses are not connected."
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
guard !status.buildNumber.isEmpty else {
|
|
991
|
+
throw BluetoothError(
|
|
992
|
+
code: "missing_glasses_version",
|
|
993
|
+
message: "Cannot check OTA update because glasses build number is unavailable."
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let manifestUrl = try resolveOtaVersionUrl(status: status)
|
|
998
|
+
let manifest = try await OtaManifestChecker.fetch(manifestUrl)
|
|
999
|
+
let otaStatus = try await waitForOtaManifestStatus(status, manifest: manifest)
|
|
1000
|
+
return try OtaManifestChecker.hasUpdate(
|
|
1001
|
+
currentBuildNumber: otaStatus.buildNumber,
|
|
1002
|
+
currentMtkVersion: otaStatus.mtkFirmwareVersion,
|
|
1003
|
+
currentBesVersion: otaStatus.besFirmwareVersion,
|
|
1004
|
+
manifest: manifest
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/// Ask connected Mentra Live glasses to report the current OTA install/session status.
|
|
1009
|
+
private func queryOtaStatus() async throws -> OtaQueryResult {
|
|
892
1010
|
try await performOtaQuery(operation: "OTA status query") {
|
|
893
1011
|
DeviceManager.shared.sendOtaQueryStatus()
|
|
894
1012
|
}
|
|
@@ -923,6 +1041,12 @@ public final class MentraBluetoothSDK {
|
|
|
923
1041
|
|
|
924
1042
|
/// Start the OTA flow after your app has presented the available update to the user.
|
|
925
1043
|
public func startOtaUpdate() async throws -> OtaStartAckEvent {
|
|
1044
|
+
let status = await getFreshGlassesStatus()
|
|
1045
|
+
let otaVersionUrl = try resolveOtaVersionUrl(status: status)
|
|
1046
|
+
return try await startOtaUpdate(otaVersionUrl: otaVersionUrl)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private func startOtaCommand(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
926
1050
|
if pendingOtaStart != nil {
|
|
927
1051
|
throw BluetoothError(
|
|
928
1052
|
code: "request_in_flight",
|
|
@@ -931,7 +1055,7 @@ public final class MentraBluetoothSDK {
|
|
|
931
1055
|
}
|
|
932
1056
|
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
933
1057
|
pendingOtaStart = pending
|
|
934
|
-
DeviceManager.shared.sendOtaStart()
|
|
1058
|
+
DeviceManager.shared.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
935
1059
|
do {
|
|
936
1060
|
let event = try await pending.wait()
|
|
937
1061
|
if pendingOtaStart === pending {
|
|
@@ -946,16 +1070,102 @@ public final class MentraBluetoothSDK {
|
|
|
946
1070
|
}
|
|
947
1071
|
}
|
|
948
1072
|
|
|
949
|
-
|
|
950
|
-
|
|
1073
|
+
func startOtaUpdate(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
1074
|
+
try await startOtaCommand(otaVersionUrl: otaVersionUrl)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
func sendOtaQueryStatus() async throws -> OtaQueryResult { try await queryOtaStatus() }
|
|
1078
|
+
|
|
1079
|
+
/// Re-run the glasses-side OTA version check after an internal clock-skew recovery.
|
|
1080
|
+
func retryOtaVersionCheck() async throws -> OtaQueryResult {
|
|
951
1081
|
try await performOtaQuery(operation: "OTA version retry") {
|
|
952
1082
|
DeviceManager.shared.retryOtaVersionCheck()
|
|
953
1083
|
}
|
|
954
1084
|
}
|
|
955
1085
|
|
|
956
|
-
func
|
|
1086
|
+
private func getFreshGlassesStatus() async -> GlassesStatus {
|
|
1087
|
+
let status = glassesStatus
|
|
1088
|
+
if !status.connected || !status.buildNumber.isEmpty {
|
|
1089
|
+
return status
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
do {
|
|
1093
|
+
let versionInfo = try await requestVersionInfo()
|
|
1094
|
+
let values = status.values.merging(versionInfo.dictionary) { existing, new in
|
|
1095
|
+
if let newString = new as? String, newString.isEmpty {
|
|
1096
|
+
return existing
|
|
1097
|
+
}
|
|
1098
|
+
return new
|
|
1099
|
+
}
|
|
1100
|
+
return GlassesStatus(values: values)
|
|
1101
|
+
} catch {
|
|
1102
|
+
return status
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private func waitForOtaManifestStatus(_ initialStatus: GlassesStatus, manifest: OtaManifest) async throws -> GlassesStatus {
|
|
1107
|
+
var status = initialStatus
|
|
1108
|
+
if OtaManifestChecker.hasBesFirmware(manifest), status.besFirmwareVersion.isEmpty {
|
|
1109
|
+
status = await waitForGlassesStatus(status, timeoutMs: Self.otaBesVersionWaitMs) {
|
|
1110
|
+
!$0.connected || !$0.besFirmwareVersion.isEmpty
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
957
1113
|
|
|
958
|
-
|
|
1114
|
+
if OtaManifestChecker.hasMtkPatches(manifest), status.mtkFirmwareVersion.isEmpty {
|
|
1115
|
+
status = await waitForGlassesStatus(status, timeoutMs: Self.otaMtkVersionWaitMs) {
|
|
1116
|
+
!$0.connected || !$0.mtkFirmwareVersion.isEmpty
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
guard status.connected else {
|
|
1121
|
+
throw BluetoothError(
|
|
1122
|
+
code: "glasses_not_connected",
|
|
1123
|
+
message: "Cannot check OTA update because glasses disconnected."
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
return status
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private func waitForGlassesStatus(
|
|
1130
|
+
_ initialStatus: GlassesStatus,
|
|
1131
|
+
timeoutMs: Int,
|
|
1132
|
+
isReady: (GlassesStatus) -> Bool
|
|
1133
|
+
) async -> GlassesStatus {
|
|
1134
|
+
let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1_000)
|
|
1135
|
+
var status = initialStatus
|
|
1136
|
+
while Date() < deadline {
|
|
1137
|
+
status = glassesStatus
|
|
1138
|
+
if isReady(status) {
|
|
1139
|
+
return status
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow * 1_000))
|
|
1143
|
+
let sleepMs = min(Self.otaVersionPollMs, remainingMs)
|
|
1144
|
+
if sleepMs <= 0 {
|
|
1145
|
+
break
|
|
1146
|
+
}
|
|
1147
|
+
try? await Task.sleep(nanoseconds: UInt64(sleepMs) * 1_000_000)
|
|
1148
|
+
}
|
|
1149
|
+
return glassesStatus
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private func resolveOtaVersionUrl(status: GlassesStatus) throws -> String {
|
|
1153
|
+
let deviceUrl = status.otaVersionUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1154
|
+
if isLegacyAsgOtaStartBuild(status.buildNumber) {
|
|
1155
|
+
return deviceUrl.isEmpty ? OtaManifestDefaults.prodOtaVersionUrl : deviceUrl
|
|
1156
|
+
}
|
|
1157
|
+
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1158
|
+
// A future glasses-advertised URL should not silently change that pairing.
|
|
1159
|
+
if let configuredOtaVersionUrl {
|
|
1160
|
+
return configuredOtaVersionUrl
|
|
1161
|
+
}
|
|
1162
|
+
return try OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
private func isLegacyAsgOtaStartBuild(_ buildNumber: String) -> Bool {
|
|
1166
|
+
guard let parsed = Int(buildNumber) else { return false }
|
|
1167
|
+
return parsed < 100_000
|
|
1168
|
+
}
|
|
959
1169
|
|
|
960
1170
|
func sendShutdown() {
|
|
961
1171
|
DeviceManager.shared.sendShutdown()
|
|
@@ -971,6 +1181,10 @@ public final class MentraBluetoothSDK {
|
|
|
971
1181
|
|
|
972
1182
|
public func invalidate() {
|
|
973
1183
|
stopStreamKeepAliveMonitor()
|
|
1184
|
+
if let bluetoothAvailabilityListenerId {
|
|
1185
|
+
BluetoothAvailability.shared.removeStateListener(bluetoothAvailabilityListenerId)
|
|
1186
|
+
self.bluetoothAvailabilityListenerId = nil
|
|
1187
|
+
}
|
|
974
1188
|
if let bridgeEventSinkId {
|
|
975
1189
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
976
1190
|
self.bridgeEventSinkId = nil
|
|
@@ -982,6 +1196,83 @@ public final class MentraBluetoothSDK {
|
|
|
982
1196
|
delegate = nil
|
|
983
1197
|
}
|
|
984
1198
|
|
|
1199
|
+
private func handleBluetoothAvailability(_ state: CBManagerState) {
|
|
1200
|
+
switch state {
|
|
1201
|
+
case .poweredOff, .resetting, .unauthorized, .unsupported:
|
|
1202
|
+
handleBluetoothUnavailable()
|
|
1203
|
+
case .poweredOn:
|
|
1204
|
+
handleBluetoothRestored()
|
|
1205
|
+
case .unknown:
|
|
1206
|
+
break
|
|
1207
|
+
@unknown default:
|
|
1208
|
+
handleBluetoothUnavailable()
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private func handleBluetoothUnavailable() {
|
|
1213
|
+
cancelActiveScanSessions(reason: .cancelled)
|
|
1214
|
+
clearBluetoothDiscoveryState()
|
|
1215
|
+
disconnectActiveConnections()
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
private func disconnectActiveConnections() {
|
|
1219
|
+
if glassesStatus.controllerConnected {
|
|
1220
|
+
DeviceManager.shared.disconnectController()
|
|
1221
|
+
shouldRestoreControllerOnBluetoothRestore = true
|
|
1222
|
+
}
|
|
1223
|
+
if glassesStatus.deviceModel == DeviceTypes.SIMULATED
|
|
1224
|
+
|| DeviceManager.shared.sgc?.type.contains(DeviceTypes.SIMULATED) == true
|
|
1225
|
+
{
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
if glassesStatus.connected || glassesStatus.connectionState != .disconnected {
|
|
1229
|
+
DeviceManager.shared.disconnect()
|
|
1230
|
+
shouldRestoreGlassesOnBluetoothRestore = true
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/// Reconnect only what `handleBluetoothUnavailable` tore down, never a
|
|
1235
|
+
/// connection the user closed themselves (explicit connect/disconnect
|
|
1236
|
+
/// calls clear the restore intent).
|
|
1237
|
+
private func handleBluetoothRestored() {
|
|
1238
|
+
let restoreGlasses = shouldRestoreGlassesOnBluetoothRestore
|
|
1239
|
+
let restoreController = shouldRestoreControllerOnBluetoothRestore
|
|
1240
|
+
clearBluetoothRestoreIntent()
|
|
1241
|
+
|
|
1242
|
+
if restoreGlasses, !glassesStatus.connected, glassesStatus.connectionState == .disconnected {
|
|
1243
|
+
DeviceManager.shared.connectDefault() // also restores the controller
|
|
1244
|
+
} else if restoreController, !glassesStatus.controllerConnected {
|
|
1245
|
+
DeviceManager.shared.connectDefaultController()
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private func clearBluetoothRestoreIntent() {
|
|
1250
|
+
shouldRestoreGlassesOnBluetoothRestore = false
|
|
1251
|
+
shouldRestoreControllerOnBluetoothRestore = false
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
private func clearBluetoothDiscoveryState() {
|
|
1255
|
+
discoveredDeviceNames.removeAll()
|
|
1256
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", false)
|
|
1257
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searchingController", false)
|
|
1258
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searchResults", [] as [[String: Any]])
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private func cancelActiveScanSessions(reason: ScanStopReason) {
|
|
1262
|
+
let ids = Array(activeScanSessions.keys)
|
|
1263
|
+
guard !ids.isEmpty else {
|
|
1264
|
+
if bluetoothStatus.searching || bluetoothStatus.searchingController {
|
|
1265
|
+
stopScan(reason: reason)
|
|
1266
|
+
}
|
|
1267
|
+
return
|
|
1268
|
+
}
|
|
1269
|
+
for (index, id) in ids.enumerated() {
|
|
1270
|
+
// Stop the underlying scan once (first session); the rest only
|
|
1271
|
+
// complete their callbacks.
|
|
1272
|
+
finishScanSession(id, reason: reason, shouldStopScan: index == 0)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
985
1276
|
private func startStreamKeepAliveMonitor(streamId: String, intervalSeconds requestedIntervalSeconds: Int) {
|
|
986
1277
|
let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds : 5
|
|
987
1278
|
let tracker = ActiveStreamKeepAlive(streamId: streamId, intervalSeconds: intervalSeconds)
|
|
@@ -1162,7 +1453,14 @@ public final class MentraBluetoothSDK {
|
|
|
1162
1453
|
guard let request = pendingVideoRecordingRequests[event.requestId] else { return }
|
|
1163
1454
|
if event.success {
|
|
1164
1455
|
if event.status == request.expectedStatus {
|
|
1165
|
-
request.
|
|
1456
|
+
if request.waitForUpload {
|
|
1457
|
+
request.stoppedEvent = event
|
|
1458
|
+
if request.uploadSucceeded {
|
|
1459
|
+
request.pending.resolve(event)
|
|
1460
|
+
}
|
|
1461
|
+
} else {
|
|
1462
|
+
request.pending.resolve(event)
|
|
1463
|
+
}
|
|
1166
1464
|
}
|
|
1167
1465
|
} else {
|
|
1168
1466
|
request.pending.reject(
|
|
@@ -1174,6 +1472,26 @@ public final class MentraBluetoothSDK {
|
|
|
1174
1472
|
}
|
|
1175
1473
|
}
|
|
1176
1474
|
|
|
1475
|
+
private func handleMediaUploadForRequests(_ event: MediaUploadEvent) {
|
|
1476
|
+
guard event.isVideo, let request = pendingVideoRecordingRequests[event.requestId], request.waitForUpload else {
|
|
1477
|
+
return
|
|
1478
|
+
}
|
|
1479
|
+
if event.isSuccess {
|
|
1480
|
+
if let stoppedEvent = request.stoppedEvent {
|
|
1481
|
+
request.pending.resolve(stoppedEvent)
|
|
1482
|
+
} else {
|
|
1483
|
+
request.uploadSucceeded = true
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
request.pending.reject(
|
|
1487
|
+
BluetoothError(
|
|
1488
|
+
code: "video_upload_failed",
|
|
1489
|
+
message: event.errorMessage ?? "Video upload failed."
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1177
1495
|
private func handleRgbLedResponseForRequests(_ event: RgbLedControlResponseEvent) {
|
|
1178
1496
|
guard let pending = pendingRgbLedRequests[event.requestId] else { return }
|
|
1179
1497
|
if event.state == "success" {
|
|
@@ -1273,6 +1591,7 @@ public final class MentraBluetoothSDK {
|
|
|
1273
1591
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
1274
1592
|
switch ObservableStore.normalizeCategory(category) {
|
|
1275
1593
|
case "glasses":
|
|
1594
|
+
analytics.observeGlassesStatus(glassesStatus)
|
|
1276
1595
|
let nextState = state
|
|
1277
1596
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
1278
1597
|
delegate?.mentraBluetoothSDK(self, didUpdateGlasses: nextState.glasses)
|
|
@@ -1424,6 +1743,10 @@ public final class MentraBluetoothSDK {
|
|
|
1424
1743
|
let event = VideoRecordingStatusEvent(values: data)
|
|
1425
1744
|
handleVideoRecordingStatusForRequests(event)
|
|
1426
1745
|
delegate?.mentraBluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
1746
|
+
case "media_success", "media_error":
|
|
1747
|
+
let event = MediaUploadEvent(values: data)
|
|
1748
|
+
handleMediaUploadForRequests(event)
|
|
1749
|
+
delegate?.mentraBluetoothSDK(self, didReceive: .mediaUpload(event))
|
|
1427
1750
|
case "rgb_led_control_response":
|
|
1428
1751
|
let event = RgbLedControlResponseEvent(values: data)
|
|
1429
1752
|
handleRgbLedResponseForRequests(event)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
@MainActor
|
|
4
|
+
public enum MentraBluetoothSDKDebug {
|
|
5
|
+
public static func setOtaVersionUrl(_ otaVersionUrl: String, on sdk: MentraBluetoothSDK) throws {
|
|
6
|
+
try sdk.setOtaVersionUrl(otaVersionUrl)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public static func getOtaVersionUrl(on sdk: MentraBluetoothSDK) throws -> String {
|
|
10
|
+
try sdk.getOtaVersionUrl()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -54,6 +54,17 @@ class ObservableStore {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
func remove(_ category: String, _ key: String) {
|
|
58
|
+
let normalizedCategory = Self.normalizeCategory(category)
|
|
59
|
+
let fullKey = "\(normalizedCategory).\(key)"
|
|
60
|
+
guard values[fullKey] != nil else { return }
|
|
61
|
+
values.removeValue(forKey: fullKey)
|
|
62
|
+
// Emit updated category snapshot so UI listeners clear the removed key
|
|
63
|
+
let snapshot = getCategory(normalizedCategory)
|
|
64
|
+
onEmit?(normalizedCategory, snapshot)
|
|
65
|
+
for listener in Array(listeners.values) { listener(normalizedCategory, snapshot) }
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
func get(_ category: String, _ key: String) -> Any? {
|
|
58
69
|
values["\(Self.normalizeCategory(category)).\(key)"]
|
|
59
70
|
}
|