@mentra/bluetooth-sdk 3.2.1-beta.319 → 3.2.1-beta.355
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +67 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +64 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +73 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +257 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +225 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +168 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +557 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +308 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +270 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +194 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +199 -0
- package/build/BluetoothSdk.types.d.ts +11 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +5 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +23 -10
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +4 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/G2.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +66 -3
- package/ios/Source/sgcs/Nimo.swift +182 -371
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +219 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +150 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +112 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +161 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +11 -0
- package/src/_private/BluetoothSdkModule.ts +6 -0
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- 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
|
+
}
|