@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,308 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NimoCanvasCoordinatorTest {
|
|
7
|
+
private class Clock : NimoScheduler {
|
|
8
|
+
private data class Job(val time: Long, val task: () -> Unit, var canceled: Boolean = false)
|
|
9
|
+
private var now = 0L
|
|
10
|
+
private val jobs = mutableListOf<Job>()
|
|
11
|
+
override fun post(delayMs: Long, task: () -> Unit): () -> Unit {
|
|
12
|
+
val job = Job(now + delayMs, task)
|
|
13
|
+
jobs += job
|
|
14
|
+
return { job.canceled = true }
|
|
15
|
+
}
|
|
16
|
+
fun advance(ms: Long) {
|
|
17
|
+
val end = now + ms
|
|
18
|
+
while (true) {
|
|
19
|
+
val job = jobs.filter { it.time <= end }.minByOrNull { it.time } ?: break
|
|
20
|
+
jobs.remove(job); now = job.time
|
|
21
|
+
if (!job.canceled) job.task()
|
|
22
|
+
}
|
|
23
|
+
now = end
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private class Fixture {
|
|
28
|
+
val clock = Clock()
|
|
29
|
+
var connection: Any = Any()
|
|
30
|
+
val characteristic = Any()
|
|
31
|
+
val writes = mutableListOf<ByteArray>()
|
|
32
|
+
val failures = mutableListOf<String>()
|
|
33
|
+
val rejections = mutableListOf<Int>()
|
|
34
|
+
var holdPreemptions = 0
|
|
35
|
+
var rejectWrites = false
|
|
36
|
+
val queue = NimoGattQueue<Any>(clock, { _, bytes -> writes += bytes; !rejectWrites }, { failures += it })
|
|
37
|
+
val canvas = NimoCanvasCoordinator(clock, { 20 },
|
|
38
|
+
{ frames, started, completed -> queue.enqueue(connection, characteristic, frames, started, completed) },
|
|
39
|
+
{ queue.reset(); failures += it }, { rejections += it }, { holdPreemptions++ })
|
|
40
|
+
init { queue.connected(connection) }
|
|
41
|
+
fun written(success: Boolean = true) {
|
|
42
|
+
queue.written(connection, characteristic, success)
|
|
43
|
+
clock.advance(5)
|
|
44
|
+
}
|
|
45
|
+
fun ack(key: Int, status: Int = 0) = canvas.response(key, if (status != 0) byteArrayOf(status.toByte())
|
|
46
|
+
else if (key == 4) byteArrayOf(0, 0xFD.toByte(), 0, 0, 0) else byteArrayOf(0, 0xFD.toByte()))
|
|
47
|
+
fun launch(frame: ByteArray = byteArrayOf(0, 0, 1)) {
|
|
48
|
+
canvas.offer(frame, "one:1"); canvas.readiness(true)
|
|
49
|
+
written(); ack(1)
|
|
50
|
+
}
|
|
51
|
+
fun finishUpdate(frame: ByteArray) { repeat(NimoCanvasCodec.frames(4, frame, 20).size) { written() }; ack(4) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test fun bothNotificationDescriptorsGateCharacteristicWrites() {
|
|
55
|
+
val f = Fixture()
|
|
56
|
+
val rx = Any(); val mic = Any()
|
|
57
|
+
val subscribed = mutableListOf<String>()
|
|
58
|
+
f.queue.subscribe(f.connection, rx, { subscribed += "rx"; true }) {
|
|
59
|
+
f.queue.subscribe(f.connection, mic, { subscribed += "mic"; true }) {}
|
|
60
|
+
}
|
|
61
|
+
f.canvas.offer(byteArrayOf(0, 0, 1), "one"); f.canvas.readiness(true)
|
|
62
|
+
assertTrue(f.writes.isEmpty())
|
|
63
|
+
f.queue.descriptorWritten(f.connection, Any(), true)
|
|
64
|
+
assertTrue(f.writes.isEmpty())
|
|
65
|
+
f.queue.descriptorWritten(f.connection, rx, true)
|
|
66
|
+
assertEquals(listOf("rx", "mic"), subscribed)
|
|
67
|
+
assertTrue(f.writes.isEmpty())
|
|
68
|
+
f.queue.descriptorWritten(f.connection, mic, true)
|
|
69
|
+
assertEquals(1, f.writes.size)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test fun entireFragmentChainRemainsContiguousAmongOtherCommands() {
|
|
73
|
+
val f = Fixture()
|
|
74
|
+
val frame = ByteArray(85) { it.toByte() }
|
|
75
|
+
f.launch(frame)
|
|
76
|
+
val update = NimoCanvasCodec.frames(4, frame, 20)
|
|
77
|
+
val other = byteArrayOf(42)
|
|
78
|
+
f.queue.enqueue(f.connection, f.characteristic, listOf(other))
|
|
79
|
+
repeat(update.size) { f.written() }
|
|
80
|
+
assertEquals(update.size + 2, f.writes.size)
|
|
81
|
+
update.forEachIndexed { i, bytes -> assertArrayEquals(bytes, f.writes[i + 1]) }
|
|
82
|
+
assertArrayEquals(other, f.writes.last())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test fun earlyAndDuplicateAcksCannotReleaseAnUnfinishedChain() {
|
|
86
|
+
val f = Fixture()
|
|
87
|
+
val a = ByteArray(60) { it.toByte() }; val b = byteArrayOf(0, 0, 1)
|
|
88
|
+
f.launch(a)
|
|
89
|
+
f.canvas.offer(b, "one:1")
|
|
90
|
+
f.ack(4) // Before the final fragment has started: stale/uncorrelated.
|
|
91
|
+
val fragments = NimoCanvasCodec.frames(4, a, 20).size
|
|
92
|
+
repeat(fragments - 1) { f.written() }
|
|
93
|
+
val before = f.writes.size
|
|
94
|
+
f.ack(4) // Final fragment started, but its callback has not completed.
|
|
95
|
+
f.ack(4, 6) // Duplicate must not replace the first valid early ACK.
|
|
96
|
+
assertEquals(before, f.writes.size)
|
|
97
|
+
f.queue.written(f.connection, f.characteristic, true)
|
|
98
|
+
f.queue.written(f.connection, f.characteristic, true) // Duplicate callback within pacing window.
|
|
99
|
+
assertEquals(before, f.writes.size)
|
|
100
|
+
f.clock.advance(5)
|
|
101
|
+
assertEquals(before + 1, f.writes.size)
|
|
102
|
+
assertTrue(f.rejections.isEmpty())
|
|
103
|
+
f.finishUpdate(b)
|
|
104
|
+
f.ack(4) // Late duplicate with no business command in flight.
|
|
105
|
+
assertTrue(f.failures.isEmpty())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test fun finalCallbackFailureAndMissingCallbackNeverReleaseNextFragment() {
|
|
109
|
+
for (missing in listOf(false, true)) {
|
|
110
|
+
val f = Fixture(); f.launch(ByteArray(60))
|
|
111
|
+
val before = f.writes.size
|
|
112
|
+
if (missing) f.clock.advance(3_000) else f.written(false)
|
|
113
|
+
assertEquals(before, f.writes.size)
|
|
114
|
+
assertEquals(1, f.failures.size)
|
|
115
|
+
f.queue.written(f.connection, f.characteristic, true)
|
|
116
|
+
f.clock.advance(5)
|
|
117
|
+
assertEquals(before, f.writes.size)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test fun writeStartRetriesAreBoundedAndDescriptorFailuresReset() {
|
|
122
|
+
val f = Fixture(); f.rejectWrites = true
|
|
123
|
+
f.canvas.offer(byteArrayOf(0, 0, 1), "one"); f.canvas.readiness(true)
|
|
124
|
+
f.clock.advance(200)
|
|
125
|
+
assertEquals(3, f.writes.size)
|
|
126
|
+
assertEquals(1, f.failures.size)
|
|
127
|
+
for (failure in listOf("start", "callback", "missing")) {
|
|
128
|
+
val link = Fixture(); val descriptor = Any()
|
|
129
|
+
link.queue.subscribe(link.connection, descriptor, { failure != "start" }) { fail("Must not become ready") }
|
|
130
|
+
if (failure == "callback") link.queue.descriptorWritten(link.connection, descriptor, false)
|
|
131
|
+
if (failure == "missing") link.clock.advance(5_000)
|
|
132
|
+
assertEquals(1, link.failures.size)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test fun ackDeadlineResetsAndReplacedConnectionCallbacksAreIgnored() {
|
|
137
|
+
val f = Fixture(); f.launch()
|
|
138
|
+
f.written() // Update is transported, but no business ACK.
|
|
139
|
+
f.clock.advance(45_000)
|
|
140
|
+
assertEquals(1, f.failures.size)
|
|
141
|
+
f.ack(4)
|
|
142
|
+
val old = f.connection
|
|
143
|
+
f.connection = Any(); f.queue.connected(f.connection)
|
|
144
|
+
f.canvas.readiness(true)
|
|
145
|
+
val before = f.writes.size
|
|
146
|
+
f.queue.written(old, f.characteristic, true)
|
|
147
|
+
f.clock.advance(5)
|
|
148
|
+
assertEquals(before, f.writes.size)
|
|
149
|
+
f.written(); f.ack(1); f.finishUpdate(byteArrayOf(0, 0, 1))
|
|
150
|
+
assertEquals(1, f.failures.size)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test fun rejectedUpdateImmediatelyDrainsDifferentQueuedScene() {
|
|
154
|
+
val f = Fixture(); f.launch()
|
|
155
|
+
f.written()
|
|
156
|
+
val b = byteArrayOf(1, 0, 1)
|
|
157
|
+
f.canvas.offer(b, "one:1")
|
|
158
|
+
val before = f.writes.size
|
|
159
|
+
f.ack(4, 6)
|
|
160
|
+
assertEquals(listOf(6), f.rejections)
|
|
161
|
+
assertEquals(before + 1, f.writes.size)
|
|
162
|
+
assertArrayEquals(NimoCanvasCodec.frames(4, b, 20).first(), f.writes.last())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test fun confirmedReadinessRelaunchesAfterNotReadyAndPreservesLatestScene() {
|
|
166
|
+
val f = Fixture(); f.launch(); f.written()
|
|
167
|
+
f.ack(4, 7)
|
|
168
|
+
val b = byteArrayOf(1, 0, 1)
|
|
169
|
+
f.canvas.offer(b, "one:1")
|
|
170
|
+
val before = f.writes.size
|
|
171
|
+
f.canvas.readiness(true)
|
|
172
|
+
assertEquals(before, f.writes.size)
|
|
173
|
+
f.canvas.confirmedReadiness(true)
|
|
174
|
+
assertEquals(before + 1, f.writes.size)
|
|
175
|
+
assertArrayEquals(NimoCanvasCodec.frames(1, writeCapacity = 20).single(), f.writes.last())
|
|
176
|
+
f.written(); f.ack(1)
|
|
177
|
+
assertArrayEquals(NimoCanvasCodec.frames(4, b, 20).single(), f.writes.last())
|
|
178
|
+
f.finishUpdate(b)
|
|
179
|
+
assertEquals(listOf(7), f.rejections)
|
|
180
|
+
assertTrue(f.failures.isEmpty())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Test fun newerSceneSurvivesExitReportAndWaitsForExitAck() {
|
|
184
|
+
val f = Fixture(); f.launch(); f.finishUpdate(byteArrayOf(0, 0, 1))
|
|
185
|
+
f.canvas.exit()
|
|
186
|
+
f.canvas.offer(byteArrayOf(1, 0, 1), "two:1")
|
|
187
|
+
assertFalse(f.canvas.nativeApp(0xFD, false))
|
|
188
|
+
f.written()
|
|
189
|
+
val before = f.writes.size
|
|
190
|
+
f.ack(3)
|
|
191
|
+
assertEquals(before + 1, f.writes.size)
|
|
192
|
+
assertArrayEquals(NimoCanvasCodec.frames(1, writeCapacity = 20).single(), f.writes.last())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Test fun actualNativeTakeoverDuringExitInvalidatesEncodingAndDoesNotRelaunch() {
|
|
196
|
+
val f = Fixture(); f.launch(); f.finishUpdate(byteArrayOf(0, 0, 1))
|
|
197
|
+
f.canvas.exit()
|
|
198
|
+
f.canvas.offer(byteArrayOf(1, 0, 1), "two:1")
|
|
199
|
+
assertTrue(f.canvas.nativeApp(0, true))
|
|
200
|
+
f.written()
|
|
201
|
+
val before = f.writes.size
|
|
202
|
+
f.ack(3)
|
|
203
|
+
assertEquals(before, f.writes.size)
|
|
204
|
+
assertTrue(f.failures.isEmpty())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@Test fun diagnosticHoldWaitsForCurrentBusinessAckAndThenResumesLatestScene() {
|
|
208
|
+
val f = Fixture()
|
|
209
|
+
val initial = byteArrayOf(0, 0, 1)
|
|
210
|
+
val latest = byteArrayOf(2, 0, 1)
|
|
211
|
+
f.launch(initial)
|
|
212
|
+
var ready = 0
|
|
213
|
+
f.canvas.hold { ready++ }
|
|
214
|
+
f.canvas.offer(byteArrayOf(1, 0, 1), "one:1")
|
|
215
|
+
f.canvas.offer(latest, "one:1")
|
|
216
|
+
|
|
217
|
+
assertEquals(0, ready)
|
|
218
|
+
f.finishUpdate(initial)
|
|
219
|
+
assertEquals(1, ready)
|
|
220
|
+
val beforeRelease = f.writes.size
|
|
221
|
+
|
|
222
|
+
f.canvas.releaseHold()
|
|
223
|
+
|
|
224
|
+
assertEquals(beforeRelease + 1, f.writes.size)
|
|
225
|
+
assertArrayEquals(NimoCanvasCodec.frames(4, latest, 20).first(), f.writes.last())
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Test fun scopeTakeoverPreemptsHoldWithoutFlushingHeldScene() {
|
|
229
|
+
val f = Fixture()
|
|
230
|
+
val initial = byteArrayOf(0, 0, 1)
|
|
231
|
+
val held = byteArrayOf(1, 0, 1)
|
|
232
|
+
val takeover = byteArrayOf(2, 0, 1)
|
|
233
|
+
f.launch(initial); f.finishUpdate(initial)
|
|
234
|
+
var ready = 0
|
|
235
|
+
f.canvas.hold { ready++ }
|
|
236
|
+
f.canvas.offer(held, "one:1")
|
|
237
|
+
val beforeTakeover = f.writes.size
|
|
238
|
+
|
|
239
|
+
f.canvas.offer(takeover, "two:1")
|
|
240
|
+
|
|
241
|
+
assertEquals(1, ready)
|
|
242
|
+
assertEquals(1, f.holdPreemptions)
|
|
243
|
+
assertEquals(beforeTakeover + 1, f.writes.size)
|
|
244
|
+
assertArrayEquals(NimoCanvasCodec.frames(4, takeover, 20).first(), f.writes.last())
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@Test fun firstScopePreemptsHoldAcquiredBeforeAnyCanvasScene() {
|
|
248
|
+
val f = Fixture()
|
|
249
|
+
var ready = 0
|
|
250
|
+
f.canvas.readiness(true)
|
|
251
|
+
f.canvas.hold { ready++ }
|
|
252
|
+
assertEquals(1, ready)
|
|
253
|
+
|
|
254
|
+
f.canvas.offer(byteArrayOf(0, 0, 1), "first:1")
|
|
255
|
+
|
|
256
|
+
assertEquals(1, f.holdPreemptions)
|
|
257
|
+
assertEquals(1, f.writes.size)
|
|
258
|
+
assertArrayEquals(NimoCanvasCodec.frames(1, writeCapacity = 20).single(), f.writes.single())
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Test fun reentrantCompletionCannotReleaseNewConnectionWrite() {
|
|
262
|
+
val f = Fixture(); val old = f.connection
|
|
263
|
+
f.queue.enqueue(old, f.characteristic, listOf(byteArrayOf(1)), completed = {
|
|
264
|
+
f.queue.reset()
|
|
265
|
+
f.connection = Any(); f.queue.connected(f.connection)
|
|
266
|
+
f.queue.enqueue(f.connection, f.characteristic, listOf(byteArrayOf(2), byteArrayOf(3)))
|
|
267
|
+
})
|
|
268
|
+
f.written()
|
|
269
|
+
assertEquals(2, f.writes.size)
|
|
270
|
+
f.queue.written(old, f.characteristic, true)
|
|
271
|
+
f.clock.advance(5)
|
|
272
|
+
assertEquals(2, f.writes.size)
|
|
273
|
+
f.written()
|
|
274
|
+
assertEquals(3, f.writes.size)
|
|
275
|
+
assertArrayEquals(byteArrayOf(3), f.writes.last())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@Test fun staleEncodeAfterNewSceneExitDisconnectOrDisposalCannotDeliver() {
|
|
279
|
+
for (invalidate in listOf("new", "exit", "disconnect", "dispose")) {
|
|
280
|
+
val main = Clock(); val worker = Clock()
|
|
281
|
+
val delivered = mutableListOf<Int>(); var releases = 0; var stopped = 0
|
|
282
|
+
val encoder = NimoCanvasEncoder(main, worker, { stopped++ }, { throw it })
|
|
283
|
+
encoder.submit({ byteArrayOf(1) }, { releases++ }) { delivered += it[0].toInt() }
|
|
284
|
+
worker.advance(0) // Completed encoding is now pending on Main.
|
|
285
|
+
when (invalidate) {
|
|
286
|
+
"new" -> encoder.submit({ byteArrayOf(2) }, { releases++ }) { delivered += it[0].toInt() }
|
|
287
|
+
"dispose" -> encoder.close()
|
|
288
|
+
else -> encoder.invalidate()
|
|
289
|
+
}
|
|
290
|
+
worker.advance(0); main.advance(0)
|
|
291
|
+
assertEquals(if (invalidate == "new") listOf(2) else emptyList(), delivered)
|
|
292
|
+
encoder.close(); encoder.close()
|
|
293
|
+
assertEquals(1, stopped)
|
|
294
|
+
assertEquals(if (invalidate == "new") 2 else 1, releases)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@Test fun canceledAndDisposedBitmapSourcesAreReleasedExactlyOnce() {
|
|
299
|
+
val main = Clock(); val worker = Clock(); var encoded = 0; var released = 0
|
|
300
|
+
val encoder = NimoCanvasEncoder(main, worker, {}, { throw it })
|
|
301
|
+
encoder.submit({ encoded++; byteArrayOf(1) }, { released++ }) { fail("Canceled work delivered") }
|
|
302
|
+
encoder.invalidate(); worker.advance(0); main.advance(0)
|
|
303
|
+
assertEquals(0, encoded); assertEquals(1, released)
|
|
304
|
+
encoder.close()
|
|
305
|
+
assertFalse(encoder.submit({ encoded++; byteArrayOf(2) }, { released++ }) {})
|
|
306
|
+
assertEquals(0, encoded); assertEquals(2, released)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -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
|
+
}
|