@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.
Files changed (191) hide show
  1. package/README.md +72 -10
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  5. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
  15. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  16. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  18. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
  19. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  31. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  32. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
  33. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  34. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  35. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  37. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  43. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  45. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  48. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  50. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  51. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  52. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  53. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  54. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  55. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  56. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  57. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  58. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  59. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  62. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  63. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  65. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  66. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  67. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  68. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  69. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
  70. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  71. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  72. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  73. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  74. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  75. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  76. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  77. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  78. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  79. package/build/BluetoothSdk.types.d.ts +16 -0
  80. package/build/BluetoothSdk.types.d.ts.map +1 -1
  81. package/build/BluetoothSdk.types.js +1 -0
  82. package/build/BluetoothSdk.types.js.map +1 -1
  83. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  84. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  85. package/build/_private/BluetoothSdkModule.js +5 -1
  86. package/build/_private/BluetoothSdkModule.js.map +1 -1
  87. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  88. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  89. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  90. package/build/_private/nativeLogConsole.d.ts +19 -0
  91. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  92. package/build/_private/nativeLogConsole.js +12 -0
  93. package/build/_private/nativeLogConsole.js.map +1 -1
  94. package/build/generated/changelogCatalog.d.ts +5 -2
  95. package/build/generated/changelogCatalog.d.ts.map +1 -1
  96. package/build/generated/changelogCatalog.js +6 -2
  97. package/build/generated/changelogCatalog.js.map +1 -1
  98. package/build/generated/releaseMetadata.js +6 -6
  99. package/build/generated/releaseMetadata.js.map +1 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +1 -0
  102. package/build/index.js.map +1 -1
  103. package/build/ota-transport/index.d.ts +4 -0
  104. package/build/ota-transport/index.d.ts.map +1 -1
  105. package/build/ota-transport/index.js +2 -0
  106. package/build/ota-transport/index.js.map +1 -1
  107. package/ios/BluetoothSdkModule.swift +17 -4
  108. package/ios/MentraBluetoothSDK.podspec +3 -2
  109. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  110. package/ios/Source/BluetoothSdkDefaults.swift +7 -2
  111. package/ios/Source/Bridge.swift +15 -2
  112. package/ios/Source/DeviceManager.swift +102 -66
  113. package/ios/Source/DeviceStore.swift +26 -7
  114. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  115. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  116. package/ios/Source/MentraBluetoothSDK.swift +313 -54
  117. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  118. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  119. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  120. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  121. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  122. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  123. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  124. package/ios/Source/requests/DisplayRequests.swift +1 -1
  125. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  126. package/ios/Source/sgcs/Ar99.swift +1 -2
  127. package/ios/Source/sgcs/G1.swift +68 -38
  128. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  129. package/ios/Source/sgcs/G2.swift +261 -247
  130. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  131. package/ios/Source/sgcs/Mach1.swift +19 -7
  132. package/ios/Source/sgcs/MentraLive.swift +272 -197
  133. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  134. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  135. package/ios/Source/sgcs/MentraNex.swift +8 -31
  136. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  137. package/ios/Source/sgcs/Nimo.swift +280 -408
  138. package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
  139. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  140. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  141. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  142. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  143. package/ios/Source/sgcs/SGCManager.swift +25 -1
  144. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  145. package/ios/Source/status/DeviceStatus.swift +1 -0
  146. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  147. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  148. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  149. package/ios/Source/utils/G1Text.swift +2 -24
  150. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  151. package/ios/Source/utils/G2Text.swift +11 -0
  152. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  153. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  154. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  155. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  156. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  157. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  158. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  159. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  160. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  161. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  162. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  163. package/ios/Tests/G1PairingTests.swift +162 -0
  164. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  165. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  166. package/ios/Tests/G2TextTests.swift +10 -0
  167. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  168. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  169. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  170. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  171. package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
  172. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  173. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  174. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  175. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  176. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  177. package/ios/Tests/VersionInfoCommandTests.swift +194 -0
  178. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  179. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  180. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  181. package/package.json +2 -2
  182. package/plugin/build/withIos.d.ts +2 -0
  183. package/plugin/build/withIos.js +12 -1
  184. package/src/BluetoothSdk.types.ts +15 -0
  185. package/src/_private/BluetoothSdkModule.ts +10 -1
  186. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  187. package/src/_private/nativeLogConsole.ts +19 -0
  188. package/src/generated/changelogCatalog.ts +6 -2
  189. package/src/generated/releaseMetadata.ts +6 -6
  190. package/src/index.ts +1 -0
  191. package/src/ota-transport/index.ts +10 -1
