@mentra/bluetooth-sdk 3.3.0-dev.317 → 3.3.0-dev.320
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/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/DeviceManager.kt +37 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +30 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3 -2
- 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 +8 -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/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/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +23 -10
- package/ios/Source/DeviceStore.swift +19 -7
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/sgcs/G2.swift +2 -2
- 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 +16 -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/package.json +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NimoCanvasSessionTest {
|
|
7
|
+
private val frameA = byteArrayOf(0, 0, 1)
|
|
8
|
+
private val frameB = byteArrayOf(1, 0, 1, 1)
|
|
9
|
+
private val frameC = byteArrayOf(2, 0, 1, 2)
|
|
10
|
+
private fun send(actions: List<NimoCanvasSession.Action>): NimoCanvasSession.Action.Send =
|
|
11
|
+
actions.single() as NimoCanvasSession.Action.Send
|
|
12
|
+
private fun success(session: NimoCanvasSession, key: Int) = session.response(key,
|
|
13
|
+
if (key == 4) byteArrayOf(0, 0xFD.toByte(), 0, 0, 0) else byteArrayOf(0, 0xFD.toByte()))
|
|
14
|
+
private fun launched(session: NimoCanvasSession): NimoCanvasSession.Action.Send {
|
|
15
|
+
assertTrue(session.offer(frameA, "one:1").isEmpty())
|
|
16
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
17
|
+
return send(success(session, 1))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test fun launchAckPrecedesUpdateAndLatestOfferWins() {
|
|
21
|
+
val session = NimoCanvasSession()
|
|
22
|
+
session.readiness(true)
|
|
23
|
+
assertEquals(1, send(session.offer(frameA, "one:1")).key)
|
|
24
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
25
|
+
assertTrue(session.offer(frameC, "one:1").isEmpty())
|
|
26
|
+
val update = send(success(session, 1))
|
|
27
|
+
assertEquals(4, update.key)
|
|
28
|
+
assertArrayEquals(frameC, update.frame)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test fun inflightUpdatesCoalesceAndDedupUsesActualBytes() {
|
|
32
|
+
val session = NimoCanvasSession()
|
|
33
|
+
launched(session)
|
|
34
|
+
session.offer(frameB, "one:1")
|
|
35
|
+
session.offer(frameC, "one:1")
|
|
36
|
+
assertArrayEquals(frameC, send(success(session, 4)).frame)
|
|
37
|
+
assertTrue(success(session, 4).isEmpty())
|
|
38
|
+
assertTrue(session.offer(frameC.copyOf(), "one:1").isEmpty())
|
|
39
|
+
assertArrayEquals(frameB, send(session.offer(frameB, "one:1")).frame)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test fun dedupDoesNotOverrideReversionWhileUpdateIsInFlight() {
|
|
43
|
+
val session = NimoCanvasSession()
|
|
44
|
+
launched(session); success(session, 4)
|
|
45
|
+
session.offer(frameB, "one:1")
|
|
46
|
+
session.offer(frameA, "one:1")
|
|
47
|
+
assertArrayEquals(frameA, send(success(session, 4)).frame)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test fun replayEpochAndReconnectForceAcceptedFrameAgain() {
|
|
51
|
+
val session = NimoCanvasSession()
|
|
52
|
+
launched(session); success(session, 4)
|
|
53
|
+
assertEquals(4, send(session.offer(frameA, "one:1", force = true)).key)
|
|
54
|
+
success(session, 4)
|
|
55
|
+
assertEquals(4, send(session.offer(frameA, "one:2")).key)
|
|
56
|
+
success(session, 4)
|
|
57
|
+
session.disconnected()
|
|
58
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
59
|
+
assertEquals(4, send(success(session, 1)).key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test fun malformedSuccessAndWrongCommandCannotAcknowledgeLaunch() {
|
|
63
|
+
val session = NimoCanvasSession()
|
|
64
|
+
session.offer(frameA, "one:1"); val launch = send(session.readiness(true))
|
|
65
|
+
assertTrue(session.response(1, byteArrayOf(0)).isEmpty())
|
|
66
|
+
assertTrue(session.response(1, byteArrayOf(0, 0x04)).isEmpty())
|
|
67
|
+
assertTrue(session.response(4, byteArrayOf(0, 0xFD.toByte(), 0, 0, 0)).isEmpty())
|
|
68
|
+
assertTrue(session.timeout(launch.ticket).single() is NimoCanvasSession.Action.Reconnect)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test fun timeoutRequiresFreshTransportBeforeLateAckCanDoAnything() {
|
|
72
|
+
val session = NimoCanvasSession()
|
|
73
|
+
val update = launched(session)
|
|
74
|
+
assertTrue(session.timeout(update.ticket).single() is NimoCanvasSession.Action.Reconnect)
|
|
75
|
+
assertTrue(success(session, 4).isEmpty())
|
|
76
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
77
|
+
session.disconnected()
|
|
78
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
79
|
+
assertTrue(session.timeout(update.ticket).isEmpty())
|
|
80
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test fun unsupportedAndInvalidDoNotRetryUnchangedFrame() {
|
|
84
|
+
for (status in listOf(1, 5, 6)) {
|
|
85
|
+
val session = NimoCanvasSession()
|
|
86
|
+
launched(session)
|
|
87
|
+
assertTrue(session.response(4, byteArrayOf(status.toByte())).single() is NimoCanvasSession.Action.Rejected)
|
|
88
|
+
assertTrue(session.offer(frameA, "one:1").isEmpty())
|
|
89
|
+
assertTrue(session.offer(frameA, "one:2", force = true).isEmpty())
|
|
90
|
+
assertArrayEquals(frameB, send(session.offer(frameB, "one:1")).frame)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test fun failedLaunchDoesNotRetryIdenticalLaunchForChangingCaptions() {
|
|
95
|
+
for (status in listOf(1, 5, 6)) {
|
|
96
|
+
val session = NimoCanvasSession()
|
|
97
|
+
session.offer(frameA, "one:1"); session.readiness(true)
|
|
98
|
+
assertTrue(session.response(1, byteArrayOf(status.toByte())).single() is NimoCanvasSession.Action.Rejected)
|
|
99
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
100
|
+
assertTrue(session.offer(frameC, "one:2", true).isEmpty())
|
|
101
|
+
session.disconnected()
|
|
102
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test fun readinessErrorWaitsForExplicitReadySignal() {
|
|
107
|
+
val session = NimoCanvasSession()
|
|
108
|
+
launched(session)
|
|
109
|
+
assertTrue(session.response(4, byteArrayOf(7)).single() is NimoCanvasSession.Action.Rejected)
|
|
110
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
111
|
+
assertTrue(session.readiness(true).isEmpty())
|
|
112
|
+
assertTrue(session.readiness(false).isEmpty())
|
|
113
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
114
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test fun freshConfirmedReadinessReleasesNotReadyWithoutFalseEdge() {
|
|
118
|
+
for (key in listOf(1, 4)) {
|
|
119
|
+
val session = NimoCanvasSession()
|
|
120
|
+
session.offer(frameA, "one:1"); session.readiness(true)
|
|
121
|
+
if (key == 4) success(session, 1)
|
|
122
|
+
assertTrue(session.response(key, byteArrayOf(7)).single() is NimoCanvasSession.Action.Rejected)
|
|
123
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
124
|
+
assertTrue("cached true must not release status 7", session.readiness(true).isEmpty())
|
|
125
|
+
assertEquals(1, send(session.confirmedReadiness(true)).key)
|
|
126
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test fun confirmedNotReadyStaysBlockedUntilConfirmedReady() {
|
|
131
|
+
val session = NimoCanvasSession()
|
|
132
|
+
launched(session)
|
|
133
|
+
session.response(4, byteArrayOf(7))
|
|
134
|
+
assertTrue(session.confirmedReadiness(false).isEmpty())
|
|
135
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
136
|
+
assertEquals(1, send(session.confirmedReadiness(true)).key)
|
|
137
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test fun repeatedReadyButRejectedLaunchHasBoundedRecovery() {
|
|
141
|
+
val session = NimoCanvasSession()
|
|
142
|
+
session.offer(frameA, "one:1"); session.readiness(true)
|
|
143
|
+
repeat(3) {
|
|
144
|
+
session.response(1, byteArrayOf(7))
|
|
145
|
+
assertEquals(1, send(session.confirmedReadiness(true)).key)
|
|
146
|
+
assertTrue("duplicate heartbeat cannot overlap Launch", session.confirmedReadiness(true).isEmpty())
|
|
147
|
+
}
|
|
148
|
+
session.response(1, byteArrayOf(7))
|
|
149
|
+
repeat(5) { assertTrue("ready-only recovery must stop after three retries", session.confirmedReadiness(true).isEmpty()) }
|
|
150
|
+
session.offer(frameB, "one:2", true)
|
|
151
|
+
assertTrue(session.confirmedReadiness(true).isEmpty())
|
|
152
|
+
session.readiness(false)
|
|
153
|
+
assertEquals(1, send(session.confirmedReadiness(true)).key)
|
|
154
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
155
|
+
success(session, 4)
|
|
156
|
+
session.offer(frameC, "one:2")
|
|
157
|
+
session.response(4, byteArrayOf(7))
|
|
158
|
+
assertEquals(1, send(session.confirmedReadiness(true)).key)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test fun confirmedReadyDoesNotUnlockRejectedLaunchOrDeviceLock() {
|
|
162
|
+
for (status in listOf(1, 5, 6, 8)) {
|
|
163
|
+
val session = NimoCanvasSession()
|
|
164
|
+
session.offer(frameA, "one:1"); session.readiness(true)
|
|
165
|
+
session.response(1, byteArrayOf(status.toByte()))
|
|
166
|
+
assertTrue(session.offer(frameB, "one:2", true).isEmpty())
|
|
167
|
+
assertTrue(session.confirmedReadiness(false).isEmpty())
|
|
168
|
+
assertTrue(session.confirmedReadiness(true).isEmpty())
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test fun deviceLockedStopsAllCanvasUntilNewTransport() {
|
|
173
|
+
val session = NimoCanvasSession()
|
|
174
|
+
launched(session)
|
|
175
|
+
session.response(4, byteArrayOf(8))
|
|
176
|
+
assertTrue(session.readiness(true).isEmpty())
|
|
177
|
+
assertTrue(session.confirmedReadiness(true).isEmpty())
|
|
178
|
+
assertTrue(session.offer(frameB, "one:2", true).isEmpty())
|
|
179
|
+
session.disconnected()
|
|
180
|
+
assertEquals(1, send(session.readiness(true)).key)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Test fun statusTwoResetsTransportInsteadOfBlindRetry() {
|
|
184
|
+
val session = NimoCanvasSession()
|
|
185
|
+
launched(session)
|
|
186
|
+
val actions = session.response(4, byteArrayOf(2))
|
|
187
|
+
assertEquals(2, actions.size)
|
|
188
|
+
assertTrue(actions.last() is NimoCanvasSession.Action.Reconnect)
|
|
189
|
+
assertTrue(session.offer(frameB, "one:1").isEmpty())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@Test fun explicitExitWaitsForCurrentCommandAndDoesNotReplayOldFrame() {
|
|
193
|
+
val session = NimoCanvasSession()
|
|
194
|
+
launched(session)
|
|
195
|
+
assertTrue(session.exit().isEmpty())
|
|
196
|
+
assertEquals(3, send(success(session, 4)).key)
|
|
197
|
+
assertTrue(session.nativeApp(0xFD, false).isEmpty())
|
|
198
|
+
assertTrue(success(session, 3).isEmpty())
|
|
199
|
+
session.disconnected()
|
|
200
|
+
assertTrue(session.readiness(true).isEmpty())
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@Test fun newSceneDuringExitLaunchesOnlyAfterExitAck() {
|
|
204
|
+
val session = NimoCanvasSession()
|
|
205
|
+
launched(session); success(session, 4)
|
|
206
|
+
assertEquals(3, send(session.exit()).key)
|
|
207
|
+
assertTrue(session.offer(frameB, "two:1").isEmpty())
|
|
208
|
+
assertTrue(session.nativeApp(0xFD, false).isEmpty())
|
|
209
|
+
assertEquals(1, send(success(session, 3)).key)
|
|
210
|
+
assertArrayEquals(frameB, send(success(session, 1)).frame)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Test fun actualNativeTakeoverDuringExitDiscardsNewerScene() {
|
|
214
|
+
val session = NimoCanvasSession()
|
|
215
|
+
launched(session); success(session, 4)
|
|
216
|
+
assertEquals(3, send(session.exit()).key)
|
|
217
|
+
session.offer(frameB, "two:1")
|
|
218
|
+
assertTrue(session.nativeApp(0, true).isEmpty())
|
|
219
|
+
assertTrue(success(session, 3).isEmpty())
|
|
220
|
+
session.disconnected()
|
|
221
|
+
assertTrue(session.readiness(true).isEmpty())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@Test fun nativeTakeoverSuppressesReplayUntilNewHostFrame() {
|
|
225
|
+
val session = NimoCanvasSession()
|
|
226
|
+
launched(session); success(session, 4)
|
|
227
|
+
assertTrue(session.nativeApp(0, true).isEmpty())
|
|
228
|
+
assertTrue(session.readiness(true).isEmpty())
|
|
229
|
+
assertEquals(1, send(session.offer(frameA, "one:1")).key)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Test fun nativeTakeoverOrReadinessLossDuringCommandResets() {
|
|
233
|
+
for (takeover in listOf(true, false)) {
|
|
234
|
+
val session = NimoCanvasSession(); launched(session)
|
|
235
|
+
val actions = if (takeover) session.nativeApp(0, true) else session.readiness(false)
|
|
236
|
+
assertTrue(actions.single() is NimoCanvasSession.Action.Reconnect)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Test fun offersCopyMutableInputAndRejectOversizedFrame() {
|
|
241
|
+
val session = NimoCanvasSession()
|
|
242
|
+
val input = frameA.copyOf(); session.offer(input, "one:1"); input[0] = 99
|
|
243
|
+
session.readiness(true)
|
|
244
|
+
assertArrayEquals(frameA, send(success(session, 1)).frame)
|
|
245
|
+
assertThrows(IllegalArgumentException::class.java) { session.offer(ByteArray(12281), "one:1") }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@Test fun diagnosticHoldRetainsOnlyLatestSceneUntilReleased() {
|
|
249
|
+
val session = NimoCanvasSession()
|
|
250
|
+
launched(session); success(session, 4)
|
|
251
|
+
|
|
252
|
+
assertTrue(session.hold(true).isEmpty())
|
|
253
|
+
assertTrue(session.offer(frameB, "two:1").isEmpty())
|
|
254
|
+
assertTrue(session.offer(frameC, "two:2", force = true).isEmpty())
|
|
255
|
+
|
|
256
|
+
val update = send(session.hold(false))
|
|
257
|
+
assertEquals(4, update.key)
|
|
258
|
+
assertArrayEquals(frameC, update.frame)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Test fun preemptedHoldDoesNotFlushStaleSceneBeforeExit() {
|
|
262
|
+
val session = NimoCanvasSession()
|
|
263
|
+
launched(session); success(session, 4)
|
|
264
|
+
session.hold(true)
|
|
265
|
+
session.offer(frameB, "two:1")
|
|
266
|
+
|
|
267
|
+
assertTrue(session.hold(false, resume = false).isEmpty())
|
|
268
|
+
assertEquals(3, send(session.exit()).key)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import androidx.test.core.app.ApplicationProvider
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.util.concurrent.TimeUnit
|
|
9
|
+
import org.json.JSONObject
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.Assert.*
|
|
12
|
+
import org.junit.Before
|
|
13
|
+
import org.junit.Test
|
|
14
|
+
import org.junit.runner.RunWith
|
|
15
|
+
import org.robolectric.RobolectricTestRunner
|
|
16
|
+
import org.robolectric.Shadows
|
|
17
|
+
import org.robolectric.annotation.Config
|
|
18
|
+
import org.robolectric.annotation.LooperMode
|
|
19
|
+
|
|
20
|
+
@RunWith(RobolectricTestRunner::class)
|
|
21
|
+
@Config(sdk = [33])
|
|
22
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
23
|
+
class NimoDiagnosticsTest {
|
|
24
|
+
private data class Write(val packet: ByteArray, val completed: () -> Unit)
|
|
25
|
+
private val writes = mutableListOf<Write>()
|
|
26
|
+
private var heldReady: (() -> Unit)? = null
|
|
27
|
+
private val holdReleases = mutableListOf<Boolean>()
|
|
28
|
+
private lateinit var diagnostics: NimoDiagnostics
|
|
29
|
+
private lateinit var root: File
|
|
30
|
+
private lateinit var previousDirectories: Set<String>
|
|
31
|
+
|
|
32
|
+
@Before fun setup() {
|
|
33
|
+
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
34
|
+
root = File(context.filesDir, "nimo-framebuffer")
|
|
35
|
+
previousDirectories = root.listFiles()?.map { it.name }?.toSet() ?: emptySet()
|
|
36
|
+
diagnostics = NimoDiagnostics(
|
|
37
|
+
context,
|
|
38
|
+
Handler(Looper.getMainLooper()),
|
|
39
|
+
{ frame, complete -> writes.add(Write(frame.copyOf(), complete)); true },
|
|
40
|
+
{ ready -> heldReady = ready },
|
|
41
|
+
{ resume -> holdReleases += resume },
|
|
42
|
+
)
|
|
43
|
+
diagnostics.connected(512)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@After fun dispose() { diagnostics.close() }
|
|
47
|
+
|
|
48
|
+
@Test fun probePreservesNotReadyAndAcceptsResponsesBeforeWriteCallbacks() {
|
|
49
|
+
start("probe")
|
|
50
|
+
driveToPageGate()
|
|
51
|
+
assertTrue(diagnostics.onPacket(fbpReply(writes[3].packet, ByteArray(240))))
|
|
52
|
+
val result = receipt()
|
|
53
|
+
assertTrue(result.getBoolean("probe_ok"))
|
|
54
|
+
assertFalse(result.getBoolean("ok"))
|
|
55
|
+
assertTrue(result.isNull("error"))
|
|
56
|
+
assertEquals(0, result.getJSONObject("readiness").getInt("ready"))
|
|
57
|
+
assertEquals(1, result.getJSONObject("readiness").getInt("tws"))
|
|
58
|
+
assertEquals(NimoFramebufferProtocol.EXPECTED_VERSION, result.getString("runtime_version"))
|
|
59
|
+
assertEquals(8, result.getInt("wire_events"))
|
|
60
|
+
assertEquals(8, File(captureDirectory(), "wire.jsonl").readLines().size)
|
|
61
|
+
writes.toList().forEach { it.completed() }
|
|
62
|
+
idle(4_000)
|
|
63
|
+
assertEquals(4, writes.size)
|
|
64
|
+
assertTrue(receipt().getBoolean("probe_ok"))
|
|
65
|
+
assertTrue(writes.all { it.packet[8] == 2.toByte() && (it.packet[9].toInt() and 255) in listOf(0x0B, 0x19, 0x15) })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test fun duplicateTriggerDoesNotStartAnotherRead() {
|
|
69
|
+
start("capture")
|
|
70
|
+
start("probe")
|
|
71
|
+
assertEquals(1, writes.size)
|
|
72
|
+
assertEquals(1, newDirectories().size)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test fun disconnectCancelsPendingTimeoutAndIgnoresLateWriteCompletion() {
|
|
76
|
+
start("capture")
|
|
77
|
+
writes.single().completed()
|
|
78
|
+
diagnostics.disconnected()
|
|
79
|
+
val error = receipt().getString("error")
|
|
80
|
+
assertTrue(error.contains("disconnected"))
|
|
81
|
+
diagnostics.connected(512)
|
|
82
|
+
writes.single().completed()
|
|
83
|
+
idle(4_000)
|
|
84
|
+
assertEquals(error, receipt().getString("error"))
|
|
85
|
+
assertEquals(1, writes.size)
|
|
86
|
+
assertFalse(diagnostics.onPacket(versionReply()))
|
|
87
|
+
assertFalse(receipt().getBoolean("ok"))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test fun timeoutStartsAtWriteCompletionAndDoesNotRetry() {
|
|
91
|
+
start("probe")
|
|
92
|
+
idle(5_000)
|
|
93
|
+
assertFalse(File(captureDirectory(), "session.json").exists())
|
|
94
|
+
writes.single().completed()
|
|
95
|
+
idle(2_900)
|
|
96
|
+
assertFalse(File(captureDirectory(), "session.json").exists())
|
|
97
|
+
idle(200)
|
|
98
|
+
assertTrue(receipt().getString("error").contains("response timeout"))
|
|
99
|
+
assertEquals(1, writes.size)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test fun captureRejectsCrcBeforeRequestingBulkPages() {
|
|
103
|
+
start("capture")
|
|
104
|
+
driveToPageGate()
|
|
105
|
+
val corrupted = fbpReply(writes[3].packet, ByteArray(240)).also { it[4] = (it[4].toInt() xor 1).toByte() }
|
|
106
|
+
assertTrue(diagnostics.onPacket(corrupted))
|
|
107
|
+
assertFalse(receipt().getBoolean("ok"))
|
|
108
|
+
assertFalse(receipt().getBoolean("probe_ok"))
|
|
109
|
+
assertTrue(receipt().getString("error").contains("CRC mismatch"))
|
|
110
|
+
assertEquals(4, writes.size)
|
|
111
|
+
assertEquals(0, receipt().getJSONArray("banks").length())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test fun totalDeadlineBoundsAWriteThatNeverCompletes() {
|
|
115
|
+
start("capture")
|
|
116
|
+
idle(600_001)
|
|
117
|
+
assertTrue(receipt().getString("error").contains("10-minute budget"))
|
|
118
|
+
writes.single().completed()
|
|
119
|
+
idle(4_000)
|
|
120
|
+
assertEquals(1, writes.size)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Test fun heldSampleBudgetIncludesCanvasDrainAndStaleReadyCannotStartCapture() {
|
|
124
|
+
start("held-sample")
|
|
125
|
+
assertTrue(writes.isEmpty())
|
|
126
|
+
assertNotNull(heldReady)
|
|
127
|
+
|
|
128
|
+
idle(15_001)
|
|
129
|
+
|
|
130
|
+
assertEquals(listOf(true), holdReleases)
|
|
131
|
+
assertTrue(receipt().getString("error").contains("15-second budget"))
|
|
132
|
+
heldReady?.invoke()
|
|
133
|
+
assertTrue(writes.isEmpty())
|
|
134
|
+
assertEquals(listOf(true), holdReleases)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test fun disconnectBeforeHeldCaptureIsReadyReleasesWithoutResumingCanvas() {
|
|
138
|
+
start("held-capture")
|
|
139
|
+
|
|
140
|
+
diagnostics.disconnected()
|
|
141
|
+
|
|
142
|
+
assertEquals(listOf(false), holdReleases)
|
|
143
|
+
assertTrue(receipt().getString("error").contains("disconnected"))
|
|
144
|
+
heldReady?.invoke()
|
|
145
|
+
assertTrue(writes.isEmpty())
|
|
146
|
+
assertEquals(listOf(false), holdReleases)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test fun duplicateHeldCaptureDoesNotReplaceHoldOrDeadline() {
|
|
150
|
+
start("held-sample")
|
|
151
|
+
val firstReady = heldReady
|
|
152
|
+
|
|
153
|
+
start("held-capture")
|
|
154
|
+
|
|
155
|
+
assertSame(firstReady, heldReady)
|
|
156
|
+
assertEquals(1, newDirectories().size)
|
|
157
|
+
idle(15_001)
|
|
158
|
+
assertEquals(listOf(true), holdReleases)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test fun insufficientCapacityFailsWithoutSendingDiagnosticCommands() {
|
|
162
|
+
diagnostics.connected(20)
|
|
163
|
+
start("capture")
|
|
164
|
+
assertEquals(0, writes.size)
|
|
165
|
+
assertTrue(receipt().getString("error").contains("Write capacity"))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Test fun samplePinsPreparedBankAndNeverClaimsStaticPairVerification() {
|
|
169
|
+
assertSample(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test fun sampleAlsoPinsPreparedBankZero() {
|
|
173
|
+
assertSample(0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test fun heldSampleStartsOnlyAfterDrainAndReleasesOnSuccess() {
|
|
177
|
+
assertSample(1, "held-sample")
|
|
178
|
+
assertEquals(listOf(true), holdReleases)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private fun assertSample(bank: Int, mode: String = "sample") {
|
|
182
|
+
val state = byteArrayOf((1 - bank).toByte(), bank.toByte(), 0, 0)
|
|
183
|
+
start(mode)
|
|
184
|
+
if (mode == "held-sample") {
|
|
185
|
+
assertTrue(writes.isEmpty())
|
|
186
|
+
requireNotNull(heldReady).invoke()
|
|
187
|
+
}
|
|
188
|
+
driveToPageGate()
|
|
189
|
+
diagnostics.onPacket(fbpReply(writes.last().packet, ByteArray(240), state))
|
|
190
|
+
// The gate always probes bank0/page0; the sample follows its prepared selector.
|
|
191
|
+
repeat(315) { page ->
|
|
192
|
+
val request = writes.last().packet
|
|
193
|
+
assertEquals(bank, request[21].toInt())
|
|
194
|
+
assertEquals(page, (request[22].toInt() and 255) or ((request[23].toInt() and 255) shl 8))
|
|
195
|
+
diagnostics.onPacket(fbpReply(request, ByteArray(240) { page.toByte() }, state))
|
|
196
|
+
}
|
|
197
|
+
val result = receipt()
|
|
198
|
+
assertTrue(result.getBoolean("sample_ok"))
|
|
199
|
+
assertFalse(result.getBoolean("ok"))
|
|
200
|
+
assertFalse(result.getBoolean("atomic"))
|
|
201
|
+
assertFalse(result.getBoolean("optical"))
|
|
202
|
+
assertEquals("non_atomic_single_pass", result.getString("evidence_kind"))
|
|
203
|
+
assertEquals(0, result.getInt("static_pairs_verified"))
|
|
204
|
+
assertEquals(1, result.getJSONArray("banks").length())
|
|
205
|
+
assertEquals(319, writes.size)
|
|
206
|
+
assertEquals(638, result.getInt("wire_events"))
|
|
207
|
+
assertFalse(result.getBoolean("pixel_stability_verified"))
|
|
208
|
+
assertEquals(mode, result.getString("mode"))
|
|
209
|
+
assertEquals(75_600, File(captureDirectory(), "bank$bank-pass1.bin").length().toInt())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@Test fun sampleRejectsSelectorChangeBetweenGateAndFirstPageWithoutRetrying() {
|
|
213
|
+
start("sample")
|
|
214
|
+
driveToPageGate()
|
|
215
|
+
diagnostics.onPacket(fbpReply(writes.last().packet, ByteArray(240)))
|
|
216
|
+
diagnostics.onPacket(fbpReply(writes.last().packet, ByteArray(240), byteArrayOf(1, 0, 0, 0)))
|
|
217
|
+
val result = receipt()
|
|
218
|
+
assertFalse(result.getBoolean("sample_ok"))
|
|
219
|
+
assertFalse(result.getBoolean("ok"))
|
|
220
|
+
assertTrue(result.getString("error").contains("Sample metadata differs from page gate"))
|
|
221
|
+
assertEquals(5, writes.size)
|
|
222
|
+
assertEquals(0, result.getJSONArray("banks").length())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Test fun sampleTimeoutCannotProduceSuccessOrStartASecondPass() {
|
|
226
|
+
start("sample")
|
|
227
|
+
driveToPageGate()
|
|
228
|
+
diagnostics.onPacket(fbpReply(writes.last().packet, ByteArray(240)))
|
|
229
|
+
writes.last().completed()
|
|
230
|
+
idle(3_001)
|
|
231
|
+
assertFalse(receipt().getBoolean("sample_ok"))
|
|
232
|
+
assertFalse(receipt().getBoolean("ok"))
|
|
233
|
+
assertEquals(5, writes.size)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@Test fun fullCaptureStillRequiresFourCompleteReadsAndTwoStaticPairs() {
|
|
237
|
+
start("capture")
|
|
238
|
+
driveToPageGate()
|
|
239
|
+
diagnostics.onPacket(fbpReply(writes.last().packet, ByteArray(240)))
|
|
240
|
+
repeat(4) { pass ->
|
|
241
|
+
repeat(315) { page ->
|
|
242
|
+
val request = writes.last().packet
|
|
243
|
+
assertEquals(pass / 2, request[21].toInt())
|
|
244
|
+
diagnostics.onPacket(fbpReply(request, ByteArray(240) { page.toByte() }))
|
|
245
|
+
}
|
|
246
|
+
if (pass < 3) assertFalse(File(captureDirectory(), "session.json").exists())
|
|
247
|
+
}
|
|
248
|
+
assertTrue(receipt().getBoolean("ok"))
|
|
249
|
+
assertEquals(2, receipt().getInt("static_pairs_verified"))
|
|
250
|
+
assertEquals(4, receipt().getJSONArray("banks").length())
|
|
251
|
+
assertEquals(1264, writes.size)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun driveToPageGate() {
|
|
255
|
+
assertEquals(1, writes.size)
|
|
256
|
+
assertFalse(diagnostics.onPacket(versionReply()))
|
|
257
|
+
assertEquals(2, writes.size)
|
|
258
|
+
assertFalse(diagnostics.onPacket(frame(0x19, byteArrayOf(0, 0, 1, 0))))
|
|
259
|
+
assertEquals(3, writes.size)
|
|
260
|
+
assertTrue(diagnostics.onPacket(fbpReply(writes[2].packet, "537cf1p1".toByteArray())))
|
|
261
|
+
assertEquals(4, writes.size)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Exercise the production coordinator without pretending Robolectric proves Android's
|
|
265
|
+
// signature-permission enforcement for the ADB receiver. That boundary needs a device test.
|
|
266
|
+
private fun start(mode: String) {
|
|
267
|
+
NimoDiagnostics::class.java.getDeclaredMethod("start", String::class.java).apply { isAccessible = true }
|
|
268
|
+
.invoke(diagnostics, mode)
|
|
269
|
+
}
|
|
270
|
+
private fun idle(milliseconds: Long) = Shadows.shadowOf(Looper.getMainLooper()).idleFor(milliseconds, TimeUnit.MILLISECONDS)
|
|
271
|
+
private fun newDirectories() = root.listFiles()?.filter { it.name !in previousDirectories } ?: emptyList()
|
|
272
|
+
private fun captureDirectory() = newDirectories().single()
|
|
273
|
+
private fun receipt() = JSONObject(File(captureDirectory(), "session.json").readText())
|
|
274
|
+
private fun versionReply() = frame(0x0B, byteArrayOf(0) + NimoFramebufferProtocol.EXPECTED_VERSION.toByteArray())
|
|
275
|
+
|
|
276
|
+
private fun fbpReply(request: ByteArray, data: ByteArray, state: ByteArray = byteArrayOf(0, 1, 0, 0)): ByteArray {
|
|
277
|
+
val payload = byteArrayOf(0) + "FBP1".toByteArray() + request.copyOfRange(16, 24) + state + state +
|
|
278
|
+
byteArrayOf(data.size.toByte(), (data.size shr 8).toByte()) + data
|
|
279
|
+
return frame(0x15, payload)
|
|
280
|
+
}
|
|
281
|
+
private fun frame(key: Int, payload: ByteArray): ByteArray {
|
|
282
|
+
val body = byteArrayOf(2, key.toByte(), payload.size.toByte(), (payload.size shr 8).toByte()) + payload
|
|
283
|
+
val crc = NimoCanvasCodec.crc16(body)
|
|
284
|
+
return byteArrayOf(0xBF.toByte(), 0, body.size.toByte(), (body.size shr 8).toByte(),
|
|
285
|
+
crc.toByte(), (crc shr 8).toByte(), 0, 0) + body
|
|
286
|
+
}
|
|
287
|
+
}
|
package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NimoFramebufferProtocolTest {
|
|
7
|
+
@Test fun hexPreservesEveryByteAndEmptyEvidence() {
|
|
8
|
+
val bytes = ByteArray(256) { it.toByte() }
|
|
9
|
+
assertEquals(bytes.joinToString("") { "%02x".format(it.toInt() and 255) }, NimoFramebufferProtocol.hex(bytes))
|
|
10
|
+
assertEquals("", NimoFramebufferProtocol.hex(byteArrayOf()))
|
|
11
|
+
}
|
|
12
|
+
private val pingRequest = NimoFramebufferProtocol.Request(0x01020304L, 0, 0, 0)
|
|
13
|
+
// Fixed vectors produced independently by the existing Python FBP1 codec.
|
|
14
|
+
private val ping = hex("bf0023008234000002151f0000464250310403020100000000000100000001000008003533376366317031")
|
|
15
|
+
private val page = hex("bf000b0175e500000215070100464250310403020101013a010001000000010000f000") +
|
|
16
|
+
ByteArray(240) { it.toByte() }
|
|
17
|
+
|
|
18
|
+
@Test fun requestAndReplyMatchIndependentGoldenVectors() {
|
|
19
|
+
assertArrayEquals(hex("bf001000227a000002150c00464250310403020100000000"),
|
|
20
|
+
NimoFramebufferProtocol.requestFrame(pingRequest))
|
|
21
|
+
val reply = NimoFramebufferProtocol.reply(ping, pingRequest)
|
|
22
|
+
assertArrayEquals("537cf1p1".toByteArray(), reply.data)
|
|
23
|
+
assertEquals(NimoFramebufferProtocol.State(0, 1, 0, 0), reply.before)
|
|
24
|
+
val expectedPage = NimoFramebufferProtocol.Request(0x01020304L, 1, 1, 314)
|
|
25
|
+
assertArrayEquals(ByteArray(240) { it.toByte() }, NimoFramebufferProtocol.reply(page, expectedPage).data)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test fun rejectsCrcLengthsDirectionIndexAndCommand() {
|
|
29
|
+
for (offset in listOf(0, 1, 2, 4, 6, 8, 9, 10)) {
|
|
30
|
+
val invalid = ping.copyOf().also { it[offset] = (it[offset].toInt() xor 1).toByte() }
|
|
31
|
+
rejects { NimoFramebufferProtocol.reply(invalid, pingRequest) }
|
|
32
|
+
}
|
|
33
|
+
rejects { NimoFramebufferProtocol.reply(ping + 0, pingRequest) }
|
|
34
|
+
rejects { NimoFramebufferProtocol.reply(ping.copyOf(ping.size - 1), pingRequest) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test fun rejectsWrongTagMagicNonceOperationBankAndPage() {
|
|
38
|
+
for (offset in listOf(13, 17, 21, 22, 23, 33, 35)) {
|
|
39
|
+
val invalid = changeAndRecrc(ping, offset, ping[offset].toInt() xor 1)
|
|
40
|
+
rejects { NimoFramebufferProtocol.reply(invalid, pingRequest) }
|
|
41
|
+
}
|
|
42
|
+
rejects { NimoFramebufferProtocol.reply(ping, pingRequest.copy(nonce = 9)) }
|
|
43
|
+
rejects { NimoFramebufferProtocol.reply(ping, pingRequest.copy(op = 1)) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test fun rejectsStatusErrorsEvenWithValidCorrelation() {
|
|
47
|
+
for (status in listOf(1, 6, 7, 255)) {
|
|
48
|
+
val payload = ping.copyOfRange(12, 35).also { it[0] = status.toByte(); it[21] = 0; it[22] = 0 }
|
|
49
|
+
rejects { NimoFramebufferProtocol.reply(frame(0x15, payload), pingRequest) }
|
|
50
|
+
}
|
|
51
|
+
rejects { NimoFramebufferProtocol.reply(changeAndRecrc(ping, 12, 6), pingRequest) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test fun rejectsMetadataChangedWithinAndAcrossPagesAndOutOfOrder() {
|
|
55
|
+
val request = NimoFramebufferProtocol.Request(0x01020304L, 1, 1, 0)
|
|
56
|
+
val first = NimoFramebufferProtocol.reply(pageAt(0), request)
|
|
57
|
+
val read = NimoFramebufferProtocol.BankRead(request.nonce, request.bank)
|
|
58
|
+
read.add(first)
|
|
59
|
+
rejects { read.add(first) }
|
|
60
|
+
rejects { NimoFramebufferProtocol.BankRead(request.nonce, request.bank).add(first.copy(page = 1)) }
|
|
61
|
+
rejects { NimoFramebufferProtocol.reply(changeAndRecrc(pageAt(0), 29, 1), request) }
|
|
62
|
+
val changed = first.copy(page = 1, before = first.before.copy(dirty = 1), after = first.after.copy(dirty = 1))
|
|
63
|
+
val changedRead = NimoFramebufferProtocol.BankRead(request.nonce, request.bank)
|
|
64
|
+
changedRead.add(first)
|
|
65
|
+
rejects { changedRead.add(changed) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test fun bankRequiresEveryPageAndIndependentEqualSecondPass() {
|
|
69
|
+
val first = completeBank(10)
|
|
70
|
+
val second = completeBank(11)
|
|
71
|
+
NimoFramebufferProtocol.verifyPair(first, second)
|
|
72
|
+
assertEquals(75600, first.bytes().size)
|
|
73
|
+
rejects { NimoFramebufferProtocol.verifyPair(first, first) }
|
|
74
|
+
rejects { NimoFramebufferProtocol.verifyPair(first, completeBank(12, changedLastPixel = true)) }
|
|
75
|
+
rejects { NimoFramebufferProtocol.BankRead(20, 1).bytes() }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test fun requestLimitsPreventArbitraryCommandsOrAddresses() {
|
|
79
|
+
for (request in listOf(pingRequest.copy(nonce = -1), pingRequest.copy(nonce = 0x100000000L),
|
|
80
|
+
pingRequest.copy(op = 2), pingRequest.copy(bank = 2), pingRequest.copy(page = 1),
|
|
81
|
+
pingRequest.copy(op = 1, page = 315))) {
|
|
82
|
+
rejects { NimoFramebufferProtocol.requestFrame(request) }
|
|
83
|
+
}
|
|
84
|
+
rejects { NimoFramebufferProtocol.queryFrame(0x0A) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test fun basicResponsesRequireExactVersionAndReadinessShape() {
|
|
88
|
+
val version = byteArrayOf(0) + NimoFramebufferProtocol.EXPECTED_VERSION.toByteArray()
|
|
89
|
+
assertEquals(NimoFramebufferProtocol.EXPECTED_VERSION,
|
|
90
|
+
NimoFramebufferProtocol.version(frame(0x0B, version)))
|
|
91
|
+
rejects { NimoFramebufferProtocol.version(frame(0x0B, version + 0)) }
|
|
92
|
+
rejects { NimoFramebufferProtocol.version(frame(0x0B, version.copyOf().also { it[0] = 7 })) }
|
|
93
|
+
assertEquals(listOf(0, 0, 1, 0), NimoFramebufferProtocol.readiness(frame(0x19, byteArrayOf(0, 0, 1, 0))))
|
|
94
|
+
rejects { NimoFramebufferProtocol.readiness(frame(0x19, byteArrayOf(0, 1, 1))) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun completeBank(nonce: Long, changedLastPixel: Boolean = false): NimoFramebufferProtocol.BankRead {
|
|
98
|
+
val read = NimoFramebufferProtocol.BankRead(nonce, 1)
|
|
99
|
+
repeat(315) { index ->
|
|
100
|
+
val original = NimoFramebufferProtocol.reply(pageAt(index),
|
|
101
|
+
NimoFramebufferProtocol.Request(0x01020304L, 1, 1, index))
|
|
102
|
+
val data = original.data.copyOf()
|
|
103
|
+
if (changedLastPixel && index == 314) data[239] = 0
|
|
104
|
+
read.add(original.copy(nonce = nonce, data = data))
|
|
105
|
+
}
|
|
106
|
+
return read
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun pageAt(index: Int): ByteArray {
|
|
110
|
+
val result = page.copyOf()
|
|
111
|
+
result[23] = index.toByte(); result[24] = (index shr 8).toByte()
|
|
112
|
+
return recrc(result)
|
|
113
|
+
}
|
|
114
|
+
private fun changeAndRecrc(value: ByteArray, index: Int, byte: Int) =
|
|
115
|
+
recrc(value.copyOf().also { it[index] = byte.toByte() })
|
|
116
|
+
private fun recrc(value: ByteArray): ByteArray {
|
|
117
|
+
val crc = NimoCanvasCodec.crc16(value.copyOfRange(8, value.size))
|
|
118
|
+
value[4] = crc.toByte(); value[5] = (crc shr 8).toByte()
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
private fun frame(key: Int, payload: ByteArray): ByteArray {
|
|
122
|
+
val body = byteArrayOf(2, key.toByte(), payload.size.toByte(), (payload.size shr 8).toByte()) + payload
|
|
123
|
+
return recrc(byteArrayOf(0xBF.toByte(), 0, body.size.toByte(), (body.size shr 8).toByte(), 0, 0, 0, 0) + body)
|
|
124
|
+
}
|
|
125
|
+
private fun rejects(block: () -> Unit) { assertThrows(IllegalArgumentException::class.java, block) }
|
|
126
|
+
private fun hex(value: String) = value.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
127
|
+
}
|