@glassly/bluetooth-sdk 0.1.0-dev.112 → 0.1.0-dev.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +24 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +7 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DisplayPacer.kt +198 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +9 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +3 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +79 -21
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +2 -2
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +37 -3
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSDK.swift +1 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/DeviceManager.swift +27 -2
- package/ios/Source/DeviceStore.swift +7 -0
- package/ios/Source/DisplayPacer.swift +179 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +3 -3
- package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
- package/ios/Source/sgcs/G2.swift +8 -1
- package/ios/Source/sgcs/G2CfwRenderer.swift +5 -3
- package/ios/Source/sgcs/G2CfwScene.swift +67 -19
- package/ios/Source/sgcs/G2Flasher.swift +2 -2
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +46 -2
- package/package.json +1 -1
- package/src/generated/releaseMetadata.ts +3 -3
|
@@ -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
|
+
}
|
|
@@ -35,8 +35,8 @@ class G2CfwSceneTest {
|
|
|
35
35
|
private fun frame(elements: List<SceneElement>, replay: Boolean = false) =
|
|
36
36
|
SceneFrame("app", 1, replay, elements, emptyList())
|
|
37
37
|
|
|
38
|
-
private fun encode(scene: G2CfwScene, cache: G2CfwTextureCache, elements: List<SceneElement
|
|
39
|
-
scene.encode(frame(elements), 32, 96, cache
|
|
38
|
+
private fun encode(scene: G2CfwScene, cache: G2CfwTextureCache, elements: List<SceneElement>, inlineText: Boolean = false) =
|
|
39
|
+
scene.encode(frame(elements), 32, 96, cache, { _, w, h -> ByteArray(w * h) { 0xFF.toByte() } }, inlineText)
|
|
40
40
|
|
|
41
41
|
private fun le(v: Int): List<Byte> = listOf((v and 0xFF).toByte(), ((v ushr 8) and 0xFF).toByte())
|
|
42
42
|
|
|
@@ -158,7 +158,7 @@ class G2CfwSceneTest {
|
|
|
158
158
|
val (scene, cache) = fresh()
|
|
159
159
|
assertNull(encode(scene, cache, (0 until 129).map { element("s$it", "circle", w = 11, h = 11) }))
|
|
160
160
|
val huge = element("big", "image", w = 2000, h = 2000, hash = "big")
|
|
161
|
-
val full = scene.encode(frame(listOf(huge)), 0, 0, cache
|
|
161
|
+
val full = scene.encode(frame(listOf(huge)), 0, 0, cache, { _, w, h -> ByteArray(w * h) { (it * 37).toByte() } })
|
|
162
162
|
assertNull(full)
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -172,4 +172,38 @@ class G2CfwSceneTest {
|
|
|
172
172
|
assertEquals(255, G2CfwScene.transitionFrames(60000))
|
|
173
173
|
assertArrayEquals(intArrayOf(107, 0, 148, 255), G2CfwScene.easingCurve("ease-in-out"))
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
fun inlineTextRecordsCarryTheirBytesAndAnimate() {
|
|
178
|
+
val (scene, cache) = fresh()
|
|
179
|
+
val mono = element("m", "text", y = 200, w = 100, h = 30, text = "grid", font = "mono", hash = "mono")
|
|
180
|
+
val first = requireNotNull(
|
|
181
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 10, w = 200, h = 60, text = "hi\nthere", color = 9), mono), true),
|
|
182
|
+
)
|
|
183
|
+
assertEquals(1, first.uploads.size)
|
|
184
|
+
assertEquals(listOf(listOf(0, 1), listOf(2)), scene.elements.map { it.indices })
|
|
185
|
+
val p = first.payload
|
|
186
|
+
assertEquals(listOf<Byte>(0, 0, 17, 1, 0x19, 0), bytes(p, 3, 9))
|
|
187
|
+
assertEquals(le(44) + le(108) + le(198) + le(58), bytes(p, 9, 17))
|
|
188
|
+
assertEquals(listOf<Byte>(2, 0x68, 0x69), bytes(p, 17, 20))
|
|
189
|
+
assertEquals(listOf<Byte>(0, 1, 17), bytes(p, 20, 23))
|
|
190
|
+
assertEquals(le(44) + le(135) + le(198) + le(31), bytes(p, 26, 34))
|
|
191
|
+
assertEquals("there".toByteArray().toList(), bytes(p, 35, 40))
|
|
192
|
+
assertEquals(0.toByte(), p[40])
|
|
193
|
+
assertEquals(14.toByte(), p[42])
|
|
194
|
+
|
|
195
|
+
val moved = requireNotNull(
|
|
196
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "hi\nthere", color = 9, transitionMs = 330), mono), true),
|
|
197
|
+
)
|
|
198
|
+
assertEquals(listOf<Byte>(5, 0, 0x02, 0x00, 10), bytes(moved.payload, 3, 8))
|
|
199
|
+
val retexted = requireNotNull(
|
|
200
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "ho\nthere", color = 9, transitionMs = 330), mono), true),
|
|
201
|
+
)
|
|
202
|
+
assertEquals(listOf<Byte>(0, 0, 17), bytes(retexted.payload, 3, 6))
|
|
203
|
+
val faded = requireNotNull(
|
|
204
|
+
encode(scene, cache, listOf(element("t", "text", x = 10, y = 40, w = 200, h = 60, text = "ho\nthere", color = 3, transitionMs = 330), mono), true),
|
|
205
|
+
)
|
|
206
|
+
assertEquals(listOf<Byte>(5, 0, 0x00, 0x01, 10), bytes(faded.payload, 3, 8))
|
|
207
|
+
assertEquals(le(0x13), bytes(faded.payload, 12, 14))
|
|
208
|
+
}
|
|
175
209
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const BLUETOOTH_SDK_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.1.0",
|
|
4
|
-
"releaseIdentity": "0.1.0-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.1.0-dev.114",
|
|
5
|
+
"releaseSetId": "glassly-0.1.0-dev.114",
|
|
6
|
+
"sourceCommit": "17263133b35c04f044ba0d3acd9f80efc81999d4",
|
|
7
7
|
"otaManifestUrl": null,
|
|
8
8
|
"otaManifestSha256": null
|
|
9
9
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,8BAA8B,GAA0C,MAAM,CAAC,MAAM,CAAC;IACjG,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface BluetoothSdkReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.1.0\",\n \"releaseIdentity\": \"0.1.0-dev.114\",\n \"releaseSetId\": \"glassly-0.1.0-dev.114\",\n \"sourceCommit\": \"17263133b35c04f044ba0d3acd9f80efc81999d4\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
|
|
|
11
11
|
static let voiceActivityDetectionEnabled = false
|
|
12
12
|
static let loudnessGateEnabled = true
|
|
13
13
|
private static let infoSdkVersionKey = "BluetoothSdkVersion"
|
|
14
|
-
private static let swiftPackageSdkVersion = "0.1.0-dev.
|
|
14
|
+
private static let swiftPackageSdkVersion = "0.1.0-dev.114"
|
|
15
15
|
private static let swiftPackageSdkVersionPlaceholder = "__GLASSLY" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
16
|
|
|
17
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
|
@@ -320,6 +320,21 @@ struct ViewState {
|
|
|
320
320
|
/// native re-dispatch coherent (nav exit re-applies a complete scene).
|
|
321
321
|
var sceneState: SceneFrame?
|
|
322
322
|
|
|
323
|
+
/// Paces scene frames and legacy layouts to display_max_fps; clears bypass it.
|
|
324
|
+
private lazy var pacer = DisplayPacer(
|
|
325
|
+
dispatchScene: { [weak self] frame in self?.dispatchSceneFrame(frame) },
|
|
326
|
+
sendLegacy: { [weak self] in self?.sendCurrentState() }
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
func setDisplayMaxFps(_ fps: Int) {
|
|
330
|
+
pacer.setMaxFps(fps)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Drop paced display work (screen off): the retained state repaints on re-enable.
|
|
334
|
+
func cancelPendingDisplay() {
|
|
335
|
+
pacer.cancel()
|
|
336
|
+
}
|
|
337
|
+
|
|
323
338
|
override init() {
|
|
324
339
|
Bridge.log("MAN: init()")
|
|
325
340
|
super.init()
|
|
@@ -669,6 +684,7 @@ struct ViewState {
|
|
|
669
684
|
Bridge.log("Initializing manager for wearable: \(wearable)")
|
|
670
685
|
if sgc != nil && sgc?.type != wearable {
|
|
671
686
|
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
687
|
+
pacer.cancel()
|
|
672
688
|
sgc?.cleanup()
|
|
673
689
|
sgc = nil
|
|
674
690
|
resetSystemTimeSync()
|
|
@@ -1310,7 +1326,11 @@ struct ViewState {
|
|
|
1310
1326
|
}
|
|
1311
1327
|
|
|
1312
1328
|
viewState = newViewState
|
|
1313
|
-
|
|
1329
|
+
if layoutType == "clear_view" {
|
|
1330
|
+
pacer.flushLegacy()
|
|
1331
|
+
} else {
|
|
1332
|
+
pacer.submitLegacy()
|
|
1333
|
+
}
|
|
1314
1334
|
}
|
|
1315
1335
|
|
|
1316
1336
|
/// Parse + store a scene frame, then dispatch it.
|
|
@@ -1349,7 +1369,11 @@ struct ViewState {
|
|
|
1349
1369
|
text: " ", data: nil, animationData: nil
|
|
1350
1370
|
)
|
|
1351
1371
|
|
|
1352
|
-
|
|
1372
|
+
if frame.elements.isEmpty {
|
|
1373
|
+
pacer.flushScene(frame)
|
|
1374
|
+
} else {
|
|
1375
|
+
pacer.submitScene(frame)
|
|
1376
|
+
}
|
|
1353
1377
|
}
|
|
1354
1378
|
|
|
1355
1379
|
/// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
|
|
@@ -1947,6 +1971,7 @@ struct ViewState {
|
|
|
1947
1971
|
}
|
|
1948
1972
|
|
|
1949
1973
|
func disconnect(clearPendingConnection: Bool = true) {
|
|
1974
|
+
pacer.cancel()
|
|
1950
1975
|
sgc?.clearDisplay() // clear the screen
|
|
1951
1976
|
sgc?.disconnect()
|
|
1952
1977
|
sgc = nil // Clear the SGC reference after disconnect
|
|
@@ -115,6 +115,7 @@ class DeviceStore {
|
|
|
115
115
|
store.set("bluetooth", "use_native_dashboard", false)
|
|
116
116
|
// Debug escape hatch: G2 CFW direct-framebuffer rendering (off = stock containers).
|
|
117
117
|
store.set("bluetooth", "cfw_scene_renderer", true)
|
|
118
|
+
store.set("bluetooth", "display_max_fps", DisplayPacer.defaultFps)
|
|
118
119
|
// G2 on-glass ANCS notification behavior + app filter (iOS).
|
|
119
120
|
store.set(
|
|
120
121
|
"bluetooth", "notification_ctrl",
|
|
@@ -231,6 +232,11 @@ class DeviceStore {
|
|
|
231
232
|
Task { await DeviceManager.shared.sgc?.cfwSceneRendererToggled(enabled) }
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
case ("bluetooth", "display_max_fps"):
|
|
236
|
+
if let fps = value as? NSNumber {
|
|
237
|
+
DeviceManager.shared.setDisplayMaxFps(fps.intValue)
|
|
238
|
+
}
|
|
239
|
+
|
|
234
240
|
case ("bluetooth", "dashboard_height"):
|
|
235
241
|
scheduleDashboardHeightToGlasses()
|
|
236
242
|
|
|
@@ -278,6 +284,7 @@ class DeviceStore {
|
|
|
278
284
|
case ("bluetooth", "screen_disabled"):
|
|
279
285
|
if let disabled = value as? Bool {
|
|
280
286
|
if disabled {
|
|
287
|
+
DeviceManager.shared.cancelPendingDisplay()
|
|
281
288
|
DeviceManager.shared.sgc?.exit()
|
|
282
289
|
} else {
|
|
283
290
|
DeviceManager.shared.sgc?.clearDisplay()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Paces display updates leaving the phone to at most `setMaxFps` per second.
|
|
4
|
+
///
|
|
5
|
+
/// Updates that arrive inside the current slot are held and sent at the next
|
|
6
|
+
/// one, last-wins: a newer scene frame replaces a pending one (with the pending
|
|
7
|
+
/// frame's per-element annotations folded in so per-element devices never miss
|
|
8
|
+
/// a change), a legacy layout replaces a pending scene and vice versa. Clears
|
|
9
|
+
/// (`flushScene` with an empty frame, `flushLegacy`) bypass the pacer and drop
|
|
10
|
+
/// pending work.
|
|
11
|
+
@MainActor
|
|
12
|
+
final class DisplayPacer {
|
|
13
|
+
/// Runs `block` after `delayMs`; the returned closure cancels it.
|
|
14
|
+
typealias Scheduler = (_ delayMs: Int64, _ block: @escaping @MainActor () -> Void) -> () -> Void
|
|
15
|
+
|
|
16
|
+
static let minFps = 2
|
|
17
|
+
static let maxFps = 20
|
|
18
|
+
static let defaultFps = 8
|
|
19
|
+
|
|
20
|
+
static func interval(forFps fps: Int) -> Int64 {
|
|
21
|
+
Int64(1000 / min(max(fps, minFps), maxFps))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func monotonicMs() -> Int64 {
|
|
25
|
+
Int64(DispatchTime.now().uptimeNanoseconds / 1_000_000)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static func taskScheduler(_ delayMs: Int64, _ block: @escaping @MainActor () -> Void) -> () -> Void {
|
|
29
|
+
let task = Task { @MainActor in
|
|
30
|
+
try? await Task.sleep(nanoseconds: UInt64(max(0, delayMs)) * 1_000_000)
|
|
31
|
+
if Task.isCancelled { return }
|
|
32
|
+
block()
|
|
33
|
+
}
|
|
34
|
+
return { task.cancel() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Fold pending frame `previous` into `next` (both unsent). The device only
|
|
38
|
+
/// ever sees the merged frame, so an element the pending frame changed must
|
|
39
|
+
/// not read as "unchanged" in the merged one, and ids the pending frame
|
|
40
|
+
/// removed must still be removed unless `next` re-creates them. A different
|
|
41
|
+
/// app is already a from-scratch replay.
|
|
42
|
+
static func merge(_ previous: SceneFrame?, into next: SceneFrame) -> SceneFrame {
|
|
43
|
+
guard let previous, previous.appId == next.appId else { return next }
|
|
44
|
+
var pending: [String: SceneElement] = [:]
|
|
45
|
+
for el in previous.elements { pending[el.id] = el }
|
|
46
|
+
let elements = next.elements.map { el -> SceneElement in
|
|
47
|
+
guard let prior = pending[el.id] else { return el }
|
|
48
|
+
if prior.change == "created" { return el.with(change: "created") }
|
|
49
|
+
if prior.change != "unchanged", el.change == "unchanged" { return el.with(change: "updated") }
|
|
50
|
+
return el
|
|
51
|
+
}
|
|
52
|
+
let painted = Set(next.elements.map(\.id))
|
|
53
|
+
var seen = Set<String>()
|
|
54
|
+
var removed: [String] = []
|
|
55
|
+
for id in previous.removed + next.removed where !painted.contains(id) && seen.insert(id).inserted {
|
|
56
|
+
removed.append(id)
|
|
57
|
+
}
|
|
58
|
+
return SceneFrame(
|
|
59
|
+
appId: next.appId, epoch: next.epoch, replay: previous.replay || next.replay,
|
|
60
|
+
elements: elements, removed: removed,
|
|
61
|
+
canvasWidth: next.canvasWidth, canvasHeight: next.canvasHeight
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private let dispatchScene: (SceneFrame) -> Void
|
|
66
|
+
private let sendLegacy: () -> Void
|
|
67
|
+
private let clock: () -> Int64
|
|
68
|
+
private let scheduler: Scheduler
|
|
69
|
+
|
|
70
|
+
private var intervalMs: Int64 = DisplayPacer.interval(forFps: DisplayPacer.defaultFps)
|
|
71
|
+
private var lastSendAt: Int64 = Int64.min / 2
|
|
72
|
+
private var pendingScene: SceneFrame?
|
|
73
|
+
private var pendingLegacy = false
|
|
74
|
+
private var cancelTimer: (() -> Void)?
|
|
75
|
+
|
|
76
|
+
init(
|
|
77
|
+
dispatchScene: @escaping (SceneFrame) -> Void,
|
|
78
|
+
sendLegacy: @escaping () -> Void,
|
|
79
|
+
clock: @escaping () -> Int64 = DisplayPacer.monotonicMs,
|
|
80
|
+
scheduler: @escaping Scheduler = DisplayPacer.taskScheduler
|
|
81
|
+
) {
|
|
82
|
+
self.dispatchScene = dispatchScene
|
|
83
|
+
self.sendLegacy = sendLegacy
|
|
84
|
+
self.clock = clock
|
|
85
|
+
self.scheduler = scheduler
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func setMaxFps(_ fps: Int) {
|
|
89
|
+
intervalMs = Self.interval(forFps: fps)
|
|
90
|
+
guard hasPending else { return }
|
|
91
|
+
stopTimer()
|
|
92
|
+
let now = clock()
|
|
93
|
+
if now - lastSendAt >= intervalMs {
|
|
94
|
+
take(now)?()
|
|
95
|
+
} else {
|
|
96
|
+
armTimer(now)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Send `frame` at the next slot; replaces any pending update.
|
|
101
|
+
func submitScene(_ frame: SceneFrame) {
|
|
102
|
+
let now = clock()
|
|
103
|
+
pendingScene = Self.merge(pendingScene, into: frame)
|
|
104
|
+
pendingLegacy = false
|
|
105
|
+
submitPending(now)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Send the current legacy view state at the next slot; replaces any pending update.
|
|
109
|
+
func submitLegacy() {
|
|
110
|
+
let now = clock()
|
|
111
|
+
pendingScene = nil
|
|
112
|
+
pendingLegacy = true
|
|
113
|
+
submitPending(now)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Immediate send for an empty (clearing) frame; pending removes ride along.
|
|
117
|
+
func flushScene(_ frame: SceneFrame) {
|
|
118
|
+
stopTimer()
|
|
119
|
+
let merged = Self.merge(pendingScene, into: frame)
|
|
120
|
+
pendingScene = nil
|
|
121
|
+
pendingLegacy = false
|
|
122
|
+
lastSendAt = clock()
|
|
123
|
+
dispatchScene(merged)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Immediate legacy send (clear_view); drops any pending scene.
|
|
127
|
+
func flushLegacy() {
|
|
128
|
+
stopTimer()
|
|
129
|
+
pendingScene = nil
|
|
130
|
+
pendingLegacy = false
|
|
131
|
+
lastSendAt = clock()
|
|
132
|
+
sendLegacy()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Drop pending work without sending (disconnect, screen off, clearDisplay).
|
|
136
|
+
func cancel() {
|
|
137
|
+
stopTimer()
|
|
138
|
+
pendingScene = nil
|
|
139
|
+
pendingLegacy = false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private var hasPending: Bool { pendingScene != nil || pendingLegacy }
|
|
143
|
+
|
|
144
|
+
private func submitPending(_ now: Int64) {
|
|
145
|
+
if now - lastSendAt >= intervalMs {
|
|
146
|
+
stopTimer()
|
|
147
|
+
take(now)?()
|
|
148
|
+
} else if cancelTimer == nil {
|
|
149
|
+
armTimer(now)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func stopTimer() {
|
|
154
|
+
cancelTimer?()
|
|
155
|
+
cancelTimer = nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Consume pending work into a send closure and stamp the slot.
|
|
159
|
+
private func take(_ now: Int64) -> (() -> Void)? {
|
|
160
|
+
let scene = pendingScene
|
|
161
|
+
let legacy = pendingLegacy
|
|
162
|
+
pendingScene = nil
|
|
163
|
+
pendingLegacy = false
|
|
164
|
+
if scene == nil, !legacy { return nil }
|
|
165
|
+
lastSendAt = now
|
|
166
|
+
if let scene { return { self.dispatchScene(scene) } }
|
|
167
|
+
return { self.sendLegacy() }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Arm the single trailing timer for the next slot.
|
|
171
|
+
private func armTimer(_ now: Int64) {
|
|
172
|
+
let delay = max(0, lastSendAt + intervalMs - now)
|
|
173
|
+
cancelTimer = scheduler(delay) { [weak self] in
|
|
174
|
+
guard let self else { return }
|
|
175
|
+
self.cancelTimer = nil
|
|
176
|
+
self.take(self.clock())?()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "0.1.0"
|
|
6
|
-
static let releaseIdentity = "0.1.0-dev.
|
|
7
|
-
static let releaseSetId = "glassly-0.1.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
6
|
+
static let releaseIdentity = "0.1.0-dev.114"
|
|
7
|
+
static let releaseSetId = "glassly-0.1.0-dev.114"
|
|
8
|
+
static let sourceCommit = "17263133b35c04f044ba0d3acd9f80efc81999d4"
|
|
9
9
|
static let otaManifestUrl = ""
|
|
10
10
|
static let otaManifestSha256 = ""
|
|
11
11
|
}
|
|
Binary file
|