@@ -0,0 +1,296 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ final class BluetoothSdkUsageTrackerTests: XCTestCase {
5
+ private var tracker = BluetoothSdkUsageTracker()
6
+
7
+ func testApiPhotoReportsItsRequestShapeLatencyAndOutcome() {
8
+ let ok = tracker.photoSettled(
9
+ size: "medium", mode: "photo", compress: "none", save: true, transferMethod: "auto",
10
+ latencyMs: 812, fileSizeBytes: 204_800, errorCode: nil
11
+ )
12
+ XCTAssertEqual(ok.name, "bluetooth_sdk_photo_taken")
13
+ XCTAssertEqual(ok.properties["trigger"] as? String, "api")
14
+ XCTAssertEqual(ok.properties["success"] as? Bool, true)
15
+ XCTAssertEqual(ok.properties["latency_ms"] as? Int64, 812)
16
+ XCTAssertEqual(ok.properties["photo_size"] as? String, "medium")
17
+ XCTAssertEqual(ok.properties["save_to_gallery"] as? Bool, true)
18
+ XCTAssertEqual(ok.properties["transfer_method"] as? String, "auto")
19
+ XCTAssertEqual(ok.properties["file_size_bytes"] as? Int64, 204_800)
20
+ XCTAssertNil(ok.properties["error_code"])
21
+
22
+ let failed = tracker.photoSettled(
23
+ size: "max", mode: "photo", compress: "none", save: false, transferMethod: "ble",
24
+ latencyMs: 30000, fileSizeBytes: nil, errorCode: "request_timeout"
25
+ )
26
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
27
+ XCTAssertEqual(failed.properties["error_code"] as? String, "request_timeout")
28
+ XCTAssertNil(failed.properties["file_size_bytes"])
29
+ }
30
+
31
+ func testFailedButtonPhotoIsReportedAsButtonTrigger() {
32
+ let event = tracker.buttonPhotoFailed(errorCode: "CAMERA_BUSY")
33
+ XCTAssertEqual(event.properties["trigger"] as? String, "button")
34
+ XCTAssertEqual(event.properties["success"] as? Bool, false)
35
+ XCTAssertEqual(event.properties["error_code"] as? String, "CAMERA_BUSY")
36
+ XCTAssertEqual(tracker.buttonPhotoFailed(errorCode: nil).properties["error_code"] as? String, "photo_request_failed")
37
+ }
38
+
39
+ func testApiRecordingReportsLengthParametersAndAppStopOnce() throws {
40
+ tracker.videoRequested(requestId: "rec-1", width: 1920, height: 1080, fps: 30, save: true, maxDurationMinutes: 0)
41
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_started", success: true, nowMs: 1000))
42
+ // A duplicate start does not move the start time.
43
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_started", success: true, nowMs: 2000))
44
+ tracker.videoStopRequested(requestId: "rec-1")
45
+
46
+ let stopped = try XCTUnwrap(tracker.videoStatus(requestId: "rec-1", status: "recording_stopped", success: true, nowMs: 61000))
47
+ XCTAssertEqual(stopped.name, "bluetooth_sdk_video_recorded")
48
+ XCTAssertEqual(stopped.properties["trigger"] as? String, "api")
49
+ XCTAssertEqual(stopped.properties["success"] as? Bool, true)
50
+ XCTAssertEqual(stopped.properties["duration_ms"] as? Int64, 60000)
51
+ XCTAssertEqual(stopped.properties["width"] as? Int, 1920)
52
+ XCTAssertEqual(stopped.properties["fps"] as? Int, 30)
53
+ XCTAssertEqual(stopped.properties["save_to_gallery"] as? Bool, true)
54
+ XCTAssertEqual(stopped.properties["stopped_by_app"] as? Bool, true)
55
+ XCTAssertNil(stopped.properties["max_duration_minutes"])
56
+
57
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-1", status: "recording_stopped", success: true, nowMs: 62000))
58
+ }
59
+
60
+ func testButtonRecordingIsReportedWithButtonTriggerAndNoAppStop() throws {
61
+ let id = "local_video_1728000000_ab12"
62
+ _ = tracker.videoStatus(requestId: id, status: "recording_started", success: true, nowMs: 0)
63
+ let stopped = try XCTUnwrap(tracker.videoStatus(requestId: id, status: "recording_stopped", success: true, nowMs: 5000))
64
+ XCTAssertEqual(stopped.properties["trigger"] as? String, "button")
65
+ XCTAssertEqual(stopped.properties["duration_ms"] as? Int64, 5000)
66
+ XCTAssertEqual(stopped.properties["stopped_by_app"] as? Bool, false)
67
+ }
68
+
69
+ func testRecordingFailureIsReportedButNotRecordingIsNotARecording() throws {
70
+ tracker.videoRequested(requestId: "rec-2", width: 0, height: 0, fps: 0, save: false, maxDurationMinutes: 5)
71
+ let failed = try XCTUnwrap(tracker.videoStatus(requestId: "rec-2", status: "battery_low", success: false, nowMs: 0))
72
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
73
+ XCTAssertEqual(failed.properties["error_code"] as? String, "battery_low")
74
+ XCTAssertEqual(failed.properties["max_duration_minutes"] as? Int, 5)
75
+ XCTAssertNil(failed.properties["duration_ms"])
76
+ XCTAssertNil(failed.properties["width"])
77
+
78
+ XCTAssertNil(tracker.videoStatus(requestId: "rec-3", status: "not_recording", success: false, nowMs: 0))
79
+ XCTAssertNil(tracker.videoStatus(requestId: "", status: "recording_stopped", success: true, nowMs: 0))
80
+ }
81
+
82
+ func testStreamIsReportedAtStartAndOnceAtItsTerminalStatus() throws {
83
+ XCTAssertNil(tracker.streamRequested(streamId: "s1", streamUrl: "rtmps://live.example.com/app/secret-key", nowMs: 6600))
84
+ // The first live status can arrive before startStream returns.
85
+ XCTAssertNil(stream("streaming", nowMs: 10000))
86
+ let started = tracker.streamStartSettled(
87
+ streamUrl: "rtmps://live.example.com/app/secret-key",
88
+ width: 1280,
89
+ height: 720,
90
+ fps: 30,
91
+ bitrate: 2_000_000,
92
+ captureAudio: true,
93
+ glassesNetwork: "wifi",
94
+ latencyMs: 3400,
95
+ errorCode: nil
96
+ )
97
+ XCTAssertEqual(started.name, "bluetooth_sdk_stream_started")
98
+ XCTAssertEqual(started.properties["success"] as? Bool, true)
99
+ XCTAssertEqual(started.properties["protocol"] as? String, "rtmp")
100
+ XCTAssertEqual(started.properties["glasses_network"] as? String, "wifi")
101
+ XCTAssertFalse(started.properties.values.contains { "\($0)".contains("secret-key") })
102
+
103
+ XCTAssertNil(stream("reconnecting", reconnectAttempt: 1, nowMs: 20000))
104
+ XCTAssertNil(stream("reconnected", reconnectAttempt: 2, nowMs: 21000))
105
+ XCTAssertNil(stream("streaming", bitrate: 1_800_000, droppedFrames: 12, nowMs: 30000))
106
+ // Statuses for another stream id do not touch this one.
107
+ XCTAssertNil(tracker.streamStatus(
108
+ streamId: "other", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
109
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 31000
110
+ ))
111
+ tracker.streamStopRequested()
112
+
113
+ let ended = try XCTUnwrap(stream("stopped", terminal: true, nowMs: 70000))
114
+ XCTAssertEqual(ended.name, "bluetooth_sdk_stream_ended")
115
+ XCTAssertEqual(ended.properties["protocol"] as? String, "rtmp")
116
+ XCTAssertEqual(ended.properties["duration_ms"] as? Int64, 60000)
117
+ XCTAssertEqual(ended.properties["end_state"] as? String, "stopped")
118
+ XCTAssertEqual(ended.properties["stopped_by_app"] as? Bool, true)
119
+ XCTAssertEqual(ended.properties["reconnect_attempts"] as? Int, 2)
120
+ XCTAssertEqual(ended.properties["bitrate"] as? Int64, 1_800_000)
121
+ XCTAssertEqual(ended.properties["dropped_frames"] as? Int64, 12)
122
+
123
+ XCTAssertNil(stream("stopped", terminal: true, nowMs: 71000))
124
+ }
125
+
126
+ func testStreamThatEndsRightAfterGoingLiveIsStillReported() throws {
127
+ _ = tracker.streamRequested(streamId: "s1", streamUrl: "rtmp://host/app", nowMs: 0)
128
+ XCTAssertNil(stream("streaming", nowMs: 1000))
129
+ let ended = try XCTUnwrap(stream("error", terminal: true, nowMs: 1500))
130
+ XCTAssertEqual(ended.properties["duration_ms"] as? Int64, 500)
131
+ XCTAssertEqual(ended.properties["end_state"] as? String, "error")
132
+ }
133
+
134
+ func testNewStartReportsTheLiveStreamItReplaces() throws {
135
+ _ = tracker.streamRequested(streamId: "s1", streamUrl: "rtmp://host/app", nowMs: 0)
136
+ _ = stream("streaming", nowMs: 1000)
137
+ let replaced = try XCTUnwrap(tracker.streamRequested(streamId: "s2", streamUrl: "srt://host:9000", nowMs: 11000))
138
+ XCTAssertEqual(replaced.name, "bluetooth_sdk_stream_ended")
139
+ XCTAssertEqual(replaced.properties["protocol"] as? String, "rtmp")
140
+ XCTAssertEqual(replaced.properties["end_state"] as? String, "replaced")
141
+ XCTAssertEqual(replaced.properties["duration_ms"] as? Int64, 10000)
142
+ // The replaced stream's own late terminal status is not reported again.
143
+ XCTAssertNil(stream("stopped", terminal: true, nowMs: 12000))
144
+ XCTAssertNil(tracker.streamStatus(
145
+ streamId: "s2", state: "streaming", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
146
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 12000
147
+ ))
148
+ let second = try XCTUnwrap(tracker.streamStatus(
149
+ streamId: "s2", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
150
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 20000
151
+ ))
152
+ XCTAssertEqual(second.properties["protocol"] as? String, "srt")
153
+ XCTAssertEqual(second.properties["duration_ms"] as? Int64, 8000)
154
+ }
155
+
156
+ func testOlderGlassesEndAStreamOnStoppedAndErrorDetailsLoseTheirUrls() throws {
157
+ _ = tracker.streamRequested(streamId: "s2", streamUrl: "https://whip.example.com/whip/abc", nowMs: 0)
158
+ XCTAssertNil(tracker.streamStatus(
159
+ streamId: nil, state: "streaming", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
160
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 500
161
+ ))
162
+ XCTAssertNil(tracker.streamStatus(
163
+ streamId: nil, state: "error", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
164
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 1000
165
+ ))
166
+ let ended = try XCTUnwrap(tracker.streamStatus(
167
+ streamId: nil, state: "reconnect_failed", terminal: nil, reconnectAttempt: nil, bitrate: nil, fps: nil,
168
+ droppedFrames: nil, temperatureC: nil, errorDetails: "connect to https://whip.example.com/whip/abc failed",
169
+ nowMs: 9000
170
+ ))
171
+ XCTAssertEqual(ended.properties["protocol"] as? String, "whip")
172
+ XCTAssertEqual(ended.properties["end_state"] as? String, "reconnect_failed")
173
+ XCTAssertEqual(ended.properties["error_code"] as? String, "connect to <url> failed")
174
+ XCTAssertEqual(ended.properties["stopped_by_app"] as? Bool, false)
175
+ }
176
+
177
+ func testStreamThatNeverWentLiveEndsWithoutASecondEvent() {
178
+ _ = tracker.streamRequested(streamId: "s3", streamUrl: "srt://host:9000", nowMs: 0)
179
+ let failed = tracker.streamStartSettled(
180
+ streamUrl: "srt://host:9000", width: nil, height: nil, fps: nil, bitrate: nil,
181
+ captureAudio: true, glassesNetwork: "none", latencyMs: 30000, errorCode: "stream_start_failed"
182
+ )
183
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
184
+ XCTAssertEqual(failed.properties["error_code"] as? String, "stream_start_failed")
185
+ XCTAssertEqual(failed.properties["protocol"] as? String, "srt")
186
+ XCTAssertNil(tracker.streamStatus(
187
+ streamId: "s3", state: "stopped", terminal: true, reconnectAttempt: nil, bitrate: nil, fps: nil,
188
+ droppedFrames: nil, temperatureC: nil, errorDetails: nil, nowMs: 1000
189
+ ))
190
+ // Nothing live was replaced either.
191
+ XCTAssertNil(tracker.streamRequested(streamId: "s4", streamUrl: "srt://host:9000", nowMs: 2000))
192
+ }
193
+
194
+ func testStreamProtocolFollowsTheGlassesSchemeMapping() {
195
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("rtmp://a"), "rtmp")
196
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("RTMPS://a"), "rtmp")
197
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("srt://a"), "srt")
198
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("http://a"), "whip")
199
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("rtmpt://a"), "unknown")
200
+ XCTAssertEqual(BluetoothSdkUsageTracker.streamProtocol("not a url"), "unknown")
201
+ }
202
+
203
+ func testMicSessionEndsWhenTheSourceTurnsOffOrChanges() throws {
204
+ XCTAssertNil(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 0))
205
+ XCTAssertNil(tracker.observeMic(currentMic: "glasses", transcribing: false, nowMs: 1000))
206
+ XCTAssertNil(tracker.observeMic(currentMic: "glasses", transcribing: true, nowMs: 2000))
207
+
208
+ let switched = try XCTUnwrap(tracker.observeMic(currentMic: "phone", transcribing: false, nowMs: 11000))
209
+ XCTAssertEqual(switched.name, "bluetooth_sdk_mic_session")
210
+ XCTAssertEqual(switched.properties["mic_source"] as? String, "glasses")
211
+ XCTAssertEqual(switched.properties["duration_ms"] as? Int64, 10000)
212
+ XCTAssertEqual(switched.properties["local_transcription"] as? Bool, true)
213
+
214
+ let off = try XCTUnwrap(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 14000))
215
+ XCTAssertEqual(off.properties["mic_source"] as? String, "phone")
216
+ XCTAssertEqual(off.properties["duration_ms"] as? Int64, 3000)
217
+ XCTAssertEqual(off.properties["local_transcription"] as? Bool, false)
218
+ XCTAssertNil(tracker.observeMic(currentMic: "", transcribing: false, nowMs: 15000))
219
+ }
220
+
221
+ func testWifiAndHotspotSetupReportOutcomeAndLatency() {
222
+ let wifi = tracker.wifiSetupSettled(latencyMs: 4000, errorCode: nil)
223
+ XCTAssertEqual(wifi.properties["success"] as? Bool, true)
224
+ XCTAssertEqual(wifi.properties["latency_ms"] as? Int64, 4000)
225
+ XCTAssertEqual(
226
+ tracker.wifiSetupSettled(latencyMs: 15000, errorCode: "connect_timeout").properties["error_code"] as? String,
227
+ "connect_timeout"
228
+ )
229
+ XCTAssertEqual(tracker.hotspotStartSettled(latencyMs: 2500, errorCode: nil).name, "bluetooth_sdk_hotspot_started")
230
+ }
231
+
232
+ func testOtaSessionIsReportedOnceWithItsStartingVersions() throws {
233
+ XCTAssertNil(ota("ota-1", "in_progress", nowMs: 0, fromBuild: "38"))
234
+ XCTAssertNil(ota("ota-1", "step_complete", nowMs: 60000, fromBuild: "39"))
235
+ let done = try XCTUnwrap(ota("ota-1", "complete", nowMs: 300_000, fromBuild: "40"))
236
+ XCTAssertEqual(done.name, "bluetooth_sdk_ota_finished")
237
+ XCTAssertEqual(done.properties["success"] as? Bool, true)
238
+ XCTAssertEqual(done.properties["duration_ms"] as? Int64, 300_000)
239
+ XCTAssertEqual(done.properties["from_build_number"] as? String, "38")
240
+ XCTAssertEqual(done.properties["total_steps"] as? Int, 2)
241
+ XCTAssertNil(done.properties["failed_step_type"])
242
+ XCTAssertNil(ota("ota-1", "complete", nowMs: 301_000, fromBuild: "40"))
243
+
244
+ let failed = try XCTUnwrap(ota(
245
+ "ota-2", "failed", nowMs: 0, fromBuild: "40", stepType: "bes",
246
+ error: "download failed: https://ota.example.com/x.bin"
247
+ ))
248
+ XCTAssertEqual(failed.properties["success"] as? Bool, false)
249
+ XCTAssertEqual(failed.properties["failed_step_type"] as? String, "bes")
250
+ XCTAssertEqual(failed.properties["error_message"] as? String, "download failed: <url>")
251
+ XCTAssertNil(failed.properties["duration_ms"])
252
+ }
253
+
254
+ func testRequestTimeoutsAreCappedPerTracker() throws {
255
+ var capped = BluetoothSdkUsageTracker(maxRequestTimeouts: 2)
256
+ let first = try XCTUnwrap(capped.requestTimedOut(operation: "photo request"))
257
+ XCTAssertEqual(first.name, "bluetooth_sdk_request_failed")
258
+ XCTAssertEqual(first.properties["operation"] as? String, "photo request")
259
+ XCTAssertEqual(first.properties["error_code"] as? String, "request_timeout")
260
+ XCTAssertNotNil(capped.requestTimedOut(operation: "start stream"))
261
+ XCTAssertNil(capped.requestTimedOut(operation: "start stream"))
262
+ }
263
+
264
+ func testDetailsAreSanitizedAndCapped() {
265
+ XCTAssertNil(BluetoothSdkUsageTracker.sanitizedDetail(" "))
266
+ XCTAssertEqual(BluetoothSdkUsageTracker.sanitizedDetail(String(repeating: "x", count: 500))?.count, 200)
267
+ }
268
+
269
+ private func stream(
270
+ _ state: String,
271
+ terminal: Bool? = nil,
272
+ reconnectAttempt: Int? = nil,
273
+ bitrate: Int64? = nil,
274
+ droppedFrames: Int64? = nil,
275
+ nowMs: Int64
276
+ ) -> AnalyticsEvent? {
277
+ tracker.streamStatus(
278
+ streamId: "s1", state: state, terminal: terminal, reconnectAttempt: reconnectAttempt, bitrate: bitrate,
279
+ fps: nil, droppedFrames: droppedFrames, temperatureC: nil, errorDetails: nil, nowMs: nowMs
280
+ )
281
+ }
282
+
283
+ private func ota(
284
+ _ sessionId: String,
285
+ _ status: String,
286
+ nowMs: Int64,
287
+ fromBuild: String,
288
+ stepType: String = "apk",
289
+ error: String? = nil
290
+ ) -> AnalyticsEvent? {
291
+ tracker.otaStatus(
292
+ sessionId: sessionId, status: status, stepType: stepType, totalSteps: 2, errorMessage: error,
293
+ fromBuildNumber: fromBuild, fromBesFirmwareVersion: "", fromMtkFirmwareVersion: "", nowMs: nowMs
294
+ )
295
+ }
296
+ }
@@ -5,14 +5,14 @@ final class DashboardContentFormatterTests: XCTestCase {
5
5
  func testEmptyContentReturnsStatusHeaderOnly() {
6
6
  XCTAssertEqual(
7
7
  DashboardContentFormatter.template(for: ""),
8
- "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
8
+ "$TIME12$ $DATE$ $GBATT$"
9
9
  )
10
10
  }
11
11
 
12
12
  func testNonEmptyContentIsAppendedExactlyAfterBlankLine() {
13
13
  XCTAssertEqual(
14
14
  DashboardContentFormatter.template(for: " Next meeting\nRoom 2 "),
15
- "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$\n\n Next meeting\nRoom 2 "
15
+ "$TIME12$ $DATE$ $GBATT$\n\n Next meeting\nRoom 2 "
16
16
  )
17
17
  }
18
18
  }
