@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.2.1

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 (133) hide show
  1. package/README.md +4 -4
  2. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  3. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  7. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
  11. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  13. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  24. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  29. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  31. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  34. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  35. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  36. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  37. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  38. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  39. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  40. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  41. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  42. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  43. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
  45. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  48. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  50. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  51. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  52. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  53. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  54. package/build/BluetoothSdk.types.d.ts +13 -0
  55. package/build/BluetoothSdk.types.d.ts.map +1 -1
  56. package/build/BluetoothSdk.types.js +1 -0
  57. package/build/BluetoothSdk.types.js.map +1 -1
  58. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  59. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  60. package/build/_private/BluetoothSdkModule.js +3 -0
  61. package/build/_private/BluetoothSdkModule.js.map +1 -1
  62. package/build/generated/changelogCatalog.d.ts +2 -2
  63. package/build/generated/changelogCatalog.js +2 -2
  64. package/build/generated/changelogCatalog.js.map +1 -1
  65. package/build/generated/releaseMetadata.js +5 -5
  66. package/build/generated/releaseMetadata.js.map +1 -1
  67. package/build/index.d.ts.map +1 -1
  68. package/build/index.js +1 -0
  69. package/build/index.js.map +1 -1
  70. package/ios/BluetoothSdkModule.swift +9 -0
  71. package/ios/MentraBluetoothSDK.podspec +3 -2
  72. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  73. package/ios/Source/BluetoothSdkDefaults.swift +6 -1
  74. package/ios/Source/Bridge.swift +15 -2
  75. package/ios/Source/DeviceManager.swift +96 -63
  76. package/ios/Source/DeviceStore.swift +26 -7
  77. package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
  78. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  79. package/ios/Source/MentraBluetoothSDK.swift +18 -0
  80. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  81. package/ios/Source/requests/DisplayRequests.swift +1 -1
  82. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  83. package/ios/Source/sgcs/Ar99.swift +1 -2
  84. package/ios/Source/sgcs/G1.swift +68 -38
  85. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  86. package/ios/Source/sgcs/G2.swift +261 -247
  87. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  88. package/ios/Source/sgcs/Mach1.swift +19 -7
  89. package/ios/Source/sgcs/MentraLive.swift +201 -156
  90. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  91. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  92. package/ios/Source/sgcs/MentraNex.swift +8 -31
  93. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  94. package/ios/Source/sgcs/Nimo.swift +280 -408
  95. package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
  96. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  97. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  98. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  99. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  100. package/ios/Source/sgcs/SGCManager.swift +24 -0
  101. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  102. package/ios/Source/status/DeviceStatus.swift +1 -0
  103. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  104. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  105. package/ios/Source/utils/G2Text.swift +11 -0
  106. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  107. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  108. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  109. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  110. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  111. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  112. package/ios/Tests/G1PairingTests.swift +162 -0
  113. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  114. package/ios/Tests/G2TextTests.swift +10 -0
  115. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  116. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  117. package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
  118. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  119. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  120. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  121. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  122. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  123. package/ios/Tests/VersionInfoCommandTests.swift +53 -0
  124. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  125. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  126. package/package.json +2 -2
  127. package/plugin/build/withIos.d.ts +2 -0
  128. package/plugin/build/withIos.js +12 -1
  129. package/src/BluetoothSdk.types.ts +14 -0
  130. package/src/_private/BluetoothSdkModule.ts +8 -0
  131. package/src/generated/changelogCatalog.ts +2 -2
  132. package/src/generated/releaseMetadata.ts +5 -5
  133. package/src/index.ts +1 -0
