@mentra/bluetooth-sdk 3.2.1 → 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 +68 -6
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- 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 +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +2 -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/DeviceManager.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +223 -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/debug/BleTraceLogger.kt +3 -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/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +93 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +3 -1
- 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/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +20 -7
- package/build/BluetoothSdk.types.d.ts +3 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +2 -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 +4 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +5 -1
- 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/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 +8 -4
- package/ios/Source/BluetoothSdkDefaults.swift +2 -2
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +295 -54
- 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/sgcs/MentraLive.swift +71 -41
- package/ios/Source/sgcs/NimoCanvasCodec.swift +4 -1
- package/ios/Source/sgcs/SGCManager.swift +1 -1
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -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/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +17 -4
- package/ios/Tests/VersionInfoCommandTests.swift +142 -1
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +2 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +5 -1
- package/src/generated/releaseMetadata.ts +6 -6
- 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] = [:]
|
|
@@ -350,6 +362,31 @@ public final class MentraBluetoothSDK {
|
|
|
350
362
|
}
|
|
351
363
|
analytics.initializeGlassesStatus(glassesStatus)
|
|
352
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
|
+
}
|
|
353
390
|
}
|
|
354
391
|
|
|
355
392
|
public var state: MentraBluetoothState {
|
|
@@ -545,6 +582,7 @@ public final class MentraBluetoothSDK {
|
|
|
545
582
|
}
|
|
546
583
|
|
|
547
584
|
public func cancelConnectionAttempt() {
|
|
585
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectCancel)
|
|
548
586
|
clearBluetoothRestoreIntent()
|
|
549
587
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
550
588
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -558,6 +596,7 @@ public final class MentraBluetoothSDK {
|
|
|
558
596
|
}
|
|
559
597
|
|
|
560
598
|
public func disconnect() {
|
|
599
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectUser)
|
|
561
600
|
clearBluetoothRestoreIntent()
|
|
562
601
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
563
602
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -566,6 +605,7 @@ public final class MentraBluetoothSDK {
|
|
|
566
605
|
}
|
|
567
606
|
|
|
568
607
|
public func forget() {
|
|
608
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectForget)
|
|
569
609
|
clearBluetoothRestoreIntent()
|
|
570
610
|
DeviceManager.shared.forget()
|
|
571
611
|
}
|
|
@@ -678,7 +718,7 @@ public final class MentraBluetoothSDK {
|
|
|
678
718
|
send: (String) throws -> Void
|
|
679
719
|
) async throws -> SettingsAckEvent {
|
|
680
720
|
let requestId = "settings-\(setting)-\(UUID().uuidString)"
|
|
681
|
-
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)")
|
|
721
|
+
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)", onTimeout: requestTimeoutReporter)
|
|
682
722
|
pendingSettingsRequests[requestId] = pending
|
|
683
723
|
do {
|
|
684
724
|
try send(requestId)
|
|
@@ -951,7 +991,7 @@ public final class MentraBluetoothSDK {
|
|
|
951
991
|
// the scan screen auto-starts a scan on mount and can be pushed twice.
|
|
952
992
|
return try await existing.value
|
|
953
993
|
}
|
|
954
|
-
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
994
|
+
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request", onTimeout: requestTimeoutReporter)
|
|
955
995
|
let request = PendingWifiScan(pending: pending, scanId: "scan-\(UUID().uuidString)")
|
|
956
996
|
pendingWifiScan = request
|
|
957
997
|
// Claim the scan-results store for this scan before the command goes out,
|
|
@@ -991,11 +1031,15 @@ public final class MentraBluetoothSDK {
|
|
|
991
1031
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
992
1032
|
)
|
|
993
1033
|
}
|
|
994
|
-
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
1034
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request", onTimeout: requestTimeoutReporter)
|
|
995
1035
|
pendingWifiStatus = PendingWifiStatusRequest(ssid: ssid, pending: pending)
|
|
996
|
-
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
997
1036
|
do {
|
|
998
|
-
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
|
+
}
|
|
999
1043
|
if pendingWifiStatus?.pending === pending {
|
|
1000
1044
|
pendingWifiStatus = nil
|
|
1001
1045
|
}
|
|
@@ -1018,7 +1062,7 @@ public final class MentraBluetoothSDK {
|
|
|
1018
1062
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
1019
1063
|
)
|
|
1020
1064
|
}
|
|
1021
|
-
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request")
|
|
1065
|
+
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request", onTimeout: requestTimeoutReporter)
|
|
1022
1066
|
let requestId = "forget-\(UUID().uuidString)"
|
|
1023
1067
|
let request = PendingWifiForgetRequest(
|
|
1024
1068
|
ssid: ssid,
|
|
@@ -1063,7 +1107,7 @@ public final class MentraBluetoothSDK {
|
|
|
1063
1107
|
error: "saved_wifi_networks_unsupported"
|
|
1064
1108
|
)
|
|
1065
1109
|
}
|
|
1066
|
-
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request")
|
|
1110
|
+
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request", onTimeout: requestTimeoutReporter)
|
|
1067
1111
|
let request = PendingSavedWifiNetworks(
|
|
1068
1112
|
requestId: requestId,
|
|
1069
1113
|
sid: wifiSessionCapabilities.sessionId,
|
|
@@ -1100,12 +1144,18 @@ public final class MentraBluetoothSDK {
|
|
|
1100
1144
|
_ = try? await previous.value
|
|
1101
1145
|
}
|
|
1102
1146
|
let pending = PendingResponse<HotspotStatusEvent>(
|
|
1103
|
-
operation: "hotspot \(enabled ? "enable" : "disable") request"
|
|
1147
|
+
operation: "hotspot \(enabled ? "enable" : "disable") request",
|
|
1148
|
+
onTimeout: self.requestTimeoutReporter
|
|
1104
1149
|
)
|
|
1105
1150
|
self.pendingHotspotStatus = PendingHotspotStatusRequest(enabled: enabled, pending: pending)
|
|
1106
|
-
DeviceManager.shared.setHotspotState(enabled)
|
|
1107
1151
|
do {
|
|
1108
|
-
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
|
+
}
|
|
1109
1159
|
if self.pendingHotspotStatus?.pending === pending {
|
|
1110
1160
|
self.pendingHotspotStatus = nil
|
|
1111
1161
|
}
|
|
@@ -1138,13 +1188,30 @@ public final class MentraBluetoothSDK {
|
|
|
1138
1188
|
Bridge.log(
|
|
1139
1189
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
1140
1190
|
)
|
|
1141
|
-
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
1191
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)", telemetryOperation: "photo request", onTimeout: requestTimeoutReporter)
|
|
1142
1192
|
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
1143
|
-
DeviceManager.shared.requestPhoto(routedRequest)
|
|
1144
1193
|
do {
|
|
1145
|
-
let event = try await
|
|
1146
|
-
?
|
|
1147
|
-
|
|
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
|
+
}
|
|
1148
1215
|
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
1149
1216
|
return event
|
|
1150
1217
|
} catch {
|
|
@@ -1163,7 +1230,7 @@ public final class MentraBluetoothSDK {
|
|
|
1163
1230
|
mfnr: Bool? = nil
|
|
1164
1231
|
) async throws -> CameraStatusEvent {
|
|
1165
1232
|
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
1166
|
-
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)
|
|
1167
1234
|
pendingCameraStatusRequests[effectiveRequestId] = pending
|
|
1168
1235
|
do {
|
|
1169
1236
|
// Inside the catch so an unsupported-device throw also clears the pending entry.
|
|
@@ -1201,7 +1268,7 @@ public final class MentraBluetoothSDK {
|
|
|
1201
1268
|
message: "A gallery status query is already waiting for a glasses response."
|
|
1202
1269
|
)
|
|
1203
1270
|
}
|
|
1204
|
-
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query")
|
|
1271
|
+
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query", onTimeout: requestTimeoutReporter)
|
|
1205
1272
|
pendingGalleryStatus = pending
|
|
1206
1273
|
DeviceManager.shared.queryGalleryStatus()
|
|
1207
1274
|
do {
|
|
@@ -1219,33 +1286,53 @@ public final class MentraBluetoothSDK {
|
|
|
1219
1286
|
}
|
|
1220
1287
|
|
|
1221
1288
|
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1222
|
-
try requireGlassesConnected(operation: "start stream")
|
|
1223
|
-
guard streamSession.supported else {
|
|
1224
|
-
throw BluetoothSdkError(code: "stream_control_unsupported", message: "Update the glasses software before starting a stream.")
|
|
1225
|
-
}
|
|
1226
1289
|
var values = request.values
|
|
1227
1290
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
1228
1291
|
values["streamId"] = streamId
|
|
1229
|
-
let
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
+
}
|
|
1244
1331
|
}
|
|
1245
1332
|
}
|
|
1246
1333
|
|
|
1247
1334
|
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
1248
|
-
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)
|
|
1249
1336
|
pendingRgbLedRequests[request.requestId] = pending
|
|
1250
1337
|
DeviceManager.shared.rgbLedControl(
|
|
1251
1338
|
requestId: request.requestId,
|
|
@@ -1267,13 +1354,14 @@ public final class MentraBluetoothSDK {
|
|
|
1267
1354
|
}
|
|
1268
1355
|
|
|
1269
1356
|
public func stopStream() async throws -> StreamStatusEvent {
|
|
1357
|
+
analytics.updateUsage { $0.streamStopRequested() }
|
|
1270
1358
|
guard pendingStreamStop == nil else {
|
|
1271
1359
|
throw BluetoothSdkError(
|
|
1272
1360
|
code: "request_in_flight",
|
|
1273
1361
|
message: "A stream stop command is already waiting for a glasses response."
|
|
1274
1362
|
)
|
|
1275
1363
|
}
|
|
1276
|
-
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream")
|
|
1364
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream", onTimeout: requestTimeoutReporter)
|
|
1277
1365
|
// The seq draw and the BLE hand-off stay in this single await-free
|
|
1278
1366
|
// MainActor turn, sharing startStream's ordering critical section:
|
|
1279
1367
|
// the stop takes its own slot in the send order, so its ack can
|
|
@@ -1302,7 +1390,9 @@ public final class MentraBluetoothSDK {
|
|
|
1302
1390
|
}
|
|
1303
1391
|
try requireGlassesConnected(operation: "start video recording")
|
|
1304
1392
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
1305
|
-
operation: "start video recording \(request.requestId)"
|
|
1393
|
+
operation: "start video recording \(request.requestId)",
|
|
1394
|
+
telemetryOperation: "start video recording",
|
|
1395
|
+
onTimeout: requestTimeoutReporter
|
|
1306
1396
|
)
|
|
1307
1397
|
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
1308
1398
|
throw BluetoothSdkError(
|
|
@@ -1314,6 +1404,16 @@ public final class MentraBluetoothSDK {
|
|
|
1314
1404
|
expectedStatus: "recording_started",
|
|
1315
1405
|
pending: pending
|
|
1316
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
|
+
}
|
|
1317
1417
|
DeviceManager.shared.startVideoRecording(
|
|
1318
1418
|
request.requestId,
|
|
1319
1419
|
request.save,
|
|
@@ -1340,7 +1440,7 @@ public final class MentraBluetoothSDK {
|
|
|
1340
1440
|
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
1341
1441
|
}
|
|
1342
1442
|
try requireGlassesConnected(operation: "stop video recording")
|
|
1343
|
-
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)
|
|
1344
1444
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1345
1445
|
throw BluetoothSdkError(
|
|
1346
1446
|
code: "request_in_flight",
|
|
@@ -1353,6 +1453,7 @@ public final class MentraBluetoothSDK {
|
|
|
1353
1453
|
pending: pending,
|
|
1354
1454
|
waitForUpload: waitForUpload
|
|
1355
1455
|
)
|
|
1456
|
+
analytics.updateUsage { $0.videoStopRequested(requestId: requestId) }
|
|
1356
1457
|
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1357
1458
|
do {
|
|
1358
1459
|
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
@@ -1373,7 +1474,7 @@ public final class MentraBluetoothSDK {
|
|
|
1373
1474
|
)
|
|
1374
1475
|
}
|
|
1375
1476
|
try requireGlassesConnected(operation: "query video recording status")
|
|
1376
|
-
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)
|
|
1377
1478
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1378
1479
|
throw BluetoothSdkError(
|
|
1379
1480
|
code: "request_in_flight",
|
|
@@ -1396,27 +1497,35 @@ public final class MentraBluetoothSDK {
|
|
|
1396
1497
|
}
|
|
1397
1498
|
|
|
1398
1499
|
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
1399
|
-
|
|
1500
|
+
if let request = pendingVersionInfo {
|
|
1501
|
+
logVersionInfoRequest(request, stage: "refused", extra: ["code": "request_in_flight"])
|
|
1400
1502
|
throw BluetoothSdkError(
|
|
1401
1503
|
code: "request_in_flight",
|
|
1402
1504
|
message: "A version info request is already waiting for a glasses response."
|
|
1403
1505
|
)
|
|
1404
1506
|
}
|
|
1405
1507
|
let requestId = UUID().uuidString
|
|
1406
|
-
let pending = PendingResponse<VersionInfoResult>(operation: "version info request")
|
|
1508
|
+
let pending = PendingResponse<VersionInfoResult>(operation: "version info request", onTimeout: requestTimeoutReporter)
|
|
1407
1509
|
let request = PendingVersionInfoRequest(pending: pending, requestId: requestId)
|
|
1408
1510
|
pendingVersionInfo = request
|
|
1511
|
+
logVersionInfoRequest(request, stage: "registered")
|
|
1409
1512
|
DeviceManager.shared.requestVersionInfo(requestId: requestId)
|
|
1410
1513
|
do {
|
|
1411
1514
|
let status = try await pending.wait()
|
|
1412
1515
|
if pendingVersionInfo === request {
|
|
1413
1516
|
pendingVersionInfo = nil
|
|
1414
1517
|
}
|
|
1518
|
+
logVersionInfoRequest(request, stage: "resolved")
|
|
1415
1519
|
return status
|
|
1416
1520
|
} catch {
|
|
1417
1521
|
if pendingVersionInfo === request {
|
|
1418
1522
|
pendingVersionInfo = nil
|
|
1419
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
|
+
])
|
|
1420
1529
|
throw error
|
|
1421
1530
|
}
|
|
1422
1531
|
}
|
|
@@ -1494,7 +1603,7 @@ public final class MentraBluetoothSDK {
|
|
|
1494
1603
|
message: "An OTA status query is already waiting for a glasses response."
|
|
1495
1604
|
)
|
|
1496
1605
|
}
|
|
1497
|
-
let pending = PendingResponse<OtaQueryResult>(operation: operation)
|
|
1606
|
+
let pending = PendingResponse<OtaQueryResult>(operation: operation, onTimeout: requestTimeoutReporter)
|
|
1498
1607
|
pendingOtaQuery = pending
|
|
1499
1608
|
sendRequest()
|
|
1500
1609
|
do {
|
|
@@ -1525,7 +1634,7 @@ public final class MentraBluetoothSDK {
|
|
|
1525
1634
|
message: "An OTA start command is already waiting for a glasses response."
|
|
1526
1635
|
)
|
|
1527
1636
|
}
|
|
1528
|
-
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
1637
|
+
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command", onTimeout: requestTimeoutReporter)
|
|
1529
1638
|
pendingOtaStart = pending
|
|
1530
1639
|
DeviceManager.shared.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
1531
1640
|
do {
|
|
@@ -1656,8 +1765,8 @@ public final class MentraBluetoothSDK {
|
|
|
1656
1765
|
DeviceManager.shared.sendReboot()
|
|
1657
1766
|
}
|
|
1658
1767
|
|
|
1659
|
-
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1660
|
-
DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1768
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) throws {
|
|
1769
|
+
try DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1661
1770
|
}
|
|
1662
1771
|
|
|
1663
1772
|
public func invalidate() {
|
|
@@ -1674,6 +1783,7 @@ public final class MentraBluetoothSDK {
|
|
|
1674
1783
|
DeviceStore.shared.store.removeListener(storeListenerId)
|
|
1675
1784
|
self.storeListenerId = nil
|
|
1676
1785
|
}
|
|
1786
|
+
updateCheck?.stop()
|
|
1677
1787
|
delegate = nil
|
|
1678
1788
|
}
|
|
1679
1789
|
|
|
@@ -2267,17 +2377,90 @@ public final class MentraBluetoothSDK {
|
|
|
2267
2377
|
)
|
|
2268
2378
|
}
|
|
2269
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
|
+
|
|
2270
2445
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2271
2446
|
switch ObservableStore.normalizeCategory(category) {
|
|
2272
2447
|
case "glasses":
|
|
2273
2448
|
if changes["connected"] as? Bool == false {
|
|
2274
2449
|
resetWifiProtocolSession(sessionId: "", code: "wifi_session_disconnected")
|
|
2275
2450
|
}
|
|
2276
|
-
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 }
|
|
2277
2454
|
let nextState = state
|
|
2278
2455
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2279
2456
|
delegate?.mentraBluetoothSDK(self, didUpdateGlasses: nextState.glasses)
|
|
2280
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
|
+
}
|
|
2281
2464
|
let nextState = state
|
|
2282
2465
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2283
2466
|
delegate?.mentraBluetoothSDK(self, didUpdateSdkState: nextState.sdk)
|
|
@@ -2379,17 +2562,48 @@ public final class MentraBluetoothSDK {
|
|
|
2379
2562
|
|
|
2380
2563
|
private func handleVersionInfoForRequest(_ data: [String: Any]) {
|
|
2381
2564
|
guard let request = pendingVersionInfo else { return }
|
|
2382
|
-
|
|
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 {
|
|
2383
2586
|
case .ignored:
|
|
2384
|
-
|
|
2587
|
+
logVersionInfoRequest(request, stage: "response-ignored", extra: response)
|
|
2385
2588
|
case .waiting:
|
|
2386
|
-
|
|
2589
|
+
logVersionInfoRequest(request, stage: "response-waiting", extra: response)
|
|
2387
2590
|
case let .complete(result):
|
|
2591
|
+
logVersionInfoRequest(request, stage: "response-complete", extra: response)
|
|
2388
2592
|
pendingVersionInfo = nil
|
|
2389
2593
|
request.pending.resolve(result)
|
|
2390
2594
|
}
|
|
2391
2595
|
}
|
|
2392
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
|
+
|
|
2393
2607
|
private func dispatchBridgeEvent(_ eventName: String, _ data: [String: Any]) {
|
|
2394
2608
|
switch eventName {
|
|
2395
2609
|
case "log":
|
|
@@ -2487,6 +2701,11 @@ public final class MentraBluetoothSDK {
|
|
|
2487
2701
|
case "photo_response":
|
|
2488
2702
|
let event = PhotoResponseEvent(values: data)
|
|
2489
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
|
+
}
|
|
2490
2709
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
2491
2710
|
case "photo_status":
|
|
2492
2711
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
@@ -2497,6 +2716,10 @@ public final class MentraBluetoothSDK {
|
|
|
2497
2716
|
case "video_recording_status":
|
|
2498
2717
|
let event = VideoRecordingStatusEvent(values: data)
|
|
2499
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
|
+
}
|
|
2500
2723
|
delegate?.mentraBluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
2501
2724
|
case "media_success", "media_error":
|
|
2502
2725
|
let event = MediaUploadEvent(values: data)
|
|
@@ -2512,6 +2735,7 @@ public final class MentraBluetoothSDK {
|
|
|
2512
2735
|
if data["revision"] != nil && !streamSession.accept(data) { return }
|
|
2513
2736
|
let event = StreamStatusEvent(values: data)
|
|
2514
2737
|
handleStreamStatusForRequests(event)
|
|
2738
|
+
recordStreamStatus(event)
|
|
2515
2739
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
2516
2740
|
case "keep_alive_ack":
|
|
2517
2741
|
let event = KeepAliveAckEvent(values: data)
|
|
@@ -2532,6 +2756,23 @@ public final class MentraBluetoothSDK {
|
|
|
2532
2756
|
BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
|
|
2533
2757
|
)
|
|
2534
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
|
+
}
|
|
2535
2776
|
delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
|
|
2536
2777
|
case "settings_ack":
|
|
2537
2778
|
let event = SettingsAckEvent(values: data)
|