@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,263 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import org.assertj.core.api.Assertions.assertThat
4
+ import org.junit.Test
5
+
6
+ class BluetoothSdkUsageTrackerTest {
7
+ private val tracker = BluetoothSdkUsageTracker()
8
+
9
+ @Test
10
+ fun `an API photo reports its request shape, latency and outcome`() {
11
+ val ok = tracker.photoSettled("medium", "photo", "none", save = true, transferMethod = "auto", latencyMs = 812, fileSizeBytes = 204_800, errorCode = null)
12
+ assertThat(ok.name).isEqualTo("bluetooth_sdk_photo_taken")
13
+ assertThat(ok.properties)
14
+ .containsEntry("trigger", "api")
15
+ .containsEntry("success", true)
16
+ .containsEntry("latency_ms", 812L)
17
+ .containsEntry("photo_size", "medium")
18
+ .containsEntry("save_to_gallery", true)
19
+ .containsEntry("transfer_method", "auto")
20
+ .containsEntry("file_size_bytes", 204_800L)
21
+ .doesNotContainKey("error_code")
22
+
23
+ val failed = tracker.photoSettled("max", "photo", "none", save = false, transferMethod = "ble", latencyMs = 30_000, fileSizeBytes = null, errorCode = "request_timeout")
24
+ assertThat(failed.properties)
25
+ .containsEntry("success", false)
26
+ .containsEntry("error_code", "request_timeout")
27
+ .doesNotContainKey("file_size_bytes")
28
+ }
29
+
30
+ @Test
31
+ fun `a failed button photo is reported as a button trigger`() {
32
+ val event = tracker.buttonPhotoFailed("CAMERA_BUSY")
33
+ assertThat(event.properties).containsEntry("trigger", "button").containsEntry("success", false).containsEntry("error_code", "CAMERA_BUSY")
34
+ }
35
+
36
+ @Test
37
+ fun `an API recording reports its length, parameters and app stop once`() {
38
+ tracker.videoRequested("rec-1", width = 1920, height = 1080, fps = 30, save = true, maxDurationMinutes = 0)
39
+ assertThat(tracker.videoStatus("rec-1", "recording_started", success = true, nowMs = 1_000)).isNull()
40
+ // A duplicate start does not move the start time.
41
+ assertThat(tracker.videoStatus("rec-1", "recording_started", success = true, nowMs = 2_000)).isNull()
42
+ tracker.videoStopRequested("rec-1")
43
+
44
+ val stopped = tracker.videoStatus("rec-1", "recording_stopped", success = true, nowMs = 61_000)!!
45
+ assertThat(stopped.name).isEqualTo("bluetooth_sdk_video_recorded")
46
+ assertThat(stopped.properties)
47
+ .containsEntry("trigger", "api")
48
+ .containsEntry("success", true)
49
+ .containsEntry("duration_ms", 60_000L)
50
+ .containsEntry("width", 1920)
51
+ .containsEntry("fps", 30)
52
+ .containsEntry("save_to_gallery", true)
53
+ .containsEntry("stopped_by_app", true)
54
+ .doesNotContainKey("max_duration_minutes")
55
+
56
+ assertThat(tracker.videoStatus("rec-1", "recording_stopped", success = true, nowMs = 62_000)).isNull()
57
+ }
58
+
59
+ @Test
60
+ fun `a button recording is reported with a button trigger and no app stop`() {
61
+ val id = "local_video_1728000000_ab12"
62
+ tracker.videoStatus(id, "recording_started", success = true, nowMs = 0)
63
+ val stopped = tracker.videoStatus(id, "recording_stopped", success = true, nowMs = 5_000)!!
64
+ assertThat(stopped.properties)
65
+ .containsEntry("trigger", "button")
66
+ .containsEntry("duration_ms", 5_000L)
67
+ .containsEntry("stopped_by_app", false)
68
+ }
69
+
70
+ @Test
71
+ fun `a recording failure is reported, but not_recording is not a recording`() {
72
+ tracker.videoRequested("rec-2", 0, 0, 0, save = false, maxDurationMinutes = 5)
73
+ val failed = tracker.videoStatus("rec-2", "battery_low", success = false, nowMs = 0)!!
74
+ assertThat(failed.properties)
75
+ .containsEntry("success", false)
76
+ .containsEntry("error_code", "battery_low")
77
+ .containsEntry("max_duration_minutes", 5)
78
+ .doesNotContainKey("duration_ms")
79
+ .doesNotContainKey("width")
80
+
81
+ assertThat(tracker.videoStatus("rec-3", "not_recording", success = false, nowMs = 0)).isNull()
82
+ assertThat(tracker.videoStatus("", "recording_stopped", success = true, nowMs = 0)).isNull()
83
+ }
84
+
85
+ @Test
86
+ fun `a stream is reported at start and once at its terminal status`() {
87
+ assertThat(tracker.streamRequested("s1", "rtmps://live.example.com/app/secret-key", nowMs = 6_600)).isNull()
88
+ // The first live status can arrive before startStream returns.
89
+ assertThat(stream("streaming", nowMs = 10_000)).isNull()
90
+ val started =
91
+ tracker.streamStartSettled(
92
+ streamUrl = "rtmps://live.example.com/app/secret-key",
93
+ width = 1280,
94
+ height = 720,
95
+ fps = 30,
96
+ bitrate = 2_000_000,
97
+ captureAudio = true,
98
+ glassesNetwork = "wifi",
99
+ latencyMs = 3_400,
100
+ errorCode = null,
101
+ )
102
+ assertThat(started.name).isEqualTo("bluetooth_sdk_stream_started")
103
+ assertThat(started.properties)
104
+ .containsEntry("success", true)
105
+ .containsEntry("protocol", "rtmp")
106
+ .containsEntry("glasses_network", "wifi")
107
+ assertThat(started.properties.values.map { it.toString() }).noneMatch { it.contains("secret-key") }
108
+
109
+ assertThat(stream("reconnecting", reconnectAttempt = 1, nowMs = 20_000)).isNull()
110
+ assertThat(stream("reconnected", reconnectAttempt = 2, nowMs = 21_000)).isNull()
111
+ assertThat(stream("streaming", bitrate = 1_800_000, droppedFrames = 12, nowMs = 30_000)).isNull()
112
+ // Statuses for another stream id do not touch this one.
113
+ assertThat(tracker.streamStatus("other", "stopped", true, null, null, null, null, null, null, 31_000)).isNull()
114
+ tracker.streamStopRequested()
115
+
116
+ val ended = stream("stopped", terminal = true, nowMs = 70_000)!!
117
+ assertThat(ended.name).isEqualTo("bluetooth_sdk_stream_ended")
118
+ assertThat(ended.properties)
119
+ .containsEntry("protocol", "rtmp")
120
+ .containsEntry("duration_ms", 60_000L)
121
+ .containsEntry("end_state", "stopped")
122
+ .containsEntry("stopped_by_app", true)
123
+ .containsEntry("reconnect_attempts", 2)
124
+ .containsEntry("bitrate", 1_800_000L)
125
+ .containsEntry("dropped_frames", 12L)
126
+
127
+ assertThat(stream("stopped", terminal = true, nowMs = 71_000)).isNull()
128
+ }
129
+
130
+ @Test
131
+ fun `a stream that ends right after going live is still reported`() {
132
+ tracker.streamRequested("s1", "rtmp://host/app", nowMs = 0)
133
+ assertThat(stream("streaming", nowMs = 1_000)).isNull()
134
+ val ended = stream("error", terminal = true, nowMs = 1_500)!!
135
+ assertThat(ended.properties).containsEntry("duration_ms", 500L).containsEntry("end_state", "error")
136
+ }
137
+
138
+ @Test
139
+ fun `a new start reports the live stream it replaces`() {
140
+ tracker.streamRequested("s1", "rtmp://host/app", nowMs = 0)
141
+ stream("streaming", nowMs = 1_000)
142
+ val replaced = tracker.streamRequested("s2", "srt://host:9000", nowMs = 11_000)!!
143
+ assertThat(replaced.name).isEqualTo("bluetooth_sdk_stream_ended")
144
+ assertThat(replaced.properties)
145
+ .containsEntry("protocol", "rtmp")
146
+ .containsEntry("end_state", "replaced")
147
+ .containsEntry("duration_ms", 10_000L)
148
+ // The replaced stream's own late terminal status is not reported again.
149
+ assertThat(stream("stopped", terminal = true, nowMs = 12_000)).isNull()
150
+ assertThat(tracker.streamStatus("s2", "streaming", null, null, null, null, null, null, null, 12_000)).isNull()
151
+ val second = tracker.streamStatus("s2", "stopped", true, null, null, null, null, null, null, 20_000)!!
152
+ assertThat(second.properties).containsEntry("protocol", "srt").containsEntry("duration_ms", 8_000L)
153
+ }
154
+
155
+ @Test
156
+ fun `older glasses without a terminal flag end a stream on stopped, and error details lose their URLs`() {
157
+ tracker.streamRequested("s2", "https://whip.example.com/whip/abc", nowMs = 0)
158
+ assertThat(tracker.streamStatus(null, "streaming", null, null, null, null, null, null, null, 500)).isNull()
159
+ assertThat(tracker.streamStatus(null, "error", null, null, null, null, null, null, null, 1_000)).isNull()
160
+ val ended =
161
+ tracker.streamStatus(null, "reconnect_failed", null, null, null, null, null, null, "connect to https://whip.example.com/whip/abc failed", 9_000)!!
162
+ assertThat(ended.properties)
163
+ .containsEntry("protocol", "whip")
164
+ .containsEntry("end_state", "reconnect_failed")
165
+ .containsEntry("error_code", "connect to <url> failed")
166
+ .containsEntry("stopped_by_app", false)
167
+ }
168
+
169
+ @Test
170
+ fun `a stream that never went live ends without a second event`() {
171
+ tracker.streamRequested("s3", "srt://host:9000", nowMs = 0)
172
+ val failed = tracker.streamStartSettled("srt://host:9000", null, null, null, null, true, "none", 30_000, "stream_start_failed")
173
+ assertThat(failed.properties).containsEntry("success", false).containsEntry("error_code", "stream_start_failed").containsEntry("protocol", "srt")
174
+ assertThat(tracker.streamStatus("s3", "stopped", true, null, null, null, null, null, null, 1_000)).isNull()
175
+ // Nothing live was replaced either.
176
+ assertThat(tracker.streamRequested("s4", "srt://host:9000", nowMs = 2_000)).isNull()
177
+ }
178
+
179
+ @Test
180
+ fun `stream protocol follows the glasses scheme mapping`() {
181
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("rtmp://a")).isEqualTo("rtmp")
182
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("RTMPS://a")).isEqualTo("rtmp")
183
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("srt://a")).isEqualTo("srt")
184
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("http://a")).isEqualTo("whip")
185
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("rtmpt://a")).isEqualTo("unknown")
186
+ assertThat(BluetoothSdkUsageTracker.streamProtocol("not a url")).isEqualTo("unknown")
187
+ }
188
+
189
+ @Test
190
+ fun `a mic session ends when the source turns off or changes`() {
191
+ assertThat(tracker.observeMic("", transcribing = false, nowMs = 0)).isNull()
192
+ assertThat(tracker.observeMic("glasses", transcribing = false, nowMs = 1_000)).isNull()
193
+ assertThat(tracker.observeMic("glasses", transcribing = true, nowMs = 2_000)).isNull()
194
+
195
+ val switched = tracker.observeMic("phone", transcribing = false, nowMs = 11_000)!!
196
+ assertThat(switched.name).isEqualTo("bluetooth_sdk_mic_session")
197
+ assertThat(switched.properties)
198
+ .containsEntry("mic_source", "glasses")
199
+ .containsEntry("duration_ms", 10_000L)
200
+ .containsEntry("local_transcription", true)
201
+
202
+ val off = tracker.observeMic("", transcribing = false, nowMs = 14_000)!!
203
+ assertThat(off.properties).containsEntry("mic_source", "phone").containsEntry("duration_ms", 3_000L).containsEntry("local_transcription", false)
204
+ assertThat(tracker.observeMic("", transcribing = false, nowMs = 15_000)).isNull()
205
+ }
206
+
207
+ @Test
208
+ fun `wifi and hotspot setup report outcome and latency`() {
209
+ assertThat(tracker.wifiSetupSettled(4_000, null).properties).containsEntry("success", true).containsEntry("latency_ms", 4_000L)
210
+ assertThat(tracker.wifiSetupSettled(15_000, "connect_timeout").properties).containsEntry("error_code", "connect_timeout")
211
+ assertThat(tracker.hotspotStartSettled(2_500, null).name).isEqualTo("bluetooth_sdk_hotspot_started")
212
+ }
213
+
214
+ @Test
215
+ fun `an OTA session is reported once with its starting versions`() {
216
+ assertThat(ota("ota-1", "in_progress", nowMs = 0, fromBuild = "38")).isNull()
217
+ assertThat(ota("ota-1", "step_complete", nowMs = 60_000, fromBuild = "39")).isNull()
218
+ val done = ota("ota-1", "complete", nowMs = 300_000, fromBuild = "40")!!
219
+ assertThat(done.name).isEqualTo("bluetooth_sdk_ota_finished")
220
+ assertThat(done.properties)
221
+ .containsEntry("success", true)
222
+ .containsEntry("duration_ms", 300_000L)
223
+ .containsEntry("from_build_number", "38")
224
+ .containsEntry("total_steps", 2)
225
+ .doesNotContainKey("failed_step_type")
226
+ assertThat(ota("ota-1", "complete", nowMs = 301_000, fromBuild = "40")).isNull()
227
+
228
+ val failed = ota("ota-2", "failed", nowMs = 0, fromBuild = "40", stepType = "bes", error = "download failed: https://ota.example.com/x.bin")!!
229
+ assertThat(failed.properties)
230
+ .containsEntry("success", false)
231
+ .containsEntry("failed_step_type", "bes")
232
+ .containsEntry("error_message", "download failed: <url>")
233
+ .doesNotContainKey("duration_ms")
234
+ }
235
+
236
+ @Test
237
+ fun `request timeouts are capped per tracker`() {
238
+ val capped = BluetoothSdkUsageTracker(maxRequestTimeouts = 2)
239
+ assertThat(capped.requestTimedOut("photo request")!!.properties)
240
+ .containsEntry("operation", "photo request")
241
+ .containsEntry("error_code", "request_timeout")
242
+ assertThat(capped.requestTimedOut("start stream")).isNotNull()
243
+ assertThat(capped.requestTimedOut("start stream")).isNull()
244
+ }
245
+
246
+ private fun stream(
247
+ state: String,
248
+ terminal: Boolean? = null,
249
+ reconnectAttempt: Int? = null,
250
+ bitrate: Long? = null,
251
+ droppedFrames: Long? = null,
252
+ nowMs: Long,
253
+ ) = tracker.streamStatus("s1", state, terminal, reconnectAttempt, bitrate, null, droppedFrames, null, null, nowMs)
254
+
255
+ private fun ota(
256
+ sessionId: String,
257
+ status: String,
258
+ nowMs: Long,
259
+ fromBuild: String,
260
+ stepType: String = "apk",
261
+ error: String? = null,
262
+ ) = tracker.otaStatus(sessionId, status, stepType, totalSteps = 2, errorMessage = error, fromBuildNumber = fromBuild, fromBesFirmwareVersion = "", fromMtkFirmwareVersion = "", nowMs = nowMs)
263
+ }
@@ -7,7 +7,7 @@ class DashboardContentFormatterTest {
7
7
  @Test
8
8
  fun emptyContentReturnsStatusHeaderOnly() {
9
9
  assertEquals(
10
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
10
+ "\$TIME12$ \$DATE$ \$GBATT$",
11
11
  DashboardContentFormatter.template(""),
12
12
  )
13
13
  }
@@ -15,7 +15,7 @@ class DashboardContentFormatterTest {
15
15
  @Test
16
16
  fun nonEmptyContentIsAppendedExactlyAfterBlankLine() {
17
17
  assertEquals(
18
- "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$\n\n Next meeting\nRoom 2 ",
18
+ "\$TIME12$ \$DATE$ \$GBATT$\n\n Next meeting\nRoom 2 ",
19
19
  DashboardContentFormatter.template(" Next meeting\nRoom 2 "),
20
20
  )
21
21
  }
@@ -0,0 +1,59 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import androidx.test.core.app.ApplicationProvider
4
+ import com.mentra.bluetoothsdk.sgcs.Simulated
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+ import org.robolectric.annotation.Config
10
+ import org.robolectric.annotation.LooperMode
11
+
12
+ @RunWith(RobolectricTestRunner::class)
13
+ @Config(sdk = [33])
14
+ @LooperMode(LooperMode.Mode.PAUSED)
15
+ class DeviceIdentityPairingTest {
16
+ @Test fun `readiness promotes a coherent name and address while preserving same-device reconnects`() {
17
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
18
+ val saved = listOf("bluetooth", "glasses").associateWith { DeviceStore.store.getCategory(it) }
19
+ val singleton = DeviceManager::class.java.getDeclaredField("_instance").apply { isAccessible = true }
20
+ val previous = singleton.get(null)
21
+ val cases = listOf(
22
+ // pending name, pending address, saved model, expected address
23
+ listOf("new", "", "test", ""),
24
+ listOf("new", "new-address", "test", "new-address"),
25
+ listOf("old", "", "test", "old-address"),
26
+ listOf("", "", "test", "old-address"),
27
+ listOf("old", "", "other-model", ""),
28
+ )
29
+ try {
30
+ for ((pendingName, pendingAddress, savedModel, expectedAddress) in cases) {
31
+ val manager = DeviceManager(initializeHardware = false)
32
+ singleton.set(null, manager)
33
+ manager.sgc = Simulated().apply { type = "test" }
34
+ DeviceStore.set("bluetooth", "default_wearable", savedModel)
35
+ DeviceStore.set("bluetooth", "device_name", "old")
36
+ DeviceStore.set("bluetooth", "device_address", "old-address")
37
+ DeviceStore.set("bluetooth", "pending_device_name", pendingName)
38
+ DeviceStore.set("bluetooth", "pending_device_address", pendingAddress)
39
+ DeviceStore.set("bluetooth", "shouldSendBootingMessage", false)
40
+ try {
41
+ manager.handleDeviceReady()
42
+ assertEquals(pendingName.ifEmpty { "old" }, DeviceStore.get("bluetooth", "device_name"))
43
+ assertEquals(expectedAddress, DeviceStore.get("bluetooth", "device_address"))
44
+ assertEquals("", DeviceStore.get("bluetooth", "pending_device_name"))
45
+ assertEquals("", DeviceStore.get("bluetooth", "pending_device_address"))
46
+ } finally {
47
+ manager.disconnect()
48
+ manager.cleanup()
49
+ }
50
+ }
51
+ } finally {
52
+ singleton.set(null, previous)
53
+ saved.forEach { (category, values) ->
54
+ (DeviceStore.store.getCategory(category).keys - values.keys).forEach { DeviceStore.store.remove(category, it) }
55
+ values.forEach { (key, value) -> DeviceStore.set(category, key, value) }
56
+ }
57
+ }
58
+ }
59
+ }