@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.
Files changed (155) hide show
  1. package/README.md +47 -6
  2. package/android/build.gradle +7 -7
  3. package/android/lc3Lib/build.gradle +2 -2
  4. package/android/src/main/assets/G2Firmware/README.md +27 -2
  5. 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
  6. 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
  7. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +38 -0
  8. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +50 -14
  9. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +18 -0
  10. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +139 -10
  11. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +28 -1
  12. package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +199 -0
  13. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +10 -1
  14. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +4 -4
  15. package/android/src/main/java/com/glassly/bluetoothsdk/GlassesNotifications.kt +155 -0
  16. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +1065 -255
  17. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
  18. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
  19. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
  20. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
  21. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
  22. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
  23. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
  24. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
  25. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
  26. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
  27. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
  28. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
  29. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
  30. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
  31. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
  32. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
  33. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
  34. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
  35. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
  36. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
  37. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
  38. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
  39. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
  40. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
  41. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
  42. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
  43. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
  44. package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
  45. package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
  46. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
  47. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
  48. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
  49. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
  50. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
  51. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
  52. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
  53. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
  54. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
  55. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
  56. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
  57. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
  58. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
  59. package/build/BluetoothSdk.types.d.ts +209 -20
  60. package/build/BluetoothSdk.types.d.ts.map +1 -1
  61. package/build/BluetoothSdk.types.js.map +1 -1
  62. package/build/_internal.d.ts +1 -1
  63. package/build/_internal.d.ts.map +1 -1
  64. package/build/_internal.js.map +1 -1
  65. package/build/_private/BluetoothSdkModule.d.ts +30 -1
  66. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  67. package/build/_private/BluetoothSdkModule.js.map +1 -1
  68. package/build/generated/changelogCatalog.d.ts +28 -1
  69. package/build/generated/changelogCatalog.d.ts.map +1 -1
  70. package/build/generated/changelogCatalog.js +37 -1
  71. package/build/generated/changelogCatalog.js.map +1 -1
  72. package/build/generated/releaseMetadata.js +4 -4
  73. package/build/generated/releaseMetadata.js.map +1 -1
  74. package/build/index.d.ts +1 -1
  75. package/build/index.d.ts.map +1 -1
  76. package/build/index.js +14 -0
  77. package/build/index.js.map +1 -1
  78. package/ios/BluetoothSdkModule.swift +102 -13
  79. package/ios/GlasslyBluetoothSDK.podspec +1 -1
  80. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
  81. package/ios/Source/BluetoothSDK.swift +53 -0
  82. package/ios/Source/BluetoothSdkDefaults.swift +1 -1
  83. package/ios/Source/Bridge.swift +13 -0
  84. package/ios/Source/DeviceManager.swift +151 -33
  85. package/ios/Source/DeviceStore.swift +28 -14
  86. package/ios/Source/DisplayPacer.swift +180 -0
  87. package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
  88. package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
  89. package/ios/Source/GlassesNotifications.swift +181 -0
  90. package/ios/Source/ObservableStore.swift +14 -2
  91. package/ios/Source/Resources/G2Firmware/README.md +27 -2
  92. package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
  93. package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
  94. package/ios/Source/controllers/R1.swift +0 -1
  95. package/ios/Source/internal/BluetoothAvailability.swift +31 -0
  96. package/ios/Source/sgcs/G2.swift +1420 -495
  97. package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
  98. package/ios/Source/sgcs/G2BleLink.swift +102 -0
  99. package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
  100. package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
  101. package/ios/Source/sgcs/G2CfwScene.swift +779 -0
  102. package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
  103. package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
  104. package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
  105. package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
  106. package/ios/Source/sgcs/G2Flasher.swift +9 -9
  107. package/ios/Source/sgcs/G2Keyboard.swift +780 -0
  108. package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
  109. package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
  110. package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
  111. package/ios/Source/sgcs/G2RingBattery.swift +22 -0
  112. package/ios/Source/sgcs/G2Sensors.swift +75 -0
  113. package/ios/Source/sgcs/G2TouchInput.swift +51 -0
  114. package/ios/Source/sgcs/G2WritePacing.swift +18 -0
  115. package/ios/Source/sgcs/MentraNex.swift +44 -44
  116. package/ios/Source/sgcs/SGCManager.swift +120 -13
  117. package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
  118. package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
  119. package/ios/Source/status/DeviceStatus.swift +27 -0
  120. package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
  121. package/ios/Source/utils/Enums.swift +0 -1
  122. package/ios/Source/utils/G1Text.swift +0 -1
  123. package/ios/Source/utils/G2NotifyFile.swift +20 -10
  124. package/ios/Source/utils/JSCExperiment.swift +6 -6
  125. package/ios/Source/utils/Models.swift +0 -1
  126. package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
  127. package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
  128. package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
  129. package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
  130. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
  131. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
  132. package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
  133. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
  134. package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
  135. package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
  136. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
  137. package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
  138. package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
  139. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
  140. package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
  141. package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
  142. package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
  143. package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
  144. package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
  145. package/package.json +1 -1
  146. package/plugin/build/withAndroid.js +2 -2
  147. package/plugin/build/withIos.js +1 -1
  148. package/scripts/gen-nex-proto.sh +12 -0
  149. package/scripts/inject-ios-sdk-version.mjs +1 -1
  150. package/src/BluetoothSdk.types.ts +214 -19
  151. package/src/_internal.ts +1 -1
  152. package/src/_private/BluetoothSdkModule.ts +40 -0
  153. package/src/generated/changelogCatalog.ts +37 -1
  154. package/src/generated/releaseMetadata.ts +4 -4
  155. package/src/index.ts +24 -0
