@glassly/bluetooth-sdk 0.1.0-dev.99 → 0.2.8-dev.445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,485 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import zlib
|
|
3
|
+
|
|
4
|
+
/// Private SID-0xf0 message-stream transport of the Glassly CFW (GLASSLYCFW/31,
|
|
5
|
+
/// upstream Faceclaw/10+). Custom commands (modes 3/6/8/9/10/11/15/16/17/18/19/
|
|
6
|
+
/// 20/36/37/38) no longer ride an EvenHub image container: they are records in
|
|
7
|
+
/// a byte stream that the firmware reconstructs before any TPL reassembly, on
|
|
8
|
+
/// whichever lenses the options byte selects (the other lens receives them over
|
|
9
|
+
/// the frame bridge, in arrival order), and each record is answered with an ACK
|
|
10
|
+
/// or NACK notification through the BLE ingress lens.
|
|
11
|
+
///
|
|
12
|
+
/// Wire format (see g2flash `patches/message_transport.c`):
|
|
13
|
+
///
|
|
14
|
+
/// * packet: `[0xAA][0x21][seq][len+2][1][1][0xF0][0][options][stream bytes][crc16 LE]`
|
|
15
|
+
/// where `options` = lens bits (1 left, 2 right) | 0x80 RESET on the first
|
|
16
|
+
/// packet of a stream | 0x40 END on its last, `seq` increments per packet
|
|
17
|
+
/// (uint8 wrap) and the CRC (CCITT-FALSE) covers `options` + stream bytes.
|
|
18
|
+
/// The stream id is the RESET packet's `seq`.
|
|
19
|
+
/// * record: `[flags][len:u16][crc16 of the plain message:u16][body]` where
|
|
20
|
+
/// flags = lens bits | 4 COMPRESSED | 8 RESET_CONTEXT. A compressed body is
|
|
21
|
+
/// the output of one persistent deflate context ending in a SYNC_FLUSH
|
|
22
|
+
/// (never Z_FINISH); RESET_CONTEXT tells the glasses to start a fresh
|
|
23
|
+
/// inflater, which the phone must set whenever its own deflater is new.
|
|
24
|
+
/// * ACK: `[1][stream][ordinal:u16][lens][size:u16][crc:u16]` + up to three
|
|
25
|
+
/// 7-byte history entries `[stream][ordinal:u16][size:u16][crc:u16]` of
|
|
26
|
+
/// preceding successes; NACK is `[3]...` with no history.
|
|
27
|
+
///
|
|
28
|
+
/// This client sends one record per stream (RESET..END), so every ordinal is
|
|
29
|
+
/// 0 and a stream id identifies the message. After a NACK or a timeout the
|
|
30
|
+
/// glasses have dropped their inflater, so the encoder starts a new context
|
|
31
|
+
/// and the next record carries RESET_CONTEXT.
|
|
32
|
+
enum G2CfwTransport {
|
|
33
|
+
static let serviceId: UInt8 = 0xF0
|
|
34
|
+
static let lensLeft: UInt8 = 1
|
|
35
|
+
static let lensRight: UInt8 = 2
|
|
36
|
+
static let lensBoth: UInt8 = 3
|
|
37
|
+
static let flagCompressed: UInt8 = 4
|
|
38
|
+
static let flagResetContext: UInt8 = 8
|
|
39
|
+
static let packetReset: UInt8 = 0x80
|
|
40
|
+
static let packetEnd: UInt8 = 0x40
|
|
41
|
+
static let ackKind: UInt8 = 1
|
|
42
|
+
static let nackKind: UInt8 = 3
|
|
43
|
+
/// GLASSLYCFW/38: an object-cache reply, sent by each lens before its ACK.
|
|
44
|
+
static let replyKind: UInt8 = 5
|
|
45
|
+
/// A record body is length-prefixed with two bytes.
|
|
46
|
+
static let maxMessageBytes = 0xFFFF
|
|
47
|
+
/// The firmware caps a packet at 252 stream bytes; our writes stay within
|
|
48
|
+
/// the driver's 232-byte packet payload (options byte + chunk + CRC).
|
|
49
|
+
static let maxChunkBytes = 229
|
|
50
|
+
|
|
51
|
+
/// Persistent deflate context: records share dictionary state across
|
|
52
|
+
/// messages, so a keyframe after a delta compresses against it.
|
|
53
|
+
final class Encoder {
|
|
54
|
+
private var stream = z_stream()
|
|
55
|
+
private var ready = false
|
|
56
|
+
private(set) var needsContextReset = true
|
|
57
|
+
|
|
58
|
+
deinit {
|
|
59
|
+
if ready { deflateEnd(&stream) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Drop the context: the next record carries RESET_CONTEXT so the
|
|
63
|
+
/// glasses discard theirs too (they already have after a NACK).
|
|
64
|
+
func resetContext() {
|
|
65
|
+
if ready {
|
|
66
|
+
deflateEnd(&stream)
|
|
67
|
+
ready = false
|
|
68
|
+
}
|
|
69
|
+
needsContextReset = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// One stream record for `message`, or nil when it cannot be framed.
|
|
73
|
+
/// Falls back to an uncompressed record (still resetting the context)
|
|
74
|
+
/// when deflate would not fit the two-byte length.
|
|
75
|
+
///
|
|
76
|
+
/// Each lens inflates only the records addressed to it, so a record for
|
|
77
|
+
/// one lens can neither continue the shared context (the other lens
|
|
78
|
+
/// would fall out of sync) nor be continued from: it is deflated from a
|
|
79
|
+
/// fresh context and the next record starts fresh again.
|
|
80
|
+
func record(_ message: Data, targets: UInt8) -> Data? {
|
|
81
|
+
guard message.count <= G2CfwTransport.maxMessageBytes, targets & ~G2CfwTransport.lensBoth == 0
|
|
82
|
+
else { return nil }
|
|
83
|
+
let exclusive = targets != G2CfwTransport.lensBoth
|
|
84
|
+
if exclusive { resetContext() }
|
|
85
|
+
defer { if exclusive { resetContext() } }
|
|
86
|
+
let crc = BinaryCodec.crc16CCITT(message)
|
|
87
|
+
|
|
88
|
+
var flags = targets
|
|
89
|
+
var body: Data
|
|
90
|
+
if let compressed = deflateSyncFlush(message), compressed.count <= G2CfwTransport.maxMessageBytes {
|
|
91
|
+
body = compressed
|
|
92
|
+
flags |= G2CfwTransport.flagCompressed
|
|
93
|
+
if needsContextReset {
|
|
94
|
+
flags |= G2CfwTransport.flagResetContext
|
|
95
|
+
needsContextReset = false
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// The glasses cannot resume a context this record skipped.
|
|
99
|
+
resetContext()
|
|
100
|
+
body = message
|
|
101
|
+
flags |= G2CfwTransport.flagResetContext
|
|
102
|
+
}
|
|
103
|
+
var record = Data(capacity: 5 + body.count)
|
|
104
|
+
record.append(flags)
|
|
105
|
+
record.append(UInt8(body.count & 0xFF))
|
|
106
|
+
record.append(UInt8((body.count >> 8) & 0xFF))
|
|
107
|
+
record.append(UInt8(crc & 0xFF))
|
|
108
|
+
record.append(UInt8((crc >> 8) & 0xFF))
|
|
109
|
+
record.append(body)
|
|
110
|
+
return record
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func deflateSyncFlush(_ message: Data) -> Data? {
|
|
114
|
+
if !ready {
|
|
115
|
+
stream = z_stream()
|
|
116
|
+
guard deflateInit_(&stream, Z_DEFAULT_COMPRESSION, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) == Z_OK
|
|
117
|
+
else { return nil }
|
|
118
|
+
ready = true
|
|
119
|
+
needsContextReset = true
|
|
120
|
+
}
|
|
121
|
+
let bound = Int(deflateBound(&stream, UInt(message.count))) + 16
|
|
122
|
+
var output = Data(count: bound)
|
|
123
|
+
var input = message
|
|
124
|
+
var produced = 0
|
|
125
|
+
let status: Int32 = input.withUnsafeMutableBytes { inRaw -> Int32 in
|
|
126
|
+
output.withUnsafeMutableBytes { outRaw -> Int32 in
|
|
127
|
+
guard let outBase = outRaw.bindMemory(to: Bytef.self).baseAddress else { return Z_MEM_ERROR }
|
|
128
|
+
stream.next_in = inRaw.bindMemory(to: Bytef.self).baseAddress
|
|
129
|
+
stream.avail_in = uInt(message.count)
|
|
130
|
+
stream.next_out = outBase
|
|
131
|
+
stream.avail_out = uInt(bound)
|
|
132
|
+
let rc = deflate(&stream, Z_SYNC_FLUSH)
|
|
133
|
+
produced = bound - Int(stream.avail_out)
|
|
134
|
+
stream.next_in = nil
|
|
135
|
+
stream.next_out = nil
|
|
136
|
+
return rc
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
guard status == Z_OK, stream.avail_in == 0, produced >= 4 else {
|
|
140
|
+
resetContext()
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
output.removeSubrange(produced ..< output.count)
|
|
144
|
+
return output
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Split `stream` (one or more records) into SID-0xf0 packets. `sequence`
|
|
149
|
+
/// is the first packet's `seq` and the stream id; packets count up from it.
|
|
150
|
+
static func packets(stream: Data, sequence: UInt8, targets: UInt8, chunkBytes: Int = maxChunkBytes) -> [Data] {
|
|
151
|
+
let chunk = max(1, min(chunkBytes, 252))
|
|
152
|
+
var packets: [Data] = []
|
|
153
|
+
var offset = 0
|
|
154
|
+
var index: UInt8 = 0
|
|
155
|
+
repeat {
|
|
156
|
+
let end = min(offset + chunk, stream.count)
|
|
157
|
+
var options = targets & lensBoth
|
|
158
|
+
if offset == 0 { options |= packetReset }
|
|
159
|
+
if end >= stream.count { options |= packetEnd }
|
|
160
|
+
var body = Data(capacity: 1 + end - offset)
|
|
161
|
+
body.append(options)
|
|
162
|
+
if end > offset { body.append(stream[stream.startIndex + offset ..< stream.startIndex + end]) }
|
|
163
|
+
var packet = Data(capacity: 10 + body.count)
|
|
164
|
+
packet.append(contentsOf: [0xAA, 0x21, sequence &+ index, UInt8(body.count + 2), 1, 1, serviceId, 0])
|
|
165
|
+
packet.append(body)
|
|
166
|
+
packet.append(contentsOf: BinaryCodec.crc16CCITTLittleEndian(body))
|
|
167
|
+
packets.append(packet)
|
|
168
|
+
index &+= 1
|
|
169
|
+
offset = end
|
|
170
|
+
} while offset < stream.count
|
|
171
|
+
return packets
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
struct Ack: Equatable {
|
|
175
|
+
struct Entry: Equatable {
|
|
176
|
+
let streamId: UInt8
|
|
177
|
+
let ordinal: UInt16
|
|
178
|
+
let size: UInt16
|
|
179
|
+
let crc: UInt16
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let success: Bool
|
|
183
|
+
let lens: UInt8
|
|
184
|
+
/// The message this reply is about, then preceding successes (newest first).
|
|
185
|
+
let entries: [Entry]
|
|
186
|
+
var current: Entry { entries[0] }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Parse a reply body (the packet payload with header and CRC stripped).
|
|
190
|
+
static func parseAck(_ body: Data) -> Ack? {
|
|
191
|
+
let b = [UInt8](body)
|
|
192
|
+
guard b.count >= 9, (b.count - 9) % 7 == 0, b.count <= 9 + 3 * 7,
|
|
193
|
+
b[0] == ackKind || b[0] == nackKind,
|
|
194
|
+
b[4] == lensLeft || b[4] == lensRight
|
|
195
|
+
else { return nil }
|
|
196
|
+
if b[0] == nackKind, b.count != 9 { return nil }
|
|
197
|
+
var entries = [Ack.Entry(
|
|
198
|
+
streamId: b[1], ordinal: UInt16(b[2]) | UInt16(b[3]) << 8,
|
|
199
|
+
size: UInt16(b[5]) | UInt16(b[6]) << 8, crc: UInt16(b[7]) | UInt16(b[8]) << 8
|
|
200
|
+
)]
|
|
201
|
+
var i = 9
|
|
202
|
+
while i < b.count {
|
|
203
|
+
entries.append(Ack.Entry(
|
|
204
|
+
streamId: b[i], ordinal: UInt16(b[i + 1]) | UInt16(b[i + 2]) << 8,
|
|
205
|
+
size: UInt16(b[i + 3]) | UInt16(b[i + 4]) << 8, crc: UInt16(b[i + 5]) | UInt16(b[i + 6]) << 8
|
|
206
|
+
))
|
|
207
|
+
i += 7
|
|
208
|
+
}
|
|
209
|
+
return Ack(success: b[0] == ackKind, lens: b[4], entries: entries)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Object-cache reply (GLASSLYCFW/38): `[5][stream][ordinal:u16][lens]` then
|
|
213
|
+
/// `[mode][request:u16][status][epoch:u16][revision:u32][extra]`.
|
|
214
|
+
struct CacheReply: Equatable {
|
|
215
|
+
static let applied: UInt8 = 0, stale: UInt8 = 1, missing: UInt8 = 2, capacity: UInt8 = 3
|
|
216
|
+
static let refused: UInt8 = 4, unchanged: UInt8 = 5, delta: UInt8 = 6, snapshot: UInt8 = 7, reset: UInt8 = 8
|
|
217
|
+
let streamId: UInt8
|
|
218
|
+
let ordinal: UInt16
|
|
219
|
+
let lens: UInt8
|
|
220
|
+
let mode: UInt8
|
|
221
|
+
let request: UInt16
|
|
222
|
+
let status: UInt8
|
|
223
|
+
let epoch: UInt16
|
|
224
|
+
let revision: UInt32
|
|
225
|
+
let extra: [UInt8]
|
|
226
|
+
|
|
227
|
+
/// A RESET reply's store size in bytes (0 when the reply did not carry it).
|
|
228
|
+
var storeBytes: Int {
|
|
229
|
+
guard mode == 41, status == Self.applied, extra.count >= 3 else { return 0 }
|
|
230
|
+
return (Int(extra[1]) | Int(extra[2]) << 8) * 1024
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Ids a MISSING reply names (objects and assets share the field).
|
|
234
|
+
var missingIds: [UInt32] {
|
|
235
|
+
guard status == Self.missing, extra.count >= 2 else { return [] }
|
|
236
|
+
var out: [UInt32] = []
|
|
237
|
+
var i = 2
|
|
238
|
+
while i + 4 <= extra.count {
|
|
239
|
+
out.append(UInt32(extra[i]) | UInt32(extra[i + 1]) << 8 | UInt32(extra[i + 2]) << 16 | UInt32(extra[i + 3]) << 24)
|
|
240
|
+
i += 4
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
var summary: String {
|
|
246
|
+
let names = ["applied", "stale", "missing", "capacity", "refused", "unchanged", "delta", "snapshot", "reset"]
|
|
247
|
+
let name = Int(status) < names.count ? names[Int(status)] : "status \(status)"
|
|
248
|
+
switch status {
|
|
249
|
+
case Self.missing: return "\(name) \(missingIds)"
|
|
250
|
+
case Self.refused: return "\(name) reason \(extra.first ?? 0)"
|
|
251
|
+
case Self.applied, Self.capacity: return (extra.first ?? 0) > 0 ? "\(name), evicted \(extra[0])" : name
|
|
252
|
+
default: return name
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static func parseCacheReply(_ body: Data) -> CacheReply? {
|
|
258
|
+
let b = [UInt8](body)
|
|
259
|
+
guard b.count >= 15, b[0] == replyKind, b[4] == lensLeft || b[4] == lensRight else { return nil }
|
|
260
|
+
return CacheReply(
|
|
261
|
+
streamId: b[1], ordinal: UInt16(b[2]) | UInt16(b[3]) << 8, lens: b[4],
|
|
262
|
+
mode: b[5], request: UInt16(b[6]) | UInt16(b[7]) << 8, status: b[8],
|
|
263
|
+
epoch: UInt16(b[9]) | UInt16(b[10]) << 8,
|
|
264
|
+
revision: UInt32(b[11]) | UInt32(b[12]) << 8 | UInt32(b[13]) << 16 | UInt32(b[14]) << 24,
|
|
265
|
+
extra: Array(b[15...])
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// True for messages of the object cache (modes 37-41), which carry an epoch and a request id.
|
|
270
|
+
static func isCacheMessage(_ message: Data) -> Bool {
|
|
271
|
+
message.count >= 5 && message[message.startIndex] >= 37 && message[message.startIndex] <= 41
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Copy of a cache message with the session epoch filled in and, when the
|
|
275
|
+
/// encoder left the request id 0 (never for STATE, whose bytes are the
|
|
276
|
+
/// revision), `request` stamped: the firmware acknowledges a SHOW or HIDE
|
|
277
|
+
/// repeating the last applied request id without applying it again, so
|
|
278
|
+
/// every distinct message needs its own. Other messages return unchanged.
|
|
279
|
+
static func stamp(_ message: Data, epoch: UInt16, request: UInt16) -> Data {
|
|
280
|
+
guard isCacheMessage(message) else { return message }
|
|
281
|
+
var out = Data(message)
|
|
282
|
+
out[out.startIndex + 1] = UInt8(epoch & 0xFF)
|
|
283
|
+
out[out.startIndex + 2] = UInt8(epoch >> 8)
|
|
284
|
+
if out[out.startIndex] != 40, out[out.startIndex + 3] == 0, out[out.startIndex + 4] == 0, request != 0 {
|
|
285
|
+
out[out.startIndex + 3] = UInt8(request & 0xFF)
|
|
286
|
+
out[out.startIndex + 4] = UInt8(request >> 8)
|
|
287
|
+
}
|
|
288
|
+
return out
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Read-only, single-packet parse of a raw notify frame: the reply body if
|
|
292
|
+
/// this is a complete SID-0xf0 frame with a valid CRC.
|
|
293
|
+
static func replyBody(fromNotifyFrame raw: Data) -> Data? {
|
|
294
|
+
guard raw.count >= 10 else { return nil }
|
|
295
|
+
let b = { (i: Int) -> UInt8 in raw[raw.startIndex + i] }
|
|
296
|
+
guard b(0) == 0xAA, Int(b(3)) + 8 == raw.count, b(4) == 1, b(5) == 1, b(6) == serviceId else { return nil }
|
|
297
|
+
let resultCode = (Int(b(7)) >> 1) & 0x0F
|
|
298
|
+
guard resultCode == 0 else { return nil }
|
|
299
|
+
let body = raw.subdata(in: (raw.startIndex + 8) ..< (raw.endIndex - 2))
|
|
300
|
+
let crc = BinaryCodec.crc16CCITT(body)
|
|
301
|
+
guard b(raw.count - 2) == UInt8(crc & 0xFF), b(raw.count - 1) == UInt8(crc >> 8) else { return nil }
|
|
302
|
+
return body
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// ACK deadline for a message of `streamBytes`: a both-lens stream acks at
|
|
306
|
+
/// ~1.1 KB/s once the right lens has it over the frame bridge (GLASSLYCFW/39),
|
|
307
|
+
/// so allow 1 ms per byte on top of 3.5 s. A tighter deadline times out
|
|
308
|
+
/// uploads that are still landing, and each timeout re-uploads more.
|
|
309
|
+
static func ackTimeoutNs(streamBytes: Int) -> UInt64 {
|
|
310
|
+
3_500_000_000 + UInt64(streamBytes) * 1_000_000
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Thread-safe holder for the single in-flight stream ACK continuation — the
|
|
315
|
+
/// SID-0xf0 counterpart of `ImgAckBox`, resolved inline on the BLE callback
|
|
316
|
+
/// queue. A message sent to both lenses completes when each lens has acked
|
|
317
|
+
/// it (the right lens's reply returns through the left, our ingress); any
|
|
318
|
+
/// NACK, a timeout, or cancellation fails it.
|
|
319
|
+
final class G2CfwAckBox {
|
|
320
|
+
private let lock = NSLock()
|
|
321
|
+
private var streamId: UInt8?
|
|
322
|
+
private var expectedLenses: UInt8 = 0
|
|
323
|
+
private var ackedLenses: UInt8 = 0
|
|
324
|
+
private var cont: CheckedContinuation<Bool, Never>?
|
|
325
|
+
private var token: UInt64 = 0
|
|
326
|
+
private var timer: Task<Void, Never>?
|
|
327
|
+
private var lastSuccessMs: Int64 = 0
|
|
328
|
+
private var lastOutcome: (at: TimeInterval, reason: String)?
|
|
329
|
+
/// Object-cache replies of the armed stream, by lens; kept until the next arm.
|
|
330
|
+
private var replies: [UInt8: G2CfwTransport.CacheReply] = [:]
|
|
331
|
+
|
|
332
|
+
func outcome() -> (at: TimeInterval, reason: String)? {
|
|
333
|
+
lock.lock()
|
|
334
|
+
defer { lock.unlock() }
|
|
335
|
+
return lastOutcome
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// The cache replies the last message drew, by lens (1 left, 2 right).
|
|
339
|
+
func cacheReplies() -> [UInt8: G2CfwTransport.CacheReply] {
|
|
340
|
+
lock.lock()
|
|
341
|
+
defer { lock.unlock() }
|
|
342
|
+
return replies
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Feed one cache reply; it is kept when it belongs to the armed stream.
|
|
346
|
+
func record(_ reply: G2CfwTransport.CacheReply) {
|
|
347
|
+
lock.lock()
|
|
348
|
+
if let armed = streamId, armed == reply.streamId, reply.ordinal == 0 {
|
|
349
|
+
replies[reply.lens] = reply
|
|
350
|
+
}
|
|
351
|
+
lock.unlock()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Milliseconds since the last fully acked stream, or Int64.max if none yet.
|
|
355
|
+
func msSinceLastSuccess() -> Int64 {
|
|
356
|
+
lock.lock()
|
|
357
|
+
let last = lastSuccessMs
|
|
358
|
+
lock.unlock()
|
|
359
|
+
guard last > 0 else { return .max }
|
|
360
|
+
return Int64(Date().timeIntervalSince1970 * 1000) - last
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Arm before sending, then `send`. The supplied guard expires with this
|
|
364
|
+
/// wait: queued packets must check it before writing, even while waiting
|
|
365
|
+
/// for BLE credits. A timed-out stream must not replay during recovery.
|
|
366
|
+
@MainActor
|
|
367
|
+
func wait(streamId: UInt8, lenses: UInt8, timeoutNs: UInt64, send: (_ isPending: @escaping () -> Bool) -> Void) async -> Bool {
|
|
368
|
+
await withTaskCancellationHandler {
|
|
369
|
+
await withCheckedContinuation { cont in
|
|
370
|
+
let token = arm(streamId: streamId, lenses: lenses, cont: cont)
|
|
371
|
+
guard !Task.isCancelled else {
|
|
372
|
+
timeout(token: token)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
let timer = Task.detached { [self] in
|
|
376
|
+
try? await Task.sleep(nanoseconds: timeoutNs)
|
|
377
|
+
guard !Task.isCancelled else { return }
|
|
378
|
+
timeout(token: token)
|
|
379
|
+
}
|
|
380
|
+
attachTimer(timer, token: token)
|
|
381
|
+
send { [self] in isPending(token: token) }
|
|
382
|
+
}
|
|
383
|
+
} onCancel: {
|
|
384
|
+
self.fail(reason: "cancelled")
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private func isPending(token: UInt64) -> Bool {
|
|
389
|
+
lock.lock()
|
|
390
|
+
defer { lock.unlock() }
|
|
391
|
+
return self.token == token && cont != nil
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
func arm(streamId: UInt8, lenses: UInt8, cont: CheckedContinuation<Bool, Never>) -> UInt64 {
|
|
395
|
+
lock.lock()
|
|
396
|
+
token &+= 1
|
|
397
|
+
let t = token
|
|
398
|
+
self.streamId = streamId
|
|
399
|
+
expectedLenses = lenses & G2CfwTransport.lensBoth
|
|
400
|
+
ackedLenses = 0
|
|
401
|
+
self.cont = cont
|
|
402
|
+
lastOutcome = nil
|
|
403
|
+
replies.removeAll()
|
|
404
|
+
let stale = timer
|
|
405
|
+
timer = nil
|
|
406
|
+
lock.unlock()
|
|
407
|
+
stale?.cancel()
|
|
408
|
+
return t
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
func attachTimer(_ task: Task<Void, Never>, token: UInt64) {
|
|
412
|
+
lock.lock()
|
|
413
|
+
let current = self.token == token && cont != nil
|
|
414
|
+
if current {
|
|
415
|
+
timer = task
|
|
416
|
+
}
|
|
417
|
+
lock.unlock()
|
|
418
|
+
if !current {
|
|
419
|
+
task.cancel()
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
func timeout(token: UInt64) {
|
|
424
|
+
lock.lock()
|
|
425
|
+
guard self.token == token, let c = cont else {
|
|
426
|
+
lock.unlock()
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
clearLocked(reason: "timeout")
|
|
430
|
+
lock.unlock()
|
|
431
|
+
c.resume(returning: false)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private func fail(reason: String) {
|
|
435
|
+
lock.lock()
|
|
436
|
+
guard let c = cont else {
|
|
437
|
+
lock.unlock()
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
let t = timer
|
|
441
|
+
clearLocked(reason: reason)
|
|
442
|
+
lock.unlock()
|
|
443
|
+
t?.cancel()
|
|
444
|
+
c.resume(returning: false)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// Feed one reply. Returns true when it resolved the in-flight message.
|
|
448
|
+
@discardableResult
|
|
449
|
+
func resolve(_ ack: G2CfwTransport.Ack) -> Bool {
|
|
450
|
+
lock.lock()
|
|
451
|
+
guard let armed = streamId, armed == ack.current.streamId, ack.current.ordinal == 0, let c = cont else {
|
|
452
|
+
lock.unlock()
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
if !ack.success {
|
|
456
|
+
let t = timer
|
|
457
|
+
clearLocked(reason: "nack")
|
|
458
|
+
lock.unlock()
|
|
459
|
+
t?.cancel()
|
|
460
|
+
c.resume(returning: false)
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
ackedLenses |= ack.lens & expectedLenses
|
|
464
|
+
guard ackedLenses == expectedLenses else {
|
|
465
|
+
lock.unlock()
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
let t = timer
|
|
469
|
+
lastSuccessMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
470
|
+
clearLocked(reason: "ack")
|
|
471
|
+
lock.unlock()
|
|
472
|
+
t?.cancel()
|
|
473
|
+
c.resume(returning: true)
|
|
474
|
+
return true
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private func clearLocked(reason: String) {
|
|
478
|
+
streamId = nil
|
|
479
|
+
expectedLenses = 0
|
|
480
|
+
ackedLenses = 0
|
|
481
|
+
cont = nil
|
|
482
|
+
timer = nil
|
|
483
|
+
lastOutcome = (ProcessInfo.processInfo.systemUptime, reason)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Bounded, main-actor counters. No payload text, per-packet logging, or sample arrays.
|
|
4
|
+
/// Durations use uptime; wall time is only for correlating exported reports.
|
|
5
|
+
/// Disabled by default; opt in explicitly when investigating display performance.
|
|
6
|
+
/// Lines stay small: a sample is logged only for a window with display work, a
|
|
7
|
+
/// backlog or a late tick; zero, false and empty values and sub-millisecond
|
|
8
|
+
/// timings are left out; the link's fixed facts go in a separate session line.
|
|
9
|
+
@MainActor
|
|
10
|
+
final class G2DisplayDiagnostics {
|
|
11
|
+
/// Link upkeep that runs whether or not anything is drawn: IMU reports,
|
|
12
|
+
/// touch debouncing and heartbeat writes alone leave a window idle.
|
|
13
|
+
private static let ambientCounters: Set<String> = [
|
|
14
|
+
"imuReceived", "imuSamples", "touchDebounced", "txBytes", "txPackets", "readyCallbacksL", "readyCallbacksR",
|
|
15
|
+
]
|
|
16
|
+
private static let ambientTimings: Set<String> = ["notifyDispatchMs", "creditWaitMsL", "creditWaitMsR", "writeQueueMs"]
|
|
17
|
+
/// A tick this late means the main actor or the process was held up: logged even when idle.
|
|
18
|
+
static let stretchedWindow: TimeInterval = 2
|
|
19
|
+
/// The session line is repeated this often so a trimmed log excerpt still has it.
|
|
20
|
+
static let sessionRepeat: TimeInterval = 60
|
|
21
|
+
|
|
22
|
+
nonisolated static var now: TimeInterval {
|
|
23
|
+
ProcessInfo.processInfo.systemUptime
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
struct Timing {
|
|
27
|
+
var count = 0
|
|
28
|
+
var total = 0.0
|
|
29
|
+
var maximum = 0.0
|
|
30
|
+
mutating func add(_ ms: Double) {
|
|
31
|
+
count += 1
|
|
32
|
+
total += max(0, ms)
|
|
33
|
+
maximum = max(maximum, ms)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@MainActor final class Fragment {
|
|
38
|
+
let queuedAt = G2DisplayDiagnostics.now
|
|
39
|
+
var firstWrite: TimeInterval?
|
|
40
|
+
var lastWrite: TimeInterval?
|
|
41
|
+
func wrote() {
|
|
42
|
+
let now = G2DisplayDiagnostics.now
|
|
43
|
+
if firstWrite == nil {
|
|
44
|
+
firstWrite = now
|
|
45
|
+
}
|
|
46
|
+
lastWrite = now
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private let enabled: Bool
|
|
51
|
+
private var started: TimeInterval
|
|
52
|
+
private var counters: [String: Int] = [:]
|
|
53
|
+
private var timings: [String: Timing] = [:]
|
|
54
|
+
/// Short: it is on every line, and only has to tell apart the runs in one log.
|
|
55
|
+
private let run = String(UUID().uuidString.prefix(8)).lowercased()
|
|
56
|
+
private var sequence = 0
|
|
57
|
+
private var lastStall: TimeInterval = -.infinity
|
|
58
|
+
private var sessionKey: String?
|
|
59
|
+
private var lastSession: TimeInterval = -.infinity
|
|
60
|
+
|
|
61
|
+
init(enabled: Bool = false, now: TimeInterval = G2DisplayDiagnostics.now) {
|
|
62
|
+
self.enabled = enabled
|
|
63
|
+
started = now
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var isSampleDue: Bool {
|
|
67
|
+
enabled && Self.now - started >= 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func count(_ name: String, _ amount: Int = 1) {
|
|
71
|
+
guard enabled else { return }
|
|
72
|
+
counters[name, default: 0] += amount
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func timing(_ name: String, _ milliseconds: Double) {
|
|
76
|
+
guard enabled else { return }
|
|
77
|
+
timings[name, default: Timing()].add(milliseconds)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func event(_ type: String, fields: [String: Any]) -> String {
|
|
81
|
+
sequence += 1
|
|
82
|
+
var record = fields
|
|
83
|
+
record["v"] = 1
|
|
84
|
+
record["run"] = run
|
|
85
|
+
record["id"] = sequence
|
|
86
|
+
record["t"] = Int64(Date().timeIntervalSince1970 * 1000)
|
|
87
|
+
record["type"] = type
|
|
88
|
+
let data = try? JSONSerialization.data(withJSONObject: record, options: [.sortedKeys])
|
|
89
|
+
return "G2PERF " + (data.flatMap { String(data: $0, encoding: .utf8) } ?? "{}")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// At most one detail line/second; every failure still enters the summary counters.
|
|
93
|
+
func stall(fields: [String: Any], now: TimeInterval = G2DisplayDiagnostics.now) -> String? {
|
|
94
|
+
guard enabled, now - lastStall >= 1 else { return nil }
|
|
95
|
+
lastStall = now
|
|
96
|
+
return event("stall", fields: fields)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// The link's fixed facts (firmware, MTUs, frame cap): logged when they
|
|
100
|
+
/// change and once a minute, instead of on every sample.
|
|
101
|
+
func session(_ fields: [String: Any], now: TimeInterval = G2DisplayDiagnostics.now) -> String? {
|
|
102
|
+
guard enabled else { return nil }
|
|
103
|
+
let key = fields.sorted { $0.key < $1.key }.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
|
|
104
|
+
guard key != sessionKey || now - lastSession >= Self.sessionRepeat else { return nil }
|
|
105
|
+
sessionKey = key
|
|
106
|
+
lastSession = now
|
|
107
|
+
return event("session", fields: fields)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// The window's summary, or nil when it is not due or was idle. `busy`
|
|
111
|
+
/// means something is queued for the glasses, which is never idle.
|
|
112
|
+
func sample(context: [String: Any], busy: Bool = false, now: TimeInterval = G2DisplayDiagnostics.now) -> String? {
|
|
113
|
+
guard enabled else { return nil }
|
|
114
|
+
let elapsed = now - started
|
|
115
|
+
guard elapsed >= 1 else { return nil }
|
|
116
|
+
started = now
|
|
117
|
+
let counters = counters
|
|
118
|
+
let timings = timings
|
|
119
|
+
self.counters.removeAll(keepingCapacity: true)
|
|
120
|
+
self.timings.removeAll(keepingCapacity: true)
|
|
121
|
+
let active = busy || elapsed >= Self.stretchedWindow
|
|
122
|
+
|| counters.keys.contains { !Self.ambientCounters.contains($0) }
|
|
123
|
+
|| timings.keys.contains { !Self.ambientTimings.contains($0) }
|
|
124
|
+
guard active else { return nil }
|
|
125
|
+
var fields = context
|
|
126
|
+
fields["windowMs"] = Int((elapsed * 1000).rounded())
|
|
127
|
+
for (key, value) in counters {
|
|
128
|
+
fields[key] = value
|
|
129
|
+
}
|
|
130
|
+
// A timing that never reached a millisecond shows no delay: left out.
|
|
131
|
+
for (key, value) in timings where value.maximum >= 1 {
|
|
132
|
+
fields[key + "Avg"] = Self.tenths(value.total / Double(value.count))
|
|
133
|
+
fields[key + "Max"] = Self.tenths(value.maximum)
|
|
134
|
+
}
|
|
135
|
+
return event("sample", fields: fields.filter { !Self.isEmpty($0.value) })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// One decimal that serializes as written: a Double 7.2 prints as 7.2000000000000002.
|
|
139
|
+
private static func tenths(_ value: Double) -> NSDecimalNumber {
|
|
140
|
+
let scaled = Int((value * 10).rounded())
|
|
141
|
+
return NSDecimalNumber(mantissa: UInt64(abs(scaled)), exponent: -1, isNegative: scaled < 0)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static func isEmpty(_ value: Any) -> Bool {
|
|
145
|
+
switch value {
|
|
146
|
+
case let value as NSDecimalNumber: return value == .zero
|
|
147
|
+
case let value as Bool: return !value
|
|
148
|
+
case let value as Int: return value == 0
|
|
149
|
+
case let value as Double: return value == 0
|
|
150
|
+
case let value as String: return value.isEmpty
|
|
151
|
+
default: return false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -288,8 +288,8 @@ enum G2FlashProtocol {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
static let goldenStock = GoldenVector(
|
|
291
|
-
sha256: "
|
|
292
|
-
ps:
|
|
291
|
+
sha256: "187ccf2bcc5c17a212106e8a376745511e8289c4232b634a7ea94b9bf25a0979",
|
|
292
|
+
ps: 3_758_720, progEnd: 0x007C_DA60, pass: true
|
|
293
293
|
)
|
|
294
294
|
|
|
295
295
|
/// Run the parse+guard on `img` and assert it reproduces the golden vector. Returns
|
|
@@ -27,18 +27,18 @@ enum G2FirmwareCatalog {
|
|
|
27
27
|
G2FirmwareArtifact(
|
|
28
28
|
id: "stock",
|
|
29
29
|
displayName: "Stock firmware",
|
|
30
|
-
version: "2.
|
|
31
|
-
fileName: "g2-stock-2.
|
|
32
|
-
sha256: "
|
|
33
|
-
size:
|
|
30
|
+
version: "2.3.0.24",
|
|
31
|
+
fileName: "g2-stock-2.3.0.24.evenota.bin",
|
|
32
|
+
sha256: "187ccf2bcc5c17a212106e8a376745511e8289c4232b634a7ea94b9bf25a0979",
|
|
33
|
+
size: 4_537_963
|
|
34
34
|
),
|
|
35
35
|
G2FirmwareArtifact(
|
|
36
36
|
id: "glassly-cfw",
|
|
37
|
-
displayName: "Glassly CFW",
|
|
38
|
-
version: "2.
|
|
39
|
-
fileName: "g2-glassly-cfw-2.
|
|
40
|
-
sha256: "
|
|
41
|
-
size:
|
|
37
|
+
displayName: "Glassly CFW v39",
|
|
38
|
+
version: "2.3.0.24",
|
|
39
|
+
fileName: "g2-glassly-cfw-2.3.0.24.evenota.bin",
|
|
40
|
+
sha256: "4c7bf9a552df1a42f26b3378425c7c2121ca4058fed7283ec863c3f8d2cd13fe",
|
|
41
|
+
size: 4_661_963
|
|
42
42
|
),
|
|
43
43
|
]
|
|
44
44
|
|