@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +16 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +272 -197
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +25 -1
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -134,9 +134,12 @@ private final class PendingVideoRecordingRequest {
|
|
|
134
134
|
private final class PendingVersionInfoRequest {
|
|
135
135
|
let pending: PendingResponse<VersionInfoResult>
|
|
136
136
|
let accumulator: VersionInfoResponseAccumulator
|
|
137
|
+
let requestId: String
|
|
138
|
+
let startedAt = ProcessInfo.processInfo.systemUptime
|
|
137
139
|
|
|
138
140
|
init(pending: PendingResponse<VersionInfoResult>, requestId: String) {
|
|
139
141
|
self.pending = pending
|
|
142
|
+
self.requestId = requestId
|
|
140
143
|
accumulator = VersionInfoResponseAccumulator(expectedRequestId: requestId)
|
|
141
144
|
}
|
|
142
145
|
}
|
|
@@ -163,13 +166,18 @@ private final class PendingStreamStart {
|
|
|
163
166
|
@MainActor
|
|
164
167
|
final class PendingResponse<T> {
|
|
165
168
|
private let operation: String
|
|
169
|
+
/// The operation without per-request ids, so timeouts group in analytics.
|
|
170
|
+
private let telemetryOperation: String
|
|
171
|
+
private let onTimeout: ((String) -> Void)?
|
|
166
172
|
private var continuation: CheckedContinuation<T, Error>?
|
|
167
173
|
private var timeoutTask: Task<Void, Never>?
|
|
168
174
|
private var result: Result<T, Error>?
|
|
169
175
|
private var completed = false
|
|
170
176
|
|
|
171
|
-
init(operation: String) {
|
|
177
|
+
init(operation: String, telemetryOperation: String? = nil, onTimeout: ((String) -> Void)? = nil) {
|
|
172
178
|
self.operation = operation
|
|
179
|
+
self.telemetryOperation = telemetryOperation ?? operation
|
|
180
|
+
self.onTimeout = onTimeout
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
func resolve(_ value: T) {
|
|
@@ -207,10 +215,12 @@ final class PendingResponse<T> {
|
|
|
207
215
|
} catch {
|
|
208
216
|
return
|
|
209
217
|
}
|
|
210
|
-
self
|
|
218
|
+
guard let self, !self.completed else { return }
|
|
219
|
+
self.onTimeout?(self.telemetryOperation)
|
|
220
|
+
self.reject(
|
|
211
221
|
BluetoothSdkError(
|
|
212
222
|
code: "request_timeout",
|
|
213
|
-
message: "\(self
|
|
223
|
+
message: "\(self.operation) timed out waiting for glasses response."
|
|
214
224
|
)
|
|
215
225
|
)
|
|
216
226
|
}
|
|
@@ -254,6 +264,7 @@ public final class MentraBluetoothSDK {
|
|
|
254
264
|
private static let otaBesVersionWaitMs = 5000
|
|
255
265
|
private static let otaMtkVersionWaitMs = 2000
|
|
256
266
|
private static let otaVersionPollMs = 100
|
|
267
|
+
private static let micStateKeys: Set<String> = ["currentMic", "should_send_transcript", "local_stt_fallback_active"]
|
|
257
268
|
|
|
258
269
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
259
270
|
|
|
@@ -272,6 +283,7 @@ public final class MentraBluetoothSDK {
|
|
|
272
283
|
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
273
284
|
private let streamSession = StreamSessionState()
|
|
274
285
|
private let analytics: BluetoothSdkAnalytics
|
|
286
|
+
private var updateCheck: BluetoothSdkUpdateCheck?
|
|
275
287
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
276
288
|
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
277
289
|
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
@@ -299,8 +311,22 @@ public final class MentraBluetoothSDK {
|
|
|
299
311
|
private let wifiSessionCapabilities = WifiSessionCapabilities()
|
|
300
312
|
private var configuredOtaVersionUrl: String?
|
|
301
313
|
|
|
314
|
+
/// Whether the app declares the `bluetooth-central` background mode. When false on iOS, the
|
|
315
|
+
/// glasses stay disconnected after a link drop until the user opens the app, so a software
|
|
316
|
+
/// update started on a locked phone stalls. Always true on macOS.
|
|
317
|
+
public static var supportsBackgroundReconnect: Bool {
|
|
318
|
+
BluetoothBackgroundMode.isDeclared(in: Bundle.main.infoDictionary)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private static var didWarnMissingBackgroundMode = false
|
|
322
|
+
|
|
302
323
|
public init(configuration: MentraBluetoothSDKConfiguration = .default) {
|
|
303
324
|
self.configuration = configuration
|
|
325
|
+
// Once per process: apps may create the SDK more than once.
|
|
326
|
+
if !Self.didWarnMissingBackgroundMode, !Self.supportsBackgroundReconnect {
|
|
327
|
+
Self.didWarnMissingBackgroundMode = true
|
|
328
|
+
Bridge.log(BluetoothBackgroundMode.missingWarning)
|
|
329
|
+
}
|
|
304
330
|
analytics = BluetoothSdkAnalytics(configuration: configuration.analytics)
|
|
305
331
|
bluetoothAvailabilityListenerId = BluetoothAvailability.shared.addStateListener { [weak self] state in
|
|
306
332
|
Task { @MainActor [weak self] in
|
|
@@ -336,6 +362,31 @@ public final class MentraBluetoothSDK {
|
|
|
336
362
|
}
|
|
337
363
|
analytics.initializeGlassesStatus(glassesStatus)
|
|
338
364
|
analytics.captureStarted()
|
|
365
|
+
let updateCheck = BluetoothSdkUpdateCheck(
|
|
366
|
+
surface: BluetoothSdkUpdateCheck.surface(forAnalyticsSurface: configuration.analytics.surface)
|
|
367
|
+
) { [weak self] banner, result in
|
|
368
|
+
NSLog("%@", banner)
|
|
369
|
+
var values: [String: Any] = [
|
|
370
|
+
"message": banner,
|
|
371
|
+
"status": result.status,
|
|
372
|
+
"installedVersion": BluetoothSdkDefaults.sdkVersion ?? "unknown",
|
|
373
|
+
]
|
|
374
|
+
if let latestVersion = result.latestVersion { values["latestVersion"] = latestVersion }
|
|
375
|
+
let notice = values
|
|
376
|
+
Task { @MainActor [weak self] in
|
|
377
|
+
guard let self else { return }
|
|
378
|
+
self.delegate?.mentraBluetoothSDK(self, didReceive: .raw(name: "sdk_update_notice", values: notice))
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
self.updateCheck = updateCheck
|
|
382
|
+
updateCheck.start()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// Reports a glasses command that timed out, without its per-request id.
|
|
386
|
+
private var requestTimeoutReporter: (String) -> Void {
|
|
387
|
+
{ [weak self] operation in
|
|
388
|
+
self?.analytics.recordUsage { $0.requestTimedOut(operation: operation) }
|
|
389
|
+
}
|
|
339
390
|
}
|
|
340
391
|
|
|
341
392
|
public var state: MentraBluetoothState {
|
|
@@ -531,6 +582,7 @@ public final class MentraBluetoothSDK {
|
|
|
531
582
|
}
|
|
532
583
|
|
|
533
584
|
public func cancelConnectionAttempt() {
|
|
585
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectCancel)
|
|
534
586
|
clearBluetoothRestoreIntent()
|
|
535
587
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
536
588
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -544,6 +596,7 @@ public final class MentraBluetoothSDK {
|
|
|
544
596
|
}
|
|
545
597
|
|
|
546
598
|
public func disconnect() {
|
|
599
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectUser)
|
|
547
600
|
clearBluetoothRestoreIntent()
|
|
548
601
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
549
602
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -552,6 +605,7 @@ public final class MentraBluetoothSDK {
|
|
|
552
605
|
}
|
|
553
606
|
|
|
554
607
|
public func forget() {
|
|
608
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectForget)
|
|
555
609
|
clearBluetoothRestoreIntent()
|
|
556
610
|
DeviceManager.shared.forget()
|
|
557
611
|
}
|
|
@@ -664,7 +718,7 @@ public final class MentraBluetoothSDK {
|
|
|
664
718
|
send: (String) throws -> Void
|
|
665
719
|
) async throws -> SettingsAckEvent {
|
|
666
720
|
let requestId = "settings-\(setting)-\(UUID().uuidString)"
|
|
667
|
-
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)")
|
|
721
|
+
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)", onTimeout: requestTimeoutReporter)
|
|
668
722
|
pendingSettingsRequests[requestId] = pending
|
|
669
723
|
do {
|
|
670
724
|
try send(requestId)
|
|
@@ -688,6 +742,10 @@ public final class MentraBluetoothSDK {
|
|
|
688
742
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "loudness_gate_enabled", enabled)
|
|
689
743
|
}
|
|
690
744
|
|
|
745
|
+
public func setAutoPowerOffEnabled(_ enabled: Bool) async throws {
|
|
746
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_power_off_enabled", enabled)
|
|
747
|
+
}
|
|
748
|
+
|
|
691
749
|
@available(*, deprecated, message: "Sticky action-button photo presets are deprecated. Prefer per-request requestPhoto(...) options (e.g. mode .text for text sensor size/crop, or explicit per-shot fields). This method still works but will be removed in a future release.")
|
|
692
750
|
public func setPhotoCaptureDefaults(_ settings: PhotoCaptureDefaults) async throws -> SettingsAckEvent {
|
|
693
751
|
try await performSettingsCommand(
|
|
@@ -933,7 +991,7 @@ public final class MentraBluetoothSDK {
|
|
|
933
991
|
// the scan screen auto-starts a scan on mount and can be pushed twice.
|
|
934
992
|
return try await existing.value
|
|
935
993
|
}
|
|
936
|
-
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
994
|
+
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request", onTimeout: requestTimeoutReporter)
|
|
937
995
|
let request = PendingWifiScan(pending: pending, scanId: "scan-\(UUID().uuidString)")
|
|
938
996
|
pendingWifiScan = request
|
|
939
997
|
// Claim the scan-results store for this scan before the command goes out,
|
|
@@ -973,11 +1031,15 @@ public final class MentraBluetoothSDK {
|
|
|
973
1031
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
974
1032
|
)
|
|
975
1033
|
}
|
|
976
|
-
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
1034
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request", onTimeout: requestTimeoutReporter)
|
|
977
1035
|
pendingWifiStatus = PendingWifiStatusRequest(ssid: ssid, pending: pending)
|
|
978
|
-
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
979
1036
|
do {
|
|
980
|
-
let event = try await
|
|
1037
|
+
let event = try await withUsageOutcome(record: { latencyMs, errorCode, _ in
|
|
1038
|
+
analytics.recordUsage { $0.wifiSetupSettled(latencyMs: latencyMs, errorCode: errorCode) }
|
|
1039
|
+
}) {
|
|
1040
|
+
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
1041
|
+
return try await pending.wait()
|
|
1042
|
+
}
|
|
981
1043
|
if pendingWifiStatus?.pending === pending {
|
|
982
1044
|
pendingWifiStatus = nil
|
|
983
1045
|
}
|
|
@@ -1000,7 +1062,7 @@ public final class MentraBluetoothSDK {
|
|
|
1000
1062
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
1001
1063
|
)
|
|
1002
1064
|
}
|
|
1003
|
-
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request")
|
|
1065
|
+
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request", onTimeout: requestTimeoutReporter)
|
|
1004
1066
|
let requestId = "forget-\(UUID().uuidString)"
|
|
1005
1067
|
let request = PendingWifiForgetRequest(
|
|
1006
1068
|
ssid: ssid,
|
|
@@ -1045,7 +1107,7 @@ public final class MentraBluetoothSDK {
|
|
|
1045
1107
|
error: "saved_wifi_networks_unsupported"
|
|
1046
1108
|
)
|
|
1047
1109
|
}
|
|
1048
|
-
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request")
|
|
1110
|
+
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request", onTimeout: requestTimeoutReporter)
|
|
1049
1111
|
let request = PendingSavedWifiNetworks(
|
|
1050
1112
|
requestId: requestId,
|
|
1051
1113
|
sid: wifiSessionCapabilities.sessionId,
|
|
@@ -1082,12 +1144,18 @@ public final class MentraBluetoothSDK {
|
|
|
1082
1144
|
_ = try? await previous.value
|
|
1083
1145
|
}
|
|
1084
1146
|
let pending = PendingResponse<HotspotStatusEvent>(
|
|
1085
|
-
operation: "hotspot \(enabled ? "enable" : "disable") request"
|
|
1147
|
+
operation: "hotspot \(enabled ? "enable" : "disable") request",
|
|
1148
|
+
onTimeout: self.requestTimeoutReporter
|
|
1086
1149
|
)
|
|
1087
1150
|
self.pendingHotspotStatus = PendingHotspotStatusRequest(enabled: enabled, pending: pending)
|
|
1088
|
-
DeviceManager.shared.setHotspotState(enabled)
|
|
1089
1151
|
do {
|
|
1090
|
-
let event = try await
|
|
1152
|
+
let event = try await self.withUsageOutcome(record: { latencyMs, errorCode, _ in
|
|
1153
|
+
guard enabled else { return }
|
|
1154
|
+
self.analytics.recordUsage { $0.hotspotStartSettled(latencyMs: latencyMs, errorCode: errorCode) }
|
|
1155
|
+
}) {
|
|
1156
|
+
DeviceManager.shared.setHotspotState(enabled)
|
|
1157
|
+
return try await pending.wait()
|
|
1158
|
+
}
|
|
1091
1159
|
if self.pendingHotspotStatus?.pending === pending {
|
|
1092
1160
|
self.pendingHotspotStatus = nil
|
|
1093
1161
|
}
|
|
@@ -1120,13 +1188,30 @@ public final class MentraBluetoothSDK {
|
|
|
1120
1188
|
Bridge.log(
|
|
1121
1189
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
1122
1190
|
)
|
|
1123
|
-
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
1191
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)", telemetryOperation: "photo request", onTimeout: requestTimeoutReporter)
|
|
1124
1192
|
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
1125
|
-
DeviceManager.shared.requestPhoto(routedRequest)
|
|
1126
1193
|
do {
|
|
1127
|
-
let event = try await
|
|
1128
|
-
?
|
|
1129
|
-
|
|
1194
|
+
let event = try await withUsageOutcome(record: { (latencyMs, errorCode, event: PhotoResponseEvent?) in
|
|
1195
|
+
var fileSizeBytes: Int64?
|
|
1196
|
+
if case let .success(_, _, _, _, _, size?, _) = event?.response { fileSizeBytes = Int64(size) }
|
|
1197
|
+
analytics.recordUsage {
|
|
1198
|
+
$0.photoSettled(
|
|
1199
|
+
size: routedRequest.size.rawValue,
|
|
1200
|
+
mode: routedRequest.mode.rawValue,
|
|
1201
|
+
compress: routedRequest.compress.rawValue,
|
|
1202
|
+
save: routedRequest.save,
|
|
1203
|
+
transferMethod: routedRequest.transferMethod,
|
|
1204
|
+
latencyMs: latencyMs,
|
|
1205
|
+
fileSizeBytes: fileSizeBytes,
|
|
1206
|
+
errorCode: errorCode
|
|
1207
|
+
)
|
|
1208
|
+
}
|
|
1209
|
+
}) {
|
|
1210
|
+
DeviceManager.shared.requestPhoto(routedRequest)
|
|
1211
|
+
return try await pending.wait(timeoutMs: routedRequest.presendThumbnail
|
|
1212
|
+
? MentraBluetoothSDK.photoThumbnailRequestTimeoutMs
|
|
1213
|
+
: MentraBluetoothSDK.photoRequestTimeoutMs)
|
|
1214
|
+
}
|
|
1130
1215
|
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
1131
1216
|
return event
|
|
1132
1217
|
} catch {
|
|
@@ -1145,7 +1230,7 @@ public final class MentraBluetoothSDK {
|
|
|
1145
1230
|
mfnr: Bool? = nil
|
|
1146
1231
|
) async throws -> CameraStatusEvent {
|
|
1147
1232
|
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
1148
|
-
let pending = PendingResponse<CameraStatusEvent>(operation: "camera warm up \(effectiveRequestId)")
|
|
1233
|
+
let pending = PendingResponse<CameraStatusEvent>(operation: "camera warm up \(effectiveRequestId)", telemetryOperation: "camera warm up", onTimeout: requestTimeoutReporter)
|
|
1149
1234
|
pendingCameraStatusRequests[effectiveRequestId] = pending
|
|
1150
1235
|
do {
|
|
1151
1236
|
// Inside the catch so an unsupported-device throw also clears the pending entry.
|
|
@@ -1183,7 +1268,7 @@ public final class MentraBluetoothSDK {
|
|
|
1183
1268
|
message: "A gallery status query is already waiting for a glasses response."
|
|
1184
1269
|
)
|
|
1185
1270
|
}
|
|
1186
|
-
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query")
|
|
1271
|
+
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query", onTimeout: requestTimeoutReporter)
|
|
1187
1272
|
pendingGalleryStatus = pending
|
|
1188
1273
|
DeviceManager.shared.queryGalleryStatus()
|
|
1189
1274
|
do {
|
|
@@ -1201,33 +1286,53 @@ public final class MentraBluetoothSDK {
|
|
|
1201
1286
|
}
|
|
1202
1287
|
|
|
1203
1288
|
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1204
|
-
try requireGlassesConnected(operation: "start stream")
|
|
1205
|
-
guard streamSession.supported else {
|
|
1206
|
-
throw BluetoothSdkError(code: "stream_control_unsupported", message: "Update the glasses software before starting a stream.")
|
|
1207
|
-
}
|
|
1208
1289
|
var values = request.values
|
|
1209
1290
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
1210
1291
|
values["streamId"] = streamId
|
|
1211
|
-
let
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1292
|
+
let message = values
|
|
1293
|
+
return try await withUsageOutcome(record: { latencyMs, errorCode, _ in
|
|
1294
|
+
let glassesNetwork = glassesNetwork()
|
|
1295
|
+
analytics.recordUsage {
|
|
1296
|
+
$0.streamStartSettled(
|
|
1297
|
+
streamUrl: request.streamUrl,
|
|
1298
|
+
width: request.video?.width,
|
|
1299
|
+
height: request.video?.height,
|
|
1300
|
+
fps: request.video?.fps,
|
|
1301
|
+
bitrate: request.video?.bitrate,
|
|
1302
|
+
captureAudio: request.captureAudio,
|
|
1303
|
+
glassesNetwork: glassesNetwork,
|
|
1304
|
+
latencyMs: latencyMs,
|
|
1305
|
+
errorCode: errorCode
|
|
1306
|
+
)
|
|
1307
|
+
}
|
|
1308
|
+
}) {
|
|
1309
|
+
try requireGlassesConnected(operation: "start stream")
|
|
1310
|
+
guard streamSession.supported else {
|
|
1311
|
+
throw BluetoothSdkError(code: "stream_control_unsupported", message: "Update the glasses software before starting a stream.")
|
|
1312
|
+
}
|
|
1313
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "start stream \(streamId)", telemetryOperation: "start stream", onTimeout: requestTimeoutReporter)
|
|
1314
|
+
let requestedAt = BluetoothSdkAnalytics.epochMillis()
|
|
1315
|
+
analytics.recordUsage { $0.streamRequested(streamId: streamId, streamUrl: request.streamUrl, nowMs: requestedAt) }
|
|
1316
|
+
// seq assignment through the BLE hand-off must stay await-free so this
|
|
1317
|
+
// MainActor turn is one critical section: rejectPreemptedStreamStarts
|
|
1318
|
+
// only works if seq order equals the order the commands reach the
|
|
1319
|
+
// glasses' FIFO.
|
|
1320
|
+
streamStartSeq += 1
|
|
1321
|
+
pendingStreamStarts[streamId] = PendingStreamStart(seq: streamStartSeq, pending: pending)
|
|
1322
|
+
DeviceManager.shared.startStream(message)
|
|
1323
|
+
do {
|
|
1324
|
+
let event = try await pending.wait(timeoutMs: 30000)
|
|
1325
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1326
|
+
return event
|
|
1327
|
+
} catch {
|
|
1328
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1329
|
+
throw error
|
|
1330
|
+
}
|
|
1226
1331
|
}
|
|
1227
1332
|
}
|
|
1228
1333
|
|
|
1229
1334
|
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
1230
|
-
let pending = PendingResponse<RgbLedControlResponseEvent>(operation: "RGB LED command \(request.requestId)")
|
|
1335
|
+
let pending = PendingResponse<RgbLedControlResponseEvent>(operation: "RGB LED command \(request.requestId)", telemetryOperation: "RGB LED command", onTimeout: requestTimeoutReporter)
|
|
1231
1336
|
pendingRgbLedRequests[request.requestId] = pending
|
|
1232
1337
|
DeviceManager.shared.rgbLedControl(
|
|
1233
1338
|
requestId: request.requestId,
|
|
@@ -1249,13 +1354,14 @@ public final class MentraBluetoothSDK {
|
|
|
1249
1354
|
}
|
|
1250
1355
|
|
|
1251
1356
|
public func stopStream() async throws -> StreamStatusEvent {
|
|
1357
|
+
analytics.updateUsage { $0.streamStopRequested() }
|
|
1252
1358
|
guard pendingStreamStop == nil else {
|
|
1253
1359
|
throw BluetoothSdkError(
|
|
1254
1360
|
code: "request_in_flight",
|
|
1255
1361
|
message: "A stream stop command is already waiting for a glasses response."
|
|
1256
1362
|
)
|
|
1257
1363
|
}
|
|
1258
|
-
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream")
|
|
1364
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream", onTimeout: requestTimeoutReporter)
|
|
1259
1365
|
// The seq draw and the BLE hand-off stay in this single await-free
|
|
1260
1366
|
// MainActor turn, sharing startStream's ordering critical section:
|
|
1261
1367
|
// the stop takes its own slot in the send order, so its ack can
|
|
@@ -1284,7 +1390,9 @@ public final class MentraBluetoothSDK {
|
|
|
1284
1390
|
}
|
|
1285
1391
|
try requireGlassesConnected(operation: "start video recording")
|
|
1286
1392
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
1287
|
-
operation: "start video recording \(request.requestId)"
|
|
1393
|
+
operation: "start video recording \(request.requestId)",
|
|
1394
|
+
telemetryOperation: "start video recording",
|
|
1395
|
+
onTimeout: requestTimeoutReporter
|
|
1288
1396
|
)
|
|
1289
1397
|
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
1290
1398
|
throw BluetoothSdkError(
|
|
@@ -1296,6 +1404,16 @@ public final class MentraBluetoothSDK {
|
|
|
1296
1404
|
expectedStatus: "recording_started",
|
|
1297
1405
|
pending: pending
|
|
1298
1406
|
)
|
|
1407
|
+
analytics.updateUsage {
|
|
1408
|
+
$0.videoRequested(
|
|
1409
|
+
requestId: request.requestId,
|
|
1410
|
+
width: request.width,
|
|
1411
|
+
height: request.height,
|
|
1412
|
+
fps: request.fps,
|
|
1413
|
+
save: request.save,
|
|
1414
|
+
maxDurationMinutes: request.maxRecordingTimeMinutes
|
|
1415
|
+
)
|
|
1416
|
+
}
|
|
1299
1417
|
DeviceManager.shared.startVideoRecording(
|
|
1300
1418
|
request.requestId,
|
|
1301
1419
|
request.save,
|
|
@@ -1322,7 +1440,7 @@ public final class MentraBluetoothSDK {
|
|
|
1322
1440
|
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
1323
1441
|
}
|
|
1324
1442
|
try requireGlassesConnected(operation: "stop video recording")
|
|
1325
|
-
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
1443
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)", telemetryOperation: "stop video recording", onTimeout: requestTimeoutReporter)
|
|
1326
1444
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1327
1445
|
throw BluetoothSdkError(
|
|
1328
1446
|
code: "request_in_flight",
|
|
@@ -1335,6 +1453,7 @@ public final class MentraBluetoothSDK {
|
|
|
1335
1453
|
pending: pending,
|
|
1336
1454
|
waitForUpload: waitForUpload
|
|
1337
1455
|
)
|
|
1456
|
+
analytics.updateUsage { $0.videoStopRequested(requestId: requestId) }
|
|
1338
1457
|
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1339
1458
|
do {
|
|
1340
1459
|
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
@@ -1355,7 +1474,7 @@ public final class MentraBluetoothSDK {
|
|
|
1355
1474
|
)
|
|
1356
1475
|
}
|
|
1357
1476
|
try requireGlassesConnected(operation: "query video recording status")
|
|
1358
|
-
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)")
|
|
1477
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)", telemetryOperation: "query video recording status", onTimeout: requestTimeoutReporter)
|
|
1359
1478
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1360
1479
|
throw BluetoothSdkError(
|
|
1361
1480
|
code: "request_in_flight",
|
|
@@ -1378,27 +1497,35 @@ public final class MentraBluetoothSDK {
|
|
|
1378
1497
|
}
|
|
1379
1498
|
|
|
1380
1499
|
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
1381
|
-
|
|
1500
|
+
if let request = pendingVersionInfo {
|
|
1501
|
+
logVersionInfoRequest(request, stage: "refused", extra: ["code": "request_in_flight"])
|
|
1382
1502
|
throw BluetoothSdkError(
|
|
1383
1503
|
code: "request_in_flight",
|
|
1384
1504
|
message: "A version info request is already waiting for a glasses response."
|
|
1385
1505
|
)
|
|
1386
1506
|
}
|
|
1387
1507
|
let requestId = UUID().uuidString
|
|
1388
|
-
let pending = PendingResponse<VersionInfoResult>(operation: "version info request")
|
|
1508
|
+
let pending = PendingResponse<VersionInfoResult>(operation: "version info request", onTimeout: requestTimeoutReporter)
|
|
1389
1509
|
let request = PendingVersionInfoRequest(pending: pending, requestId: requestId)
|
|
1390
1510
|
pendingVersionInfo = request
|
|
1511
|
+
logVersionInfoRequest(request, stage: "registered")
|
|
1391
1512
|
DeviceManager.shared.requestVersionInfo(requestId: requestId)
|
|
1392
1513
|
do {
|
|
1393
1514
|
let status = try await pending.wait()
|
|
1394
1515
|
if pendingVersionInfo === request {
|
|
1395
1516
|
pendingVersionInfo = nil
|
|
1396
1517
|
}
|
|
1518
|
+
logVersionInfoRequest(request, stage: "resolved")
|
|
1397
1519
|
return status
|
|
1398
1520
|
} catch {
|
|
1399
1521
|
if pendingVersionInfo === request {
|
|
1400
1522
|
pendingVersionInfo = nil
|
|
1401
1523
|
}
|
|
1524
|
+
let sdkError = error as? BluetoothSdkError
|
|
1525
|
+
logVersionInfoRequest(request, stage: "rejected", extra: [
|
|
1526
|
+
"code": sdkError?.code ?? String((error as NSError).code),
|
|
1527
|
+
"message": String((sdkError?.message ?? error.localizedDescription).prefix(256)),
|
|
1528
|
+
])
|
|
1402
1529
|
throw error
|
|
1403
1530
|
}
|
|
1404
1531
|
}
|
|
@@ -1476,7 +1603,7 @@ public final class MentraBluetoothSDK {
|
|
|
1476
1603
|
message: "An OTA status query is already waiting for a glasses response."
|
|
1477
1604
|
)
|
|
1478
1605
|
}
|
|
1479
|
-
let pending = PendingResponse<OtaQueryResult>(operation: operation)
|
|
1606
|
+
let pending = PendingResponse<OtaQueryResult>(operation: operation, onTimeout: requestTimeoutReporter)
|
|
1480
1607
|
pendingOtaQuery = pending
|
|
1481
1608
|
sendRequest()
|
|
1482
1609
|
do {
|
|
@@ -1507,7 +1634,7 @@ public final class MentraBluetoothSDK {
|
|
|
1507
1634
|
message: "An OTA start command is already waiting for a glasses response."
|
|
1508
1635
|
)
|
|
1509
1636
|
}
|
|
1510
|
-
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
1637
|
+
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command", onTimeout: requestTimeoutReporter)
|
|
1511
1638
|
pendingOtaStart = pending
|
|
1512
1639
|
DeviceManager.shared.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
1513
1640
|
do {
|
|
@@ -1638,8 +1765,8 @@ public final class MentraBluetoothSDK {
|
|
|
1638
1765
|
DeviceManager.shared.sendReboot()
|
|
1639
1766
|
}
|
|
1640
1767
|
|
|
1641
|
-
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1642
|
-
DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1768
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) throws {
|
|
1769
|
+
try DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1643
1770
|
}
|
|
1644
1771
|
|
|
1645
1772
|
public func invalidate() {
|
|
@@ -1656,6 +1783,7 @@ public final class MentraBluetoothSDK {
|
|
|
1656
1783
|
DeviceStore.shared.store.removeListener(storeListenerId)
|
|
1657
1784
|
self.storeListenerId = nil
|
|
1658
1785
|
}
|
|
1786
|
+
updateCheck?.stop()
|
|
1659
1787
|
delegate = nil
|
|
1660
1788
|
}
|
|
1661
1789
|
|
|
@@ -2249,17 +2377,90 @@ public final class MentraBluetoothSDK {
|
|
|
2249
2377
|
)
|
|
2250
2378
|
}
|
|
2251
2379
|
|
|
2380
|
+
private func recordStreamStatus(_ event: StreamStatusEvent) {
|
|
2381
|
+
guard analytics.isEnabled else { return }
|
|
2382
|
+
let attempt: Int?
|
|
2383
|
+
var statusErrorDetails: String?
|
|
2384
|
+
switch event.status {
|
|
2385
|
+
case let .reconnecting(_, reconnectAttempt, _, _, _, _), let .reconnected(_, reconnectAttempt, _, _):
|
|
2386
|
+
attempt = reconnectAttempt
|
|
2387
|
+
case let .error(_, errorDetails, _, _):
|
|
2388
|
+
attempt = nil
|
|
2389
|
+
statusErrorDetails = errorDetails
|
|
2390
|
+
default:
|
|
2391
|
+
attempt = nil
|
|
2392
|
+
}
|
|
2393
|
+
let errorDetails = event.errorDetails ?? statusErrorDetails
|
|
2394
|
+
let stats = event.stats
|
|
2395
|
+
let now = BluetoothSdkAnalytics.epochMillis()
|
|
2396
|
+
analytics.recordUsage {
|
|
2397
|
+
$0.streamStatus(
|
|
2398
|
+
streamId: event.streamId,
|
|
2399
|
+
state: event.state.rawValue,
|
|
2400
|
+
terminal: event.terminal,
|
|
2401
|
+
reconnectAttempt: attempt,
|
|
2402
|
+
bitrate: stats?.bitrate,
|
|
2403
|
+
fps: stats?.fps,
|
|
2404
|
+
droppedFrames: stats?.droppedFrames,
|
|
2405
|
+
temperatureC: stats?.temperatureC,
|
|
2406
|
+
errorDetails: errorDetails,
|
|
2407
|
+
nowMs: now
|
|
2408
|
+
)
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
private func glassesNetwork() -> String {
|
|
2413
|
+
let glasses = glassesStatus
|
|
2414
|
+
if case .enabled = glasses.hotspot { return "hotspot" }
|
|
2415
|
+
if case .connected = glasses.wifi { return "wifi" }
|
|
2416
|
+
return "none"
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/// Runs a glasses command and reports its latency and outcome to usage
|
|
2420
|
+
/// analytics. Cancellation is the caller giving up, not a command outcome, so
|
|
2421
|
+
/// it is not reported.
|
|
2422
|
+
private func withUsageOutcome<T>(
|
|
2423
|
+
record: (_ latencyMs: Int64, _ errorCode: String?, _ result: T?) -> Void,
|
|
2424
|
+
_ block: () async throws -> T
|
|
2425
|
+
) async throws -> T {
|
|
2426
|
+
let startedAt = ProcessInfo.processInfo.systemUptime
|
|
2427
|
+
func elapsedMs() -> Int64 {
|
|
2428
|
+
Int64(((ProcessInfo.processInfo.systemUptime - startedAt) * 1000).rounded(.down))
|
|
2429
|
+
}
|
|
2430
|
+
let result: T
|
|
2431
|
+
do {
|
|
2432
|
+
result = try await block()
|
|
2433
|
+
} catch {
|
|
2434
|
+
let sdkError = error as? BluetoothSdkError
|
|
2435
|
+
let cancelled = error is CancellationError || sdkError?.code == "request_cancelled"
|
|
2436
|
+
if analytics.isEnabled, !cancelled {
|
|
2437
|
+
record(elapsedMs(), sdkError?.code ?? String(describing: type(of: error)), nil)
|
|
2438
|
+
}
|
|
2439
|
+
throw error
|
|
2440
|
+
}
|
|
2441
|
+
if analytics.isEnabled { record(elapsedMs(), nil, result) }
|
|
2442
|
+
return result
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2252
2445
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2253
2446
|
switch ObservableStore.normalizeCategory(category) {
|
|
2254
2447
|
case "glasses":
|
|
2255
2448
|
if changes["connected"] as? Bool == false {
|
|
2256
2449
|
resetWifiProtocolSession(sessionId: "", code: "wifi_session_disconnected")
|
|
2257
2450
|
}
|
|
2258
|
-
analytics.
|
|
2451
|
+
// CBCentralManager state is read here on the main actor, not on the analytics queue.
|
|
2452
|
+
let phoneBluetoothOff = BluetoothAvailability.shared.isPoweredOff
|
|
2453
|
+
analytics.observeGlassesStatus(glassesStatus) { phoneBluetoothOff }
|
|
2259
2454
|
let nextState = state
|
|
2260
2455
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2261
2456
|
delegate?.mentraBluetoothSDK(self, didUpdateGlasses: nextState.glasses)
|
|
2262
2457
|
case ObservableStore.bluetoothCategory:
|
|
2458
|
+
if analytics.isEnabled, changes.keys.contains(where: { Self.micStateKeys.contains($0) }) {
|
|
2459
|
+
let bluetooth = bluetoothStatus
|
|
2460
|
+
let transcribing = bluetooth.shouldSendTranscript || bluetooth.localSttFallbackActive
|
|
2461
|
+
let now = BluetoothSdkAnalytics.epochMillis()
|
|
2462
|
+
analytics.recordUsage { $0.observeMic(currentMic: bluetooth.currentMic, transcribing: transcribing, nowMs: now) }
|
|
2463
|
+
}
|
|
2263
2464
|
let nextState = state
|
|
2264
2465
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2265
2466
|
delegate?.mentraBluetoothSDK(self, didUpdateSdkState: nextState.sdk)
|
|
@@ -2361,17 +2562,48 @@ public final class MentraBluetoothSDK {
|
|
|
2361
2562
|
|
|
2362
2563
|
private func handleVersionInfoForRequest(_ data: [String: Any]) {
|
|
2363
2564
|
guard let request = pendingVersionInfo else { return }
|
|
2364
|
-
|
|
2565
|
+
let outcome = request.accumulator.accept(data)
|
|
2566
|
+
var response: [String: Any] = [
|
|
2567
|
+
"responseRequestId": String((data[VersionInfoResponseAccumulator.responseRequestIdKey] as? String ?? "").prefix(128)),
|
|
2568
|
+
"responseChunk": String((data[VersionInfoResponseAccumulator.responseChunkKey] as? String ?? "").prefix(64)),
|
|
2569
|
+
]
|
|
2570
|
+
for key in [VersionInfoResponseAccumulator.responseIndexKey,
|
|
2571
|
+
VersionInfoResponseAccumulator.responseCountKey,
|
|
2572
|
+
VersionInfoResponseAccumulator.responseFinalKey]
|
|
2573
|
+
{
|
|
2574
|
+
// Typed strings survive the logger's Bool/NSNumber sanitization and
|
|
2575
|
+
// preserve malformed metadata rather than making it look valid.
|
|
2576
|
+
if let value = data[key] as? NSNumber {
|
|
2577
|
+
response[key] = CFGetTypeID(value) == CFBooleanGetTypeID()
|
|
2578
|
+
? "boolean:\(value.boolValue)" : "number:\(value.stringValue)"
|
|
2579
|
+
} else if let value = data[key] as? String {
|
|
2580
|
+
response[key] = "string:\(value.prefix(64))"
|
|
2581
|
+
} else if let value = data[key] {
|
|
2582
|
+
response[key] = "unsupported:\(type(of: value))"
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
switch outcome {
|
|
2365
2586
|
case .ignored:
|
|
2366
|
-
|
|
2587
|
+
logVersionInfoRequest(request, stage: "response-ignored", extra: response)
|
|
2367
2588
|
case .waiting:
|
|
2368
|
-
|
|
2589
|
+
logVersionInfoRequest(request, stage: "response-waiting", extra: response)
|
|
2369
2590
|
case let .complete(result):
|
|
2591
|
+
logVersionInfoRequest(request, stage: "response-complete", extra: response)
|
|
2370
2592
|
pendingVersionInfo = nil
|
|
2371
2593
|
request.pending.resolve(result)
|
|
2372
2594
|
}
|
|
2373
2595
|
}
|
|
2374
2596
|
|
|
2597
|
+
private func logVersionInfoRequest(_ request: PendingVersionInfoRequest, stage: String, extra: [String: Any] = [:]) {
|
|
2598
|
+
var payload: [String: Any] = [
|
|
2599
|
+
"requestId": request.requestId,
|
|
2600
|
+
"stage": stage,
|
|
2601
|
+
"elapsedMs": Int((ProcessInfo.processInfo.systemUptime - request.startedAt) * 1000),
|
|
2602
|
+
]
|
|
2603
|
+
payload.merge(extra) { _, value in value }
|
|
2604
|
+
BleTraceLogger.logMap(direction: "phone_app", layer: "sdk_version_request", type: "version_info", payload: payload)
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2375
2607
|
private func dispatchBridgeEvent(_ eventName: String, _ data: [String: Any]) {
|
|
2376
2608
|
switch eventName {
|
|
2377
2609
|
case "log":
|
|
@@ -2469,6 +2701,11 @@ public final class MentraBluetoothSDK {
|
|
|
2469
2701
|
case "photo_response":
|
|
2470
2702
|
let event = PhotoResponseEvent(values: data)
|
|
2471
2703
|
handlePhotoResponseForRequests(event)
|
|
2704
|
+
if case let .error(requestId, errorCode, _, _) = event.response,
|
|
2705
|
+
requestId == BluetoothSdkUsageTracker.localPhotoRequestId
|
|
2706
|
+
{
|
|
2707
|
+
analytics.recordUsage { $0.buttonPhotoFailed(errorCode: errorCode) }
|
|
2708
|
+
}
|
|
2472
2709
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
2473
2710
|
case "photo_status":
|
|
2474
2711
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
@@ -2479,6 +2716,10 @@ public final class MentraBluetoothSDK {
|
|
|
2479
2716
|
case "video_recording_status":
|
|
2480
2717
|
let event = VideoRecordingStatusEvent(values: data)
|
|
2481
2718
|
handleVideoRecordingStatusForRequests(event)
|
|
2719
|
+
let now = BluetoothSdkAnalytics.epochMillis()
|
|
2720
|
+
analytics.recordUsage {
|
|
2721
|
+
$0.videoStatus(requestId: event.requestId, status: event.status, success: event.success, nowMs: now)
|
|
2722
|
+
}
|
|
2482
2723
|
delegate?.mentraBluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
2483
2724
|
case "media_success", "media_error":
|
|
2484
2725
|
let event = MediaUploadEvent(values: data)
|
|
@@ -2494,6 +2735,7 @@ public final class MentraBluetoothSDK {
|
|
|
2494
2735
|
if data["revision"] != nil && !streamSession.accept(data) { return }
|
|
2495
2736
|
let event = StreamStatusEvent(values: data)
|
|
2496
2737
|
handleStreamStatusForRequests(event)
|
|
2738
|
+
recordStreamStatus(event)
|
|
2497
2739
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
2498
2740
|
case "keep_alive_ack":
|
|
2499
2741
|
let event = KeepAliveAckEvent(values: data)
|
|
@@ -2514,6 +2756,23 @@ public final class MentraBluetoothSDK {
|
|
|
2514
2756
|
BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
|
|
2515
2757
|
)
|
|
2516
2758
|
}
|
|
2759
|
+
if analytics.isEnabled {
|
|
2760
|
+
let glasses = glassesStatus
|
|
2761
|
+
let now = BluetoothSdkAnalytics.epochMillis()
|
|
2762
|
+
analytics.recordUsage {
|
|
2763
|
+
$0.otaStatus(
|
|
2764
|
+
sessionId: event.sessionId,
|
|
2765
|
+
status: event.status,
|
|
2766
|
+
stepType: event.stepType,
|
|
2767
|
+
totalSteps: event.totalSteps,
|
|
2768
|
+
errorMessage: event.errorMessage,
|
|
2769
|
+
fromBuildNumber: glasses.buildNumber,
|
|
2770
|
+
fromBesFirmwareVersion: glasses.besFirmwareVersion,
|
|
2771
|
+
fromMtkFirmwareVersion: glasses.mtkFirmwareVersion,
|
|
2772
|
+
nowMs: now
|
|
2773
|
+
)
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2517
2776
|
delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
|
|
2518
2777
|
case "settings_ack":
|
|
2519
2778
|
let event = SettingsAckEvent(values: data)
|