@@ -0,0 +1,489 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.assertArrayEquals
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertNotNull
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Test
10
+
11
+ class G2CfwSceneTest {
12
+ private fun element(
13
+ id: String,
14
+ type: String,
15
+ x: Int = 0,
16
+ y: Int = 0,
17
+ w: Int = 0,
18
+ h: Int = 0,
19
+ text: String? = null,
20
+ border: Int = 0,
21
+ radius: Int = 0,
22
+ font: String = "default",
23
+ fontSize: Int = 0,
24
+ fill: Boolean = false,
25
+ color: Int = -1,
26
+ points: List<Int> = emptyList(),
27
+ startDeg: Int = 0,
28
+ endDeg: Int = 0,
29
+ transitionMs: Int = 0,
30
+ easing: String = "ease",
31
+ hash: String = "h",
32
+ path: List<Int> = emptyList(), pathScale: Int = 256, fillRule: Int = 0, rotationQ8: Int = 0, pivotX: Int? = null, pivotY: Int? = null,
33
+ depth: Int = 0,
34
+ ) = SceneElement(
35
+ id = id, type = type, x = x, y = y, w = w, h = h, text = text, data = null, border = border,
36
+ radius = radius, change = "created", contentHash = hash, font = font, fontSize = fontSize, fill = fill,
37
+ color = color,
38
+ points = points, startDeg = startDeg, endDeg = endDeg, transitionMs = transitionMs, easing = easing,
39
+ path = path, pathScale = pathScale, fillRule = fillRule, rotationQ8 = rotationQ8, pivotX = pivotX, pivotY = pivotY,
40
+ depth = depth,
41
+ )
42
+
43
+ private fun frame(elements: List<SceneElement>, replay: Boolean = false) =
44
+ SceneFrame("app", 1, replay, elements, emptyList())
45
+
46
+ private fun encode(scene: G2CfwScene, cache: G2CfwTextureCache, elements: List<SceneElement>, inlineText: Boolean = false, tag: Int? = null, parallax: Int = 0) =
47
+ scene.encode(frame(elements), 32, 96, cache, { _, w, h -> ByteArray(w * h) { 0xFF.toByte() } }, inlineText, null, tag, parallax)
48
+
49
+ private fun le(vararg values: Int): List<Int> = values.flatMap { v -> listOf(v and 0xFF, (v ushr 8) and 0xFF) }
50
+ private fun u32(v: Int): List<Int> = listOf(v and 0xFF, (v ushr 8) and 0xFF, (v ushr 16) and 0xFF, (v ushr 24) and 0xFF)
51
+
52
+ private class Parsed(val request: Int, val flags: Int, val puts: List<List<Int>>, val refs: List<Pair<Int, Int>>, val ops: List<Int>)
53
+
54
+ /** Split a SHOW into its parts by walking the wire format. */
55
+ private fun parse(payload: ByteArray): Parsed {
56
+ val b = payload.map { it.toInt() and 0xFF }
57
+ assertEquals(38, b[0])
58
+ assertEquals("the encoder leaves the epoch to the link", listOf(0, 0), b.subList(1, 3))
59
+ var pos = 8
60
+ val puts = mutableListOf<List<Int>>()
61
+ repeat(b[7]) {
62
+ assertEquals(1, b[pos])
63
+ val recordLength = when (val type = b[pos + 9]) {
64
+ 17 -> 13 + b[pos + 9 + 12]
65
+ 18 -> 12 + (b[pos + 9 + 10] or (b[pos + 9 + 11] shl 8))
66
+ 14, 15 -> 12
67
+ 16 -> 16
68
+ else -> { assertTrue(type in 1..13); 20 }
69
+ }
70
+ puts.add(b.subList(pos, pos + 9 + recordLength))
71
+ pos += 9 + recordLength
72
+ }
73
+ val count = b[pos] or (b[pos + 1] shl 8)
74
+ pos += 2
75
+ val refs = mutableListOf<Pair<Int, Int>>()
76
+ repeat(count) {
77
+ val id = b[pos] or (b[pos + 1] shl 8) or (b[pos + 2] shl 16) or (b[pos + 3] shl 24)
78
+ val ver = b[pos + 4] or (b[pos + 5] shl 8) or (b[pos + 6] shl 16) or (b[pos + 7] shl 24)
79
+ refs.add(id to ver)
80
+ pos += 8
81
+ }
82
+ return Parsed(b[3] or (b[4] shl 8), b[5], puts, refs, b.subList(pos, b.size))
83
+ }
84
+
85
+ private fun fresh(): Pair<G2CfwScene, G2CfwTextureCache> {
86
+ val scene = G2CfwScene()
87
+ val cache = G2CfwTextureCache()
88
+ cache.reset(1)
89
+ scene.reset(1)
90
+ return scene to cache
91
+ }
92
+
93
+ @Test
94
+ fun warmUpIsOneShowOfADotWithATween() {
95
+ val p = parse(G2CfwScene.WARM_UP_SHOW)
96
+ assertEquals(1, p.flags)
97
+ assertEquals(1, p.puts.size)
98
+ assertEquals(listOf(5, 1, 0, 0), p.puts[0].subList(9, 13))
99
+ assertEquals(listOf(G2CfwScene.WARM_UP_ID to 1), p.refs)
100
+ assertEquals(listOf(5) + u32(G2CfwScene.WARM_UP_ID) + listOf(1, 0, 2, 0, 0, 255, 255), p.ops.subList(0, 12))
101
+ assertArrayEquals(byteArrayOf(39, 0, 0, 0, 0, 1, 0), G2CfwScene.HIDE_PAYLOAD)
102
+ assertArrayEquals(byteArrayOf(41, 0, 0, 0, 0, 0), G2CfwScene.RESET_PAYLOAD)
103
+ }
104
+
105
+ @Test
106
+ fun shapesBecomeObjectDefinitionsWithOffsetGeometry() {
107
+ val (scene, cache) = fresh()
108
+ val encoded = encode(
109
+ scene, cache,
110
+ listOf(
111
+ element("l", "line", points = listOf(10, 20, 30, 40), border = 3, color = 12),
112
+ element("c", "circle", x = 100, y = 100, w = 41, h = 41, fill = true),
113
+ element("a", "arc", w = 101, h = 101, border = 4, startDeg = -90, endDeg = 180),
114
+ element("r", "rect", x = 1, y = 2, w = 30, h = 20, border = 2, radius = 6, fill = true),
115
+ ),
116
+ tag = 77,
117
+ )
118
+ assertNotNull(encoded)
119
+ val p = parse(encoded!!.payload)
120
+ assertEquals(77, p.request)
121
+ assertEquals(5, p.flags)
122
+ assertEquals(5, p.puts.size)
123
+ assertEquals(u32(G2CfwScene.objectId("l", 0)), p.puts[0].subList(1, 5))
124
+ assertEquals(listOf(1, 1, 12, 3), p.puts[0].subList(9, 13))
125
+ assertEquals(le(42, 116, 62, 136), p.puts[0].subList(13, 21))
126
+ assertEquals(listOf(5, 1, 15, 0), p.puts[1].subList(9, 13))
127
+ assertEquals(le(152, 216, 20), p.puts[1].subList(13, 19))
128
+ assertEquals(listOf(12, 1, 15, 4), p.puts[2].subList(9, 13))
129
+ assertEquals(le(-90, 180), p.puts[2].subList(19, 23))
130
+ assertEquals(listOf(3, 1, 15, 0), p.puts[3].subList(9, 13))
131
+ assertEquals(listOf(2, 1, 15, 2), p.puts[4].subList(9, 13))
132
+ assertEquals(
133
+ listOf(G2CfwScene.objectId("l", 0), G2CfwScene.objectId("c", 0), G2CfwScene.objectId("a", 0), G2CfwScene.objectId("r", 0), G2CfwScene.objectId("r", 1)),
134
+ p.refs.map { it.first },
135
+ )
136
+ assertEquals(p.puts[0][5] or (p.puts[0][6] shl 8) or (p.puts[0][7] shl 16) or (p.puts[0][8] shl 24), p.refs[0].second)
137
+ assertTrue(p.ops.isEmpty())
138
+ assertTrue(encoded.uploads.isEmpty())
139
+ assertTrue(encoded.changed)
140
+ assertEquals(5, encoded.puts)
141
+ }
142
+
143
+ @Test
144
+ fun svgDefinitionAndTransformOpsMatchTheWireFormat() {
145
+ val (scene, cache) = fresh()
146
+ val path = listOf(0, 0, 0, 0, 0, 1, 160, 0, 0, 0, 1, 160, 0, 160, 0, 4)
147
+ val svg = element("s", "svg", x = 10, y = 20, path = path, pathScale = 512, fillRule = 1, rotationQ8 = 90 * 256, pivotX = 40, pivotY = 50)
148
+ val first = parse(encode(scene, cache, listOf(svg))!!.payload)
149
+ assertEquals(1, first.puts.size)
150
+ val record = first.puts[0].subList(9, first.puts[0].size)
151
+ assertEquals(listOf(18, 1, 15, 1), record.subList(0, 4))
152
+ assertEquals(le(42, 116, 512) + listOf(16, 0), record.subList(4, 12))
153
+ assertEquals(path, record.subList(12, record.size))
154
+ val id = G2CfwScene.objectId("s", 0)
155
+ assertEquals(listOf(9) + u32(id) + listOf(0, 90, 0, 0), first.ops.subList(0, 9))
156
+ assertEquals(le(72, 146, 0) + listOf(64, 26, 64, 255), first.ops.subList(9, 19))
157
+ val moved = element("s", "svg", x = 30, y = 20, path = path, pathScale = 256, fillRule = 1, rotationQ8 = 180 * 256, pivotX = 40, pivotY = 50, transitionMs = 330, easing = "linear")
158
+ val second = encode(scene, cache, listOf(moved))!!
159
+ val q = parse(second.payload)
160
+ assertTrue(q.puts.isEmpty())
161
+ assertEquals(1, q.refs.size)
162
+ assertEquals(listOf(5) + u32(id) + listOf(0x07, 0x01, 10, 0, 0, 255, 255), q.ops.subList(0, 12))
163
+ assertEquals(le(62, 116, 256, 15), q.ops.subList(12, 20))
164
+ assertEquals(listOf(9) + u32(id) + listOf(0, 180, 0, 0), q.ops.subList(20, 29))
165
+ assertEquals(le(330), q.ops.subList(33, 35))
166
+ assertEquals(0, second.hits)
167
+ assertEquals(1, second.tweens)
168
+ }
169
+
170
+ @Test
171
+ fun movedElementWithTransitionBecomesTween() {
172
+ val (scene, cache) = fresh()
173
+ encode(scene, cache, listOf(element("ball", "circle", w = 21, h = 21, fill = true)))
174
+ val encoded = encode(scene, cache, listOf(element("ball", "circle", x = 200, w = 21, h = 21, fill = true, transitionMs = 330, easing = "linear")))!!
175
+ val p = parse(encoded.payload)
176
+ val id = G2CfwScene.objectId("ball", 0)
177
+ assertTrue(p.puts.isEmpty())
178
+ assertEquals(listOf(id), p.refs.map { it.first })
179
+ assertEquals(listOf(5) + u32(id) + listOf(0x07, 0x03, 10, 0, 0, 255, 255), p.ops.subList(0, 12))
180
+ assertEquals(le(242, 106, 10, 15, 0), p.ops.subList(12, 22))
181
+ assertEquals(22, p.ops.size)
182
+ assertEquals(1, encoded.tweens)
183
+ }
184
+
185
+ @Test
186
+ fun geometryChangeWithoutTransitionAndTypeChangeRedefine() {
187
+ val (scene, cache) = fresh()
188
+ encode(scene, cache, listOf(element("s", "circle", w = 11, h = 11)))
189
+ val moved = parse(encode(scene, cache, listOf(element("s", "circle", x = 5, w = 11, h = 11)))!!.payload)
190
+ assertEquals(1, moved.puts.size)
191
+ assertTrue(moved.ops.isEmpty())
192
+ val retyped = parse(encode(scene, cache, listOf(element("s", "circle", x = 5, w = 11, h = 11, fill = true, transitionMs = 500)))!!.payload)
193
+ assertEquals(1, retyped.puts.size)
194
+ assertEquals(5, retyped.puts[0][9])
195
+ assertTrue(retyped.ops.isEmpty())
196
+ }
197
+
198
+ @Test
199
+ fun removalLeavesTheListAndReappearanceIsAReference() {
200
+ val (scene, cache) = fresh()
201
+ val a = element("a", "circle", w = 11, h = 11)
202
+ val b = element("b", "circle", w = 11, h = 11)
203
+ encode(scene, cache, listOf(a, b))
204
+ val onlyB = encode(scene, cache, listOf(b))!!
205
+ var p = parse(onlyB.payload)
206
+ assertTrue(p.puts.isEmpty())
207
+ assertEquals(listOf(G2CfwScene.objectId("b", 0)), p.refs.map { it.first })
208
+ assertTrue(onlyB.changed)
209
+ assertEquals(2, scene.known.size)
210
+ val both = encode(scene, cache, listOf(a, b))!!
211
+ p = parse(both.payload)
212
+ assertTrue(p.puts.isEmpty())
213
+ assertEquals(2, p.refs.size)
214
+ assertEquals(2, both.hits)
215
+ assertFalse(encode(scene, cache, listOf(a, b))!!.changed)
216
+ scene.hidden()
217
+ val reopen = encode(scene, cache, listOf(a, b))!!
218
+ assertTrue(reopen.changed)
219
+ assertEquals(2, reopen.hits)
220
+ assertEquals(0, parse(reopen.payload).puts.size)
221
+ }
222
+
223
+ @Test
224
+ fun hiddenMidTweenObjectsAreSnappedWhenShownAgain() {
225
+ val (scene, cache) = fresh()
226
+ encode(scene, cache, listOf(element("ball", "circle", w = 21, h = 21, fill = true)))
227
+ encode(scene, cache, listOf(element("ball", "circle", x = 200, w = 21, h = 21, fill = true, transitionMs = 3000)))
228
+ scene.hidden()
229
+ val p = parse(encode(scene, cache, listOf(element("ball", "circle", x = 200, w = 21, h = 21, fill = true)))!!.payload)
230
+ assertTrue(p.puts.isEmpty())
231
+ assertEquals(listOf(5) + u32(G2CfwScene.objectId("ball", 0)) + listOf(0x07, 0x03, 0, 64, 26, 64, 255), p.ops.subList(0, 12))
232
+ assertEquals(le(242, 106), p.ops.subList(12, 16))
233
+ }
234
+
235
+ @Test
236
+ fun objectReturningToTheFrameSnapsInsteadOfTweeningFromWhereItLeft() {
237
+ val (scene, cache) = fresh()
238
+ val bg = element("bg", "rect", w = 50, h = 10, fill = true)
239
+ fun ball(x: Int, ms: Int = 0) = element("ball", "circle", x = x, w = 21, h = 21, fill = true, transitionMs = ms, easing = "linear")
240
+ val id = G2CfwScene.objectId("ball", 0)
241
+ encode(scene, cache, listOf(bg, ball(200)))
242
+ encode(scene, cache, listOf(bg, ball(0, 330)))
243
+ encode(scene, cache, listOf(bg))
244
+ // Back at its resident definition with a transition: snapped there, not slid over from x=0.
245
+ val back = encode(scene, cache, listOf(bg, ball(200, 330)))!!
246
+ val p = parse(back.payload)
247
+ assertTrue(p.puts.isEmpty())
248
+ assertEquals(0, back.tweens)
249
+ assertEquals(listOf(5) + u32(id) + listOf(0x07, 0x03, 0, 0, 0, 255, 255), p.ops.subList(0, 12))
250
+ assertEquals(le(242, 106), p.ops.subList(12, 16))
251
+ // Once on screen it animates again.
252
+ val slid = encode(scene, cache, listOf(bg, ball(0, 330)))!!
253
+ assertEquals(1, slid.tweens)
254
+ assertEquals(listOf(0x07, 0x03, 10), parse(slid.payload).ops.subList(5, 8))
255
+ }
256
+
257
+ @Test
258
+ fun forgetRedefinesOnlyTheMissingObjects() {
259
+ val (scene, cache) = fresh()
260
+ val a = element("a", "circle", w = 11, h = 11)
261
+ val b = element("b", "circle", w = 11, h = 11)
262
+ encode(scene, cache, listOf(a, b))
263
+ scene.forget(listOf(G2CfwScene.objectId("a", 0)))
264
+ val p = parse(encode(scene, cache, listOf(a, b))!!.payload)
265
+ assertEquals(1, p.puts.size)
266
+ assertEquals(u32(G2CfwScene.objectId("a", 0)), p.puts[0].subList(1, 5))
267
+ assertEquals(2, p.refs.size)
268
+ scene.invalidate()
269
+ assertEquals(2, parse(encode(scene, cache, listOf(a, b))!!.payload).puts.size)
270
+ }
271
+
272
+ @Test
273
+ fun builtinTextAndRasterTilesUseAssets() {
274
+ val (scene, cache) = fresh()
275
+ val encoded = encode(
276
+ scene, cache,
277
+ listOf(
278
+ element("t", "text", w = 100, h = 30, text = "hi", font = "builtin"),
279
+ element("i", "image", y = 40, w = 300, h = 10, hash = "img"),
280
+ ),
281
+ )!!
282
+ val p = parse(encoded.payload)
283
+ assertEquals(3, p.puts.size)
284
+ assertEquals(listOf(15, 1, 0x1F, 0), p.puts[0].subList(9, 13))
285
+ val stringId = G2CfwTextureCache.hash31("t:hi")
286
+ assertEquals(u32(stringId), p.puts[0].subList(17, 21))
287
+ assertEquals(listOf(14, 1, 0x0F, 0), p.puts[1].subList(9, 13))
288
+ assertEquals(le(32, 136), p.puts[1].subList(13, 17))
289
+ assertEquals(le(287, 136), p.puts[2].subList(13, 17))
290
+ assertEquals(1, encoded.uploads.size)
291
+ val upload = encoded.uploads[0].map { it.toInt() and 0xFF }
292
+ assertEquals(listOf(37, 0, 0, 0, 0), upload.subList(0, 5))
293
+ assertEquals(0, upload[5])
294
+ assertEquals(u32(stringId), upload.subList(6, 10))
295
+ assertEquals(G2CfwTextureCache.KIND_STRING, upload[14])
296
+ assertEquals(u32(2), upload.subList(15, 19))
297
+ assertEquals("hi".toByteArray().map { it.toInt() }, upload.subList(25, 27))
298
+ assertEquals(G2CfwTextureCache.KIND_IMAGE, upload[36])
299
+ assertEquals(2, setOf(p.puts[1].subList(17, 21), p.puts[2].subList(17, 21)).size)
300
+ assertTrue(encode(scene, cache, listOf(element("t", "text", w = 100, h = 30, text = "hi", font = "builtin")))!!.uploads.isEmpty())
301
+ }
302
+
303
+ @Test
304
+ fun imagesShareAssetsByPixelsAndFadeAsTweens() {
305
+ val (scene, cache) = fresh()
306
+ val first = encode(
307
+ scene, cache,
308
+ listOf(
309
+ element("a", "image", w = 10, h = 10, color = 15, hash = "one"),
310
+ element("b", "image", x = 20, w = 10, h = 10, color = 15, hash = "two"),
311
+ ),
312
+ )!!
313
+ val p = parse(first.payload)
314
+ assertEquals(p.puts[0].subList(17, 21), p.puts[1].subList(17, 21))
315
+ assertEquals(1, first.uploads.size)
316
+ val faded = encode(
317
+ scene, cache,
318
+ listOf(
319
+ element("a", "image", w = 10, h = 10, color = 8, hash = "one", transitionMs = 330),
320
+ element("b", "image", x = 20, w = 10, h = 10, color = 15, hash = "two"),
321
+ ),
322
+ )!!
323
+ val q = parse(faded.payload)
324
+ assertTrue(q.puts.isEmpty())
325
+ assertEquals(listOf(0x03, 0x01, 10), q.ops.subList(5, 8))
326
+ assertEquals(le(32, 96, 8), q.ops.subList(12, 18))
327
+ }
328
+
329
+ @Test
330
+ fun tooManyObjectsReturnsNull() {
331
+ val (scene, cache) = fresh()
332
+ val many = (0 until 257).map { element("e$it", "circle", w = 5, h = 5) }
333
+ assertNull(encode(scene, cache, many))
334
+ assertNotNull(encode(scene, cache, many.take(256)))
335
+ }
336
+
337
+ @Test
338
+ fun textBytesStripControlsAndCutAtCodepoints() {
339
+ assertArrayEquals(byteArrayOf(0x61, 0x62), G2CfwScene.textBytes("a\u0001bé", 3))
340
+ assertArrayEquals("héllo".toByteArray(Charsets.UTF_8), G2CfwScene.textBytes("héllo", 64))
341
+ }
342
+
343
+ @Test
344
+ fun inlineTextRecordsCarryTheirBytesAndAnimate() {
345
+ val (scene, cache) = fresh()
346
+ fun t(y: Int, text: String, color: Int, ms: Int) = element("t", "text", x = 10, y = y, w = 200, h = 60, text = text, color = color, transitionMs = ms)
347
+ val first = encode(scene, cache, listOf(t(10, "hi\nthere", 9, 0)), inlineText = true)!!
348
+ val p = parse(first.payload)
349
+ assertEquals(2, p.puts.size)
350
+ assertEquals(listOf(17, 1, 0x19, 0), p.puts[0].subList(9, 13))
351
+ assertEquals(le(44, 108, 198, 58), p.puts[0].subList(13, 21))
352
+ assertEquals(listOf(2, 0x68, 0x69), p.puts[0].subList(21, 24))
353
+ assertEquals(le(44, 135, 198, 31), p.puts[1].subList(13, 21))
354
+ assertEquals("there".toByteArray().map { it.toInt() }, p.puts[1].subList(22, 27))
355
+ assertTrue(first.uploads.isEmpty())
356
+ val q = parse(encode(scene, cache, listOf(t(40, "hi\nthere", 9, 330)), inlineText = true)!!.payload)
357
+ assertTrue(q.puts.isEmpty())
358
+ assertEquals(listOf(0x0F, 0x01, 10), q.ops.subList(5, 8))
359
+ assertEquals(le(44, 138, 198, 58, 0x19), q.ops.subList(12, 22))
360
+ assertEquals(1, parse(encode(scene, cache, listOf(t(40, "ho\nthere", 9, 330)), inlineText = true)!!.payload).puts.size)
361
+ val r = parse(encode(scene, cache, listOf(t(40, "ho\nthere", 3, 330)), inlineText = true)!!.payload)
362
+ assertEquals(44, r.ops.size)
363
+ assertEquals(le(0x13), r.ops.subList(20, 22))
364
+ }
365
+
366
+ private val mono = G2CfwScene.MonoFace({ 6 to 12 }) { _, _, w, h -> ByteArray(w * h) { 0xFF.toByte() } }
367
+
368
+ @Test
369
+ fun monoTextBecomesCachedFontRunsThatTween() {
370
+ val (scene, cache) = fresh()
371
+ val grid = element("g", "text", w = 60, h = 24, text = "ab\ncd", font = "mono", fontSize = 12)
372
+ val encoded = scene.encode(frame(listOf(grid)), 32, 96, cache, { _, _, _ -> null }, true, mono)!!
373
+ val p = parse(encoded.payload)
374
+ assertEquals(2, p.puts.size)
375
+ assertEquals(listOf(16, 1, 15, 0), p.puts[0].subList(9, 13))
376
+ assertEquals(le(34, 98), p.puts[0].subList(13, 17))
377
+ val fontId = G2CfwTextureCache.hash31("font:12:6x12:ascii")
378
+ assertEquals(u32(fontId), p.puts[0].subList(21, 25))
379
+ assertEquals(u32(G2CfwTextureCache.hash31("t:ascii:ab")), p.puts[0].subList(17, 21))
380
+ assertFalse(encoded.uploads.isEmpty())
381
+ val entries = encoded.uploads.flatMap { it.drop(5) }.map { it.toInt() and 0xFF }
382
+ assertEquals(0, entries[0])
383
+ assertEquals(u32(fontId), entries.subList(1, 5))
384
+ assertEquals(G2CfwTextureCache.KIND_FONT, entries[9])
385
+ assertEquals(96 * 4 + 95 * (2 + 2), entries[10] or (entries[11] shl 8) or (entries[12] shl 16))
386
+ val moved = scene.encode(frame(listOf(element("g", "text", y = 20, w = 60, h = 24, text = "ab\ncd", font = "mono", fontSize = 12, transitionMs = 330))), 32, 96, cache, { _, _, _ -> null }, true, mono)!!
387
+ val q = parse(moved.payload)
388
+ assertTrue(q.puts.isEmpty())
389
+ assertTrue(moved.uploads.isEmpty())
390
+ assertEquals(listOf(0x03, 0x01, 10), q.ops.subList(5, 8))
391
+ assertEquals(le(34, 118, 15), q.ops.subList(12, 18))
392
+ }
393
+
394
+ @Test
395
+ fun halfBlockGridUsesASeparateFontAndNonAsciiKeepsTiles() {
396
+ val (scene, cache) = fresh()
397
+ val blocks = element("b", "text", w = 60, h = 12, text = "▀▄█ ", font = "mono", fontSize = 12)
398
+ val p = parse(scene.encode(frame(listOf(blocks)), 32, 96, cache, { _, _, _ -> null }, true, mono)!!.payload)
399
+ assertEquals(1, p.puts.size)
400
+ assertEquals(u32(G2CfwTextureCache.hash31("font:12:6x12:blocks")), p.puts[0].subList(21, 25))
401
+ assertEquals(u32(G2CfwTextureCache.hash31("t:blocks:▀▄█ ")), p.puts[0].subList(17, 21))
402
+ val mixed = element("m", "text", w = 60, h = 12, text = "a▀", font = "mono", fontSize = 12)
403
+ val tiles = scene.encode(frame(listOf(mixed)), 32, 96, cache, { _, w, h -> ByteArray(w * h) { 0xFF.toByte() } }, true, mono)!!
404
+ assertEquals(14, parse(tiles.payload).puts[0][9])
405
+ }
406
+
407
+ @Test
408
+ fun movingBackToTheResidentDefinitionWithoutTransitionSnapsInsteadOfRedefining() {
409
+ val (scene, cache) = fresh()
410
+ val home = element("row", "rect", x = 300, y = 40, w = 200, h = 30, border = 1, hash = "r")
411
+ val away = element("row", "rect", x = 20, y = 80, w = 500, h = 30, border = 1, transitionMs = 300, hash = "r")
412
+ val first = encode(scene, cache, listOf(home))!!
413
+ assertEquals(1, first.puts)
414
+ val moved = encode(scene, cache, listOf(away))!!
415
+ assertEquals("a transition tweens the row away; its version stays", 1, moved.tweens)
416
+ // Back home with no transition: a PUT of the same version would be a firmware
417
+ // no-op and leave the row where the tween put it, so the encoder snaps it.
418
+ val back = encode(scene, cache, listOf(home))!!
419
+ assertEquals(0, back.puts)
420
+ assertEquals(1, back.hits)
421
+ assertTrue(back.changed)
422
+ val p = parse(back.payload)
423
+ assertEquals("same version as the original definition", parse(first.payload).refs.map { it.second }, p.refs.map { it.second })
424
+ assertEquals("a TWEEN op", 5, p.ops[0])
425
+ assertEquals("of zero frames", 0, p.ops[7])
426
+ assertFalse(encode(scene, cache, listOf(home))!!.changed)
427
+ val restyled = element("row", "rect", x = 300, y = 40, w = 200, h = 30, border = 3, hash = "r")
428
+ assertEquals(1, encode(scene, cache, listOf(restyled))!!.puts)
429
+ }
430
+
431
+ @Test
432
+ fun depthShiftsEachLensMirrorTheOppositeWayAndFlatEncodesIgnoreIt() {
433
+
434
+ val cache = G2CfwTextureCache().also { it.reset(1) }
435
+ val card = element("card", "rect", x = 100, y = 40, w = 200, h = 100, border = 2, depth = 8, hash = "c")
436
+ val flat = element("bg", "rect", x = 10, y = 10, w = 50, h = 50, border = 1, hash = "b")
437
+ val left = G2CfwScene().also { it.reset(1) }
438
+ val right = G2CfwScene().also { it.reset(1) }
439
+ val mono = G2CfwScene().also { it.reset(1) }
440
+ val l = parse(encode(left, cache, listOf(flat, card), parallax = 1)!!.payload)
441
+ val r = parse(encode(right, cache, listOf(flat, card), parallax = -1)!!.payload)
442
+ val m = parse(encode(mono, cache, listOf(flat, card))!!.payload)
443
+ // A put is [1][id u32][version u32] + the record [type][flags][color][width][x i16][y i16]…
444
+ fun x(put: List<Int>) = (put[9 + 4] or (put[9 + 5] shl 8)).toShort().toInt()
445
+ assertEquals("flat elements sit at the window origin on every lens", 32 + 10, x(l.puts[0]))
446
+ assertEquals(32 + 10, x(r.puts[0]))
447
+ assertEquals("the left lens draws a near element further right", 32 + 100 + 8, x(l.puts[1]))
448
+ assertEquals("and the right lens further left", 32 + 100 - 8, x(r.puts[1]))
449
+ assertEquals("a flat encode ignores depth", 32 + 100, x(m.puts[1]))
450
+ assertEquals(l.puts[0], r.puts[0])
451
+ assertTrue(l.puts[1] != r.puts[1])
452
+ assertEquals("same object id on both lenses", l.refs[1].first, r.refs[1].first)
453
+ assertTrue("different versions", l.refs[1].second != r.refs[1].second)
454
+
455
+ // Bringing the card to the frame's plane with a transition tweens x on each lens, in opposite directions.
456
+ val level = element("card", "rect", x = 100, y = 40, w = 200, h = 100, border = 2, transitionMs = 330, hash = "c")
457
+ val l2 = encode(left, cache, listOf(flat, level), parallax = 1)!!
458
+ val r2 = encode(right, cache, listOf(flat, level), parallax = -1)!!
459
+ assertEquals(1, l2.tweens)
460
+ assertEquals(1, r2.tweens)
461
+ assertFalse(l2.payload.contentEquals(r2.payload))
462
+ assertEquals(0, parse(l2.payload).puts.size)
463
+ }
464
+
465
+ @Test
466
+ fun adoptCopiesTheMirrorSoTheSecondLensStartsFromSharedResidency() {
467
+ val (left, cache) = fresh()
468
+ val flat = element("bg", "rect", x = 10, y = 10, w = 50, h = 50, border = 1, hash = "b")
469
+ assertNotNull(encode(left, cache, listOf(flat)))
470
+ val right = G2CfwScene()
471
+ right.adopt(left)
472
+ assertEquals(1, right.generation)
473
+ assertEquals(left.active, right.active)
474
+ val again = encode(right, cache, listOf(flat), parallax = -1)!!
475
+ assertEquals("what the left lens holds, the right holds too", 0, again.puts)
476
+ assertEquals(1, again.hits)
477
+ assertFalse(again.changed)
478
+ }
479
+
480
+ @Test
481
+ fun onlyNonAnimatedImageFramesPreferRaster() {
482
+
483
+ val image = element("i", "image", w = 10, h = 10)
484
+ assertTrue(G2CfwScene.prefersRaster(frame(listOf(image))))
485
+ assertFalse(G2CfwScene.prefersRaster(frame(listOf(image, element("c", "circle", w = 5, h = 5)))))
486
+ assertFalse(G2CfwScene.prefersRaster(frame(listOf(element("i", "image", w = 10, h = 10, transitionMs = 100)))))
487
+ assertFalse(G2CfwScene.prefersRaster(frame(emptyList())))
488
+ }
489
+ }