@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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayOutputStream
|
|
4
|
+
import java.util.zip.Deflater
|
|
5
|
+
import kotlinx.coroutines.CompletableDeferred
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Private SID-0xf0 message-stream transport of the Glassly CFW (GLASSLYCFW/31,
|
|
9
|
+
* upstream Faceclaw/10+). Custom commands no longer ride an EvenHub image
|
|
10
|
+
* container: they are records in a byte stream that the firmware reconstructs
|
|
11
|
+
* before any TPL reassembly, on whichever lenses the options byte selects (the
|
|
12
|
+
* other lens receives them over the frame bridge, in arrival order), and each
|
|
13
|
+
* record is answered with an ACK or NACK notification through the BLE ingress
|
|
14
|
+
* lens.
|
|
15
|
+
*
|
|
16
|
+
* Wire format (see g2flash `patches/message_transport.c`):
|
|
17
|
+
*
|
|
18
|
+
* - packet: `[0xAA][0x21][seq][len+2][1][1][0xF0][0][options][stream bytes][crc16 LE]`
|
|
19
|
+
* where `options` = lens bits (1 left, 2 right) | 0x80 RESET on the first
|
|
20
|
+
* packet of a stream | 0x40 END on its last, `seq` increments per packet
|
|
21
|
+
* (uint8 wrap) and the CRC (CCITT-FALSE) covers `options` + stream bytes.
|
|
22
|
+
* The stream id is the RESET packet's `seq`.
|
|
23
|
+
* - record: `[flags][len:u16][crc16 of the plain message:u16][body]` where
|
|
24
|
+
* flags = lens bits | 4 COMPRESSED | 8 RESET_CONTEXT. A compressed body is
|
|
25
|
+
* the output of one persistent deflate context ending in a SYNC_FLUSH (never
|
|
26
|
+
* finished); RESET_CONTEXT tells the glasses to start a fresh inflater, which
|
|
27
|
+
* the phone must set whenever its own deflater is new.
|
|
28
|
+
* - ACK: `[1][stream][ordinal:u16][lens][size:u16][crc:u16]` + up to three
|
|
29
|
+
* 7-byte history entries of preceding successes; NACK is `[3]...` alone.
|
|
30
|
+
*
|
|
31
|
+
* This client sends one record per stream (RESET..END), so every ordinal is 0
|
|
32
|
+
* and a stream id identifies the message. After a NACK or a timeout the
|
|
33
|
+
* glasses have dropped their inflater, so the encoder starts a new context and
|
|
34
|
+
* the next record carries RESET_CONTEXT.
|
|
35
|
+
*/
|
|
36
|
+
internal object G2CfwTransport {
|
|
37
|
+
const val SERVICE_ID: Byte = 0xF0.toByte()
|
|
38
|
+
const val LENS_LEFT = 1
|
|
39
|
+
const val LENS_RIGHT = 2
|
|
40
|
+
const val LENS_BOTH = 3
|
|
41
|
+
const val FLAG_COMPRESSED = 4
|
|
42
|
+
const val FLAG_RESET_CONTEXT = 8
|
|
43
|
+
const val PACKET_RESET = 0x80
|
|
44
|
+
const val PACKET_END = 0x40
|
|
45
|
+
const val ACK_KIND = 1
|
|
46
|
+
const val NACK_KIND = 3
|
|
47
|
+
/** GLASSLYCFW/38: an object-cache reply, sent by each lens before its ACK. */
|
|
48
|
+
const val REPLY_KIND = 5
|
|
49
|
+
const val MAX_MESSAGE_BYTES = 0xFFFF
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The firmware caps a packet at 252 stream bytes; our writes stay within the
|
|
53
|
+
* driver's packet payload (`G2BLE.MAX_PACKET_PAYLOAD` = 236: options byte +
|
|
54
|
+
* chunk + CRC).
|
|
55
|
+
*/
|
|
56
|
+
const val MAX_CHUNK_BYTES = 233
|
|
57
|
+
|
|
58
|
+
fun crc16(bytes: ByteArray, from: Int = 0, to: Int = bytes.size): Int {
|
|
59
|
+
var crc = 0xFFFF
|
|
60
|
+
for (i in from until to) {
|
|
61
|
+
crc = crc xor ((bytes[i].toInt() and 0xFF) shl 8)
|
|
62
|
+
repeat(8) {
|
|
63
|
+
crc = if (crc and 0x8000 != 0) ((crc shl 1) xor 0x1021) and 0xFFFF else (crc shl 1) and 0xFFFF
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return crc
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* ACK deadline for a stream: a both-lens stream acks at ~1.1 KB/s once the
|
|
71
|
+
* right lens has it over the frame bridge (GLASSLYCFW/39), so allow 1 ms per
|
|
72
|
+
* byte on top of 3.5 s. A tighter deadline times out uploads that are still
|
|
73
|
+
* landing, and each timeout re-uploads more.
|
|
74
|
+
*/
|
|
75
|
+
fun ackTimeoutMs(streamBytes: Int): Long = 3500L + streamBytes
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Persistent deflate context: records share dictionary state across
|
|
79
|
+
* messages, so a keyframe after a delta compresses against it.
|
|
80
|
+
*/
|
|
81
|
+
class Encoder {
|
|
82
|
+
private var deflater: Deflater? = null
|
|
83
|
+
var needsContextReset = true
|
|
84
|
+
private set
|
|
85
|
+
|
|
86
|
+
/** Drop the context: the next record carries RESET_CONTEXT. */
|
|
87
|
+
fun resetContext() {
|
|
88
|
+
deflater?.end()
|
|
89
|
+
deflater = null
|
|
90
|
+
needsContextReset = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One stream record for `message`, or null when it cannot be framed.
|
|
95
|
+
* Falls back to an uncompressed record (still resetting the context)
|
|
96
|
+
* when deflate would not fit the two-byte length.
|
|
97
|
+
*/
|
|
98
|
+
fun record(message: ByteArray, targets: Int): ByteArray? {
|
|
99
|
+
if (message.size > MAX_MESSAGE_BYTES || targets and LENS_BOTH.inv() != 0) return null
|
|
100
|
+
// Each lens inflates only the records addressed to it, so a record for one
|
|
101
|
+
// lens can neither continue the shared context (the other lens would fall
|
|
102
|
+
// out of sync) nor be continued from: deflate it from a fresh context and
|
|
103
|
+
// start fresh again afterwards.
|
|
104
|
+
val exclusive = targets != LENS_BOTH
|
|
105
|
+
if (exclusive) resetContext()
|
|
106
|
+
try {
|
|
107
|
+
return frame(message, targets, crc16(message))
|
|
108
|
+
} finally {
|
|
109
|
+
if (exclusive) resetContext()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun frame(message: ByteArray, targets: Int, crc: Int): ByteArray {
|
|
114
|
+
var flags = targets
|
|
115
|
+
val compressed = deflateSyncFlush(message)
|
|
116
|
+
val body: ByteArray
|
|
117
|
+
if (compressed != null && compressed.size <= MAX_MESSAGE_BYTES) {
|
|
118
|
+
body = compressed
|
|
119
|
+
flags = flags or FLAG_COMPRESSED
|
|
120
|
+
if (needsContextReset) {
|
|
121
|
+
flags = flags or FLAG_RESET_CONTEXT
|
|
122
|
+
needsContextReset = false
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// The glasses cannot resume a context this record skipped.
|
|
126
|
+
resetContext()
|
|
127
|
+
body = message
|
|
128
|
+
flags = flags or FLAG_RESET_CONTEXT
|
|
129
|
+
}
|
|
130
|
+
val out = ByteArrayOutputStream(5 + body.size)
|
|
131
|
+
out.write(flags)
|
|
132
|
+
out.write(body.size and 0xFF)
|
|
133
|
+
out.write((body.size ushr 8) and 0xFF)
|
|
134
|
+
out.write(crc and 0xFF)
|
|
135
|
+
out.write((crc ushr 8) and 0xFF)
|
|
136
|
+
out.write(body)
|
|
137
|
+
return out.toByteArray()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun deflateSyncFlush(message: ByteArray): ByteArray? {
|
|
141
|
+
val z = deflater ?: Deflater(Deflater.DEFAULT_COMPRESSION).also {
|
|
142
|
+
deflater = it
|
|
143
|
+
needsContextReset = true
|
|
144
|
+
}
|
|
145
|
+
return try {
|
|
146
|
+
z.setInput(message)
|
|
147
|
+
val out = ByteArrayOutputStream(message.size / 2 + 64)
|
|
148
|
+
val buffer = ByteArray(8192)
|
|
149
|
+
do {
|
|
150
|
+
val n = z.deflate(buffer, 0, buffer.size, Deflater.SYNC_FLUSH)
|
|
151
|
+
out.write(buffer, 0, n)
|
|
152
|
+
} while (n == buffer.size || !z.needsInput())
|
|
153
|
+
val bytes = out.toByteArray()
|
|
154
|
+
if (bytes.size < 4) {
|
|
155
|
+
resetContext()
|
|
156
|
+
null
|
|
157
|
+
} else {
|
|
158
|
+
bytes
|
|
159
|
+
}
|
|
160
|
+
} catch (_: Exception) {
|
|
161
|
+
resetContext()
|
|
162
|
+
null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Split `stream` (one or more records) into SID-0xf0 packets. `sequence` is
|
|
169
|
+
* the first packet's `seq` and the stream id; packets count up from it.
|
|
170
|
+
*/
|
|
171
|
+
fun packets(stream: ByteArray, sequence: Int, targets: Int, chunkBytes: Int = MAX_CHUNK_BYTES): List<ByteArray> {
|
|
172
|
+
val chunk = chunkBytes.coerceIn(1, 252)
|
|
173
|
+
val packets = mutableListOf<ByteArray>()
|
|
174
|
+
var offset = 0
|
|
175
|
+
var index = 0
|
|
176
|
+
do {
|
|
177
|
+
val end = minOf(offset + chunk, stream.size)
|
|
178
|
+
var options = targets and LENS_BOTH
|
|
179
|
+
if (offset == 0) options = options or PACKET_RESET
|
|
180
|
+
if (end >= stream.size) options = options or PACKET_END
|
|
181
|
+
val body = ByteArray(1 + end - offset)
|
|
182
|
+
body[0] = options.toByte()
|
|
183
|
+
System.arraycopy(stream, offset, body, 1, end - offset)
|
|
184
|
+
val crc = crc16(body)
|
|
185
|
+
val packet = ByteArray(10 + body.size)
|
|
186
|
+
packet[0] = 0xAA.toByte()
|
|
187
|
+
packet[1] = 0x21
|
|
188
|
+
packet[2] = ((sequence + index) and 0xFF).toByte()
|
|
189
|
+
packet[3] = (body.size + 2).toByte()
|
|
190
|
+
packet[4] = 1
|
|
191
|
+
packet[5] = 1
|
|
192
|
+
packet[6] = SERVICE_ID
|
|
193
|
+
packet[7] = 0
|
|
194
|
+
System.arraycopy(body, 0, packet, 8, body.size)
|
|
195
|
+
packet[8 + body.size] = (crc and 0xFF).toByte()
|
|
196
|
+
packet[9 + body.size] = ((crc ushr 8) and 0xFF).toByte()
|
|
197
|
+
packets.add(packet)
|
|
198
|
+
index += 1
|
|
199
|
+
offset = end
|
|
200
|
+
} while (offset < stream.size)
|
|
201
|
+
return packets
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
data class AckEntry(val streamId: Int, val ordinal: Int, val size: Int, val crc: Int)
|
|
205
|
+
|
|
206
|
+
/** The message this reply is about ([entries] first), then preceding successes. */
|
|
207
|
+
data class Ack(val success: Boolean, val lens: Int, val entries: List<AckEntry>) {
|
|
208
|
+
val current: AckEntry get() = entries[0]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun u16(b: ByteArray, i: Int): Int = (b[i].toInt() and 0xFF) or ((b[i + 1].toInt() and 0xFF) shl 8)
|
|
212
|
+
|
|
213
|
+
/** Parse a reply body (the packet payload with header and CRC stripped). */
|
|
214
|
+
fun parseAck(b: ByteArray): Ack? {
|
|
215
|
+
if (b.size < 9 || (b.size - 9) % 7 != 0 || b.size > 9 + 3 * 7) return null
|
|
216
|
+
val kind = b[0].toInt() and 0xFF
|
|
217
|
+
val lens = b[4].toInt() and 0xFF
|
|
218
|
+
if (kind != ACK_KIND && kind != NACK_KIND) return null
|
|
219
|
+
if (lens != LENS_LEFT && lens != LENS_RIGHT) return null
|
|
220
|
+
if (kind == NACK_KIND && b.size != 9) return null
|
|
221
|
+
val entries = mutableListOf(AckEntry(b[1].toInt() and 0xFF, u16(b, 2), u16(b, 5), u16(b, 7)))
|
|
222
|
+
var i = 9
|
|
223
|
+
while (i < b.size) {
|
|
224
|
+
entries.add(AckEntry(b[i].toInt() and 0xFF, u16(b, i + 1), u16(b, i + 3), u16(b, i + 5)))
|
|
225
|
+
i += 7
|
|
226
|
+
}
|
|
227
|
+
return Ack(kind == ACK_KIND, lens, entries)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Object-cache reply (GLASSLYCFW/38): `[5][stream][ordinal:u16][lens]` then
|
|
232
|
+
* `[mode][request:u16][status][epoch:u16][revision:u32][extra]`.
|
|
233
|
+
*/
|
|
234
|
+
data class CacheReply(
|
|
235
|
+
val streamId: Int, val ordinal: Int, val lens: Int, val mode: Int, val request: Int,
|
|
236
|
+
val status: Int, val epoch: Int, val revision: Long, val extra: ByteArray,
|
|
237
|
+
) {
|
|
238
|
+
companion object {
|
|
239
|
+
const val APPLIED = 0
|
|
240
|
+
const val STALE = 1
|
|
241
|
+
const val MISSING = 2
|
|
242
|
+
const val CAPACITY = 3
|
|
243
|
+
const val REFUSED = 4
|
|
244
|
+
const val UNCHANGED = 5
|
|
245
|
+
const val DELTA = 6
|
|
246
|
+
const val SNAPSHOT = 7
|
|
247
|
+
const val RESET = 8
|
|
248
|
+
private val NAMES = listOf("applied", "stale", "missing", "capacity", "refused", "unchanged", "delta", "snapshot", "reset")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A RESET reply's store size in bytes (0 when the reply did not carry it). */
|
|
252
|
+
val storeBytes: Int
|
|
253
|
+
get() = if (mode == 41 && status == APPLIED && extra.size >= 3) ((extra[1].toInt() and 0xFF) or ((extra[2].toInt() and 0xFF) shl 8)) * 1024 else 0
|
|
254
|
+
|
|
255
|
+
/** Ids a MISSING reply names (objects and assets share the field). */
|
|
256
|
+
val missingIds: List<Int>
|
|
257
|
+
get() {
|
|
258
|
+
if (status != MISSING || extra.size < 2) return emptyList()
|
|
259
|
+
val out = mutableListOf<Int>()
|
|
260
|
+
var i = 2
|
|
261
|
+
while (i + 4 <= extra.size) {
|
|
262
|
+
out.add((extra[i].toInt() and 0xFF) or ((extra[i + 1].toInt() and 0xFF) shl 8) or ((extra[i + 2].toInt() and 0xFF) shl 16) or ((extra[i + 3].toInt() and 0xFF) shl 24))
|
|
263
|
+
i += 4
|
|
264
|
+
}
|
|
265
|
+
return out
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
val summary: String
|
|
269
|
+
get() {
|
|
270
|
+
val name = NAMES.getOrNull(status) ?: "status $status"
|
|
271
|
+
return when (status) {
|
|
272
|
+
MISSING -> "$name $missingIds"
|
|
273
|
+
REFUSED -> "$name reason ${extra.firstOrNull()?.toInt() ?: 0}"
|
|
274
|
+
APPLIED, CAPACITY -> if ((extra.firstOrNull()?.toInt() ?: 0) > 0) "$name, evicted ${extra[0].toInt()}" else name
|
|
275
|
+
else -> name
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fun parseCacheReply(b: ByteArray): CacheReply? {
|
|
281
|
+
if (b.size < 15 || (b[0].toInt() and 0xFF) != REPLY_KIND) return null
|
|
282
|
+
val lens = b[4].toInt() and 0xFF
|
|
283
|
+
if (lens != LENS_LEFT && lens != LENS_RIGHT) return null
|
|
284
|
+
val revision = (u16(b, 11).toLong() or (u16(b, 13).toLong() shl 16))
|
|
285
|
+
return CacheReply(b[1].toInt() and 0xFF, u16(b, 2), lens, b[5].toInt() and 0xFF, u16(b, 6), b[8].toInt() and 0xFF, u16(b, 9), revision, b.copyOfRange(15, b.size))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** True for messages of the object cache (modes 37-41), which carry an epoch and a request id. */
|
|
289
|
+
fun isCacheMessage(message: ByteArray): Boolean = message.size >= 5 && (message[0].toInt() and 0xFF) in 37..41
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Copy of a cache message with the session epoch filled in and, when the encoder left
|
|
293
|
+
* the request id 0 (never for STATE, whose bytes are the revision), `request` stamped:
|
|
294
|
+
* the firmware acknowledges a SHOW or HIDE repeating the last applied request id
|
|
295
|
+
* without applying it again, so every distinct message needs its own.
|
|
296
|
+
*/
|
|
297
|
+
fun stamp(message: ByteArray, epoch: Int, request: Int): ByteArray {
|
|
298
|
+
if (!isCacheMessage(message)) return message
|
|
299
|
+
val out = message.copyOf()
|
|
300
|
+
out[1] = (epoch and 0xFF).toByte()
|
|
301
|
+
out[2] = ((epoch ushr 8) and 0xFF).toByte()
|
|
302
|
+
if ((out[0].toInt() and 0xFF) != 40 && out[3].toInt() == 0 && out[4].toInt() == 0 && request != 0) {
|
|
303
|
+
out[3] = (request and 0xFF).toByte()
|
|
304
|
+
out[4] = ((request ushr 8) and 0xFF).toByte()
|
|
305
|
+
}
|
|
306
|
+
return out
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read-only, single-packet parse of a raw notify frame: the reply body if
|
|
311
|
+
* this is a complete SID-0xf0 frame with a valid CRC.
|
|
312
|
+
*/
|
|
313
|
+
fun replyBody(raw: ByteArray): ByteArray? {
|
|
314
|
+
if (raw.size < 10) return null
|
|
315
|
+
if (raw[0] != 0xAA.toByte() || (raw[3].toInt() and 0xFF) + 8 != raw.size) return null
|
|
316
|
+
if (raw[4].toInt() != 1 || raw[5].toInt() != 1 || raw[6] != SERVICE_ID) return null
|
|
317
|
+
if (((raw[7].toInt() and 0xFF) shr 1) and 0x0F != 0) return null
|
|
318
|
+
val body = raw.copyOfRange(8, raw.size - 2)
|
|
319
|
+
val crc = crc16(body)
|
|
320
|
+
if (raw[raw.size - 2] != (crc and 0xFF).toByte() || raw[raw.size - 1] != ((crc ushr 8) and 0xFF).toByte()) return null
|
|
321
|
+
return body
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The single in-flight stream ACK, completed on the BLE callback thread. A
|
|
327
|
+
* message sent to both lenses completes when each lens has acked it; any
|
|
328
|
+
* NACK fails it. Completing a finished deferred is a no-op, so duplicate and
|
|
329
|
+
* stale replies are harmless.
|
|
330
|
+
*/
|
|
331
|
+
internal class G2CfwAckBox {
|
|
332
|
+
@Volatile private var streamId: Int? = null
|
|
333
|
+
@Volatile private var expectedLenses = 0
|
|
334
|
+
@Volatile private var ackedLenses = 0
|
|
335
|
+
@Volatile private var pending: CompletableDeferred<Boolean>? = null
|
|
336
|
+
@Volatile var lastSuccessMs: Long = 0
|
|
337
|
+
private set
|
|
338
|
+
@Volatile var lastOutcome: String = ""
|
|
339
|
+
private set
|
|
340
|
+
private val replies = mutableMapOf<Int, G2CfwTransport.CacheReply>()
|
|
341
|
+
|
|
342
|
+
/** The cache replies the last message drew, by lens (1 left, 2 right). */
|
|
343
|
+
@Synchronized
|
|
344
|
+
fun cacheReplies(): Map<Int, G2CfwTransport.CacheReply> = replies.toMap()
|
|
345
|
+
|
|
346
|
+
/** Feed one cache reply; it is kept when it belongs to the armed stream. */
|
|
347
|
+
@Synchronized
|
|
348
|
+
fun record(reply: G2CfwTransport.CacheReply) {
|
|
349
|
+
val armed = streamId ?: return
|
|
350
|
+
if (armed == reply.streamId && reply.ordinal == 0) replies[reply.lens] = reply
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Arm for `streamId`; returns the deferred to await after sending. */
|
|
354
|
+
@Synchronized
|
|
355
|
+
fun arm(streamId: Int, lenses: Int): CompletableDeferred<Boolean> {
|
|
356
|
+
val deferred = CompletableDeferred<Boolean>()
|
|
357
|
+
this.streamId = streamId
|
|
358
|
+
expectedLenses = lenses and G2CfwTransport.LENS_BOTH
|
|
359
|
+
ackedLenses = 0
|
|
360
|
+
pending = deferred
|
|
361
|
+
lastOutcome = ""
|
|
362
|
+
replies.clear()
|
|
363
|
+
return deferred
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Queue guard scoped to this arm, even when an 8-bit stream ID is reused. */
|
|
367
|
+
@Synchronized
|
|
368
|
+
fun isPending(deferred: CompletableDeferred<Boolean>): Boolean = pending === deferred && deferred.isActive
|
|
369
|
+
|
|
370
|
+
/** Forget `deferred` if it is still the armed one (after its await ended). */
|
|
371
|
+
@Synchronized
|
|
372
|
+
fun disarm(deferred: CompletableDeferred<Boolean>, outcome: String) {
|
|
373
|
+
if (pending === deferred) {
|
|
374
|
+
pending = null
|
|
375
|
+
streamId = null
|
|
376
|
+
if (lastOutcome.isEmpty()) lastOutcome = outcome
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Feed one reply. Returns true when it resolved the in-flight message. */
|
|
381
|
+
@Synchronized
|
|
382
|
+
fun resolve(ack: G2CfwTransport.Ack): Boolean {
|
|
383
|
+
val deferred = pending ?: return false
|
|
384
|
+
val armed = streamId ?: return false
|
|
385
|
+
if (armed != ack.current.streamId || ack.current.ordinal != 0) return false
|
|
386
|
+
if (!ack.success) {
|
|
387
|
+
lastOutcome = "nack"
|
|
388
|
+
pending = null
|
|
389
|
+
streamId = null
|
|
390
|
+
deferred.complete(false)
|
|
391
|
+
return true
|
|
392
|
+
}
|
|
393
|
+
ackedLenses = ackedLenses or (ack.lens and expectedLenses)
|
|
394
|
+
if (ackedLenses != expectedLenses) return false
|
|
395
|
+
lastOutcome = "ack"
|
|
396
|
+
lastSuccessMs = System.currentTimeMillis()
|
|
397
|
+
pending = null
|
|
398
|
+
streamId = null
|
|
399
|
+
deferred.complete(true)
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import java.util.UUID
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bounded aggregates shared by the BLE writer and main thread; never retain payloads.
|
|
7
|
+
* Lines stay small: a sample is logged only for a window with display work, a
|
|
8
|
+
* backlog or a late tick; zero, false and empty values and sub-millisecond
|
|
9
|
+
* timings are left out; the link's fixed facts go in a separate session line.
|
|
10
|
+
*/
|
|
11
|
+
internal class G2DisplayDiagnostics(private var startedAt: Long) {
|
|
12
|
+
companion object {
|
|
13
|
+
/** Link upkeep that runs whether or not anything is drawn: IMU reports, touch debouncing and heartbeat writes alone leave a window idle. */
|
|
14
|
+
private val AMBIENT_COUNTERS = setOf("imuReceived", "touchDebounced", "txBytes", "txPackets")
|
|
15
|
+
private val AMBIENT_TIMINGS = setOf("notifyDispatchMs", "writeQueueMs")
|
|
16
|
+
/** A tick this late means the main thread or the process was held up: logged even when idle. */
|
|
17
|
+
const val STRETCHED_WINDOW_MS = 2000L
|
|
18
|
+
/** The session line is repeated this often so a trimmed log excerpt still has it. */
|
|
19
|
+
const val SESSION_REPEAT_MS = 60_000L
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private data class Timing(var count: Int = 0, var total: Long = 0, var maximum: Long = 0)
|
|
23
|
+
private val counters = mutableMapOf<String, Int>()
|
|
24
|
+
private val timings = mutableMapOf<String, Timing>()
|
|
25
|
+
/** Short: it is on every line, and only has to tell apart the runs in one log. */
|
|
26
|
+
private val run = UUID.randomUUID().toString().take(8)
|
|
27
|
+
private var sequence = 0
|
|
28
|
+
private var sessionKey: Map<String, Any>? = null
|
|
29
|
+
private var lastSession = 0L
|
|
30
|
+
|
|
31
|
+
@Synchronized fun count(name: String, amount: Int = 1) {
|
|
32
|
+
counters[name] = (counters[name] ?: 0) + amount
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Synchronized fun timing(name: String, milliseconds: Long) {
|
|
36
|
+
val value = milliseconds.coerceAtLeast(0)
|
|
37
|
+
val timing = timings.getOrPut(name) { Timing() }
|
|
38
|
+
timing.count++
|
|
39
|
+
timing.total += value
|
|
40
|
+
timing.maximum = maxOf(timing.maximum, value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The link's fixed facts: logged when they change and once a minute, instead of on every sample. */
|
|
44
|
+
@Synchronized fun session(now: Long, timestamp: Long, facts: Map<String, Any>): Map<String, Any>? {
|
|
45
|
+
if (facts == sessionKey && now - lastSession < SESSION_REPEAT_MS) return null
|
|
46
|
+
sessionKey = facts
|
|
47
|
+
lastSession = now
|
|
48
|
+
return facts + record("session", timestamp)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The window's summary, or null when it is not due or was idle. `busy` means something is queued for the glasses. */
|
|
52
|
+
@Synchronized fun sample(now: Long, timestamp: Long, context: Map<String, Any>, busy: Boolean = false): Map<String, Any>? {
|
|
53
|
+
val elapsed = now - startedAt
|
|
54
|
+
if (elapsed < 1000) return null
|
|
55
|
+
startedAt = now
|
|
56
|
+
val counters = counters.toMap()
|
|
57
|
+
val timings = timings.toMap()
|
|
58
|
+
this.counters.clear()
|
|
59
|
+
this.timings.clear()
|
|
60
|
+
val active = busy || elapsed >= STRETCHED_WINDOW_MS ||
|
|
61
|
+
counters.keys.any { it !in AMBIENT_COUNTERS } || timings.keys.any { it !in AMBIENT_TIMINGS }
|
|
62
|
+
if (!active) return null
|
|
63
|
+
val fields = context.toMutableMap()
|
|
64
|
+
fields["windowMs"] = elapsed
|
|
65
|
+
fields.putAll(counters)
|
|
66
|
+
// A timing that never reached a millisecond shows no delay: left out.
|
|
67
|
+
for ((key, value) in timings) {
|
|
68
|
+
if (value.maximum < 1) continue
|
|
69
|
+
fields[key + "Avg"] = Math.round(value.total * 10.0 / value.count) / 10.0
|
|
70
|
+
fields[key + "Max"] = value.maximum
|
|
71
|
+
}
|
|
72
|
+
return fields.filterValues { !isEmpty(it) } + record("sample", timestamp)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun record(type: String, timestamp: Long): Map<String, Any> =
|
|
76
|
+
mapOf("v" to 1, "run" to run, "id" to ++sequence, "t" to timestamp, "type" to type)
|
|
77
|
+
|
|
78
|
+
private fun isEmpty(value: Any): Boolean = when (value) {
|
|
79
|
+
is Boolean -> !value
|
|
80
|
+
is Number -> value.toDouble() == 0.0
|
|
81
|
+
is String -> value.isEmpty()
|
|
82
|
+
else -> false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -43,9 +43,9 @@ internal object G2FlashProtocol {
|
|
|
43
43
|
|
|
44
44
|
val goldenStock =
|
|
45
45
|
GoldenVector(
|
|
46
|
-
sha256 = "
|
|
47
|
-
ps =
|
|
48
|
-
progEnd =
|
|
46
|
+
sha256 = "187ccf2bcc5c17a212106e8a376745511e8289c4232b634a7ea94b9bf25a0979",
|
|
47
|
+
ps = 3_758_720,
|
|
48
|
+
progEnd = 0x007C_DA60,
|
|
49
49
|
pass = true,
|
|
50
50
|
)
|
|
51
51
|
|
|
@@ -40,18 +40,18 @@ internal object G2FirmwareCatalog {
|
|
|
40
40
|
G2FirmwareArtifact(
|
|
41
41
|
id = "stock",
|
|
42
42
|
displayName = "Stock firmware",
|
|
43
|
-
version = "2.
|
|
44
|
-
fileName = "g2-stock-2.
|
|
45
|
-
sha256 = "
|
|
46
|
-
size =
|
|
43
|
+
version = "2.3.0.24",
|
|
44
|
+
fileName = "g2-stock-2.3.0.24.evenota.bin",
|
|
45
|
+
sha256 = "187ccf2bcc5c17a212106e8a376745511e8289c4232b634a7ea94b9bf25a0979",
|
|
46
|
+
size = 4_537_963,
|
|
47
47
|
),
|
|
48
48
|
G2FirmwareArtifact(
|
|
49
49
|
id = "glassly-cfw",
|
|
50
|
-
displayName = "Glassly CFW",
|
|
51
|
-
version = "2.
|
|
52
|
-
fileName = "g2-glassly-cfw-2.
|
|
53
|
-
sha256 = "
|
|
54
|
-
size =
|
|
50
|
+
displayName = "Glassly CFW v39",
|
|
51
|
+
version = "2.3.0.24",
|
|
52
|
+
fileName = "g2-glassly-cfw-2.3.0.24.evenota.bin",
|
|
53
|
+
sha256 = "4c7bf9a552df1a42f26b3378425c7c2121ca4058fed7283ec863c3f8d2cd13fe",
|
|
54
|
+
size = 4_661_963,
|
|
55
55
|
),
|
|
56
56
|
)
|
|
57
57
|
|