@glassly/bluetooth-sdk 0.1.0-dev.99 → 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 +1065 -255
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2BleLink.kt +119 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +29 -28
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +138 -33
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwScene.kt +673 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +235 -162
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransport.kt +402 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnostics.kt +84 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +3 -3
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +9 -9
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Keyboard.kt +689 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardBridge.kt +452 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardHid.kt +208 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFile.kt +181 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2RingBattery.kt +20 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Sensors.kt +67 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2TouchInput.kt +39 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +5 -5
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +14 -14
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/{GlasslyBle.java → NexProto.java} +6462 -7618
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIds.kt +58 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +77 -2
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +55 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +1 -1
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +24 -149
- package/android/src/test/java/com/glassly/bluetoothsdk/DisplayPacerTest.kt +277 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/G2BleLinkTest.kt +70 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +45 -56
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +78 -5
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwSceneTest.kt +489 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +100 -43
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTransportTest.kt +252 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2DisplayDiagnosticsTest.kt +64 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2KeyboardTest.kt +271 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2NotificationFileTest.kt +204 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2RingBatteryTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2SensorsTest.kt +56 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2TouchInputTest.kt +60 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NexProtoDescriptorTest.kt +32 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NotificationMessageIdsTest.kt +58 -0
- package/build/BluetoothSdk.types.d.ts +209 -20
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +1 -1
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +30 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +28 -1
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +37 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +102 -13
- package/ios/GlasslyBluetoothSDK.podspec +1 -1
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +8 -3
- package/ios/Source/BluetoothSDK.swift +53 -0
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -0
- package/ios/Source/DeviceManager.swift +151 -33
- package/ios/Source/DeviceStore.swift +28 -14
- package/ios/Source/DisplayPacer.swift +180 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +10 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +4 -4
- package/ios/Source/GlassesNotifications.swift +181 -0
- package/ios/Source/ObservableStore.swift +14 -2
- package/ios/Source/Resources/G2Firmware/README.md +27 -2
- package/ios/Source/Resources/G2Firmware/{g2-stock-2.2.9.22.evenota.bin → g2-glassly-cfw-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/Resources/G2Firmware/{g2-glassly-cfw-2.2.9.22.evenota.bin → g2-stock-2.3.0.24.evenota.bin} +0 -0
- package/ios/Source/controllers/R1.swift +0 -1
- package/ios/Source/internal/BluetoothAvailability.swift +31 -0
- package/ios/Source/sgcs/G2.swift +1420 -495
- package/ios/Source/sgcs/G2AncsRelay.swift +268 -0
- package/ios/Source/sgcs/G2BleLink.swift +102 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +45 -27
- package/ios/Source/sgcs/G2CfwRenderer.swift +166 -33
- package/ios/Source/sgcs/G2CfwScene.swift +779 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +244 -117
- package/ios/Source/sgcs/G2CfwTransport.swift +485 -0
- package/ios/Source/sgcs/G2DisplayDiagnostics.swift +154 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +2 -2
- package/ios/Source/sgcs/G2Flasher.swift +9 -9
- package/ios/Source/sgcs/G2Keyboard.swift +780 -0
- package/ios/Source/sgcs/G2KeyboardBridge.swift +438 -0
- package/ios/Source/sgcs/G2KeyboardHid.swift +290 -0
- package/ios/Source/sgcs/G2NotificationTuning.swift +24 -0
- package/ios/Source/sgcs/G2RingBattery.swift +22 -0
- package/ios/Source/sgcs/G2Sensors.swift +75 -0
- package/ios/Source/sgcs/G2TouchInput.swift +51 -0
- package/ios/Source/sgcs/G2WritePacing.swift +18 -0
- package/ios/Source/sgcs/MentraNex.swift +44 -44
- package/ios/Source/sgcs/SGCManager.swift +120 -13
- package/ios/Source/sgcs/{glassly_ble.pb.swift → nex_ble.pb.swift} +405 -465
- package/ios/Source/speakerid/SpeakerIdEngine.swift +2 -2
- package/ios/Source/status/DeviceStatus.swift +27 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +15 -14
- package/ios/Source/utils/Enums.swift +0 -1
- package/ios/Source/utils/G1Text.swift +0 -1
- package/ios/Source/utils/G2NotifyFile.swift +20 -10
- package/ios/Source/utils/JSCExperiment.swift +6 -6
- package/ios/Source/utils/Models.swift +0 -1
- package/ios/Tests/BluetoothSDKTests/BluetoothAvailabilityTests.swift +29 -0
- package/ios/Tests/BluetoothSDKTests/DisplayPacerTests.swift +255 -0
- package/ios/Tests/BluetoothSDKTests/G2AncsRelayTests.swift +186 -0
- package/ios/Tests/BluetoothSDKTests/G2BleLinkTests.swift +52 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +46 -51
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +164 -5
- package/ios/Tests/BluetoothSDKTests/G2CfwSceneTests.swift +497 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +108 -44
- package/ios/Tests/BluetoothSDKTests/G2CfwTransportTests.swift +259 -0
- package/ios/Tests/BluetoothSDKTests/G2DisplayDiagnosticsTests.swift +89 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +3 -2
- package/ios/Tests/BluetoothSDKTests/G2ImageAckTests.swift +104 -0
- package/ios/Tests/BluetoothSDKTests/G2KeyboardTests.swift +274 -0
- package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +21 -5
- package/ios/Tests/BluetoothSDKTests/G2RingBatteryTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2SensorsTests.swift +56 -0
- package/ios/Tests/BluetoothSDKTests/G2TouchInputTests.swift +71 -0
- package/ios/Tests/BluetoothSDKTests/G2WritePacingTests.swift +35 -0
- package/ios/Tests/BluetoothSDKTests/GlassesNotificationTests.swift +95 -0
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +2 -2
- package/plugin/build/withIos.js +1 -1
- package/scripts/gen-nex-proto.sh +12 -0
- package/scripts/inject-ios-sdk-version.mjs +1 -1
- package/src/BluetoothSdk.types.ts +214 -19
- package/src/_internal.ts +1 -1
- package/src/_private/BluetoothSdkModule.ts +40 -0
- package/src/generated/changelogCatalog.ts +37 -1
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +24 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Glassly CFW BLE link speed control (contract revision 26). The firmware
|
|
5
|
+
* boots on the stock connection profile; the phone can switch each lens to
|
|
6
|
+
* the 7.5 ms / latency-0 fast profile (slow mode suppressed) and back. The
|
|
7
|
+
* wire format is pinned by `patches/ble_link.c` in g2flash.
|
|
8
|
+
*
|
|
9
|
+
* Phone -> glasses, G2SettingPackage field 127: `['B','L',1,op]`.
|
|
10
|
+
* Glasses -> phone, field 128 on every settings read reply:
|
|
11
|
+
* `['B','L',1, fast, side, wanted, applied, min16, max16, latency16,
|
|
12
|
+
* liveInterval16, liveLatency16]`
|
|
13
|
+
* Intervals are little-endian in 1.25 ms units. Modes: 0xa3 fast, 0xa4 slow,
|
|
14
|
+
* 0 = update pending. min/max/latency are the profile the last request used;
|
|
15
|
+
* the live pair is the connection as the central granted it (absent from the
|
|
16
|
+
* first revision 26 builds, which sent 13 bytes).
|
|
17
|
+
*/
|
|
18
|
+
internal object G2BleLink {
|
|
19
|
+
const val PROTOCOL_VERSION = 1
|
|
20
|
+
|
|
21
|
+
enum class Op(
|
|
22
|
+
val value: Int,
|
|
23
|
+
) {
|
|
24
|
+
STOCK(0),
|
|
25
|
+
FAST(1),
|
|
26
|
+
QUERY(2),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sealed class Mode {
|
|
30
|
+
object Fast : Mode()
|
|
31
|
+
|
|
32
|
+
object Slow : Mode()
|
|
33
|
+
|
|
34
|
+
object Pending : Mode()
|
|
35
|
+
|
|
36
|
+
data class Other(
|
|
37
|
+
val raw: Int,
|
|
38
|
+
) : Mode()
|
|
39
|
+
|
|
40
|
+
val label: String
|
|
41
|
+
get() =
|
|
42
|
+
when (this) {
|
|
43
|
+
Fast -> "fast"
|
|
44
|
+
Slow -> "slow"
|
|
45
|
+
Pending -> "pending"
|
|
46
|
+
is Other -> "0x%02x".format(raw)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
companion object {
|
|
50
|
+
fun of(raw: Int): Mode =
|
|
51
|
+
when (raw) {
|
|
52
|
+
0xA3 -> Fast
|
|
53
|
+
0xA4 -> Slow
|
|
54
|
+
0 -> Pending
|
|
55
|
+
else -> Other(raw)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
data class Status(
|
|
61
|
+
val fast: Boolean,
|
|
62
|
+
val side: Int,
|
|
63
|
+
val wanted: Mode,
|
|
64
|
+
val applied: Mode,
|
|
65
|
+
/** The profile the last request asked for. */
|
|
66
|
+
val requestedMinMs: Double,
|
|
67
|
+
val requestedMaxMs: Double,
|
|
68
|
+
val requestedLatency: Int,
|
|
69
|
+
/** What the central granted; null on firmware that predates the field. */
|
|
70
|
+
val liveIntervalMs: Double?,
|
|
71
|
+
val liveLatency: Int?,
|
|
72
|
+
) {
|
|
73
|
+
val summary: String
|
|
74
|
+
get() {
|
|
75
|
+
val text = StringBuilder()
|
|
76
|
+
text.append(if (fast) "FAST" else "stock")
|
|
77
|
+
text.append(" wanted=${wanted.label} applied=${applied.label}")
|
|
78
|
+
text.append(" requested %.2f-%.2f ms / latency %d".format(requestedMinMs, requestedMaxMs, requestedLatency))
|
|
79
|
+
if (liveIntervalMs != null && liveLatency != null) {
|
|
80
|
+
text.append(" live %.2f ms / latency %d".format(liveIntervalMs, liveLatency))
|
|
81
|
+
}
|
|
82
|
+
return text.toString()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------- Encoding (phone -> glasses) ----------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* G2SettingPackage{commandId=1 (DEVICE_RECEIVE_INFO), magic=0, field 127}.
|
|
90
|
+
* Hand-framed like the firmware's replies: tag 127/wire 2 is 1018 = fa 07.
|
|
91
|
+
*/
|
|
92
|
+
fun controlPacket(op: Op): ByteArray =
|
|
93
|
+
byteArrayOf(0x08, 0x01, 0x10, 0x00, 0xFA.toByte(), 0x07, 4, 0x42, 0x4C, PROTOCOL_VERSION.toByte(), op.value.toByte())
|
|
94
|
+
|
|
95
|
+
// ---------- Decoding (glasses -> phone) ----------
|
|
96
|
+
|
|
97
|
+
fun decodeStatus(data: ByteArray): Status? {
|
|
98
|
+
if (data.size < 13 || data[0] != 0x42.toByte() || data[1] != 0x4C.toByte() || data[2] != PROTOCOL_VERSION.toByte()) return null
|
|
99
|
+
val live = data.size >= 17
|
|
100
|
+
return Status(
|
|
101
|
+
fast = data[3].toInt() != 0,
|
|
102
|
+
side = data[4].toInt() and 0xFF,
|
|
103
|
+
wanted = Mode.of(data[5].toInt() and 0xFF),
|
|
104
|
+
applied = Mode.of(data[6].toInt() and 0xFF),
|
|
105
|
+
requestedMinMs = ms(u16(data, 7)),
|
|
106
|
+
requestedMaxMs = ms(u16(data, 9)),
|
|
107
|
+
requestedLatency = u16(data, 11),
|
|
108
|
+
liveIntervalMs = if (live) ms(u16(data, 13)) else null,
|
|
109
|
+
liveLatency = if (live) u16(data, 15) else null,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun ms(units: Int): Double = units * 1.25
|
|
114
|
+
|
|
115
|
+
private fun u16(
|
|
116
|
+
b: ByteArray,
|
|
117
|
+
i: Int,
|
|
118
|
+
): Int = (b[i].toInt() and 0xFF) or ((b[i + 1].toInt() and 0xFF) shl 8)
|
|
119
|
+
}
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
package com.glassly.bluetoothsdk.sgcs
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
* field 100 ("EVENCFW/NN feat feat …"; stock firmware never emits the field).
|
|
6
|
-
*
|
|
7
|
-
* Pure so the identity rules stay unit-testable — they gate the connect
|
|
8
|
-
* banner: legacy text drawn before classification and cleared after it splits
|
|
9
|
-
* draw/clear across render paths, and the stock text container then never
|
|
10
|
-
* leaves the glass.
|
|
3
|
+
/** Firmware identity advertised in settings field 100. Retain its revision for
|
|
4
|
+
* future protocol changes; current features depend only on the firmware family.
|
|
11
5
|
*/
|
|
12
|
-
data class G2CfwClassification(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
val
|
|
17
|
-
) {
|
|
18
|
-
|
|
6
|
+
data class G2CfwClassification(val family: Family, val capabilityString: String?) {
|
|
7
|
+
enum class Family { STOCK, UPSTREAM, GLASSLY }
|
|
8
|
+
|
|
9
|
+
/** Numeric contract revision, or null for stock/unknown revision text. */
|
|
10
|
+
val revision: Int?
|
|
11
|
+
get() = capabilityString?.substringBefore(' ')?.substringAfter('/')?.toIntOrNull()?.takeIf { it > 0 }
|
|
12
|
+
|
|
13
|
+
val isCfw: Boolean get() = family != Family.STOCK
|
|
14
|
+
val isGlassly: Boolean get() = family == Family.GLASSLY
|
|
15
|
+
val displayName: String? get() = when (family) {
|
|
16
|
+
Family.STOCK -> null
|
|
17
|
+
Family.UPSTREAM -> "g2flash CFW"
|
|
18
|
+
Family.GLASSLY -> revision?.let { "Glassly CFW v$it" } ?: "Glassly CFW"
|
|
19
|
+
}
|
|
19
20
|
|
|
20
21
|
companion object {
|
|
21
|
-
val STOCK = G2CfwClassification(
|
|
22
|
+
val STOCK = G2CfwClassification(Family.STOCK, null)
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
* The one revision gate: everything the app uses (SID-0xf0 transport,
|
|
26
|
+
* scenes, settle reports, fast BLE link) shipped together in this
|
|
27
|
+
* firmware line, so nothing older is special-cased; it is treated as stock.
|
|
28
|
+
*/
|
|
29
|
+
const val MINIMUM_REVISION = 39
|
|
30
|
+
|
|
31
|
+
/** Partial settings notifications can omit field 100. Keep the connection's
|
|
32
|
+
* known identity so a missing field cannot switch the active render path.
|
|
30
33
|
*/
|
|
31
34
|
fun classify(
|
|
32
35
|
field100: ByteArray?,
|
|
@@ -34,13 +37,11 @@ data class G2CfwClassification(
|
|
|
34
37
|
currentCapability: String?,
|
|
35
38
|
): G2CfwClassification? {
|
|
36
39
|
val capability = field100?.toString(Charsets.UTF_8)?.trimEnd('\u0000')
|
|
40
|
+
if (capability?.startsWith("GLASSLYCFW/") == true) {
|
|
41
|
+
return G2CfwClassification(Family.GLASSLY, capability)
|
|
42
|
+
}
|
|
37
43
|
if (capability?.startsWith("EVENCFW/") == true) {
|
|
38
|
-
|
|
39
|
-
return G2CfwClassification(
|
|
40
|
-
capabilityString = capability,
|
|
41
|
-
features = tokens.drop(1),
|
|
42
|
-
protocolVersion = tokens.firstOrNull()?.removePrefix("EVENCFW/")?.toIntOrNull() ?: 0,
|
|
43
|
-
)
|
|
44
|
+
return G2CfwClassification(Family.UPSTREAM, capability)
|
|
44
45
|
}
|
|
45
46
|
if (definitive && currentCapability == null) return STOCK
|
|
46
47
|
return null
|
|
@@ -13,7 +13,6 @@ import android.text.StaticLayout
|
|
|
13
13
|
import android.text.TextPaint
|
|
14
14
|
import android.util.Base64
|
|
15
15
|
import java.io.ByteArrayOutputStream
|
|
16
|
-
import java.util.zip.DeflaterOutputStream
|
|
17
16
|
import kotlin.math.roundToInt
|
|
18
17
|
|
|
19
18
|
/** Phone-side full-scene rasterizer for Glassly CFW's direct-framebuffer protocol. */
|
|
@@ -34,6 +33,9 @@ internal object G2CfwRenderer {
|
|
|
34
33
|
val height: Int,
|
|
35
34
|
)
|
|
36
35
|
|
|
36
|
+
/** The global setting is 1..3; the firmware placement tables are zero-based. */
|
|
37
|
+
fun depthLevelForSetting(depth: Int): Int = depth.coerceIn(1, 3) - 1
|
|
38
|
+
|
|
37
39
|
fun lensBoxes(
|
|
38
40
|
canvasWidth: Int,
|
|
39
41
|
canvasHeight: Int,
|
|
@@ -74,6 +76,117 @@ internal object G2CfwRenderer {
|
|
|
74
76
|
* glyph raster memo survives resets — rasters don't change.
|
|
75
77
|
*/
|
|
76
78
|
private val textureCache = G2CfwTextureCache()
|
|
79
|
+
/**
|
|
80
|
+
* Retained object cache (modes 37-41); shares the texture cache with the
|
|
81
|
+
* raster path. Two lens mirrors: while every frame is flat only the left
|
|
82
|
+
* one is encoded and its SHOW goes to both lenses; the first frame with
|
|
83
|
+
* global or element depth copies it into the right mirror ([stereoSplit]) and from
|
|
84
|
+
* then on each lens gets its own SHOW whenever the two encodings differ.
|
|
85
|
+
*/
|
|
86
|
+
private val scenes = listOf(G2CfwScene(), G2CfwScene())
|
|
87
|
+
private var stereoSplit = false
|
|
88
|
+
|
|
89
|
+
/** One frame of the object cache: the SHOWs to send, in order, each with its lens targets, after the asset [uploads]. */
|
|
90
|
+
class StereoEncoded(
|
|
91
|
+
val shows: List<Show>,
|
|
92
|
+
val uploads: List<ByteArray>,
|
|
93
|
+
/** False when the frame is the one already on screen (nothing new to send). */
|
|
94
|
+
val changed: Boolean,
|
|
95
|
+
val puts: Int,
|
|
96
|
+
val hits: Int,
|
|
97
|
+
val tweens: Int,
|
|
98
|
+
) {
|
|
99
|
+
class Show(val payload: ByteArray, val lenses: Int)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Encode [frame] as SHOWs of the object cache (plus the asset PUT messages
|
|
104
|
+
* that must precede them): one SHOW for both lenses while the lens mirrors
|
|
105
|
+
* agree, else one per lens (element depth shifts the two encodings apart).
|
|
106
|
+
* [invalidate] means residency is unknown (a send failed): every object the
|
|
107
|
+
* frame shows is redefined, and the mirrors re-converge. Null when the frame
|
|
108
|
+
* does not fit the object table or the asset store.
|
|
109
|
+
*/
|
|
110
|
+
fun encodeVector(
|
|
111
|
+
frame: SceneFrame,
|
|
112
|
+
boxes: LensBoxes,
|
|
113
|
+
cacheGeneration: Int,
|
|
114
|
+
invalidate: Boolean,
|
|
115
|
+
inlineText: Boolean,
|
|
116
|
+
tag: Int? = null,
|
|
117
|
+
): StereoEncoded? = synchronized(this) {
|
|
118
|
+
if (textureCache.generation != cacheGeneration) textureCache.reset(cacheGeneration)
|
|
119
|
+
if (scenes[0].generation != textureCache.generation) {
|
|
120
|
+
scenes.forEach { it.reset(textureCache.generation) }
|
|
121
|
+
stereoSplit = false
|
|
122
|
+
} else if (invalidate) {
|
|
123
|
+
scenes.forEach { it.invalidate() }
|
|
124
|
+
stereoSplit = false
|
|
125
|
+
}
|
|
126
|
+
// Both mirrors rasterize the same element pixels: do it once per frame.
|
|
127
|
+
val rasters = HashMap<String, ByteArray?>()
|
|
128
|
+
val rasterizeOne: (SceneElement, Int, Int) -> ByteArray? = { element, w, h ->
|
|
129
|
+
rasters.getOrPut("${element.id}|${element.contentHash}|${w}x$h") {
|
|
130
|
+
rasterize(SceneFrame(frame.appId, frame.epoch, false, listOf(element.atOrigin()), emptyList(), w, h), w, h)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
val mono = G2CfwScene.MonoFace({ monoCellMetrics(it) }) { cp, size, cw, lh -> monoGlyphRaster(cp, size, cw, lh) }
|
|
134
|
+
if (!stereoSplit && (boxes.leftX != boxes.rightX || boxes.leftY != boxes.rightY || frame.elements.any { it.depth != 0 })) {
|
|
135
|
+
scenes[1].adopt(scenes[0])
|
|
136
|
+
stereoSplit = true
|
|
137
|
+
}
|
|
138
|
+
val left = scenes[0].encode(
|
|
139
|
+
frame, boxes.leftX, boxes.leftY, textureCache, rasterizeOne, inlineText, mono, tag,
|
|
140
|
+
parallax = if (stereoSplit) 1 else 0,
|
|
141
|
+
) ?: return null
|
|
142
|
+
if (!stereoSplit) {
|
|
143
|
+
return StereoEncoded(
|
|
144
|
+
listOf(StereoEncoded.Show(left.payload, G2CfwTransport.LENS_BOTH)), left.uploads, left.changed,
|
|
145
|
+
left.puts, left.hits, left.tweens,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
val right = scenes[1].encode(frame, boxes.rightX, boxes.rightY, textureCache, rasterizeOne, inlineText, mono, tag, parallax = -1)
|
|
149
|
+
?: return null
|
|
150
|
+
val shows = mutableListOf<StereoEncoded.Show>()
|
|
151
|
+
if (left.payload.contentEquals(right.payload)) {
|
|
152
|
+
shows.add(StereoEncoded.Show(left.payload, G2CfwTransport.LENS_BOTH))
|
|
153
|
+
} else {
|
|
154
|
+
// A lens whose frame did not change keeps quiet, unless neither changed (then both SHOWs go).
|
|
155
|
+
val both = left.changed == right.changed
|
|
156
|
+
if (both || left.changed) shows.add(StereoEncoded.Show(left.payload, G2CfwTransport.LENS_LEFT))
|
|
157
|
+
if (both || right.changed) shows.add(StereoEncoded.Show(right.payload, G2CfwTransport.LENS_RIGHT))
|
|
158
|
+
}
|
|
159
|
+
StereoEncoded(
|
|
160
|
+
shows, left.uploads + right.uploads, left.changed || right.changed,
|
|
161
|
+
left.puts + right.puts, left.hits + right.hits, left.tweens + right.tweens,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The panel went blank while the cache stayed: hidden objects mid-animation are re-snapped on their next show. */
|
|
166
|
+
fun sceneHidden() {
|
|
167
|
+
synchronized(this) { scenes.forEach { it.hidden() } }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The RESET reply named the store each lens could allocate; predict against the smaller one. */
|
|
171
|
+
fun setStoreCapacity(bytes: Int) {
|
|
172
|
+
synchronized(this) { textureCache.storeCapacity = bytes.coerceIn(G2CfwTextureCache.GRANULE, G2CfwTextureCache.STORE_SIZE) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A lens answered MISSING: these ids (objects or assets) are redefined on the next frame. */
|
|
176
|
+
fun forget(ids: Collection<Int>) {
|
|
177
|
+
synchronized(this) {
|
|
178
|
+
scenes.forEach { it.forget(ids) }
|
|
179
|
+
|
|
180
|
+
textureCache.forget(ids)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Upload payloads that were dropped or failed: their assets may not be resident. */
|
|
185
|
+
fun forgetUploads(payloads: Collection<ByteArray>) {
|
|
186
|
+
synchronized(this) {
|
|
187
|
+
textureCache.forget(payloads.flatMap { G2CfwTextureCache.assetIdsInUploadPayload(it) })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
77
190
|
private val glyphRasterMemo = mutableMapOf<String, ByteArray>()
|
|
78
191
|
|
|
79
192
|
private fun monoPaint(fontSize: Float): Paint =
|
|
@@ -261,10 +374,13 @@ internal object G2CfwRenderer {
|
|
|
261
374
|
// A full cache resets between frames (never mid-frame: draws already
|
|
262
375
|
// planned this frame reference the old offsets). Bumping past the
|
|
263
376
|
// caller's generation keeps the caller's own bumps monotonic.
|
|
264
|
-
if ((useTexGlyphs || useTexImages) &&
|
|
377
|
+
if ((useTexGlyphs || useTexImages) && textureCache.generation != cacheGeneration) {
|
|
265
378
|
textureCache.reset(cacheGeneration)
|
|
379
|
+
scenes.forEach { it.reset(cacheGeneration) }
|
|
380
|
+
stereoSplit = false
|
|
266
381
|
}
|
|
267
382
|
|
|
383
|
+
|
|
268
384
|
var grids: List<MonoGrid> = emptyList()
|
|
269
385
|
val raster: ByteArray
|
|
270
386
|
if (useTexGlyphs) {
|
|
@@ -509,7 +625,9 @@ internal object G2CfwRenderer {
|
|
|
509
625
|
if (grid != null &&
|
|
510
626
|
elementBoxes.none { it.z > grid.z && it.intersects(rect) }
|
|
511
627
|
) {
|
|
512
|
-
val font = textureCache.font(grid.fontSize, grid.cellWidth, grid.lineHeight)
|
|
628
|
+
val font = textureCache.font(grid.fontSize, grid.cellWidth, grid.lineHeight) { code ->
|
|
629
|
+
memoizedGlyphRaster(code, grid.fontSize, grid.cellWidth, grid.lineHeight)
|
|
630
|
+
}
|
|
513
631
|
if (font != null) {
|
|
514
632
|
val r0 = (rect.y - grid.originY) / grid.lineHeight
|
|
515
633
|
val r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
|
|
@@ -544,13 +662,7 @@ internal object G2CfwRenderer {
|
|
|
544
662
|
}
|
|
545
663
|
for (col in c0..c1) {
|
|
546
664
|
val scalar = grid.scalar(row, col)
|
|
547
|
-
val resident =
|
|
548
|
-
scalar in 32..126 &&
|
|
549
|
-
textureCache.ensureGlyph(font, scalar) {
|
|
550
|
-
memoizedGlyphRaster(
|
|
551
|
-
scalar, grid.fontSize, grid.cellWidth, grid.lineHeight
|
|
552
|
-
)
|
|
553
|
-
}
|
|
665
|
+
val resident = scalar in 32..126 && scalar in font.glyphs
|
|
554
666
|
if (resident) {
|
|
555
667
|
flushResidual(col)
|
|
556
668
|
if (runChars.isEmpty()) runStartCol = col
|
|
@@ -578,7 +690,7 @@ internal object G2CfwRenderer {
|
|
|
578
690
|
for ((runX, runY, chars) in newRuns) {
|
|
579
691
|
val payload =
|
|
580
692
|
G2CfwTextureCache.stringRunPayload(
|
|
581
|
-
|
|
693
|
+
fontId = font.id,
|
|
582
694
|
x = boxes.leftX + runX, y = boxes.leftY + runY,
|
|
583
695
|
options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS, chars = chars,
|
|
584
696
|
)
|
|
@@ -619,11 +731,11 @@ internal object G2CfwRenderer {
|
|
|
619
731
|
gray, (y - candidate.box.y0) * w, w,
|
|
620
732
|
)
|
|
621
733
|
}
|
|
622
|
-
val
|
|
734
|
+
val asset = textureCache.ensureImage(candidate.key, gray, w, h)
|
|
623
735
|
val draw =
|
|
624
|
-
|
|
736
|
+
asset?.let {
|
|
625
737
|
G2CfwTextureCache.imageDrawPayload(
|
|
626
|
-
|
|
738
|
+
assetId = it,
|
|
627
739
|
x = boxes.leftX + candidate.box.x0,
|
|
628
740
|
y = boxes.leftY + candidate.box.y0,
|
|
629
741
|
options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS,
|
|
@@ -994,7 +1106,7 @@ internal object G2CfwRenderer {
|
|
|
994
1106
|
canvas.clipRect(rect)
|
|
995
1107
|
when (element.type) {
|
|
996
1108
|
"text" -> if (!skipBuiltinInk) drawText(canvas, element, rect)
|
|
997
|
-
"image" -> decodedImages[index]?.let { drawImage(canvas, it, rect) }
|
|
1109
|
+
"image" -> decodedImages[index]?.let { drawImage(canvas, it, rect, element.color) }
|
|
998
1110
|
}
|
|
999
1111
|
|
|
1000
1112
|
val border = maxOf(if (element.type == "rect") 1 else 0, element.border)
|
|
@@ -1064,7 +1176,8 @@ internal object G2CfwRenderer {
|
|
|
1064
1176
|
canvas.restoreToCount(save)
|
|
1065
1177
|
}
|
|
1066
1178
|
|
|
1067
|
-
|
|
1179
|
+
/** [color] is the ink level 0..15 (-1 = as drawn): it scales the pixels, as the texture ramp does on vector firmware. */
|
|
1180
|
+
private fun drawImage(canvas: Canvas, bitmap: Bitmap, rect: RectF, color: Int) {
|
|
1068
1181
|
val scale = minOf(rect.width() / bitmap.width.coerceAtLeast(1), rect.height() / bitmap.height.coerceAtLeast(1))
|
|
1069
1182
|
val targetWidth = bitmap.width * scale
|
|
1070
1183
|
val targetHeight = bitmap.height * scale
|
|
@@ -1077,7 +1190,9 @@ internal object G2CfwRenderer {
|
|
|
1077
1190
|
rect.centerX() + targetWidth / 2f,
|
|
1078
1191
|
rect.centerY() + targetHeight / 2f,
|
|
1079
1192
|
),
|
|
1080
|
-
Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
|
|
1193
|
+
Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
|
|
1194
|
+
if (color >= 0) alpha = color.coerceIn(0, 15) * 255 / 15
|
|
1195
|
+
},
|
|
1081
1196
|
)
|
|
1082
1197
|
}
|
|
1083
1198
|
|
|
@@ -1102,7 +1217,8 @@ internal object G2CfwRenderer {
|
|
|
1102
1217
|
h: Int,
|
|
1103
1218
|
frameId: Int,
|
|
1104
1219
|
): ByteArray? {
|
|
1105
|
-
|
|
1220
|
+
if (rle.isEmpty()) return null
|
|
1221
|
+
// Plain RLE: the SID-0xf0 transport deflates the whole message.
|
|
1106
1222
|
return byteArrayOf(
|
|
1107
1223
|
0x83.toByte(),
|
|
1108
1224
|
(lx / 4).toByte(),
|
|
@@ -1115,7 +1231,7 @@ internal object G2CfwRenderer {
|
|
|
1115
1231
|
(h / 2).toByte(),
|
|
1116
1232
|
(frameId and 0xFF).toByte(),
|
|
1117
1233
|
((frameId ushr 8) and 0xFF).toByte(),
|
|
1118
|
-
) +
|
|
1234
|
+
) + rle
|
|
1119
1235
|
}
|
|
1120
1236
|
|
|
1121
1237
|
fun bundle(segments: List<ByteArray>): ByteArray? {
|
|
@@ -1158,15 +1274,12 @@ internal object G2CfwRenderer {
|
|
|
1158
1274
|
return encodePixels(panel)
|
|
1159
1275
|
}
|
|
1160
1276
|
|
|
1161
|
-
fun blackPayload(): ByteArray?
|
|
1162
|
-
val compressed = zlib(rleEncodeSolid(0, FRAME_WIDTH * FRAME_HEIGHT)) ?: return null
|
|
1163
|
-
return byteArrayOf(6) + compressed
|
|
1164
|
-
}
|
|
1277
|
+
fun blackPayload(): ByteArray? = byteArrayOf(6) + rleEncodeSolid(0, FRAME_WIDTH * FRAME_HEIGHT)
|
|
1165
1278
|
|
|
1279
|
+
/** `[6][rle]`: plain RLE, the SID-0xf0 transport deflates the whole message. */
|
|
1166
1280
|
fun encodePixels(grayscale: ByteArray): ByteArray? {
|
|
1167
1281
|
if (grayscale.size != FRAME_WIDTH * FRAME_HEIGHT) return null
|
|
1168
|
-
|
|
1169
|
-
return byteArrayOf(6) + compressed
|
|
1282
|
+
return byteArrayOf(6) + rleEncode(pack4Bpp(grayscale))
|
|
1170
1283
|
}
|
|
1171
1284
|
|
|
1172
1285
|
/**
|
|
@@ -1242,12 +1355,4 @@ internal object G2CfwRenderer {
|
|
|
1242
1355
|
}
|
|
1243
1356
|
}
|
|
1244
1357
|
|
|
1245
|
-
private fun zlib(data: ByteArray): ByteArray? =
|
|
1246
|
-
try {
|
|
1247
|
-
ByteArrayOutputStream().also { output ->
|
|
1248
|
-
DeflaterOutputStream(output).use { it.write(data) }
|
|
1249
|
-
}.toByteArray()
|
|
1250
|
-
} catch (_: Exception) {
|
|
1251
|
-
null
|
|
1252
|
-
}
|
|
1253
1358
|
}
|