@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,290 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Bounded HID report-descriptor decoder for the G2 keyboard bridge. HOGP
|
|
4
|
+
/// passes the Report ID out of band (Report Reference descriptor), so a
|
|
5
|
+
/// characteristic value is decoded as-is; byte zero is never stripped the way
|
|
6
|
+
/// USB HID would. Ported from g2flash `demos/keyboard-hid.ts`; the descriptor
|
|
7
|
+
/// limits match the firmware's 512-byte Report Map cap.
|
|
8
|
+
enum G2KeyboardHid {
|
|
9
|
+
struct Usage: Hashable {
|
|
10
|
+
let page: Int
|
|
11
|
+
let usage: Int
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct KeyEvent: Equatable {
|
|
15
|
+
let page: Int
|
|
16
|
+
let usage: Int
|
|
17
|
+
let down: Bool
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct Field {
|
|
21
|
+
let bit: Int
|
|
22
|
+
let size: Int
|
|
23
|
+
let count: Int
|
|
24
|
+
let variable: Bool
|
|
25
|
+
let min: Int
|
|
26
|
+
let max: Int
|
|
27
|
+
let usages: [Usage]
|
|
28
|
+
let range: (Usage, Usage)?
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
struct Report {
|
|
32
|
+
let id: Int
|
|
33
|
+
var bits: Int
|
|
34
|
+
var fields: [Field]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
struct DescriptorError: Error, CustomStringConvertible {
|
|
38
|
+
let reason: String
|
|
39
|
+
var description: String {
|
|
40
|
+
"Unsupported or malformed HID descriptor: \(reason)"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
struct ReportError: Error, CustomStringConvertible {
|
|
45
|
+
let reason: String
|
|
46
|
+
var description: String {
|
|
47
|
+
reason
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private struct Global {
|
|
52
|
+
var page = 0, min = 0, max = 0, size = 0, count = 0, id = 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Parse a Report Map into its input reports keyed by Report ID (0 when the
|
|
56
|
+
/// map has no Report ID items). Rejects long items, usage delimiters, mixed
|
|
57
|
+
/// numbered/unnumbered reports and oversized dimensions.
|
|
58
|
+
static func parseReportMap(_ bytes: [UInt8]) throws -> [Int: Report] {
|
|
59
|
+
func fail(_ reason: String) -> DescriptorError {
|
|
60
|
+
DescriptorError(reason: reason)
|
|
61
|
+
}
|
|
62
|
+
guard !bytes.isEmpty, bytes.count <= 512 else { throw fail("report map must contain 1..512 bytes") }
|
|
63
|
+
var reports: [Int: Report] = [:]
|
|
64
|
+
var order: [Int] = []
|
|
65
|
+
var g = Global()
|
|
66
|
+
var stack: [Global] = []
|
|
67
|
+
var usages: [Usage] = []
|
|
68
|
+
var lo: Usage?
|
|
69
|
+
var hi: Usage?
|
|
70
|
+
var depth = 0
|
|
71
|
+
var p = 0
|
|
72
|
+
while p < bytes.count {
|
|
73
|
+
let prefix = Int(bytes[p])
|
|
74
|
+
p += 1
|
|
75
|
+
if prefix == 0xFE {
|
|
76
|
+
throw fail("long items")
|
|
77
|
+
}
|
|
78
|
+
let n = [0, 1, 2, 4][prefix & 3]
|
|
79
|
+
guard p + n <= bytes.count else { throw fail("truncated item") }
|
|
80
|
+
var v = 0
|
|
81
|
+
for j in 0 ..< n {
|
|
82
|
+
v += Int(bytes[p + j]) << (8 * j)
|
|
83
|
+
}
|
|
84
|
+
p += n
|
|
85
|
+
let signed = n > 0 && v >= 1 << (n * 8 - 1) ? v - (1 << (n * 8)) : v
|
|
86
|
+
let type = (prefix >> 2) & 3
|
|
87
|
+
let tag = prefix >> 4
|
|
88
|
+
func usage(_ v: Int, _ n: Int) -> Usage {
|
|
89
|
+
n == 4 ? Usage(page: v >> 16, usage: v & 0xFFFF) : Usage(page: g.page, usage: v)
|
|
90
|
+
}
|
|
91
|
+
switch type {
|
|
92
|
+
case 1:
|
|
93
|
+
switch tag {
|
|
94
|
+
case 0: g.page = v
|
|
95
|
+
case 1: g.min = signed
|
|
96
|
+
case 2: g.max = g.min < 0 ? signed : v
|
|
97
|
+
case 7: g.size = v
|
|
98
|
+
case 8:
|
|
99
|
+
if v == 0 || v > 255 {
|
|
100
|
+
throw fail("Report ID")
|
|
101
|
+
}
|
|
102
|
+
g.id = v
|
|
103
|
+
case 9: g.count = v
|
|
104
|
+
case 10:
|
|
105
|
+
if stack.count >= 8 {
|
|
106
|
+
throw fail("global stack overflow")
|
|
107
|
+
}
|
|
108
|
+
stack.append(g)
|
|
109
|
+
case 11:
|
|
110
|
+
guard let popped = stack.popLast() else { throw fail("global stack underflow") }
|
|
111
|
+
g = popped
|
|
112
|
+
case 3, 4, 5, 6: break // physical range, unit/exponent
|
|
113
|
+
default: throw fail("unknown global item")
|
|
114
|
+
}
|
|
115
|
+
case 2:
|
|
116
|
+
if tag == 0 {
|
|
117
|
+
if usages.count >= 512 {
|
|
118
|
+
throw fail("too many usages")
|
|
119
|
+
}
|
|
120
|
+
usages.append(usage(v, n))
|
|
121
|
+
} else if tag == 1 {
|
|
122
|
+
lo = usage(v, n)
|
|
123
|
+
} else if tag == 2 {
|
|
124
|
+
hi = usage(v, n)
|
|
125
|
+
} else if tag == 10 {
|
|
126
|
+
throw fail("usage delimiters")
|
|
127
|
+
} else if tag > 9 {
|
|
128
|
+
throw fail("unknown local item")
|
|
129
|
+
}
|
|
130
|
+
case 0:
|
|
131
|
+
if tag == 10 {
|
|
132
|
+
depth += 1
|
|
133
|
+
if depth > 32 {
|
|
134
|
+
throw fail("collection depth")
|
|
135
|
+
}
|
|
136
|
+
} else if tag == 12 {
|
|
137
|
+
depth -= 1
|
|
138
|
+
if depth < 0 {
|
|
139
|
+
throw fail("collection underflow")
|
|
140
|
+
}
|
|
141
|
+
} else if tag == 8 {
|
|
142
|
+
if g.size == 0 || g.size > 32 || g.count == 0 || g.count > 512 {
|
|
143
|
+
throw fail("field dimensions")
|
|
144
|
+
}
|
|
145
|
+
if reports[g.id] == nil {
|
|
146
|
+
if reports.count >= 16 {
|
|
147
|
+
throw fail("too many input reports")
|
|
148
|
+
}
|
|
149
|
+
reports[g.id] = Report(id: g.id, bits: 0, fields: [])
|
|
150
|
+
order.append(g.id)
|
|
151
|
+
}
|
|
152
|
+
var r = reports[g.id]!
|
|
153
|
+
var range: (Usage, Usage)?
|
|
154
|
+
if lo != nil || hi != nil {
|
|
155
|
+
guard let lo, let hi, lo.page == hi.page, hi.usage >= lo.usage else { throw fail("usage range") }
|
|
156
|
+
range = (lo, hi)
|
|
157
|
+
}
|
|
158
|
+
let field = Field(
|
|
159
|
+
bit: r.bits, size: g.size, count: g.count, variable: v & 2 != 0,
|
|
160
|
+
min: g.min, max: g.max, usages: usages, range: range
|
|
161
|
+
)
|
|
162
|
+
r.bits += g.size * g.count
|
|
163
|
+
if r.bits > 4096 {
|
|
164
|
+
throw fail("report exceeds 512 bytes")
|
|
165
|
+
}
|
|
166
|
+
// Constant padding and relative pointer axes are not keys.
|
|
167
|
+
if v & 1 == 0, v & 4 == 0 {
|
|
168
|
+
r.fields.append(field)
|
|
169
|
+
}
|
|
170
|
+
reports[g.id] = r
|
|
171
|
+
} else if tag != 9, tag != 11 {
|
|
172
|
+
throw fail("unknown main item")
|
|
173
|
+
}
|
|
174
|
+
usages = []
|
|
175
|
+
lo = nil
|
|
176
|
+
hi = nil
|
|
177
|
+
default:
|
|
178
|
+
throw fail("reserved item type")
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if depth != 0 || !stack.isEmpty {
|
|
182
|
+
throw fail("unbalanced collection/global stack")
|
|
183
|
+
}
|
|
184
|
+
if reports[0] != nil, reports.count > 1 {
|
|
185
|
+
throw fail("mixed numbered and unnumbered input reports")
|
|
186
|
+
}
|
|
187
|
+
if reports.isEmpty {
|
|
188
|
+
throw fail("no input reports")
|
|
189
|
+
}
|
|
190
|
+
return reports
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private static func usageAt(_ f: Field, _ index: Int) -> Usage? {
|
|
194
|
+
if index < 0 {
|
|
195
|
+
return nil
|
|
196
|
+
}
|
|
197
|
+
if index < f.usages.count {
|
|
198
|
+
return f.usages[index]
|
|
199
|
+
}
|
|
200
|
+
let rangeIndex = index - f.usages.count
|
|
201
|
+
if let range = f.range, rangeIndex <= range.1.usage - range.0.usage {
|
|
202
|
+
return Usage(page: range.0.page, usage: range.0.usage + rangeIndex)
|
|
203
|
+
}
|
|
204
|
+
// Variable items reuse their last usage when Report Count exceeds usages.
|
|
205
|
+
if f.variable {
|
|
206
|
+
return f.range?.1 ?? f.usages.last
|
|
207
|
+
}
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Keyboard (page 7) and consumer (page 12) usages currently asserted in
|
|
212
|
+
/// one input report value. Throws on a length mismatch or a rollover report.
|
|
213
|
+
static func reportKeys(_ report: Report, _ data: [UInt8]) throws -> [Usage] {
|
|
214
|
+
guard data.count == (report.bits + 7) / 8 else {
|
|
215
|
+
throw ReportError(reason: "HID input length does not match Report Map")
|
|
216
|
+
}
|
|
217
|
+
var keys: [Usage] = []
|
|
218
|
+
var seen = Set<Usage>()
|
|
219
|
+
for f in report.fields {
|
|
220
|
+
for i in 0 ..< f.count {
|
|
221
|
+
var value = 0
|
|
222
|
+
for j in 0 ..< f.size {
|
|
223
|
+
let bit = f.bit + i * f.size + j
|
|
224
|
+
if data[bit >> 3] & UInt8(1 << (bit & 7)) != 0 {
|
|
225
|
+
value += 1 << j
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if f.min < 0, value >= 1 << (f.size - 1) {
|
|
229
|
+
value -= 1 << f.size
|
|
230
|
+
}
|
|
231
|
+
if value < f.min || value > f.max {
|
|
232
|
+
continue
|
|
233
|
+
} // null state
|
|
234
|
+
guard let u = usageAt(f, f.variable ? i : value - f.min) else { continue }
|
|
235
|
+
if f.variable, value == 0 {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
if u.usage == 0 {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if u.page == 7, u.usage <= 3 {
|
|
242
|
+
throw ReportError(reason: "HID keyboard rollover/error report")
|
|
243
|
+
}
|
|
244
|
+
if u.page == 7 || u.page == 12, !seen.contains(u) {
|
|
245
|
+
seen.insert(u)
|
|
246
|
+
keys.append(u)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return keys
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Key ownership is per characteristic, then unioned: releasing one report
|
|
254
|
+
/// cannot release a modifier still held by another report.
|
|
255
|
+
struct HeldKeys {
|
|
256
|
+
private var reports: [UInt16: [Usage]] = [:]
|
|
257
|
+
private var held: [Usage] = []
|
|
258
|
+
|
|
259
|
+
mutating func update(handle: UInt16, report: Report, data: [UInt8]) throws -> [KeyEvent] {
|
|
260
|
+
reports[handle] = try reportKeys(report, data)
|
|
261
|
+
return diff()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mutating func reset() -> [KeyEvent] {
|
|
265
|
+
reports.removeAll()
|
|
266
|
+
return diff()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private mutating func diff() -> [KeyEvent] {
|
|
270
|
+
var next: [Usage] = []
|
|
271
|
+
var nextSet = Set<Usage>()
|
|
272
|
+
for handle in reports.keys.sorted() {
|
|
273
|
+
for u in reports[handle]! where !nextSet.contains(u) {
|
|
274
|
+
nextSet.insert(u)
|
|
275
|
+
next.append(u)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let heldSet = Set(held)
|
|
279
|
+
var events: [KeyEvent] = []
|
|
280
|
+
for u in held where !nextSet.contains(u) {
|
|
281
|
+
events.append(KeyEvent(page: u.page, usage: u.usage, down: false))
|
|
282
|
+
}
|
|
283
|
+
for u in next where !heldSet.contains(u) {
|
|
284
|
+
events.append(KeyEvent(page: u.page, usage: u.usage, down: true))
|
|
285
|
+
}
|
|
286
|
+
held = next
|
|
287
|
+
return events
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Config values snapped to what the G2 notification firmware accepts.
|
|
4
|
+
enum G2NotificationTuning {
|
|
5
|
+
/// The firmware takes 0 (its own choice) or one of three popup durations.
|
|
6
|
+
static let popupSecondsChoices: [Int] = [5, 7, 10]
|
|
7
|
+
|
|
8
|
+
static func snapPopupSeconds(_ seconds: Int) -> Int32 {
|
|
9
|
+
guard seconds > 0 else { return 0 }
|
|
10
|
+
// Ties round up: distance compared first, then the larger choice wins.
|
|
11
|
+
let snapped = popupSecondsChoices.min { lhs, rhs in
|
|
12
|
+
let dl = abs(lhs - seconds)
|
|
13
|
+
let dr = abs(rhs - seconds)
|
|
14
|
+
return dl == dr ? lhs > rhs : dl < dr
|
|
15
|
+
}
|
|
16
|
+
return Int32(snapped ?? popupSecondsChoices[0])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// The firmware filter is an allowlist; with nothing blocked it stays off so
|
|
20
|
+
/// an incomplete allowlist can never drop a notification.
|
|
21
|
+
static func filterEnabled(for config: GlassesNotificationConfig) -> Bool {
|
|
22
|
+
!config.blockedApps.isEmpty
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Settings field 106, sent as its own notification after the device-info poll by GLASSLYCFW/24 and later.
|
|
4
|
+
/// This is the R1 connected to the glasses, independent of the phone controller.
|
|
5
|
+
struct G2RingBattery: Equatable {
|
|
6
|
+
let connected: Bool
|
|
7
|
+
let level: Int
|
|
8
|
+
let charging: Bool
|
|
9
|
+
|
|
10
|
+
static let disconnected = G2RingBattery(connected: false, level: -1, charging: false)
|
|
11
|
+
|
|
12
|
+
static func decode(_ data: Data) -> G2RingBattery? {
|
|
13
|
+
let bytes = Array(data)
|
|
14
|
+
guard bytes.count == 5, bytes[0] == 0x52, bytes[1] == 0x42, bytes[2] == 1 else { return nil }
|
|
15
|
+
guard bytes[3] & 1 != 0 else { return .disconnected }
|
|
16
|
+
guard bytes[3] & 2 != 0 else {
|
|
17
|
+
return G2RingBattery(connected: true, level: -1, charging: false)
|
|
18
|
+
}
|
|
19
|
+
guard bytes[4] <= 100 else { return nil }
|
|
20
|
+
return G2RingBattery(connected: true, level: Int(bytes[4]), charging: bytes[3] & 4 != 0)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Upstream mode 16 / settings field 105 and mode 10 / navigation field 100.
|
|
4
|
+
/// Validate the report format independently of the advertised CFW revision.
|
|
5
|
+
enum G2Sensors {
|
|
6
|
+
static func heading(_ fields: [Int: Any]) -> Int? {
|
|
7
|
+
guard let raw = fields[1] else { return 0 }
|
|
8
|
+
guard let heading = raw as? Int32, (0 ... 359).contains(heading) else { return nil }
|
|
9
|
+
return Int(heading)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static func command(sensor: String, operation: String, options: [String: Double]) -> Data? {
|
|
13
|
+
func number(_ key: String, _ fallback: Int, _ range: ClosedRange<Int>) -> Int? {
|
|
14
|
+
guard let value = options[key] else { return fallback }
|
|
15
|
+
guard value.isFinite, value.rounded() == value,
|
|
16
|
+
value >= Double(range.lowerBound), value <= Double(range.upperBound) else { return nil }
|
|
17
|
+
return Int(value)
|
|
18
|
+
}
|
|
19
|
+
func u16(_ n: Int) -> [UInt8] {
|
|
20
|
+
[UInt8(n & 255), UInt8(n >> 8)]
|
|
21
|
+
}
|
|
22
|
+
if sensor == "light" {
|
|
23
|
+
if operation == "query" {
|
|
24
|
+
return Data([16, 0])
|
|
25
|
+
}
|
|
26
|
+
if operation == "stop" {
|
|
27
|
+
return Data([16, 2])
|
|
28
|
+
}
|
|
29
|
+
guard operation == "start", let interval = number("intervalMs", 500, 100 ... 5000),
|
|
30
|
+
let delta = number("minDelta", 1, 0 ... 65535),
|
|
31
|
+
let heartbeat = number("heartbeatMs", 5000, 0 ... 65535) else { return nil }
|
|
32
|
+
return Data([16, 1, 1] + u16(interval) + u16(delta) + u16(heartbeat))
|
|
33
|
+
}
|
|
34
|
+
if sensor == "compass" {
|
|
35
|
+
if operation == "stop" {
|
|
36
|
+
return Data([10, 0])
|
|
37
|
+
}
|
|
38
|
+
guard operation == "start", let interval = number("intervalMs", 1000, 50 ... 2000),
|
|
39
|
+
let change = number("minChangeDegrees", 5, 0 ... 359) else { return nil }
|
|
40
|
+
return Data([10, 2] + u16(interval) + u16(change))
|
|
41
|
+
}
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static func uint(_ b: [UInt8], _ offset: Int, _ count: Int) -> Int64 {
|
|
46
|
+
(0 ..< count).reduce(Int64(0)) { $0 | (Int64(b[offset + $1]) << ($1 * 8)) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func light(_ data: Data) -> [String: Any]? {
|
|
50
|
+
let b = Array(data)
|
|
51
|
+
guard b.count == 24, b[0] == 65, b[1] == 76, b[2] == 1, b[3] <= 3 else { return nil }
|
|
52
|
+
return [
|
|
53
|
+
"reason": ["query", "started", "poll", "stopped"][Int(b[3])],
|
|
54
|
+
"opened": b[4] & 1 != 0, "passive": b[4] & 2 != 0,
|
|
55
|
+
"autoBrightness": b[4] & 4 != 0, "readSucceeded": b[4] & 8 != 0,
|
|
56
|
+
"status": Int(b[5]), "value": uint(b, 6, 4), "peak": uint(b, 10, 4),
|
|
57
|
+
"targetLevel": uint(b, 14, 2), "brightness": Int(b[16]), "gear": Int(b[17]),
|
|
58
|
+
"scaleQ10": uint(b, 18, 2), "sampleTimestampMs": uint(b, 20, 4),
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static func compass(_ data: Data) -> [String: Any]? {
|
|
63
|
+
let b = Array(data)
|
|
64
|
+
guard b.count == 12, b[0] == 67, b[1] == 77, b[2] == 1,
|
|
65
|
+
b[3] <= 3 || b[3] == 255, b[4] <= 2 || b[4] == 255, b[5] <= 3 else { return nil }
|
|
66
|
+
return [
|
|
67
|
+
"accuracy": b[3] == 255 ? NSNull() : Int(b[3]) as Any,
|
|
68
|
+
"anomalies": b[4] == 255 ? NSNull() : Int(b[4]) as Any,
|
|
69
|
+
"source": ["unknown", "grv", "gmrv", "rv"][Int(b[5])],
|
|
70
|
+
"sampleMatched": b[6] & 128 != 0, "grvValid": b[6] & 1 != 0,
|
|
71
|
+
"gmrvValid": b[6] & 2 != 0, "rvValid": b[6] & 4 != 0,
|
|
72
|
+
"magneticDataFresh": b[6] & 8 != 0, "sampleTimestampMs": uint(b, 8, 4),
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Capture before the BLE callback hops to the main queue. Processing time is
|
|
4
|
+
/// not input time: several seconds of queued input can drain in milliseconds.
|
|
5
|
+
struct G2InputArrival {
|
|
6
|
+
let uptime: TimeInterval
|
|
7
|
+
let timestamp: Int64
|
|
8
|
+
|
|
9
|
+
init(uptime: TimeInterval = ProcessInfo.processInfo.systemUptime,
|
|
10
|
+
timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000))
|
|
11
|
+
{
|
|
12
|
+
self.uptime = uptime
|
|
13
|
+
self.timestamp = timestamp
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// CFW's field-100 extension identifies raw R1 reports forwarded once through
|
|
18
|
+
/// the right lens. Its tick is a ring clock, not a phone timestamp.
|
|
19
|
+
struct G2RingInput {
|
|
20
|
+
let wireType: UInt8
|
|
21
|
+
let tick: UInt32
|
|
22
|
+
|
|
23
|
+
init?(fields: [Int: Any]) {
|
|
24
|
+
guard fields[2] as? Int32 == 2, let data = fields[100] as? Data else { return nil }
|
|
25
|
+
let bytes = [UInt8](data)
|
|
26
|
+
guard bytes.count == 12, bytes[0] == 0x52, bytes[1] == 0x49,
|
|
27
|
+
bytes[2] == 1, bytes[3] == 1, bytes[7] == 0 else { return nil }
|
|
28
|
+
let events: [UInt8: Int32] = [0: 9, 1: 0, 2: 3, 4: 1, 5: 2, 8: 10, 9: 11, 10: 14]
|
|
29
|
+
guard fields[1] as? Int32 == events[bytes[4], default: 127] else { return nil }
|
|
30
|
+
wireType = bytes[4]
|
|
31
|
+
tick = UInt32(bytes[8]) | UInt32(bytes[9]) << 8 | UInt32(bytes[10]) << 16 | UInt32(bytes[11]) << 24
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
struct G2TouchDebounce {
|
|
36
|
+
private var lastAcceptedAt: TimeInterval?
|
|
37
|
+
|
|
38
|
+
mutating func accepts(event: Int32?, ring: G2RingInput?, receivedAt: TimeInterval) -> Bool {
|
|
39
|
+
// The raw ring stream deliberately bypasses stock suppression. Do not
|
|
40
|
+
// reinstate it on the phone or discard click/swipe completions after a
|
|
41
|
+
// touch_down. Edges and unknown markers never consume a debounce slot.
|
|
42
|
+
if ring != nil || event == 9 || event == 10 || event == 14 || event == 127 {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
if let lastAcceptedAt, receivedAt - lastAcceptedAt < 0.1 {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
lastAcceptedAt = receivedAt
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Background CFW traffic uses the original G2 timed writer. Reports show
|
|
4
|
+
/// readiness remaining false for seconds even while gestures and timers run.
|
|
5
|
+
/// Keep foreground throughput without letting that gate freeze background UI.
|
|
6
|
+
enum G2WritePacing: Equatable {
|
|
7
|
+
case timed, creditBurst
|
|
8
|
+
|
|
9
|
+
static let packetDelayNs: UInt64 = 6_000_000
|
|
10
|
+
|
|
11
|
+
init(cfwEnabled: Bool, applicationState: UIApplication.State) {
|
|
12
|
+
self = cfwEnabled && applicationState == .active ? .creditBurst : .timed
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func canWrite(peripheralReady: Bool) -> Bool {
|
|
16
|
+
self == .timed || peripheralReady
|
|
17
|
+
}
|
|
18
|
+
}
|