@mentra/bluetooth-sdk 3.2.1-beta.315 → 3.2.1-beta.354

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 (83) hide show
  1. package/README.md +1 -1
  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 +4 -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 +67 -12
  8. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
  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/services/Foreground.kt +33 -0
  14. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  15. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +64 -14
  16. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +73 -3
  17. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +257 -395
  18. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
  19. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +225 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +168 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  24. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  25. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +557 -0
  26. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  27. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  28. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  29. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  30. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  31. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
  32. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +308 -0
  33. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +270 -0
  34. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  35. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  36. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +194 -0
  37. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +199 -0
  38. package/build/BluetoothSdk.types.d.ts +11 -0
  39. package/build/BluetoothSdk.types.d.ts.map +1 -1
  40. package/build/BluetoothSdk.types.js.map +1 -1
  41. package/build/_private/BluetoothSdkModule.d.ts +2 -0
  42. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  43. package/build/_private/BluetoothSdkModule.js +3 -0
  44. package/build/_private/BluetoothSdkModule.js.map +1 -1
  45. package/build/generated/changelogCatalog.d.ts +1 -1
  46. package/build/generated/changelogCatalog.js +1 -1
  47. package/build/generated/changelogCatalog.js.map +1 -1
  48. package/build/generated/releaseMetadata.js +5 -5
  49. package/build/generated/releaseMetadata.js.map +1 -1
  50. package/build/index.d.ts.map +1 -1
  51. package/build/index.js +1 -0
  52. package/build/index.js.map +1 -1
  53. package/ios/BluetoothSdkModule.swift +5 -0
  54. package/ios/MentraBluetoothSDK.podspec +3 -2
  55. package/ios/Source/BluetoothSdkDefaults.swift +6 -1
  56. package/ios/Source/Bridge.swift +15 -2
  57. package/ios/Source/DeviceManager.swift +23 -10
  58. package/ios/Source/DeviceStore.swift +26 -7
  59. package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
  60. package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
  61. package/ios/Source/MentraBluetoothSDK.swift +4 -0
  62. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  63. package/ios/Source/sgcs/G2.swift +2 -2
  64. package/ios/Source/sgcs/MentraLive.swift +66 -3
  65. package/ios/Source/sgcs/Nimo.swift +182 -371
  66. package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
  67. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +219 -0
  68. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  69. package/ios/Source/sgcs/NimoCanvasSession.swift +150 -0
  70. package/ios/Source/sgcs/SGCManager.swift +24 -0
  71. package/ios/Source/utils/G2Text.swift +11 -0
  72. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  73. package/ios/Tests/G2TextTests.swift +10 -0
  74. package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
  75. package/ios/Tests/NimoCanvasSessionTests.swift +112 -0
  76. package/ios/Tests/NimoCanvasTransportTests.swift +161 -0
  77. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  78. package/package.json +2 -2
  79. package/src/BluetoothSdk.types.ts +11 -0
  80. package/src/_private/BluetoothSdkModule.ts +6 -0
  81. package/src/generated/changelogCatalog.ts +1 -1
  82. package/src/generated/releaseMetadata.ts +5 -5
  83. package/src/index.ts +1 -0