@@ -0,0 +1,180 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ /** Serialized business-command state machine. An ACK means acceptance, never visible pixels. */
4
+ internal class NimoCanvasSession {
5
+ sealed class Action {
6
+ data class Send(val key: Int, val frame: ByteArray, val ticket: Long) : Action()
7
+ data class Reconnect(val reason: String) : Action()
8
+ data class Rejected(val status: Int) : Action()
9
+ }
10
+ private data class Flight(val key: Int, val frame: ByteArray, val ticket: Long)
11
+ private var ready = false
12
+ private var transportReady = false
13
+ private var active = false
14
+ private var held = false
15
+ private var locked = false
16
+ private var launchBlocked = false
17
+ private var waitingReadiness = false
18
+ private var observedNotReady = false
19
+ private var readinessRetries = 0
20
+ private var probeRetries = 0
21
+ private var exitRequested = false
22
+ private var desired: ByteArray? = null
23
+ private var accepted: ByteArray? = null
24
+ private var rejected: ByteArray? = null
25
+ private var scope: String? = null
26
+ private var forceRevision = 0L
27
+ private var sentRevision = 0L
28
+ private var ticket = 0L
29
+ private var flight: Flight? = null
30
+
31
+ fun offer(frame: ByteArray, scope: String, force: Boolean = false): List<Action> {
32
+ require(frame.size in 3..(NimoCanvasCodec.MAX_STREAM - 8))
33
+ if (this.scope != scope || force) forceRevision++
34
+ this.scope = scope
35
+ desired = frame.copyOf()
36
+ exitRequested = false
37
+ return pump()
38
+ }
39
+
40
+ /** Enter a blank canvas on a fresh connection unless a newer host scene is retained. */
41
+ fun activate(): List<Action> {
42
+ if (desired == null) desired = byteArrayOf(0, 0, 1)
43
+ exitRequested = false
44
+ return pump()
45
+ }
46
+
47
+ /** Launch itself checks peer readiness in firmware. Retry only explicit NOT_READY,
48
+ * with a small budget; never retry an ambiguous/missing business ACK on this link. */
49
+ fun retryNotReady(): List<Action> {
50
+ if (!transportReady || !waitingReadiness || desired == null || probeRetries >= 3) return emptyList()
51
+ probeRetries++
52
+ waitingReadiness = false
53
+ ready = true
54
+ return pump()
55
+ }
56
+
57
+ /** Debug capture seam; production behavior is unchanged unless explicitly held. */
58
+ fun hold(value: Boolean, resume: Boolean = true): List<Action> {
59
+ held = value
60
+ return if (!value && resume) pump() else emptyList()
61
+ }
62
+
63
+ fun currentScope(): String? = scope
64
+
65
+ fun readiness(value: Boolean): List<Action> = updateReadiness(value, confirmed = false)
66
+
67
+ /** Newly received device predicates, not a replay of cached heartbeat state.
68
+ * The caller validates the report and discards traffic from older bearers. */
69
+ fun confirmedReadiness(value: Boolean): List<Action> = updateReadiness(value, confirmed = true)
70
+
71
+ private fun updateReadiness(value: Boolean, confirmed: Boolean): List<Action> {
72
+ transportReady = value
73
+ if (!value) { readinessRetries = 0; probeRetries = 0 }
74
+ if (!value && flight != null) return resetLink("Readiness lost during canvas command")
75
+ if (waitingReadiness) {
76
+ if (!value) observedNotReady = true
77
+ else if (!observedNotReady) {
78
+ if (!confirmed || readinessRetries >= 3) return emptyList()
79
+ readinessRetries++
80
+ }
81
+ }
82
+ ready = value
83
+ if (!value) { active = false; accepted = null }
84
+ if (value) waitingReadiness = false
85
+ return pump()
86
+ }
87
+
88
+ /** A new transport generation has no accepted frame. Retain only the latest desired scene. */
89
+ fun disconnected() {
90
+ ready = false; transportReady = false; active = false; held = false; locked = false; launchBlocked = false
91
+ waitingReadiness = false; observedNotReady = false
92
+ readinessRetries = 0; probeRetries = 0
93
+ accepted = null; rejected = null; flight = null; ticket++; forceRevision++
94
+ }
95
+
96
+ fun exit(): List<Action> {
97
+ desired = null; scope = null; accepted = null; rejected = null; exitRequested = true
98
+ return pump()
99
+ }
100
+
101
+ /** Firmware can restore its stock UI after reconnect. Keep host ownership and
102
+ * replay the newest scene, serializing behind any outstanding command ACK. */
103
+ fun nativeApp(appId: Int, entered: Boolean): List<Action> {
104
+ if ((appId == NimoCanvasCodec.APP_ID && !entered) || (appId != NimoCanvasCodec.APP_ID && entered)) {
105
+ active = false
106
+ accepted = null
107
+ return pump()
108
+ }
109
+ return emptyList()
110
+ }
111
+
112
+ fun acceptsResponse(key: Int, payload: ByteArray): Boolean {
113
+ val current = flight ?: return false
114
+ if (current.key != key || payload.isEmpty()) return false
115
+ val status = payload[0].toInt() and 255
116
+ val echo = if (key == 4) byteArrayOf(0, NimoCanvasCodec.APP_ID.toByte(), 0, 0, 0)
117
+ else byteArrayOf(0, NimoCanvasCodec.APP_ID.toByte())
118
+ if (status == 0 && !payload.contentEquals(echo)) return false
119
+ if (status != 0 && payload.size != 1 &&
120
+ !(payload.size == echo.size && payload.copyOfRange(1, payload.size).contentEquals(echo.copyOfRange(1, echo.size)))) return false
121
+ return true
122
+ }
123
+
124
+ fun response(key: Int, payload: ByteArray): List<Action> {
125
+ if (!acceptsResponse(key, payload)) return emptyList()
126
+ val current = flight ?: return emptyList()
127
+ val status = payload[0].toInt() and 255
128
+ flight = null
129
+ if (status != 0) {
130
+ when (status) {
131
+ 7 -> { ready = false; waitingReadiness = true; observedNotReady = false; active = false; accepted = null }
132
+ 8 -> { locked = true; active = false; accepted = null }
133
+ 2 -> return listOf(Action.Rejected(status)) + resetLink("Canvas device timeout is ambiguous")
134
+ else -> {
135
+ if (key == 4) rejected = current.frame
136
+ else {
137
+ if (key == 1) launchBlocked = true else desired = null
138
+ active = false; exitRequested = false
139
+ }
140
+ }
141
+ }
142
+ return listOf(Action.Rejected(status)) + pump()
143
+ }
144
+ when (key) {
145
+ 1 -> { active = true; accepted = null }
146
+ 4 -> { accepted = current.frame; rejected = null; readinessRetries = 0; probeRetries = 0 }
147
+ 3 -> { active = false; accepted = null; exitRequested = false }
148
+ }
149
+ return pump()
150
+ }
151
+
152
+ fun timeout(expectedTicket: Long): List<Action> {
153
+ if (flight?.ticket != expectedTicket) return emptyList()
154
+ return resetLink("Canvas ACK timeout; reconnect before reusing a command key")
155
+ }
156
+
157
+ private fun resetLink(reason: String): List<Action> {
158
+ ready = false; transportReady = false; active = false; accepted = null; flight = null; ticket++; forceRevision++
159
+ return listOf(Action.Reconnect(reason))
160
+ }
161
+
162
+ private fun pump(): List<Action> {
163
+ if (!ready || held || locked || launchBlocked || waitingReadiness || flight != null) return emptyList()
164
+ val next = desired
165
+ val key = when {
166
+ exitRequested && active -> 3
167
+ exitRequested -> { exitRequested = false; return emptyList() }
168
+ next == null -> return emptyList()
169
+ !active -> 1
170
+ next.contentEquals(rejected) -> return emptyList()
171
+ next.contentEquals(accepted) && sentRevision == forceRevision -> return emptyList()
172
+ else -> 4
173
+ }
174
+ val frame = if (key == 4) next!!.copyOf() else ByteArray(0)
175
+ if (key == 4) sentRevision = forceRevision
176
+ val sending = Flight(key, frame, ++ticket)
177
+ flight = sending
178
+ return listOf(Action.Send(key, frame.copyOf(), sending.ticket))
179
+ }
180
+ }
@@ -105,6 +105,10 @@ abstract class SGCManager {
105
105
 
106
106
  // Display Control
107
107
  abstract fun setBrightness(level: Int, autoMode: Boolean)
108
+ /** Whether settings should briefly replace the display with a brightness confirmation. */
109
+ open val showBrightnessConfirmation: Boolean = true
110
+ /** Whether connection readiness should briefly replace the display with a welcome message. */
111
+ open val showConnectionConfirmation: Boolean = true
108
112
  abstract fun clearDisplay()
109
113
  abstract fun sendText(text: String)
110
114
  abstract fun sendTextWall(text: String)
@@ -191,6 +195,10 @@ abstract class SGCManager {
191
195
  * device (e.g. NIMO-style) overrides this whole method instead and
192
196
  * serializes [SceneFrame.elements], ignoring the annotations.
193
197
  */
198
+ // Full-frame adapters replace both scenes and legacy layouts atomically;
199
+ // a preparatory clear would introduce a blank frame (or race the new frame).
200
+ open val sceneHandoffRequiresClear: Boolean = true
201
+
194
202
  open fun applySceneFrame(frame: SceneFrame) {
195
203
  if (frame.replay) {
196
204
  onSceneReplay(frame.appId)
@@ -350,6 +358,9 @@ abstract class SGCManager {
350
358
  // Mentra Live center-mic loudness / Barrier gate
351
359
  open fun sendLoudnessGateSetting() {}
352
360
 
361
+ // Mentra Live power-off after a stretch of not being worn
362
+ open fun sendAutoPowerOffSetting() {}
363
+
353
364
  // Mentra Live mic tuning (super-mode only). No value authorized means the
354
365
  // implementation sends an explicit reset rather than skipping the send.
355
366
  open fun sendMicTuningSetting() {}
@@ -0,0 +1,17 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ /** An address is usable only when its accompanying name identifies the requested device. */
4
+ internal object SelectedDeviceAddress {
5
+ fun resolve(
6
+ target: String,
7
+ pendingName: String?,
8
+ pendingAddress: String?,
9
+ savedName: String?,
10
+ savedAddress: String?,
11
+ ): String? {
12
+ if (target.isBlank()) return null
13
+ if (pendingName == target && !pendingAddress.isNullOrBlank()) return pendingAddress
14
+ if (savedName == target && !savedAddress.isNullOrBlank()) return savedAddress
15
+ return null
16
+ }
17
+ }
@@ -4,6 +4,7 @@ import android.bluetooth.BluetoothGatt
4
4
  import android.bluetooth.BluetoothGattCallback
5
5
  import android.bluetooth.BluetoothGattCharacteristic
6
6
  import android.bluetooth.BluetoothGattDescriptor
7
+ import android.os.Build
7
8
 
8
9
  /**
9
10
  * Binder callbacks only capture their arguments. Session validation and all callback effects run
@@ -45,6 +46,10 @@ internal abstract class SerializedGattCallback(
45
46
  final override fun onCharacteristicChanged(
46
47
  gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
47
48
  ) {
49
+ // Android 13 introduced the value callback; some framework versions also call
50
+ // this deprecated overload for the same notification. Decoding both replays
51
+ // each LC3 packet and doubles recorded audio. Older Android uses this path.
52
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return
48
53
  val value = characteristic.value?.copyOf() ?: return
49
54
  dispatch(gatt) { handleCharacteristicChanged(gatt, characteristic, value) }
50
55
  }
@@ -0,0 +1,7 @@
1
+ package com.mentra.bluetoothsdk.utils
2
+
3
+ /** Encode blank rows without triggering the EvenHub empty-line parser bug. */
4
+ internal object G2Text {
5
+ fun containerContent(text: String): String =
6
+ if (text.isEmpty()) " " else text.split("\n").joinToString("\n") { it.ifEmpty { "\u200B" } }
7
+ }
@@ -0,0 +1,20 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+
6
+ /** No diagnostic receiver, commands, or screen capture in release builds. */
7
+ @Suppress("UNUSED_PARAMETER")
8
+ internal class NimoDiagnostics(
9
+ context: Context,
10
+ handler: Handler,
11
+ send: (ByteArray, () -> Unit) -> Boolean,
12
+ requestCanvasHold: ((() -> Unit) -> Unit) = { _ -> },
13
+ releaseCanvasHold: (resume: Boolean) -> Unit = { _ -> },
14
+ ) {
15
+ fun connected(maxWriteBytes: Int) {}
16
+ fun disconnected() {}
17
+ fun close() {}
18
+ fun onPacket(packet: ByteArray): Boolean = false
19
+ fun cancelHeldCapture(reason: String, resumeCanvas: Boolean = false) {}
20
+ }
@@ -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
+ }