@glassly/bluetooth-sdk 0.1.0-dev.98 → 0.2.8-dev.444
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 +1075 -257
- 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 +1434 -504
- 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
|
@@ -3,35 +3,64 @@ package com.glassly.bluetoothsdk.sgcs
|
|
|
3
3
|
import java.io.ByteArrayOutputStream
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Phone-side
|
|
7
|
-
*
|
|
6
|
+
* Phone-side mirror of the Glassly CFW's asset store (GLASSLYCFW/38, g2flash
|
|
7
|
+
* scene.c): the images, cached fonts and strings that scene objects reference
|
|
8
|
+
* by 32-bit id.
|
|
8
9
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The glasses hold assets in a 256 KiB store behind their own allocator and
|
|
11
|
+
* evict the least recently used unreferenced ones when room is needed, so the
|
|
12
|
+
* wire never carries a storage offset: every asset has an id (a hash of what
|
|
13
|
+
* it is) and a version (a hash of its bytes), and a PUT_ASSET entry uploads it.
|
|
14
|
+
* This mirror remembers which ids it has queued for upload and predicts the
|
|
15
|
+
* store's occupancy; when the prediction overflows it forgets what the glasses
|
|
16
|
+
* are most likely to have evicted. A wrong prediction is harmless: the firmware
|
|
17
|
+
* answers a SHOW that references an absent asset with MISSING, and
|
|
18
|
+
* [forget] makes the next frame re-upload exactly those.
|
|
19
|
+
*
|
|
20
|
+
* The model is valid for one cache generation: the owner bumps a generation
|
|
21
|
+
* counter whenever the on-glass cache is lost (lease end, reset, failed send)
|
|
22
|
+
* and the model re-learns from scratch.
|
|
14
23
|
*
|
|
15
24
|
* Cached image format: `[width:u8][height:u8][4bpp RLE]` where the RLE covers
|
|
16
25
|
* exactly width*height pixels with NO row-pad nibbles (unlike modes 3/6).
|
|
17
|
-
*
|
|
18
|
-
* characters 32..127
|
|
19
|
-
*
|
|
20
|
-
* allocations start past them.
|
|
26
|
+
* A font asset is a 96-entry little-endian u32 table of glyph image offsets
|
|
27
|
+
* (characters 32..127, relative to the asset; 0 = absent) followed by the
|
|
28
|
+
* glyph images, uploaded whole.
|
|
21
29
|
*/
|
|
22
30
|
internal class G2CfwTextureCache {
|
|
23
31
|
companion object {
|
|
24
|
-
const val
|
|
32
|
+
const val STORE_SIZE = 256 * 1024
|
|
33
|
+
const val GRANULE = 16
|
|
34
|
+
const val MAX_ASSETS = 256
|
|
25
35
|
const val FONT_TABLE_CHARS = 96 // ASCII 32..127
|
|
36
|
+
const val FONT_TABLE_ENTRY_BYTES = 4
|
|
26
37
|
|
|
27
|
-
/**
|
|
28
|
-
const val MAX_UPLOAD_PAYLOAD_BYTES =
|
|
38
|
+
/** A PUT message is one transport message; keep them a few round trips long. */
|
|
39
|
+
const val MAX_UPLOAD_PAYLOAD_BYTES = 8192
|
|
29
40
|
|
|
30
41
|
/**
|
|
31
42
|
* 0x0F: identity ramp (top color 15), opaque — a full-cell glyph or a
|
|
32
43
|
* full-box image drawn opaque self-erases whatever was under it.
|
|
33
44
|
*/
|
|
34
45
|
const val OPAQUE_DRAW_OPTIONS = 0x0F
|
|
46
|
+
const val KIND_IMAGE = 0
|
|
47
|
+
const val KIND_FONT = 1
|
|
48
|
+
const val KIND_STRING = 2
|
|
49
|
+
|
|
50
|
+
/** Nonzero 31-bit FNV-1a of a key: asset ids and content versions. */
|
|
51
|
+
fun hash31(s: String): Int = hash31(s.toByteArray(Charsets.UTF_8))
|
|
52
|
+
|
|
53
|
+
fun hash31(bytes: ByteArray): Int {
|
|
54
|
+
var h = 0x811C9DC5.toInt()
|
|
55
|
+
for (b in bytes) {
|
|
56
|
+
h = h xor (b.toInt() and 0xFF)
|
|
57
|
+
h *= 0x01000193
|
|
58
|
+
}
|
|
59
|
+
h = h and 0x7FFFFFFF
|
|
60
|
+
return if (h == 0) 1 else h
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun footprint(length: Int): Int = (length + GRANULE - 1) / GRANULE * GRANULE
|
|
35
64
|
|
|
36
65
|
/**
|
|
37
66
|
* RLE over a flat 8-bit grayscale pixel sequence (quantized to 4bpp),
|
|
@@ -39,232 +68,276 @@ internal class G2CfwTextureCache {
|
|
|
39
68
|
* contract. Token forms match modes 3/6.
|
|
40
69
|
*/
|
|
41
70
|
fun rleEncodePixels(gray: ByteArray): ByteArray {
|
|
42
|
-
val
|
|
71
|
+
val out = ByteArrayOutputStream()
|
|
43
72
|
var index = 0
|
|
44
73
|
while (index < gray.size) {
|
|
45
|
-
|
|
46
|
-
// the composited frame's exact levels or the phone-side shadow
|
|
47
|
-
// diverges from the panel.
|
|
48
|
-
val color = G2CfwRenderer.grayNibble(gray[index].toInt())
|
|
74
|
+
val color = G2CfwRenderer.grayNibble(gray[index].toInt() and 0xFF)
|
|
49
75
|
var end = index + 1
|
|
50
|
-
while (end < gray.size && G2CfwRenderer.grayNibble(gray[end].toInt()) == color) end
|
|
76
|
+
while (end < gray.size && G2CfwRenderer.grayNibble(gray[end].toInt() and 0xFF) == color) end++
|
|
51
77
|
var remaining = end - index
|
|
52
78
|
while (remaining > 0) {
|
|
53
79
|
val chunk = minOf(remaining, 0xFFFF)
|
|
54
80
|
when {
|
|
55
|
-
chunk <= 15 ->
|
|
56
|
-
chunk <= 255 -> {
|
|
57
|
-
|
|
58
|
-
output.write(chunk)
|
|
59
|
-
}
|
|
60
|
-
else -> {
|
|
61
|
-
output.write(color)
|
|
62
|
-
output.write(0)
|
|
63
|
-
output.write(chunk and 0xFF)
|
|
64
|
-
output.write((chunk ushr 8) and 0xFF)
|
|
65
|
-
}
|
|
81
|
+
chunk <= 15 -> out.write((chunk shl 4) or color)
|
|
82
|
+
chunk <= 255 -> { out.write(color); out.write(chunk) }
|
|
83
|
+
else -> { out.write(color); out.write(0); out.write(chunk and 0xFF); out.write((chunk ushr 8) and 0xFF) }
|
|
66
84
|
}
|
|
67
85
|
remaining -= chunk
|
|
68
86
|
}
|
|
69
87
|
index = end
|
|
70
88
|
}
|
|
71
|
-
return
|
|
89
|
+
return out.toByteArray()
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
fun ByteArrayOutputStream.writeU32(value: Int) {
|
|
93
|
+
write(value and 0xFF)
|
|
94
|
+
write((value ushr 8) and 0xFF)
|
|
95
|
+
write((value ushr 16) and 0xFF)
|
|
96
|
+
write((value ushr 24) and 0xFF)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fun ByteArrayOutputStream.writeI16(value: Int) {
|
|
100
|
+
val v = value.coerceIn(-32768, 32767)
|
|
101
|
+
write(v and 0xFF)
|
|
102
|
+
write((v ushr 8) and 0xFF)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** PUT_ASSET entries for one asset: `[0][id][version][kind][total][offset][len][data]` per chunk. */
|
|
106
|
+
fun putAssetEntries(id: Int, version: Int, kind: Int, data: ByteArray): List<ByteArray> {
|
|
107
|
+
val out = mutableListOf<ByteArray>()
|
|
108
|
+
var offset = 0
|
|
109
|
+
val chunk = MAX_UPLOAD_PAYLOAD_BYTES - 32
|
|
110
|
+
do {
|
|
111
|
+
val len = minOf(chunk, data.size - offset)
|
|
112
|
+
val entry = ByteArrayOutputStream()
|
|
113
|
+
entry.write(0)
|
|
114
|
+
entry.writeU32(id)
|
|
115
|
+
entry.writeU32(version)
|
|
116
|
+
entry.write(kind)
|
|
117
|
+
entry.writeU32(data.size)
|
|
118
|
+
entry.writeU32(offset)
|
|
119
|
+
entry.write(len and 0xFF); entry.write((len ushr 8) and 0xFF)
|
|
120
|
+
entry.write(data, offset, len)
|
|
121
|
+
out.add(entry.toByteArray())
|
|
122
|
+
offset += len
|
|
123
|
+
} while (offset < data.size)
|
|
124
|
+
return out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Asset ids a [takeUploadPayloads] payload puts, in order. */
|
|
128
|
+
fun assetIdsInUploadPayload(payload: ByteArray): List<Int> {
|
|
129
|
+
if (payload.size <= 5 || payload[0].toInt() != 37) return emptyList()
|
|
130
|
+
val ids = mutableListOf<Int>()
|
|
131
|
+
var i = 5
|
|
132
|
+
while (i + 20 <= payload.size && payload[i].toInt() == 0) {
|
|
133
|
+
val id = (payload[i + 1].toInt() and 0xFF) or ((payload[i + 2].toInt() and 0xFF) shl 8) or
|
|
134
|
+
((payload[i + 3].toInt() and 0xFF) shl 16) or ((payload[i + 4].toInt() and 0xFF) shl 24)
|
|
135
|
+
if (ids.lastOrNull() != id) ids.add(id)
|
|
136
|
+
i += 20 + ((payload[i + 18].toInt() and 0xFF) or ((payload[i + 19].toInt() and 0xFF) shl 8))
|
|
82
137
|
}
|
|
83
|
-
|
|
138
|
+
return ids
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Mode-19 image draw: `[19][asset id:u32][x:i16][y:i16][options]`. */
|
|
142
|
+
fun imageDrawPayload(assetId: Int, x: Int, y: Int, options: Int): ByteArray? {
|
|
143
|
+
if (assetId == 0 || x !in -32768..32767 || y !in -32768..32767) return null
|
|
144
|
+
val payload = ByteArrayOutputStream()
|
|
145
|
+
payload.write(19)
|
|
146
|
+
payload.writeU32(assetId)
|
|
147
|
+
payload.writeI16(x)
|
|
148
|
+
payload.writeI16(y)
|
|
149
|
+
payload.write(options)
|
|
84
150
|
return payload.toByteArray()
|
|
85
151
|
}
|
|
86
152
|
|
|
87
153
|
/**
|
|
88
|
-
* Mode-
|
|
89
|
-
*
|
|
90
|
-
* Every char must be 32..126 and resident (the firmware validates each
|
|
154
|
+
* Mode-20 glyph run: `[20][font asset id:u32][x:i16][y:i16][options][strlen:u8][chars]`.
|
|
155
|
+
* Every char must be 32..126 and present in the font (the firmware validates each
|
|
91
156
|
* referenced glyph and rejects the whole draw otherwise).
|
|
92
157
|
*/
|
|
93
|
-
fun stringRunPayload(
|
|
94
|
-
|
|
95
|
-
x
|
|
96
|
-
y: Int,
|
|
97
|
-
options: Int,
|
|
98
|
-
chars: ByteArray,
|
|
99
|
-
): ByteArray? {
|
|
100
|
-
if (chars.isEmpty() || chars.size > 255) return null
|
|
101
|
-
if (fontTableOffset !in 0..(CACHE_SIZE - FONT_TABLE_CHARS * 2)) return null
|
|
102
|
-
if (x !in 0..0xFFFF || y !in 0..0xFFFF) return null
|
|
158
|
+
fun stringRunPayload(fontId: Int, x: Int, y: Int, options: Int, chars: ByteArray): ByteArray? {
|
|
159
|
+
if (chars.isEmpty() || chars.size > 255 || fontId == 0) return null
|
|
160
|
+
if (x !in -32768..32767 || y !in -32768..32767) return null
|
|
103
161
|
if (chars.any { (it.toInt() and 0xFF) < 32 || (it.toInt() and 0xFF) > 126 }) return null
|
|
104
162
|
val payload = ByteArrayOutputStream()
|
|
105
|
-
payload.write(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
payload.write(options and 0xFF)
|
|
163
|
+
payload.write(20)
|
|
164
|
+
payload.writeU32(fontId)
|
|
165
|
+
payload.writeI16(x)
|
|
166
|
+
payload.writeI16(y)
|
|
167
|
+
payload.write(options)
|
|
111
168
|
payload.write(chars.size)
|
|
112
169
|
payload.write(chars)
|
|
113
170
|
return payload.toByteArray()
|
|
114
171
|
}
|
|
115
172
|
}
|
|
116
173
|
|
|
117
|
-
class FontEntry(val
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
174
|
+
class FontEntry(val id: Int, val cellWidth: Int, val lineHeight: Int, val glyphs: Set<Int>)
|
|
175
|
+
|
|
176
|
+
private class Asset(val id: Int, val version: Int, val bytes: Int, var lastUse: Int)
|
|
121
177
|
|
|
122
178
|
var generation = -1
|
|
123
179
|
private set
|
|
124
180
|
|
|
125
|
-
|
|
126
|
-
|
|
181
|
+
/** Bytes the glasses could allocate for the store (the RESET reply says; 256 KiB when unknown). */
|
|
182
|
+
var storeCapacity = STORE_SIZE
|
|
127
183
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
* cache; static icons promote on their second sighting.
|
|
132
|
-
*/
|
|
184
|
+
private val fonts = mutableMapOf<String, FontEntry>()
|
|
185
|
+
private val assets = mutableMapOf<String, Asset>()
|
|
186
|
+
private val byId = mutableMapOf<Int, String>()
|
|
133
187
|
private val seenImageHashes = ArrayDeque<String>()
|
|
188
|
+
private val pendingEntries = mutableListOf<ByteArray>()
|
|
189
|
+
private var useClock = 0
|
|
134
190
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/** Mode-12 entries not yet flushed into payloads. */
|
|
139
|
-
private val pendingUploads = mutableListOf<Pair<Int, ByteArray>>()
|
|
191
|
+
/** Predicted store occupancy of everything this mirror believes resident. */
|
|
192
|
+
var predictedBytes = 0
|
|
193
|
+
private set
|
|
140
194
|
|
|
141
|
-
/**
|
|
142
|
-
* The bump allocator filled up. The reference client recycles a full
|
|
143
|
-
* cache (`TextureCacheState.reset` "or we ran out of space"); without
|
|
144
|
-
* recovery every later ensure fails and the texture path silently dies
|
|
145
|
-
* for the rest of the lease. The renderer checks this before each frame
|
|
146
|
-
* and starts a fresh generation.
|
|
147
|
-
*/
|
|
195
|
+
/** The prediction overflowed and the mirror dropped its oldest assets; advisory. */
|
|
148
196
|
var exhausted = false
|
|
149
197
|
private set
|
|
150
198
|
|
|
151
|
-
/**
|
|
152
|
-
* Forget everything and adopt `generation` (the on-glass cache was freed
|
|
153
|
-
* or its state became unknown).
|
|
154
|
-
*/
|
|
199
|
+
/** Forget everything and adopt `generation` (the on-glass cache was freed or reset). */
|
|
155
200
|
fun reset(generation: Int) {
|
|
156
201
|
this.generation = generation
|
|
157
202
|
fonts.clear()
|
|
158
|
-
|
|
203
|
+
assets.clear()
|
|
204
|
+
byId.clear()
|
|
159
205
|
seenImageHashes.clear()
|
|
160
|
-
|
|
161
|
-
|
|
206
|
+
pendingEntries.clear()
|
|
207
|
+
predictedBytes = 0
|
|
208
|
+
useClock = 0
|
|
162
209
|
exhausted = false
|
|
163
210
|
}
|
|
164
211
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
/** The glasses reported these asset ids absent: upload them again on next use. */
|
|
213
|
+
fun forget(assetIds: Collection<Int>) {
|
|
214
|
+
for (id in assetIds) {
|
|
215
|
+
val key = byId.remove(id) ?: continue
|
|
216
|
+
val asset = assets.remove(key) ?: continue
|
|
217
|
+
predictedBytes -= asset.bytes
|
|
218
|
+
fonts.entries.removeAll { it.value.id == id }
|
|
169
219
|
}
|
|
170
|
-
val offset = nextOffset
|
|
171
|
-
nextOffset += length
|
|
172
|
-
return offset
|
|
173
220
|
}
|
|
174
221
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
val
|
|
182
|
-
val
|
|
183
|
-
|
|
184
|
-
|
|
222
|
+
fun holds(assetId: Int): Boolean = byId.containsKey(assetId)
|
|
223
|
+
|
|
224
|
+
private fun ensureAsset(key: String, kind: Int, data: ByteArray): Int? {
|
|
225
|
+
useClock += 1
|
|
226
|
+
assets[key]?.let { it.lastUse = useClock; return it.id }
|
|
227
|
+
if (data.isEmpty() || data.size > storeCapacity) return null
|
|
228
|
+
val id = hash31(key)
|
|
229
|
+
val other = byId[id]
|
|
230
|
+
if (other != null && other != key) return null
|
|
231
|
+
val footprint = footprint(data.size)
|
|
232
|
+
makeRoom(footprint)
|
|
233
|
+
val asset = Asset(id, hash31(data), footprint, useClock)
|
|
234
|
+
assets[key] = asset
|
|
235
|
+
byId[id] = key
|
|
236
|
+
predictedBytes += footprint
|
|
237
|
+
pendingEntries.addAll(putAssetEntries(id, asset.version, kind, data))
|
|
238
|
+
return id
|
|
185
239
|
}
|
|
186
240
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (font.glyphOffsets[index] != null) return true
|
|
197
|
-
val gray = rasterize() ?: return false
|
|
198
|
-
if (gray.size != font.cellWidth * font.lineHeight) return false
|
|
199
|
-
val image = ByteArrayOutputStream()
|
|
200
|
-
image.write(font.cellWidth)
|
|
201
|
-
image.write(font.lineHeight)
|
|
202
|
-
image.write(rleEncodePixels(gray))
|
|
203
|
-
val bytes = image.toByteArray()
|
|
204
|
-
val offset = allocate(bytes.size) ?: return false
|
|
205
|
-
font.glyphOffsets[index] = offset
|
|
206
|
-
pendingUploads.add(offset to bytes)
|
|
207
|
-
pendingUploads.add(
|
|
208
|
-
(font.tableOffset + index * 2) to
|
|
209
|
-
byteArrayOf((offset and 0xFF).toByte(), ((offset ushr 8) and 0xFF).toByte())
|
|
210
|
-
)
|
|
211
|
-
return true
|
|
241
|
+
private fun makeRoom(footprint: Int) {
|
|
242
|
+
while (predictedBytes + footprint > storeCapacity || assets.size >= MAX_ASSETS) {
|
|
243
|
+
val victim = assets.entries.minByOrNull { it.value.lastUse } ?: break
|
|
244
|
+
exhausted = true
|
|
245
|
+
assets.remove(victim.key)
|
|
246
|
+
byId.remove(victim.value.id)
|
|
247
|
+
fonts.entries.removeAll { it.value.id == victim.value.id }
|
|
248
|
+
predictedBytes -= victim.value.bytes
|
|
249
|
+
}
|
|
212
250
|
}
|
|
213
251
|
|
|
214
252
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
253
|
+
* The font asset for `fontSize`, rasterizing every glyph the variant needs and
|
|
254
|
+
* uploading the whole font on first use. `rasterize` supplies the full-cell
|
|
255
|
+
* 8-bit grayscale raster (cellWidth × lineHeight) for a wire code; a code it
|
|
256
|
+
* cannot raster is left absent from the font.
|
|
217
257
|
*/
|
|
258
|
+
fun font(
|
|
259
|
+
fontSize: Int,
|
|
260
|
+
cellWidth: Int,
|
|
261
|
+
lineHeight: Int,
|
|
262
|
+
variant: String = "ascii",
|
|
263
|
+
codes: List<Int> = (32..126).toList(),
|
|
264
|
+
rasterize: (Int) -> ByteArray?,
|
|
265
|
+
): FontEntry? {
|
|
266
|
+
val key = "font:$fontSize:${cellWidth}x$lineHeight:$variant"
|
|
267
|
+
fonts[key]?.let { existing ->
|
|
268
|
+
if (assets.containsKey(key)) {
|
|
269
|
+
useClock += 1
|
|
270
|
+
assets[key]?.lastUse = useClock
|
|
271
|
+
return existing
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
val table = ByteArray(FONT_TABLE_CHARS * FONT_TABLE_ENTRY_BYTES)
|
|
275
|
+
val body = ByteArrayOutputStream()
|
|
276
|
+
val glyphs = mutableSetOf<Int>()
|
|
277
|
+
for (code in codes) {
|
|
278
|
+
if (code < 32 || code > 127) continue
|
|
279
|
+
val gray = rasterize(code) ?: continue
|
|
280
|
+
if (gray.size != cellWidth * lineHeight) continue
|
|
281
|
+
val offset = table.size + body.size()
|
|
282
|
+
val index = (code - 32) * FONT_TABLE_ENTRY_BYTES
|
|
283
|
+
table[index] = (offset and 0xFF).toByte()
|
|
284
|
+
table[index + 1] = ((offset ushr 8) and 0xFF).toByte()
|
|
285
|
+
table[index + 2] = ((offset ushr 16) and 0xFF).toByte()
|
|
286
|
+
table[index + 3] = ((offset ushr 24) and 0xFF).toByte()
|
|
287
|
+
body.write(cellWidth); body.write(lineHeight)
|
|
288
|
+
body.write(rleEncodePixels(gray))
|
|
289
|
+
glyphs.add(code)
|
|
290
|
+
}
|
|
291
|
+
if (glyphs.isEmpty()) return null
|
|
292
|
+
val id = ensureAsset(key, KIND_FONT, table + body.toByteArray()) ?: return null
|
|
293
|
+
val entry = FontEntry(id, cellWidth, lineHeight, glyphs)
|
|
294
|
+
fonts[key] = entry
|
|
295
|
+
return entry
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Record a sighting of an image content hash; true when it was already seen in an earlier frame. */
|
|
218
299
|
fun noteImageSighting(hash: String): Boolean {
|
|
219
|
-
if (
|
|
300
|
+
if (assets.containsKey(hash)) return true
|
|
220
301
|
if (hash in seenImageHashes) return true
|
|
221
302
|
seenImageHashes.addLast(hash)
|
|
222
303
|
if (seenImageHashes.size > 32) seenImageHashes.removeFirst()
|
|
223
304
|
return false
|
|
224
305
|
}
|
|
225
306
|
|
|
226
|
-
/**
|
|
227
|
-
* The cache offset of an image, uploading it on first residency.
|
|
228
|
-
* `gray` is the width×height 8-bit grayscale raster.
|
|
229
|
-
*/
|
|
307
|
+
/** The asset id of an image, uploading it on first residency. `gray` is the width×height 8-bit raster. */
|
|
230
308
|
fun ensureImage(hash: String, gray: ByteArray, width: Int, height: Int): Int? {
|
|
231
|
-
|
|
309
|
+
assets[hash]?.let { useClock += 1; it.lastUse = useClock; return it.id }
|
|
232
310
|
if (width !in 1..255 || height !in 1..255 || gray.size != width * height) return null
|
|
233
311
|
val image = ByteArrayOutputStream()
|
|
234
312
|
image.write(width)
|
|
235
313
|
image.write(height)
|
|
236
314
|
image.write(rleEncodePixels(gray))
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
315
|
+
return ensureAsset(hash, KIND_IMAGE, image.toByteArray())
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The asset id of a string (1..128 bytes of UTF-8, no NUL), uploading it on first use. */
|
|
319
|
+
fun ensureBytes(key: String, bytes: ByteArray): Int? {
|
|
320
|
+
if (bytes.isEmpty() || bytes.size > 128 || bytes.any { it.toInt() == 0 }) return null
|
|
321
|
+
return ensureAsset(key, KIND_STRING, bytes)
|
|
242
322
|
}
|
|
243
323
|
|
|
244
324
|
/**
|
|
245
|
-
* Drain the pending
|
|
246
|
-
* payloads, each at most [MAX_UPLOAD_PAYLOAD_BYTES]
|
|
247
|
-
* the budget gets a payload of its own (the BLE fragmenter splits it).
|
|
325
|
+
* Drain the pending PUT_ASSET entries into `[37][epoch 0][request 0][entries]`
|
|
326
|
+
* payloads, each at most [MAX_UPLOAD_PAYLOAD_BYTES]; the link stamps the epoch and request.
|
|
248
327
|
*/
|
|
249
328
|
fun takeUploadPayloads(): List<ByteArray> {
|
|
250
|
-
if (
|
|
329
|
+
if (pendingEntries.isEmpty()) return emptyList()
|
|
251
330
|
val payloads = mutableListOf<ByteArray>()
|
|
252
|
-
var current = ByteArrayOutputStream().apply { write(
|
|
253
|
-
for (
|
|
254
|
-
|
|
255
|
-
record.write(offset and 0xFF)
|
|
256
|
-
record.write((offset ushr 8) and 0xFF)
|
|
257
|
-
record.write(data.size and 0xFF)
|
|
258
|
-
record.write((data.size ushr 8) and 0xFF)
|
|
259
|
-
record.write(data)
|
|
260
|
-
if (current.size() > 1 && current.size() + record.size() > MAX_UPLOAD_PAYLOAD_BYTES) {
|
|
331
|
+
var current = ByteArrayOutputStream().apply { write(byteArrayOf(37, 0, 0, 0, 0)) }
|
|
332
|
+
for (entry in pendingEntries) {
|
|
333
|
+
if (current.size() > 5 && current.size() + entry.size > MAX_UPLOAD_PAYLOAD_BYTES) {
|
|
261
334
|
payloads.add(current.toByteArray())
|
|
262
|
-
current = ByteArrayOutputStream().apply { write(
|
|
335
|
+
current = ByteArrayOutputStream().apply { write(byteArrayOf(37, 0, 0, 0, 0)) }
|
|
263
336
|
}
|
|
264
|
-
|
|
337
|
+
current.write(entry)
|
|
265
338
|
}
|
|
266
|
-
if (current.size() >
|
|
267
|
-
|
|
339
|
+
if (current.size() > 5) payloads.add(current.toByteArray())
|
|
340
|
+
pendingEntries.clear()
|
|
268
341
|
return payloads
|
|
269
342
|
}
|
|
270
343
|
}
|