@mentra/bluetooth-sdk 3.3.0-dev.822 → 3.3.0-dev.826
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/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/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +1 -0
- 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/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +223 -36
- 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/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/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/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +247 -48
- 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/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +1 -0
- package/src/_private/BluetoothSdkModule.ts +2 -1
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -166,13 +166,18 @@ private final class PendingStreamStart {
|
|
|
166
166
|
@MainActor
|
|
167
167
|
final class PendingResponse<T> {
|
|
168
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)?
|
|
169
172
|
private var continuation: CheckedContinuation<T, Error>?
|
|
170
173
|
private var timeoutTask: Task<Void, Never>?
|
|
171
174
|
private var result: Result<T, Error>?
|
|
172
175
|
private var completed = false
|
|
173
176
|
|
|
174
|
-
init(operation: String) {
|
|
177
|
+
init(operation: String, telemetryOperation: String? = nil, onTimeout: ((String) -> Void)? = nil) {
|
|
175
178
|
self.operation = operation
|
|
179
|
+
self.telemetryOperation = telemetryOperation ?? operation
|
|
180
|
+
self.onTimeout = onTimeout
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
func resolve(_ value: T) {
|
|
@@ -210,10 +215,12 @@ final class PendingResponse<T> {
|
|
|
210
215
|
} catch {
|
|
211
216
|
return
|
|
212
217
|
}
|
|
213
|
-
self
|
|
218
|
+
guard let self, !self.completed else { return }
|
|
219
|
+
self.onTimeout?(self.telemetryOperation)
|
|
220
|
+
self.reject(
|
|
214
221
|
BluetoothSdkError(
|
|
215
222
|
code: "request_timeout",
|
|
216
|
-
message: "\(self
|
|
223
|
+
message: "\(self.operation) timed out waiting for glasses response."
|
|
217
224
|
)
|
|
218
225
|
)
|
|
219
226
|
}
|
|
@@ -257,6 +264,7 @@ public final class MentraBluetoothSDK {
|
|
|
257
264
|
private static let otaBesVersionWaitMs = 5000
|
|
258
265
|
private static let otaMtkVersionWaitMs = 2000
|
|
259
266
|
private static let otaVersionPollMs = 100
|
|
267
|
+
private static let micStateKeys: Set<String> = ["currentMic", "should_send_transcript", "local_stt_fallback_active"]
|
|
260
268
|
|
|
261
269
|
public weak var delegate: MentraBluetoothSDKDelegate?
|
|
262
270
|
|
|
@@ -275,6 +283,7 @@ public final class MentraBluetoothSDK {
|
|
|
275
283
|
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
276
284
|
private let streamSession = StreamSessionState()
|
|
277
285
|
private let analytics: BluetoothSdkAnalytics
|
|
286
|
+
private var updateCheck: BluetoothSdkUpdateCheck?
|
|
278
287
|
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
279
288
|
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
280
289
|
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
@@ -353,6 +362,31 @@ public final class MentraBluetoothSDK {
|
|
|
353
362
|
}
|
|
354
363
|
analytics.initializeGlassesStatus(glassesStatus)
|
|
355
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
|
+
}
|
|
356
390
|
}
|
|
357
391
|
|
|
358
392
|
public var state: MentraBluetoothState {
|
|
@@ -548,6 +582,7 @@ public final class MentraBluetoothSDK {
|
|
|
548
582
|
}
|
|
549
583
|
|
|
550
584
|
public func cancelConnectionAttempt() {
|
|
585
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectCancel)
|
|
551
586
|
clearBluetoothRestoreIntent()
|
|
552
587
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
553
588
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -561,6 +596,7 @@ public final class MentraBluetoothSDK {
|
|
|
561
596
|
}
|
|
562
597
|
|
|
563
598
|
public func disconnect() {
|
|
599
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectUser)
|
|
564
600
|
clearBluetoothRestoreIntent()
|
|
565
601
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
566
602
|
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
@@ -569,6 +605,7 @@ public final class MentraBluetoothSDK {
|
|
|
569
605
|
}
|
|
570
606
|
|
|
571
607
|
public func forget() {
|
|
608
|
+
analytics.noteDisconnectRequested(BluetoothSdkAnalyticsTracker.disconnectForget)
|
|
572
609
|
clearBluetoothRestoreIntent()
|
|
573
610
|
DeviceManager.shared.forget()
|
|
574
611
|
}
|
|
@@ -681,7 +718,7 @@ public final class MentraBluetoothSDK {
|
|
|
681
718
|
send: (String) throws -> Void
|
|
682
719
|
) async throws -> SettingsAckEvent {
|
|
683
720
|
let requestId = "settings-\(setting)-\(UUID().uuidString)"
|
|
684
|
-
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)")
|
|
721
|
+
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)", onTimeout: requestTimeoutReporter)
|
|
685
722
|
pendingSettingsRequests[requestId] = pending
|
|
686
723
|
do {
|
|
687
724
|
try send(requestId)
|
|
@@ -954,7 +991,7 @@ public final class MentraBluetoothSDK {
|
|
|
954
991
|
// the scan screen auto-starts a scan on mount and can be pushed twice.
|
|
955
992
|
return try await existing.value
|
|
956
993
|
}
|
|
957
|
-
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
994
|
+
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request", onTimeout: requestTimeoutReporter)
|
|
958
995
|
let request = PendingWifiScan(pending: pending, scanId: "scan-\(UUID().uuidString)")
|
|
959
996
|
pendingWifiScan = request
|
|
960
997
|
// Claim the scan-results store for this scan before the command goes out,
|
|
@@ -994,11 +1031,15 @@ public final class MentraBluetoothSDK {
|
|
|
994
1031
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
995
1032
|
)
|
|
996
1033
|
}
|
|
997
|
-
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
1034
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request", onTimeout: requestTimeoutReporter)
|
|
998
1035
|
pendingWifiStatus = PendingWifiStatusRequest(ssid: ssid, pending: pending)
|
|
999
|
-
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
1000
1036
|
do {
|
|
1001
|
-
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
|
+
}
|
|
1002
1043
|
if pendingWifiStatus?.pending === pending {
|
|
1003
1044
|
pendingWifiStatus = nil
|
|
1004
1045
|
}
|
|
@@ -1021,7 +1062,7 @@ public final class MentraBluetoothSDK {
|
|
|
1021
1062
|
message: "A WiFi status command is already waiting for a glasses response."
|
|
1022
1063
|
)
|
|
1023
1064
|
}
|
|
1024
|
-
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request")
|
|
1065
|
+
let pending = PendingResponse<WifiForgetResult>(operation: "WiFi forget request", onTimeout: requestTimeoutReporter)
|
|
1025
1066
|
let requestId = "forget-\(UUID().uuidString)"
|
|
1026
1067
|
let request = PendingWifiForgetRequest(
|
|
1027
1068
|
ssid: ssid,
|
|
@@ -1066,7 +1107,7 @@ public final class MentraBluetoothSDK {
|
|
|
1066
1107
|
error: "saved_wifi_networks_unsupported"
|
|
1067
1108
|
)
|
|
1068
1109
|
}
|
|
1069
|
-
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request")
|
|
1110
|
+
let pending = PendingResponse<SavedWifiNetworksResult>(operation: "Saved WiFi networks request", onTimeout: requestTimeoutReporter)
|
|
1070
1111
|
let request = PendingSavedWifiNetworks(
|
|
1071
1112
|
requestId: requestId,
|
|
1072
1113
|
sid: wifiSessionCapabilities.sessionId,
|
|
@@ -1103,12 +1144,18 @@ public final class MentraBluetoothSDK {
|
|
|
1103
1144
|
_ = try? await previous.value
|
|
1104
1145
|
}
|
|
1105
1146
|
let pending = PendingResponse<HotspotStatusEvent>(
|
|
1106
|
-
operation: "hotspot \(enabled ? "enable" : "disable") request"
|
|
1147
|
+
operation: "hotspot \(enabled ? "enable" : "disable") request",
|
|
1148
|
+
onTimeout: self.requestTimeoutReporter
|
|
1107
1149
|
)
|
|
1108
1150
|
self.pendingHotspotStatus = PendingHotspotStatusRequest(enabled: enabled, pending: pending)
|
|
1109
|
-
DeviceManager.shared.setHotspotState(enabled)
|
|
1110
1151
|
do {
|
|
1111
|
-
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
|
+
}
|
|
1112
1159
|
if self.pendingHotspotStatus?.pending === pending {
|
|
1113
1160
|
self.pendingHotspotStatus = nil
|
|
1114
1161
|
}
|
|
@@ -1141,13 +1188,30 @@ public final class MentraBluetoothSDK {
|
|
|
1141
1188
|
Bridge.log(
|
|
1142
1189
|
"NATIVE: PHOTO PIPELINE [3b/6] MentraBluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
1143
1190
|
)
|
|
1144
|
-
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
1191
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)", telemetryOperation: "photo request", onTimeout: requestTimeoutReporter)
|
|
1145
1192
|
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
1146
|
-
DeviceManager.shared.requestPhoto(routedRequest)
|
|
1147
1193
|
do {
|
|
1148
|
-
let event = try await
|
|
1149
|
-
?
|
|
1150
|
-
|
|
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
|
+
}
|
|
1151
1215
|
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
1152
1216
|
return event
|
|
1153
1217
|
} catch {
|
|
@@ -1166,7 +1230,7 @@ public final class MentraBluetoothSDK {
|
|
|
1166
1230
|
mfnr: Bool? = nil
|
|
1167
1231
|
) async throws -> CameraStatusEvent {
|
|
1168
1232
|
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
1169
|
-
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)
|
|
1170
1234
|
pendingCameraStatusRequests[effectiveRequestId] = pending
|
|
1171
1235
|
do {
|
|
1172
1236
|
// Inside the catch so an unsupported-device throw also clears the pending entry.
|
|
@@ -1204,7 +1268,7 @@ public final class MentraBluetoothSDK {
|
|
|
1204
1268
|
message: "A gallery status query is already waiting for a glasses response."
|
|
1205
1269
|
)
|
|
1206
1270
|
}
|
|
1207
|
-
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query")
|
|
1271
|
+
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query", onTimeout: requestTimeoutReporter)
|
|
1208
1272
|
pendingGalleryStatus = pending
|
|
1209
1273
|
DeviceManager.shared.queryGalleryStatus()
|
|
1210
1274
|
do {
|
|
@@ -1222,33 +1286,53 @@ public final class MentraBluetoothSDK {
|
|
|
1222
1286
|
}
|
|
1223
1287
|
|
|
1224
1288
|
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1225
|
-
try requireGlassesConnected(operation: "start stream")
|
|
1226
|
-
guard streamSession.supported else {
|
|
1227
|
-
throw BluetoothSdkError(code: "stream_control_unsupported", message: "Update the glasses software before starting a stream.")
|
|
1228
|
-
}
|
|
1229
1289
|
var values = request.values
|
|
1230
1290
|
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
1231
1291
|
values["streamId"] = streamId
|
|
1232
|
-
let
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
+
}
|
|
1247
1331
|
}
|
|
1248
1332
|
}
|
|
1249
1333
|
|
|
1250
1334
|
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
1251
|
-
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)
|
|
1252
1336
|
pendingRgbLedRequests[request.requestId] = pending
|
|
1253
1337
|
DeviceManager.shared.rgbLedControl(
|
|
1254
1338
|
requestId: request.requestId,
|
|
@@ -1270,13 +1354,14 @@ public final class MentraBluetoothSDK {
|
|
|
1270
1354
|
}
|
|
1271
1355
|
|
|
1272
1356
|
public func stopStream() async throws -> StreamStatusEvent {
|
|
1357
|
+
analytics.updateUsage { $0.streamStopRequested() }
|
|
1273
1358
|
guard pendingStreamStop == nil else {
|
|
1274
1359
|
throw BluetoothSdkError(
|
|
1275
1360
|
code: "request_in_flight",
|
|
1276
1361
|
message: "A stream stop command is already waiting for a glasses response."
|
|
1277
1362
|
)
|
|
1278
1363
|
}
|
|
1279
|
-
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream")
|
|
1364
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream", onTimeout: requestTimeoutReporter)
|
|
1280
1365
|
// The seq draw and the BLE hand-off stay in this single await-free
|
|
1281
1366
|
// MainActor turn, sharing startStream's ordering critical section:
|
|
1282
1367
|
// the stop takes its own slot in the send order, so its ack can
|
|
@@ -1305,7 +1390,9 @@ public final class MentraBluetoothSDK {
|
|
|
1305
1390
|
}
|
|
1306
1391
|
try requireGlassesConnected(operation: "start video recording")
|
|
1307
1392
|
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
1308
|
-
operation: "start video recording \(request.requestId)"
|
|
1393
|
+
operation: "start video recording \(request.requestId)",
|
|
1394
|
+
telemetryOperation: "start video recording",
|
|
1395
|
+
onTimeout: requestTimeoutReporter
|
|
1309
1396
|
)
|
|
1310
1397
|
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
1311
1398
|
throw BluetoothSdkError(
|
|
@@ -1317,6 +1404,16 @@ public final class MentraBluetoothSDK {
|
|
|
1317
1404
|
expectedStatus: "recording_started",
|
|
1318
1405
|
pending: pending
|
|
1319
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
|
+
}
|
|
1320
1417
|
DeviceManager.shared.startVideoRecording(
|
|
1321
1418
|
request.requestId,
|
|
1322
1419
|
request.save,
|
|
@@ -1343,7 +1440,7 @@ public final class MentraBluetoothSDK {
|
|
|
1343
1440
|
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
1344
1441
|
}
|
|
1345
1442
|
try requireGlassesConnected(operation: "stop video recording")
|
|
1346
|
-
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)
|
|
1347
1444
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1348
1445
|
throw BluetoothSdkError(
|
|
1349
1446
|
code: "request_in_flight",
|
|
@@ -1356,6 +1453,7 @@ public final class MentraBluetoothSDK {
|
|
|
1356
1453
|
pending: pending,
|
|
1357
1454
|
waitForUpload: waitForUpload
|
|
1358
1455
|
)
|
|
1456
|
+
analytics.updateUsage { $0.videoStopRequested(requestId: requestId) }
|
|
1359
1457
|
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1360
1458
|
do {
|
|
1361
1459
|
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
@@ -1376,7 +1474,7 @@ public final class MentraBluetoothSDK {
|
|
|
1376
1474
|
)
|
|
1377
1475
|
}
|
|
1378
1476
|
try requireGlassesConnected(operation: "query video recording status")
|
|
1379
|
-
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)
|
|
1380
1478
|
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1381
1479
|
throw BluetoothSdkError(
|
|
1382
1480
|
code: "request_in_flight",
|
|
@@ -1407,7 +1505,7 @@ public final class MentraBluetoothSDK {
|
|
|
1407
1505
|
)
|
|
1408
1506
|
}
|
|
1409
1507
|
let requestId = UUID().uuidString
|
|
1410
|
-
let pending = PendingResponse<VersionInfoResult>(operation: "version info request")
|
|
1508
|
+
let pending = PendingResponse<VersionInfoResult>(operation: "version info request", onTimeout: requestTimeoutReporter)
|
|
1411
1509
|
let request = PendingVersionInfoRequest(pending: pending, requestId: requestId)
|
|
1412
1510
|
pendingVersionInfo = request
|
|
1413
1511
|
logVersionInfoRequest(request, stage: "registered")
|
|
@@ -1505,7 +1603,7 @@ public final class MentraBluetoothSDK {
|
|
|
1505
1603
|
message: "An OTA status query is already waiting for a glasses response."
|
|
1506
1604
|
)
|
|
1507
1605
|
}
|
|
1508
|
-
let pending = PendingResponse<OtaQueryResult>(operation: operation)
|
|
1606
|
+
let pending = PendingResponse<OtaQueryResult>(operation: operation, onTimeout: requestTimeoutReporter)
|
|
1509
1607
|
pendingOtaQuery = pending
|
|
1510
1608
|
sendRequest()
|
|
1511
1609
|
do {
|
|
@@ -1536,7 +1634,7 @@ public final class MentraBluetoothSDK {
|
|
|
1536
1634
|
message: "An OTA start command is already waiting for a glasses response."
|
|
1537
1635
|
)
|
|
1538
1636
|
}
|
|
1539
|
-
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
1637
|
+
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command", onTimeout: requestTimeoutReporter)
|
|
1540
1638
|
pendingOtaStart = pending
|
|
1541
1639
|
DeviceManager.shared.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
1542
1640
|
do {
|
|
@@ -1685,6 +1783,7 @@ public final class MentraBluetoothSDK {
|
|
|
1685
1783
|
DeviceStore.shared.store.removeListener(storeListenerId)
|
|
1686
1784
|
self.storeListenerId = nil
|
|
1687
1785
|
}
|
|
1786
|
+
updateCheck?.stop()
|
|
1688
1787
|
delegate = nil
|
|
1689
1788
|
}
|
|
1690
1789
|
|
|
@@ -2278,17 +2377,90 @@ public final class MentraBluetoothSDK {
|
|
|
2278
2377
|
)
|
|
2279
2378
|
}
|
|
2280
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
|
+
|
|
2281
2445
|
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2282
2446
|
switch ObservableStore.normalizeCategory(category) {
|
|
2283
2447
|
case "glasses":
|
|
2284
2448
|
if changes["connected"] as? Bool == false {
|
|
2285
2449
|
resetWifiProtocolSession(sessionId: "", code: "wifi_session_disconnected")
|
|
2286
2450
|
}
|
|
2287
|
-
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 }
|
|
2288
2454
|
let nextState = state
|
|
2289
2455
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2290
2456
|
delegate?.mentraBluetoothSDK(self, didUpdateGlasses: nextState.glasses)
|
|
2291
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
|
+
}
|
|
2292
2464
|
let nextState = state
|
|
2293
2465
|
delegate?.mentraBluetoothSDK(self, didUpdate: nextState)
|
|
2294
2466
|
delegate?.mentraBluetoothSDK(self, didUpdateSdkState: nextState.sdk)
|
|
@@ -2529,6 +2701,11 @@ public final class MentraBluetoothSDK {
|
|
|
2529
2701
|
case "photo_response":
|
|
2530
2702
|
let event = PhotoResponseEvent(values: data)
|
|
2531
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
|
+
}
|
|
2532
2709
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoResponse(event))
|
|
2533
2710
|
case "photo_status":
|
|
2534
2711
|
delegate?.mentraBluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
@@ -2539,6 +2716,10 @@ public final class MentraBluetoothSDK {
|
|
|
2539
2716
|
case "video_recording_status":
|
|
2540
2717
|
let event = VideoRecordingStatusEvent(values: data)
|
|
2541
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
|
+
}
|
|
2542
2723
|
delegate?.mentraBluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
2543
2724
|
case "media_success", "media_error":
|
|
2544
2725
|
let event = MediaUploadEvent(values: data)
|
|
@@ -2554,6 +2735,7 @@ public final class MentraBluetoothSDK {
|
|
|
2554
2735
|
if data["revision"] != nil && !streamSession.accept(data) { return }
|
|
2555
2736
|
let event = StreamStatusEvent(values: data)
|
|
2556
2737
|
handleStreamStatusForRequests(event)
|
|
2738
|
+
recordStreamStatus(event)
|
|
2557
2739
|
delegate?.mentraBluetoothSDK(self, didReceive: .streamStatus(event))
|
|
2558
2740
|
case "keep_alive_ack":
|
|
2559
2741
|
let event = KeepAliveAckEvent(values: data)
|
|
@@ -2574,6 +2756,23 @@ public final class MentraBluetoothSDK {
|
|
|
2574
2756
|
BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
|
|
2575
2757
|
)
|
|
2576
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
|
+
}
|
|
2577
2776
|
delegate?.mentraBluetoothSDK(self, didReceive: .otaStatus(event))
|
|
2578
2777
|
case "settings_ack":
|
|
2579
2778
|
let event = SettingsAckEvent(values: data)
|
|
@@ -41,6 +41,12 @@ final class BluetoothAvailability: NSObject, CBCentralManagerDelegate {
|
|
|
41
41
|
listeners[id] = nil
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/// Whether phone Bluetooth is switched off right now. Read on the main thread,
|
|
45
|
+
/// where the central manager delivers its state.
|
|
46
|
+
var isPoweredOff: Bool {
|
|
47
|
+
(centralManager?.state ?? state) == .poweredOff
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
/// Read-only system lookup. It neither connects nor claims ownership.
|
|
45
51
|
func connectedPeripherals(withServices services: [CBUUID]) -> [CBPeripheral] {
|
|
46
52
|
dispatchPrecondition(condition: .onQueue(.main))
|