@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.
- package/README.md +4 -4
- 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 +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- 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/requests/DisplayRequests.kt +1 -1
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -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 +238 -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 +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -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/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -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/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -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 +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -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 +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +13 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -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 +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- 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 +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +201 -156
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- 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$
|
|
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
|
|
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
|
+
}
|