@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.445
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 +47 -6
- package/android/build.gradle +7 -7
- package/android/lc3Lib/build.gradle +2 -2
- package/android/src/main/assets/G2Firmware/README.md +27 -2
- package/android/src/main/assets/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/assets/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
- package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1065 -255
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1420 -495
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +24 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import com.glassly.bluetoothsdk.sgcs.SceneElement
|
|
4
|
+
import com.glassly.bluetoothsdk.sgcs.SceneFrame
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Assert.assertNotNull
|
|
8
|
+
import org.junit.Assert.assertNull
|
|
9
|
+
import org.junit.Assert.assertTrue
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
|
|
12
|
+
class DisplayPacerTest {
|
|
13
|
+
private class FakeScheduler : DisplayPacer.Scheduler {
|
|
14
|
+
var delay: Long? = null
|
|
15
|
+
var block: (() -> Unit)? = null
|
|
16
|
+
var scheduled = 0
|
|
17
|
+
|
|
18
|
+
override fun schedule(delayMs: Long, block: () -> Unit): () -> Unit {
|
|
19
|
+
scheduled++
|
|
20
|
+
delay = delayMs
|
|
21
|
+
this.block = block
|
|
22
|
+
return {
|
|
23
|
+
if (this.block === block) {
|
|
24
|
+
this.block = null
|
|
25
|
+
delay = null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun fire() {
|
|
31
|
+
val b = block ?: error("no timer armed")
|
|
32
|
+
block = null
|
|
33
|
+
delay = null
|
|
34
|
+
b()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private class Harness(fps: Int = 8) {
|
|
39
|
+
var now = 1_000L
|
|
40
|
+
val scheduler = FakeScheduler()
|
|
41
|
+
val scenes = mutableListOf<SceneFrame>()
|
|
42
|
+
var legacySends = 0
|
|
43
|
+
val pacer =
|
|
44
|
+
DisplayPacer(
|
|
45
|
+
dispatchScene = { scenes.add(it) },
|
|
46
|
+
sendLegacy = { legacySends++ },
|
|
47
|
+
clock = { now },
|
|
48
|
+
scheduler = scheduler,
|
|
49
|
+
).also { it.setMaxFps(fps) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun element(id: String, change: String = "created", x: Int = 0) =
|
|
53
|
+
SceneElement(
|
|
54
|
+
id = id, type = "text", x = x, y = 0, w = 10, h = 10, text = id, data = null,
|
|
55
|
+
border = 0, radius = 0, change = change, contentHash = "h-$id",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
private fun frame(
|
|
59
|
+
vararg elements: SceneElement,
|
|
60
|
+
removed: List<String> = emptyList(),
|
|
61
|
+
appId: String = "app",
|
|
62
|
+
replay: Boolean = false,
|
|
63
|
+
epoch: Int = 1,
|
|
64
|
+
) = SceneFrame(appId = appId, epoch = epoch, replay = replay, elements = elements.toList(), removed = removed)
|
|
65
|
+
|
|
66
|
+
private fun SceneFrame.change(id: String) = elements.first { it.id == id }.change
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun firstSubmitSendsImmediately() {
|
|
70
|
+
val h = Harness()
|
|
71
|
+
h.pacer.submitScene(frame(element("a")))
|
|
72
|
+
assertEquals(1, h.scenes.size)
|
|
73
|
+
assertNull(h.scheduler.block)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun secondSubmitInsideWindowIsHeldUntilTheSlot() {
|
|
78
|
+
val h = Harness(fps = 10) // 100 ms slots
|
|
79
|
+
h.pacer.submitScene(frame(element("a"), epoch = 1))
|
|
80
|
+
h.now += 30
|
|
81
|
+
h.pacer.submitScene(frame(element("a", change = "updated"), epoch = 2))
|
|
82
|
+
assertEquals(1, h.scenes.size)
|
|
83
|
+
assertEquals(70L, h.scheduler.delay)
|
|
84
|
+
h.now += 70
|
|
85
|
+
h.scheduler.fire()
|
|
86
|
+
assertEquals(2, h.scenes.size)
|
|
87
|
+
assertEquals(2, h.scenes[1].epoch)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test
|
|
91
|
+
fun rapidFramesCoalesceIntoOneMergedSend() {
|
|
92
|
+
val h = Harness(fps = 10)
|
|
93
|
+
h.pacer.submitScene(frame(element("a"), epoch = 1))
|
|
94
|
+
h.now += 10
|
|
95
|
+
h.pacer.submitScene(frame(element("a", change = "updated"), element("b", change = "created"), epoch = 2))
|
|
96
|
+
h.now += 10
|
|
97
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), element("b", change = "moved", x = 5), epoch = 3))
|
|
98
|
+
h.now += 10
|
|
99
|
+
h.pacer.submitScene(
|
|
100
|
+
frame(element("a", change = "unchanged"), element("b", change = "unchanged", x = 5), epoch = 4),
|
|
101
|
+
)
|
|
102
|
+
assertEquals(1, h.scheduler.scheduled)
|
|
103
|
+
h.now += 70
|
|
104
|
+
h.scheduler.fire()
|
|
105
|
+
assertEquals(2, h.scenes.size)
|
|
106
|
+
val sent = h.scenes[1]
|
|
107
|
+
assertEquals(4, sent.epoch)
|
|
108
|
+
assertEquals("updated", sent.change("a"))
|
|
109
|
+
assertEquals("created", sent.change("b"))
|
|
110
|
+
assertEquals(5, sent.elements.first { it.id == "b" }.x)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun updatedThenUnchangedBecomesUpdated() {
|
|
115
|
+
val h = Harness()
|
|
116
|
+
h.pacer.submitScene(frame(element("a")))
|
|
117
|
+
h.now += 1
|
|
118
|
+
h.pacer.submitScene(frame(element("a", change = "moved", x = 3)))
|
|
119
|
+
h.now += 1
|
|
120
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged", x = 3)))
|
|
121
|
+
h.now += 200
|
|
122
|
+
h.scheduler.fire()
|
|
123
|
+
assertEquals("updated", h.scenes[1].change("a"))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
fun removedThenRecreatedIdIsNotRemoved() {
|
|
128
|
+
val h = Harness()
|
|
129
|
+
h.pacer.submitScene(frame(element("a"), element("b")))
|
|
130
|
+
h.now += 1
|
|
131
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), removed = listOf("b")))
|
|
132
|
+
h.now += 1
|
|
133
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), element("b", change = "created")))
|
|
134
|
+
h.now += 200
|
|
135
|
+
h.scheduler.fire()
|
|
136
|
+
val sent = h.scenes[1]
|
|
137
|
+
assertTrue(sent.removed.isEmpty())
|
|
138
|
+
assertEquals("created", sent.change("b"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
fun pendingRemovesSurviveIntoTheMergedFrame() {
|
|
143
|
+
val h = Harness()
|
|
144
|
+
h.pacer.submitScene(frame(element("a"), element("b"), element("c")))
|
|
145
|
+
h.now += 1
|
|
146
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), removed = listOf("b", "c")))
|
|
147
|
+
h.now += 1
|
|
148
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), removed = listOf("c")))
|
|
149
|
+
h.now += 200
|
|
150
|
+
h.scheduler.fire()
|
|
151
|
+
assertEquals(listOf("b", "c"), h.scenes[1].removed)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
fun appSwitchDiscardsPending() {
|
|
156
|
+
val h = Harness()
|
|
157
|
+
h.pacer.submitScene(frame(element("a"), appId = "one"))
|
|
158
|
+
h.now += 1
|
|
159
|
+
h.pacer.submitScene(frame(element("a", change = "updated"), removed = listOf("z"), appId = "one"))
|
|
160
|
+
h.now += 1
|
|
161
|
+
h.pacer.submitScene(frame(element("b", change = "created"), appId = "two", replay = true))
|
|
162
|
+
h.now += 200
|
|
163
|
+
h.scheduler.fire()
|
|
164
|
+
val sent = h.scenes[1]
|
|
165
|
+
assertEquals("two", sent.appId)
|
|
166
|
+
assertTrue(sent.removed.isEmpty())
|
|
167
|
+
assertEquals(listOf("b"), sent.elements.map { it.id })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
fun emptyFrameFlushesImmediatelyWithPendingRemoves() {
|
|
172
|
+
val h = Harness()
|
|
173
|
+
h.pacer.submitScene(frame(element("a"), element("b")))
|
|
174
|
+
h.now += 1
|
|
175
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), removed = listOf("b")))
|
|
176
|
+
h.now += 1
|
|
177
|
+
h.pacer.flushScene(frame(removed = listOf("a")))
|
|
178
|
+
assertEquals(2, h.scenes.size)
|
|
179
|
+
assertEquals(setOf("a", "b"), h.scenes[1].removed.toSet())
|
|
180
|
+
assertTrue(h.scenes[1].elements.isEmpty())
|
|
181
|
+
assertNull(h.scheduler.block)
|
|
182
|
+
// Nothing left pending: a later timer must not resend.
|
|
183
|
+
h.now += 200
|
|
184
|
+
h.pacer.setMaxFps(8)
|
|
185
|
+
assertEquals(2, h.scenes.size)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@Test
|
|
189
|
+
fun clearViewFlushesImmediatelyAndDropsPendingScene() {
|
|
190
|
+
val h = Harness()
|
|
191
|
+
h.pacer.submitScene(frame(element("a")))
|
|
192
|
+
h.now += 1
|
|
193
|
+
h.pacer.submitScene(frame(element("a", change = "updated")))
|
|
194
|
+
assertNotNull(h.scheduler.block)
|
|
195
|
+
h.pacer.flushLegacy()
|
|
196
|
+
assertEquals(1, h.legacySends)
|
|
197
|
+
assertEquals(1, h.scenes.size)
|
|
198
|
+
assertNull(h.scheduler.block)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Test
|
|
202
|
+
fun legacyAfterSceneDropsThePendingScene() {
|
|
203
|
+
val h = Harness()
|
|
204
|
+
h.pacer.submitScene(frame(element("a")))
|
|
205
|
+
h.now += 1
|
|
206
|
+
h.pacer.submitScene(frame(element("a", change = "updated")))
|
|
207
|
+
h.now += 1
|
|
208
|
+
h.pacer.submitLegacy()
|
|
209
|
+
h.now += 200
|
|
210
|
+
h.scheduler.fire()
|
|
211
|
+
assertEquals(1, h.scenes.size)
|
|
212
|
+
assertEquals(1, h.legacySends)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Test
|
|
216
|
+
fun sceneAfterLegacyDropsThePendingLegacy() {
|
|
217
|
+
val h = Harness()
|
|
218
|
+
h.pacer.submitLegacy()
|
|
219
|
+
assertEquals(1, h.legacySends)
|
|
220
|
+
h.now += 1
|
|
221
|
+
h.pacer.submitLegacy()
|
|
222
|
+
h.now += 1
|
|
223
|
+
h.pacer.submitScene(frame(element("a")))
|
|
224
|
+
h.now += 200
|
|
225
|
+
h.scheduler.fire()
|
|
226
|
+
assertEquals(1, h.legacySends)
|
|
227
|
+
assertEquals(1, h.scenes.size)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@Test
|
|
231
|
+
fun setMaxFpsRearmsAPendingTimer() {
|
|
232
|
+
val h = Harness(fps = 2) // 500 ms slots
|
|
233
|
+
h.pacer.submitScene(frame(element("a")))
|
|
234
|
+
h.now += 50
|
|
235
|
+
h.pacer.submitScene(frame(element("a", change = "updated")))
|
|
236
|
+
assertEquals(450L, h.scheduler.delay)
|
|
237
|
+
h.pacer.setMaxFps(20) // 50 ms slots: the slot is already due
|
|
238
|
+
assertEquals(2, h.scenes.size)
|
|
239
|
+
assertNull(h.scheduler.block)
|
|
240
|
+
|
|
241
|
+
h.now += 10
|
|
242
|
+
h.pacer.submitScene(frame(element("a", change = "updated")))
|
|
243
|
+
assertEquals(40L, h.scheduler.delay)
|
|
244
|
+
h.pacer.setMaxFps(10)
|
|
245
|
+
assertEquals(90L, h.scheduler.delay)
|
|
246
|
+
assertEquals(2, h.scenes.size)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@Test
|
|
250
|
+
fun cancelDropsPendingWithoutSending() {
|
|
251
|
+
val h = Harness()
|
|
252
|
+
h.pacer.submitScene(frame(element("a")))
|
|
253
|
+
h.now += 1
|
|
254
|
+
h.pacer.submitScene(frame(element("a", change = "updated")))
|
|
255
|
+
h.pacer.cancel()
|
|
256
|
+
assertNull(h.scheduler.block)
|
|
257
|
+
h.now += 200
|
|
258
|
+
h.pacer.setMaxFps(8)
|
|
259
|
+
assertEquals(1, h.scenes.size)
|
|
260
|
+
assertFalse(h.scheduler.block != null)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@Test
|
|
264
|
+
fun lateSubmitAfterTheSlotSendsMergedImmediately() {
|
|
265
|
+
val h = Harness(fps = 10)
|
|
266
|
+
h.pacer.submitScene(frame(element("a")))
|
|
267
|
+
h.now += 10
|
|
268
|
+
h.pacer.submitScene(frame(element("a", change = "updated"), element("b", change = "created")))
|
|
269
|
+
// The timer is late; a submit past the slot sends now with the pending fold.
|
|
270
|
+
h.now += 200
|
|
271
|
+
h.pacer.submitScene(frame(element("a", change = "unchanged"), element("b", change = "unchanged")))
|
|
272
|
+
assertEquals(2, h.scenes.size)
|
|
273
|
+
assertEquals("updated", h.scenes[1].change("a"))
|
|
274
|
+
assertEquals("created", h.scenes[1].change("b"))
|
|
275
|
+
assertNull(h.scheduler.block)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import com.glassly.bluetoothsdk.sgcs.G2BleLink
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertNull
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
/** Pins the BLE link control wire format shared with g2flash's patches/ble_link.c. */
|
|
10
|
+
class G2BleLinkTest {
|
|
11
|
+
private fun le16(v: Int) = byteArrayOf((v and 0xFF).toByte(), (v shr 8).toByte())
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun controlPacketFramesField127OnDeviceReceiveInfo() {
|
|
15
|
+
assertArrayEquals(
|
|
16
|
+
byteArrayOf(0x08, 0x01, 0x10, 0x00, 0xFA.toByte(), 0x07, 4, 0x42, 0x4C, 1, 1),
|
|
17
|
+
G2BleLink.controlPacket(G2BleLink.Op.FAST),
|
|
18
|
+
)
|
|
19
|
+
assertEquals(0.toByte(), G2BleLink.controlPacket(G2BleLink.Op.STOCK).last())
|
|
20
|
+
assertEquals(2.toByte(), G2BleLink.controlPacket(G2BleLink.Op.QUERY).last())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
fun decodesFullStatusWithLivePair() {
|
|
25
|
+
val body = byteArrayOf(0x42, 0x4C, 1, 1, 1, 0xA3.toByte(), 0xA3.toByte()) + le16(6) + le16(6) + le16(0) + le16(12) + le16(0)
|
|
26
|
+
val status = G2BleLink.decodeStatus(body)
|
|
27
|
+
assertEquals(
|
|
28
|
+
G2BleLink.Status(
|
|
29
|
+
fast = true,
|
|
30
|
+
side = 1,
|
|
31
|
+
wanted = G2BleLink.Mode.Fast,
|
|
32
|
+
applied = G2BleLink.Mode.Fast,
|
|
33
|
+
requestedMinMs = 7.5,
|
|
34
|
+
requestedMaxMs = 7.5,
|
|
35
|
+
requestedLatency = 0,
|
|
36
|
+
liveIntervalMs = 15.0,
|
|
37
|
+
liveLatency = 0,
|
|
38
|
+
),
|
|
39
|
+
status,
|
|
40
|
+
)
|
|
41
|
+
assertEquals("FAST wanted=fast applied=fast requested 7.50-7.50 ms / latency 0 live 15.00 ms / latency 0", status?.summary)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun decodesThirteenByteStatusWithoutLivePair() {
|
|
46
|
+
val body = byteArrayOf(0x42, 0x4C, 1, 0, 2, 0xA4.toByte(), 0) + le16(36) + le16(72) + le16(4)
|
|
47
|
+
val status = G2BleLink.decodeStatus(body)!!
|
|
48
|
+
assertEquals(false, status.fast)
|
|
49
|
+
assertEquals(2, status.side)
|
|
50
|
+
assertEquals(G2BleLink.Mode.Slow, status.wanted)
|
|
51
|
+
assertEquals(G2BleLink.Mode.Pending, status.applied)
|
|
52
|
+
assertEquals(45.0, status.requestedMinMs, 0.0)
|
|
53
|
+
assertEquals(90.0, status.requestedMaxMs, 0.0)
|
|
54
|
+
assertEquals(4, status.requestedLatency)
|
|
55
|
+
assertNull(status.liveIntervalMs)
|
|
56
|
+
assertNull(status.liveLatency)
|
|
57
|
+
assertEquals("stock wanted=slow applied=pending requested 45.00-90.00 ms / latency 4", status.summary)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Test
|
|
61
|
+
fun rejectsForeignShortOrVersionedBodies() {
|
|
62
|
+
assertNull(G2BleLink.decodeStatus(byteArrayOf(0x41, 0x4E, 1, 1, 1, 0xA3.toByte(), 0xA3.toByte()) + le16(6) + le16(6) + le16(0)))
|
|
63
|
+
assertNull(G2BleLink.decodeStatus(byteArrayOf(0x42, 0x4C, 2, 1, 1, 0xA3.toByte(), 0xA3.toByte()) + le16(6) + le16(6) + le16(0)))
|
|
64
|
+
assertNull(G2BleLink.decodeStatus(byteArrayOf(0x42, 0x4C, 1, 1, 1, 0xA3.toByte(), 0xA3.toByte()) + le16(6) + le16(6)))
|
|
65
|
+
assertEquals(
|
|
66
|
+
G2BleLink.Mode.Other(0x99),
|
|
67
|
+
G2BleLink.decodeStatus(byteArrayOf(0x42, 0x4C, 1, 0, 1, 0x99.toByte(), 0) + le16(0) + le16(0) + le16(0))?.wanted,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -2,17 +2,10 @@ package com.glassly.bluetoothsdk.sgcs
|
|
|
2
2
|
|
|
3
3
|
import org.junit.Assert.assertEquals
|
|
4
4
|
import org.junit.Assert.assertFalse
|
|
5
|
-
import org.junit.Assert.assertNotNull
|
|
6
5
|
import org.junit.Assert.assertNull
|
|
7
6
|
import org.junit.Assert.assertTrue
|
|
8
7
|
import org.junit.Test
|
|
9
8
|
|
|
10
|
-
/**
|
|
11
|
-
* Pins the stock-vs-CFW identity rules that gate the connect banner. A
|
|
12
|
-
* regression here re-opens the stuck "// Glassly connected" bug: text drawn
|
|
13
|
-
* down the stock path before classification, cleared down the CFW path after
|
|
14
|
-
* it, never leaves the glass.
|
|
15
|
-
*/
|
|
16
9
|
class G2CfwClassificationTest {
|
|
17
10
|
private fun classify(
|
|
18
11
|
capability: String?,
|
|
@@ -20,70 +13,66 @@ class G2CfwClassificationTest {
|
|
|
20
13
|
current: String? = null,
|
|
21
14
|
) = G2CfwClassification.classify(capability?.toByteArray(Charsets.UTF_8), definitive, current)
|
|
22
15
|
|
|
23
|
-
// ── CFW detection ───────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
16
|
@Test
|
|
26
|
-
fun
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
fun glasslyUsesOneContractWithoutRevisionOrTokenNegotiation() {
|
|
18
|
+
for (banner in listOf("GLASSLYCFW/24", "GLASSLYCFW/24 shapes36 scene37 anim38", "GLASSLYCFW/25")) {
|
|
19
|
+
val result = classify(banner)!!
|
|
20
|
+
assertEquals(G2CfwClassification.Family.GLASSLY, result.family)
|
|
21
|
+
assertTrue(result.isCfw)
|
|
22
|
+
assertTrue(result.isGlassly)
|
|
23
|
+
assertEquals(if (banner == "GLASSLYCFW/25") "Glassly CFW v25" else "Glassly CFW v24", result.displayName)
|
|
24
|
+
assertEquals(banner, result.capabilityString)
|
|
25
|
+
}
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
@Test
|
|
36
|
-
fun
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
fun revisionIsRetainedIndependentlyOfTheFirmwareFamily() {
|
|
30
|
+
assertEquals(24, classify("GLASSLYCFW/24 shapes36 scene37 anim38")!!.revision)
|
|
31
|
+
assertEquals(25, classify("GLASSLYCFW/25")!!.revision)
|
|
32
|
+
assertEquals(22, classify("EVENCFW/22 img640")!!.revision)
|
|
33
|
+
assertEquals(24, classify("GLASSLYCFW/24\u0000\u0000")!!.revision)
|
|
34
|
+
assertNull(G2CfwClassification.STOCK.revision)
|
|
35
|
+
for (banner in listOf("GLASSLYCFW/", "GLASSLYCFW/beta", "GLASSLYCFW/0", "GLASSLYCFW/-1")) {
|
|
36
|
+
val result = classify(banner)!!
|
|
37
|
+
assertNull(result.revision)
|
|
38
|
+
assertTrue(result.isGlassly)
|
|
39
|
+
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
@Test
|
|
43
|
-
fun
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
fun evenCfwIsUpstreamAndNeverGlassly() {
|
|
44
|
+
for (banner in listOf("EVENCFW/22 img640 imgz rle font15 ringbat17", "EVENCFW/24 shapes36 scene37 anim38")) {
|
|
45
|
+
val result = classify(banner)!!
|
|
46
|
+
assertEquals(G2CfwClassification.Family.UPSTREAM, result.family)
|
|
47
|
+
assertTrue(result.isCfw)
|
|
48
|
+
assertFalse(result.isGlassly)
|
|
49
|
+
assertEquals("g2flash CFW", result.displayName)
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
@Test
|
|
51
|
-
fun
|
|
52
|
-
|
|
53
|
-
assertTrue(result!!.isCfw)
|
|
54
|
-
assertEquals(0, result.protocolVersion)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Stock classification ────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
@Test
|
|
60
|
-
fun definitiveAbsenceClassifiesStock() {
|
|
61
|
-
val result = classify(null, definitive = true)
|
|
62
|
-
assertNotNull(result)
|
|
63
|
-
assertFalse(result!!.isCfw)
|
|
64
|
-
assertEquals(G2CfwClassification.STOCK, result)
|
|
54
|
+
fun bannerTrimsNulTermination() {
|
|
55
|
+
assertEquals("GLASSLYCFW/24", classify("GLASSLYCFW/24\u0000\u0000")!!.capabilityString)
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
@Test
|
|
68
|
-
fun
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Individual settings packets can legitimately omit field 100 or
|
|
77
|
-
// arrive truncated — only a deviceReceiveRequest response is proof.
|
|
78
|
-
assertNull(classify(null, definitive = false))
|
|
79
|
-
assertNull(classify("garbage", definitive = false))
|
|
59
|
+
fun definitiveAbsenceOrUnrecognizedPrefixClassifiesStock() {
|
|
60
|
+
for (banner in listOf(null, "something-else", "GLASSLYCFW-invalid", "EVENCFW-invalid")) {
|
|
61
|
+
val result = classify(banner, definitive = true)!!
|
|
62
|
+
assertEquals(G2CfwClassification.STOCK, result)
|
|
63
|
+
assertFalse(result.isCfw)
|
|
64
|
+
assertFalse(result.isGlassly)
|
|
65
|
+
assertNull(result.displayName)
|
|
66
|
+
}
|
|
80
67
|
}
|
|
81
68
|
|
|
82
69
|
@Test
|
|
83
|
-
fun
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
fun partialSettingsPacketsPreserveTheKnownRenderPath() {
|
|
71
|
+
assertNull(classify(null))
|
|
72
|
+
assertNull(classify("garbage"))
|
|
73
|
+
for (banner in listOf("GLASSLYCFW/24", "EVENCFW/22")) {
|
|
74
|
+
assertNull(classify(null, definitive = true, current = banner))
|
|
75
|
+
assertNull(classify("garbage", definitive = true, current = banner))
|
|
76
|
+
}
|
|
88
77
|
}
|
|
89
78
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.glassly.bluetoothsdk.sgcs
|
|
2
2
|
|
|
3
|
+
import org.junit.Assert.assertArrayEquals
|
|
3
4
|
import org.junit.Assert.assertEquals
|
|
4
5
|
import org.junit.Assert.assertNotNull
|
|
5
6
|
import org.junit.Assert.assertNull
|
|
@@ -14,6 +15,72 @@ import org.robolectric.annotation.GraphicsMode
|
|
|
14
15
|
@Config(sdk = [33])
|
|
15
16
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
|
16
17
|
class G2CfwRendererTest {
|
|
18
|
+
@Test
|
|
19
|
+
fun globalDepthMovesFlatScenesWithoutMiniappDepth() {
|
|
20
|
+
val content = frame(rect(border = 1).copy(x = 100, y = 40, w = 100, h = 50))
|
|
21
|
+
// Start flat, split the retained mirror, change sides, then converge again.
|
|
22
|
+
listOf(2, 1, 3, 2).forEachIndexed { index, setting ->
|
|
23
|
+
val b = boxes(4, G2CfwRenderer.depthLevelForSetting(setting))
|
|
24
|
+
val encoded = G2CfwRenderer.encodeVector(content, b, 900, index == 0, true)!!
|
|
25
|
+
if (setting == 2) {
|
|
26
|
+
assertEquals(listOf(G2CfwTransport.LENS_BOTH), encoded.shows.map { it.lenses })
|
|
27
|
+
assertEquals(132, firstRectX(encoded.shows[0].payload))
|
|
28
|
+
} else {
|
|
29
|
+
assertEquals(listOf(G2CfwTransport.LENS_LEFT, G2CfwTransport.LENS_RIGHT), encoded.shows.map { it.lenses })
|
|
30
|
+
assertEquals(if (setting == 1) 140 else 124, firstRectX(encoded.shows[0].payload))
|
|
31
|
+
assertEquals(if (setting == 1) 124 else 140, firstRectX(encoded.shows[1].payload))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun elementDepthIsRelativeToEachGlobalLensOrigin() {
|
|
38
|
+
val content = frame(rect(border = 1).copy(x = 100, y = 40, w = 100, h = 50, depth = 5))
|
|
39
|
+
val b = boxes(4, G2CfwRenderer.depthLevelForSetting(3))
|
|
40
|
+
val encoded = G2CfwRenderer.encodeVector(content, b, 901, true, true)!!
|
|
41
|
+
assertEquals(listOf(G2CfwTransport.LENS_LEFT, G2CfwTransport.LENS_RIGHT), encoded.shows.map { it.lenses })
|
|
42
|
+
assertEquals(24 + 100 + 5, firstRectX(encoded.shows[0].payload))
|
|
43
|
+
assertEquals(40 + 100 - 5, firstRectX(encoded.shows[1].payload))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun firstRectX(payload: ByteArray): Int {
|
|
47
|
+
val bytes = payload.map { it.toInt() and 0xFF }
|
|
48
|
+
assertEquals(38, bytes[0]) // SHOW
|
|
49
|
+
assertEquals(1, bytes[7]) // one object PUT
|
|
50
|
+
// SHOW header (8), PUT opcode/id/version (9), type/flags/color/width (4).
|
|
51
|
+
return (bytes[21] or (bytes[22] shl 8)).toShort().toInt()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
fun globalDepthSettingsMapToAllThreeFirmwareLevels() {
|
|
56
|
+
assertEquals(listOf(0, 1, 2), listOf(1, 2, 3).map { G2CfwRenderer.depthLevelForSetting(it) })
|
|
57
|
+
assertEquals(0, G2CfwRenderer.depthLevelForSetting(Int.MIN_VALUE))
|
|
58
|
+
assertEquals(2, G2CfwRenderer.depthLevelForSetting(Int.MAX_VALUE))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
fun rasterFallbackBakesTextIntoStereoWindows() {
|
|
63
|
+
for (font in listOf("builtin", "mono")) {
|
|
64
|
+
val text = rect(border = 0).copy(
|
|
65
|
+
type = "text", x = 10, y = 10, w = 200, h = 60, text = "Depth", font = font, fontSize = 20,
|
|
66
|
+
)
|
|
67
|
+
for (setting in listOf(1, 3)) {
|
|
68
|
+
val b = boxes(4, G2CfwRenderer.depthLevelForSetting(setting))
|
|
69
|
+
val update = G2CfwRenderer.encodeUpdate(
|
|
70
|
+
frame(text), b, previousRaster = null, forcedRects = emptyList(),
|
|
71
|
+
fids = (1..G2CfwRenderer.REQUIRED_FID_COUNT).toList(),
|
|
72
|
+
textDraws = emptyList(), allowRectCopy = true, blankBoxes = null, omitBuiltinText = false,
|
|
73
|
+
textureGlyphsEnabled = true, textureImagesEnabled = true,
|
|
74
|
+
)!!
|
|
75
|
+
assertTrue("$font text must remain visible", update.raster.any { it.toInt() != 0 })
|
|
76
|
+
val payload = update.payload!!
|
|
77
|
+
assertEquals("a stereo bitmap, without lens-equal text/cache commands", 0x83, payload[0].toInt() and 0xFF)
|
|
78
|
+
assertEquals(b.leftX, (payload[1].toInt() and 0xFF) * 4)
|
|
79
|
+
assertEquals(b.rightX, (payload[5].toInt() and 0xFF) * 4)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
17
84
|
@Test
|
|
18
85
|
fun placementMatchesStockWindowTablesAndClamps() {
|
|
19
86
|
val defaults = boxes(height = 4, depth = 2)
|
|
@@ -52,7 +119,8 @@ class G2CfwRendererTest {
|
|
|
52
119
|
listOf(0x83, 6, 64, 144, 144, 10, 64, 144, 144, 0x34, 0x12),
|
|
53
120
|
payload!!.take(11).map { it.toInt() and 0xFF },
|
|
54
121
|
)
|
|
55
|
-
|
|
122
|
+
// Plain RLE follows the header (the transport compresses): 576*288 black pixels.
|
|
123
|
+
assertEquals(listOf(0x00, 0x00, 0xFF, 0xFF), payload.copyOfRange(11, 15).map { it.toInt() and 0xFF })
|
|
56
124
|
assertNull(G2CfwRenderer.stereoFrame(ByteArray(10), boxes(4, 2), 1))
|
|
57
125
|
}
|
|
58
126
|
|
|
@@ -94,7 +162,11 @@ class G2CfwRendererTest {
|
|
|
94
162
|
val payload = G2CfwRenderer.blackPayload()
|
|
95
163
|
assertNotNull(payload)
|
|
96
164
|
assertEquals(6, payload!![0].toInt() and 0xFF)
|
|
97
|
-
|
|
165
|
+
// 307,200 black pixels as RLE: four 65,535 runs and a 45,060 remainder.
|
|
166
|
+
assertEquals(
|
|
167
|
+
listOf(0, 0, 0xFF, 0xFF, 0, 0, 0xFF, 0xFF, 0, 0, 0xFF, 0xFF, 0, 0, 0xFF, 0xFF, 0, 0, 0x04, 0xB0),
|
|
168
|
+
payload.drop(1).map { it.toInt() and 0xFF },
|
|
169
|
+
)
|
|
98
170
|
}
|
|
99
171
|
|
|
100
172
|
@Test
|
|
@@ -212,7 +284,8 @@ class G2CfwRendererTest {
|
|
|
212
284
|
listOf(0x83, (24 + 100) / 4, (128 + 50) / 2, 8, 8, (40 + 100) / 4, (128 + 50) / 2, 8, 8, 5, 0),
|
|
213
285
|
payload!!.take(11).map { it.toInt() and 0xFF },
|
|
214
286
|
)
|
|
215
|
-
|
|
287
|
+
// Plain RLE of the 32x16 box follows (the transport compresses).
|
|
288
|
+
assertArrayEquals(G2CfwRenderer.rleEncode(G2CfwRenderer.pack4Bpp(ByteArray(32 * 16))), payload.copyOfRange(11, payload.size))
|
|
216
289
|
assertNull(
|
|
217
290
|
G2CfwRenderer.boxDeltaFrame(raster(576, 288), 576, G2CfwRenderer.CanvasRect(3, 0, 32, 16), b, 1)
|
|
218
291
|
)
|
|
@@ -272,8 +345,8 @@ class G2CfwRendererTest {
|
|
|
272
345
|
val draws = G2CfwRenderer.builtinTextDraws(frame(element), b)
|
|
273
346
|
assertEquals(
|
|
274
347
|
listOf(
|
|
275
|
-
G2CfwRenderer.TextDraw(32 + 10 + 2, 128 + 20 + 2, "one"),
|
|
276
|
-
G2CfwRenderer.TextDraw(32 + 10 + 2, 128 + 20 + 2 + 27, "two"),
|
|
348
|
+
G2CfwRenderer.TextDraw(32 + 10 + 2, 128 + 20 + 2, "one", 200),
|
|
349
|
+
G2CfwRenderer.TextDraw(32 + 10 + 2, 128 + 20 + 2 + 27, "two", 200),
|
|
277
350
|
),
|
|
278
351
|
draws,
|
|
279
352
|
)
|