@@ -15,6 +15,36 @@ final class DashboardRenderingTests: XCTestCase {
15
15
  return (manager, display)
16
16
  }
17
17
 
18
+ func testFullFrameDashboardHandoffDoesNotClearActiveScene() async {
19
+ let (manager, display) = setupDashboard()
20
+ display.sceneHandoffRequiresClear = false
21
+ await manager.setDashboardContent("new dashboard")
22
+ XCTAssertEqual(display.cleanupCount, 0)
23
+ XCTAssertEqual(display.clearDisplayCount, 0)
24
+ XCTAssertEqual(display.texts.count, 1)
25
+ XCTAssertTrue(display.texts[0].hasSuffix("new dashboard"))
26
+ }
27
+
28
+ func testHiddenSceneHandoffCannotClearMainView() async {
29
+ let (manager, display) = setupDashboard()
30
+ DeviceStore.shared.apply("glasses", "headUp", false)
31
+ manager.displayEvent([
32
+ "view": "dashboard",
33
+ "scene": ["appId": "replacement", "sceneEpoch": 2, "elements": [[String: Any]]()],
34
+ ])
35
+ await Task.yield()
36
+ XCTAssertEqual(display.cleanupCount, 0)
37
+ XCTAssertEqual(display.clearDisplayCount, 0)
38
+ XCTAssertTrue(display.scenes.isEmpty)
39
+ }
40
+
41
+ func testNimoOptsOutOfDestructiveTransientOverlays() {
42
+ let device: SGCManager = Nimo()
43
+ XCTAssertFalse(device.sceneHandoffRequiresClear)
44
+ XCTAssertFalse(device.showBrightnessConfirmation)
45
+ XCTAssertFalse(device.showConnectionConfirmation)
46
+ }
47
+
18
48
  func testSetterWaitsForCleanupAndLatestContentWins() async {
19
49
  let (manager, display) = setupDashboard()
20
50
  var firstReturned = false
@@ -74,6 +104,7 @@ final class DashboardRenderingTests: XCTestCase {
74
104
  private final class PausedDisplay: SGCManager {
75
105
  var type = "Test display"
76
106
  let hasMic = false
107
+ var sceneHandoffRequiresClear = true
77
108
  let cleanupStarted = XCTestExpectation(description: "cleanup suspended")
78
109
  var cleanupCount = 0
79
110
  var clearDisplayCount = 0
@@ -0,0 +1,157 @@
1
+ import Foundation
2
+ @testable import MentraBluetoothSDK
3
+ import XCTest
4
+
5
+ final class DemandDrivenAudioWorkerTests: XCTestCase {
6
+ private final class Processor {
7
+ let onRelease: () -> Void
8
+ init(onRelease: @escaping () -> Void = {}) {
9
+ self.onRelease = onRelease
10
+ }
11
+
12
+ deinit { onRelease() }
13
+ }
14
+
15
+ func testNoDemandAndEmptyActiveQueueScheduleNoRecurringWork() {
16
+ let queue = DispatchQueue(label: "test.speech")
17
+ var creations = 0
18
+ var processed = [Data]()
19
+ let worker = DemandDrivenAudioWorker(queue: queue, makeProcessor: {
20
+ creations += 1
21
+ return Processor()
22
+ }, process: { _, data, _ in processed.append(data) })
23
+ worker.accept(Data([1]))
24
+ queue.sync {}
25
+ XCTAssertEqual(creations, 0)
26
+ worker.setActive(true)
27
+ worker.setActive(true)
28
+ queue.sync {}
29
+ XCTAssertEqual(creations, 1)
30
+ XCTAssertTrue(worker.snapshot().initialized)
31
+ XCTAssertFalse(worker.snapshot().processing)
32
+ // A serial barrier completes with an empty queue; no sleeping consumer holds it.
33
+ queue.sync {}
34
+ XCTAssertTrue(processed.isEmpty)
35
+ worker.setActive(false)
36
+ queue.sync {}
37
+ XCTAssertFalse(worker.snapshot().initialized)
38
+ }
39
+
40
+ func testFirstAudioIsRetainedDuringAsynchronousInitializationAndBounded() {
41
+ let queue = DispatchQueue(label: "test.speech")
42
+ let loading = DispatchSemaphore(value: 0)
43
+ let release = DispatchSemaphore(value: 0)
44
+ var processed = [Data]()
45
+ let worker = DemandDrivenAudioWorker(capacity: 3, queue: queue, makeProcessor: {
46
+ XCTAssertFalse(Thread.isMainThread)
47
+ loading.signal()
48
+ release.wait()
49
+ return Processor()
50
+ }, process: { _, data, _ in processed.append(data) })
51
+ worker.setActive(true)
52
+ XCTAssertEqual(loading.wait(timeout: .now() + 2), .success)
53
+ worker.accept(Data([1]))
54
+ worker.accept(Data([2]))
55
+ XCTAssertEqual(worker.snapshot().queuedBuffers, 2)
56
+ release.signal()
57
+ queue.sync {}
58
+ XCTAssertEqual(processed, [Data([1]), Data([2])])
59
+ worker.setActive(false)
60
+ queue.sync {}
61
+ }
62
+
63
+ func testStopDuringLoadingDropsOldAudioAndCannotResurrectProcessor() {
64
+ let queue = DispatchQueue(label: "test.speech")
65
+ let loading = DispatchSemaphore(value: 0)
66
+ let release = DispatchSemaphore(value: 0)
67
+ var released = 0
68
+ var processed = 0
69
+ let worker = DemandDrivenAudioWorker(capacity: 2, queue: queue, makeProcessor: {
70
+ loading.signal()
71
+ release.wait()
72
+ return Processor { released += 1 }
73
+ }, process: { _, _, _ in processed += 1 })
74
+ worker.setActive(true)
75
+ XCTAssertEqual(loading.wait(timeout: .now() + 2), .success)
76
+ for byte: UInt8 in 1 ... 10 {
77
+ worker.accept(Data([byte]))
78
+ }
79
+ XCTAssertEqual(worker.snapshot().queuedBuffers, 2)
80
+ XCTAssertEqual(worker.snapshot().droppedBuffers, 8)
81
+ worker.setActive(false)
82
+ XCTAssertEqual(worker.snapshot().queuedBuffers, 0)
83
+ release.signal()
84
+ queue.sync {}
85
+ XCTAssertEqual(processed, 0)
86
+ XCTAssertEqual(released, 1)
87
+ XCTAssertFalse(worker.snapshot().initialized)
88
+ }
89
+
90
+ func testRestartAndRapidDemandChangesSerializeOldAndNewProcessors() {
91
+ let queue = DispatchQueue(label: "test.speech")
92
+ let processing = DispatchSemaphore(value: 0)
93
+ let release = DispatchSemaphore(value: 0)
94
+ var creations = 0
95
+ var destructions = 0
96
+ var tokens = [UInt64]()
97
+ var dataSeen = [Data]()
98
+ let worker = DemandDrivenAudioWorker(queue: queue, makeProcessor: {
99
+ XCTAssertEqual(creations, destructions)
100
+ creations += 1
101
+ return Processor { destructions += 1 }
102
+ }, process: { _, data, token in
103
+ dataSeen.append(data)
104
+ tokens.append(token)
105
+ if data == Data([1]) {
106
+ processing.signal()
107
+ release.wait()
108
+ }
109
+ })
110
+ worker.setActive(true)
111
+ worker.accept(Data([1]))
112
+ XCTAssertEqual(processing.wait(timeout: .now() + 2), .success)
113
+ worker.accept(Data([2]))
114
+ worker.restart()
115
+ worker.setActive(false)
116
+ worker.setActive(true)
117
+ worker.accept(Data([3]))
118
+ release.signal()
119
+ queue.sync {}
120
+ XCTAssertEqual(dataSeen, [Data([1]), Data([3])])
121
+ XCTAssertFalse(worker.isCurrent(tokens[0]))
122
+ XCTAssertTrue(worker.isCurrent(tokens[1]))
123
+ XCTAssertEqual(creations, 2)
124
+ worker.setActive(false)
125
+ queue.sync {}
126
+ XCTAssertEqual(destructions, 2)
127
+ XCTAssertFalse(worker.isCurrent(tokens[1]))
128
+ }
129
+
130
+ func testMissingModelDoesNotRetryEveryFrameAndRestartWithoutDemandDoesNotLoad() {
131
+ let queue = DispatchQueue(label: "test.speech")
132
+ var attempts = 0
133
+ let worker = DemandDrivenAudioWorker<Processor>(queue: queue, makeProcessor: {
134
+ attempts += 1
135
+ return nil
136
+ }, process: { _, _, _ in XCTFail("Missing processor") })
137
+ worker.restart()
138
+ queue.sync {}
139
+ XCTAssertEqual(attempts, 0)
140
+ worker.setActive(true)
141
+ for _ in 0 ..< 5 {
142
+ worker.accept(Data([1]))
143
+ queue.sync {}
144
+ }
145
+ XCTAssertEqual(attempts, 1)
146
+ XCTAssertEqual(worker.snapshot().queuedBuffers, 0)
147
+ worker.restart()
148
+ queue.sync {}
149
+ XCTAssertEqual(attempts, 2)
150
+ worker.setActive(false)
151
+ worker.setActive(true)
152
+ queue.sync {}
153
+ XCTAssertEqual(attempts, 3)
154
+ worker.setActive(false)
155
+ queue.sync {}
156
+ }
157
+ }
@@ -0,0 +1,49 @@
1
+ @testable import MentraBluetoothSDK
2
+ import XCTest
3
+
4
+ @MainActor
5
+ final class DeviceIdentityPairingTests: XCTestCase {
6
+ func testReadinessPromotesCoherentIdentityAndPreservesSameDeviceReconnects() {
7
+ let store = DeviceStore.shared.store
8
+ let saved = ["bluetooth", "glasses"].map { ($0, store.getCategory($0)) }
9
+ defer {
10
+ for (category, values) in saved {
11
+ for key in store.getCategory(category).keys where values[key] == nil {
12
+ store.remove(category, key)
13
+ }
14
+ for (key, value) in values {
15
+ store.set(category, key, value)
16
+ }
17
+ }
18
+ }
19
+ let cases = [
20
+ // pending name, pending address, saved model, expected address
21
+ ("new", "", "test", ""),
22
+ ("new", "new-address", "test", "new-address"),
23
+ ("old", "", "test", "old-address"),
24
+ ("", "", "test", "old-address"),
25
+ ("old", "", "other-model", ""),
26
+ ]
27
+ for (pendingName, pendingAddress, savedModel, expectedAddress) in cases {
28
+ let manager = DeviceManager()
29
+ let device = Simulated()
30
+ device.type = "test"
31
+ manager.sgc = device
32
+ defer {
33
+ manager.disconnect()
34
+ manager.cleanup()
35
+ }
36
+ store.set("bluetooth", "default_wearable", savedModel)
37
+ store.set("bluetooth", "device_name", "old")
38
+ store.set("bluetooth", "device_address", "old-address")
39
+ store.set("bluetooth", "pending_device_name", pendingName)
40
+ store.set("bluetooth", "pending_device_address", pendingAddress)
41
+ store.set("bluetooth", "shouldSendBootingMessage", false)
42
+ manager.handleDeviceReady()
43
+ XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_name") as? String, pendingName.isEmpty ? "old" : pendingName)
44
+ XCTAssertEqual(DeviceStore.shared.get("bluetooth", "device_address") as? String, expectedAddress)
45
+ XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_name") as? String, "")
46
+ XCTAssertEqual(DeviceStore.shared.get("bluetooth", "pending_device_address") as? String, "")
47
+ }
48
+ }
49
+ }