@@ -0,0 +1,168 @@
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 active = false
13
+ private var held = false
14
+ private var locked = false
15
+ private var launchBlocked = false
16
+ private var waitingReadiness = false
17
+ private var observedNotReady = false
18
+ private var readinessRetries = 0
19
+ private var exitRequested = false
20
+ private var desired: ByteArray? = null
21
+ private var accepted: ByteArray? = null
22
+ private var rejected: ByteArray? = null
23
+ private var scope: String? = null
24
+ private var forceRevision = 0L
25
+ private var sentRevision = 0L
26
+ private var ticket = 0L
27
+ private var flight: Flight? = null
28
+
29
+ fun offer(frame: ByteArray, scope: String, force: Boolean = false): List<Action> {
30
+ require(frame.size in 3..(NimoCanvasCodec.MAX_STREAM - 8))
31
+ if (this.scope != scope || force) forceRevision++
32
+ this.scope = scope
33
+ desired = frame.copyOf()
34
+ exitRequested = false
35
+ return pump()
36
+ }
37
+
38
+ /** Debug capture seam; production behavior is unchanged unless explicitly held. */
39
+ fun hold(value: Boolean, resume: Boolean = true): List<Action> {
40
+ held = value
41
+ return if (!value && resume) pump() else emptyList()
42
+ }
43
+
44
+ fun currentScope(): String? = scope
45
+
46
+ fun readiness(value: Boolean): List<Action> = updateReadiness(value, confirmed = false)
47
+
48
+ /** Newly received device predicates, not a replay of cached heartbeat state.
49
+ * The caller validates the report and discards traffic from older bearers. */
50
+ fun confirmedReadiness(value: Boolean): List<Action> = updateReadiness(value, confirmed = true)
51
+
52
+ private fun updateReadiness(value: Boolean, confirmed: Boolean): List<Action> {
53
+ if (!value) readinessRetries = 0
54
+ if (!value && flight != null) return resetLink("Readiness lost during canvas command")
55
+ if (waitingReadiness) {
56
+ if (!value) observedNotReady = true
57
+ else if (!observedNotReady) {
58
+ if (!confirmed || readinessRetries >= 3) return emptyList()
59
+ readinessRetries++
60
+ }
61
+ }
62
+ ready = value
63
+ if (!value) { active = false; accepted = null }
64
+ if (value) waitingReadiness = false
65
+ return pump()
66
+ }
67
+
68
+ /** A new transport generation has no accepted frame. Retain only the latest desired scene. */
69
+ fun disconnected() {
70
+ ready = false; active = false; held = false; locked = false; launchBlocked = false
71
+ waitingReadiness = false; observedNotReady = false
72
+ readinessRetries = 0
73
+ accepted = null; rejected = null; flight = null; ticket++; forceRevision++
74
+ }
75
+
76
+ fun exit(): List<Action> {
77
+ desired = null; scope = null; accepted = null; rejected = null; exitRequested = true
78
+ return pump()
79
+ }
80
+
81
+ /** A local gesture/native app takeover suppresses replay until the host submits another scene. */
82
+ fun nativeApp(appId: Int, entered: Boolean): List<Action> {
83
+ if ((appId == NimoCanvasCodec.APP_ID && !entered) || (appId != NimoCanvasCodec.APP_ID && entered)) {
84
+ // The expected FD Exit report preserves newer host content. A different
85
+ // native app entering is a real takeover, even while our Exit waits for ACK.
86
+ if (flight?.key == 3) {
87
+ active = false; accepted = null
88
+ if (appId != NimoCanvasCodec.APP_ID && entered) {
89
+ desired = null; rejected = null; scope = null
90
+ }
91
+ return emptyList()
92
+ }
93
+ val ambiguous = flight != null
94
+ desired = null; accepted = null; rejected = null; active = false; exitRequested = false; scope = null
95
+ if (ambiguous) return resetLink("Native app transition interrupted canvas command")
96
+ }
97
+ return emptyList()
98
+ }
99
+
100
+ fun acceptsResponse(key: Int, payload: ByteArray): Boolean {
101
+ val current = flight ?: return false
102
+ if (current.key != key || payload.isEmpty()) return false
103
+ val status = payload[0].toInt() and 255
104
+ val echo = if (key == 4) byteArrayOf(0, NimoCanvasCodec.APP_ID.toByte(), 0, 0, 0)
105
+ else byteArrayOf(0, NimoCanvasCodec.APP_ID.toByte())
106
+ if (status == 0 && !payload.contentEquals(echo)) return false
107
+ if (status != 0 && payload.size != 1 &&
108
+ !(payload.size == echo.size && payload.copyOfRange(1, payload.size).contentEquals(echo.copyOfRange(1, echo.size)))) return false
109
+ return true
110
+ }
111
+
112
+ fun response(key: Int, payload: ByteArray): List<Action> {
113
+ if (!acceptsResponse(key, payload)) return emptyList()
114
+ val current = flight ?: return emptyList()
115
+ val status = payload[0].toInt() and 255
116
+ flight = null
117
+ if (status != 0) {
118
+ when (status) {
119
+ 7 -> { ready = false; waitingReadiness = true; observedNotReady = false; active = false; accepted = null }
120
+ 8 -> { locked = true; active = false; accepted = null }
121
+ 2 -> return listOf(Action.Rejected(status)) + resetLink("Canvas device timeout is ambiguous")
122
+ else -> {
123
+ if (key == 4) rejected = current.frame
124
+ else {
125
+ if (key == 1) launchBlocked = true else desired = null
126
+ active = false; exitRequested = false
127
+ }
128
+ }
129
+ }
130
+ return listOf(Action.Rejected(status)) + pump()
131
+ }
132
+ when (key) {
133
+ 1 -> active = true
134
+ 4 -> { accepted = current.frame; rejected = null; readinessRetries = 0 }
135
+ 3 -> { active = false; accepted = null; exitRequested = false }
136
+ }
137
+ return pump()
138
+ }
139
+
140
+ fun timeout(expectedTicket: Long): List<Action> {
141
+ if (flight?.ticket != expectedTicket) return emptyList()
142
+ return resetLink("Canvas ACK timeout; reconnect before reusing a command key")
143
+ }
144
+
145
+ private fun resetLink(reason: String): List<Action> {
146
+ ready = false; active = false; accepted = null; flight = null; ticket++; forceRevision++
147
+ return listOf(Action.Reconnect(reason))
148
+ }
149
+
150
+ private fun pump(): List<Action> {
151
+ if (!ready || held || locked || launchBlocked || waitingReadiness || flight != null) return emptyList()
152
+ val next = desired
153
+ val key = when {
154
+ exitRequested && active -> 3
155
+ exitRequested -> { exitRequested = false; return emptyList() }
156
+ next == null -> return emptyList()
157
+ !active -> 1
158
+ next.contentEquals(rejected) -> return emptyList()
159
+ next.contentEquals(accepted) && sentRevision == forceRevision -> return emptyList()
160
+ else -> 4
161
+ }
162
+ val frame = if (key == 4) next!!.copyOf() else ByteArray(0)
163
+ if (key == 4) sentRevision = forceRevision
164
+ val sending = Flight(key, frame, ++ticket)
165
+ flight = sending
166
+ return listOf(Action.Send(key, frame.copyOf(), sending.ticket))
167
+ }
168
+ }
@@ -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,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
+ }