@mentra/bluetooth-sdk 3.1.0-dev.96 → 3.1.0
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 +46 -13
- package/android/build.gradle +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +89 -90
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHost.kt +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueue.kt +111 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +139 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransport.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +6 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +11 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2SerialResolution.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHostTest.kt +64 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueueTest.kt +86 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +148 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransportTest.kt +44 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2SerialResolutionTest.kt +25 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +8 -0
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +2 -2
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/OtaManifest.swift +6 -5
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +127 -76
- package/ios/Source/internal/BluetoothSdkAnalyticsHost.swift +91 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsQueue.swift +117 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +157 -0
- package/ios/Source/internal/BluetoothSdkAnalyticsTransport.swift +13 -0
- package/ios/Source/sgcs/G2.swift +375 -360
- package/ios/Source/sgcs/G2SerialResolution.swift +23 -0
- package/ios/Tests/BluetoothSdkAnalyticsHostTests.swift +63 -0
- package/ios/Tests/BluetoothSdkAnalyticsQueueTests.swift +97 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +122 -0
- package/ios/Tests/BluetoothSdkAnalyticsTransportTests.swift +36 -0
- package/ios/Tests/G2SerialResolutionTests.swift +19 -0
- package/ios/Tests/OtaManifestDowngradeTests.swift +20 -0
- package/package.json +1 -1
- package/plugin/build/analyticsProps.d.ts +9 -0
- package/plugin/build/analyticsProps.js +34 -0
- package/plugin/build/index.d.ts +8 -0
- package/plugin/build/withAndroid.js +12 -18
- package/plugin/build/withIos.d.ts +1 -0
- package/plugin/build/withIos.js +8 -18
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -21,18 +21,18 @@ private let g2ImuMaxControlAttempts = 3
|
|
|
21
21
|
|
|
22
22
|
// MARK: - Data Little-Endian Helpers (for BMP construction)
|
|
23
23
|
|
|
24
|
-
extension Data {
|
|
25
|
-
|
|
24
|
+
private extension Data {
|
|
25
|
+
mutating func appendLittleEndian(_ value: UInt16) {
|
|
26
26
|
var v = value.littleEndian
|
|
27
27
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
mutating func appendLittleEndian(_ value: UInt32) {
|
|
31
31
|
var v = value.littleEndian
|
|
32
32
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
mutating func appendLittleEndian(_ value: Int32) {
|
|
36
36
|
var v = value.littleEndian
|
|
37
37
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
38
38
|
}
|
|
@@ -59,47 +59,47 @@ private enum G2BLE {
|
|
|
59
59
|
|
|
60
60
|
/// Service IDs from service_id_def.proto
|
|
61
61
|
private enum ServiceID: UInt8 {
|
|
62
|
-
case dashboard = 1
|
|
63
|
-
case menu = 3
|
|
64
|
-
case notification = 4
|
|
65
|
-
case evenAI = 7
|
|
66
|
-
case navigation = 8
|
|
67
|
-
case g2Setting = 9
|
|
68
|
-
case gestureCtrl = 13
|
|
69
|
-
case onboarding = 16
|
|
70
|
-
case deviceSettings = 128
|
|
71
|
-
case evenHubCtrl = 129
|
|
72
|
-
case evenHub = 224
|
|
62
|
+
case dashboard = 1 // 0x01 - UI_BACKGROUND_DASHBOARD_APP_ID
|
|
63
|
+
case menu = 3 // 0x03 - UI_FOREGROUND_MEUN_ID (typo is intentional — matches Even's proto)
|
|
64
|
+
case notification = 4 // 0x04 - UI_FOREGROUND_NOTIFICATION_ID
|
|
65
|
+
case evenAI = 7 // 0x07 - UI_FOREGROUND_EVEN_AI_ID
|
|
66
|
+
case navigation = 8 // 0x08 - UI_BACKGROUND_NAVIGATION_ID (compass/heading lives here)
|
|
67
|
+
case g2Setting = 9 // 0x09 - UI_SETTING_APP_ID
|
|
68
|
+
case gestureCtrl = 13 // 0x0D - gesture_ctrl lifecycle signals
|
|
69
|
+
case onboarding = 16 // 0x10 - UI_ONBOARDING_APP_ID
|
|
70
|
+
case deviceSettings = 128 // 0x80 - UX_DEVICE_SETTINGS_APP_ID
|
|
71
|
+
case evenHubCtrl = 129 // 0x81 - EvenHub CTRL channel (init/registration)
|
|
72
|
+
case evenHub = 224 // 0xE0 - UI_BACKGROUND_EVENHUB_APP_ID
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/// EvenHub command IDs from EvenHub.proto
|
|
76
76
|
private enum EvenHubCmd: Int32 {
|
|
77
|
-
case createStartupPage = 0
|
|
78
|
-
case updateImageRawData = 3
|
|
79
|
-
case updateTextData = 5
|
|
80
|
-
case rebuildPage = 7
|
|
81
|
-
case shutdownPage = 9
|
|
82
|
-
case heartbeat = 12
|
|
83
|
-
case audioControl = 15
|
|
84
|
-
case imuControl = 19
|
|
77
|
+
case createStartupPage = 0 // APP_REQUEST_CREATE_STARTUP_PAGE_PACKET
|
|
78
|
+
case updateImageRawData = 3 // APP_UPDATE_IMAGE_RAW_DATA_PACKET
|
|
79
|
+
case updateTextData = 5 // APP_UPDATE_TEXT_DATA_PACKET
|
|
80
|
+
case rebuildPage = 7 // APP_REQUEST_REBUILD_PAGE_PACKET
|
|
81
|
+
case shutdownPage = 9 // APP_REQUEST_SHUTDOWN_PAGE_PACKET
|
|
82
|
+
case heartbeat = 12 // APP_REQUEST_HEARTBEAT_PACKET
|
|
83
|
+
case audioControl = 15 // APP_REQUEST_AUDIO_CTR_PACKET
|
|
84
|
+
case imuControl = 19 // APP_REQUEST_IMU_CTR_PACKET (confirmed via on-device brute-force)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/// Navigation_Cmd_list from navigation.proto (service 0x08)
|
|
88
88
|
private enum NavigationCmd: Int32 {
|
|
89
|
-
case appSendHeartbeat = 0
|
|
90
|
-
case appRequestStartUp = 5
|
|
91
|
-
case appSendBasicInfo = 7
|
|
92
|
-
case appRequestExit = 12
|
|
93
|
-
case osNotifyExit = 13
|
|
94
|
-
case osNotifyReviewChanged = 14
|
|
95
|
-
case osNotifyCompassChanged = 15
|
|
96
|
-
case osNotifyCompassCalibrateStart = 16
|
|
97
|
-
case osNotifyCompassCalibrateComplete = 17
|
|
89
|
+
case appSendHeartbeat = 0 // APP_SEND_HEARTBEAT_CMD
|
|
90
|
+
case appRequestStartUp = 5 // APP_REQUEST_START_UP — begin navigation/compass session
|
|
91
|
+
case appSendBasicInfo = 7 // APP_SEND_BASIC_INFO
|
|
92
|
+
case appRequestExit = 12 // APP_REQUEST_EXIT
|
|
93
|
+
case osNotifyExit = 13 // OS_NOTIFY_EXIT
|
|
94
|
+
case osNotifyReviewChanged = 14 // OS_NOTIFY_REVIEW_CHANGED
|
|
95
|
+
case osNotifyCompassChanged = 15 // OS_NOTIFY_COMPASS_CHANGED — heading update
|
|
96
|
+
case osNotifyCompassCalibrateStart = 16 // OS_NOTIFY_COMPASS_CALIBRATE_STRAT (sic)
|
|
97
|
+
case osNotifyCompassCalibrateComplete = 17 // OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/// EvenHub response command IDs (from glasses → phone)
|
|
101
101
|
private enum EvenHubResponseCmd: Int32 {
|
|
102
|
-
case osNotifyEventToApp = 2
|
|
102
|
+
case osNotifyEventToApp = 2 // OS_NOITY_EVENT_TO_APP_PACKET - touch/gesture events
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/// OsEventTypeList from EvenHub.proto
|
|
@@ -112,16 +112,16 @@ private enum OsEventType: Int32 {
|
|
|
112
112
|
case foregroundExit = 5
|
|
113
113
|
case abnormalExit = 6
|
|
114
114
|
case systemExit = 7
|
|
115
|
-
case imuDataReport = 8
|
|
115
|
+
case imuDataReport = 8 // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/// g2_settingCommandId from g2_setting.proto
|
|
119
119
|
private enum G2SettingCommandId: Int32 {
|
|
120
120
|
case none = 0
|
|
121
|
-
case deviceReceiveInfo = 1
|
|
122
|
-
case deviceReceiveRequest = 2
|
|
123
|
-
case deviceSendToApp = 3
|
|
124
|
-
case deviceRespondToApp = 4
|
|
121
|
+
case deviceReceiveInfo = 1 // Send settings TO glasses
|
|
122
|
+
case deviceReceiveRequest = 2 // Request info FROM glasses
|
|
123
|
+
case deviceSendToApp = 3 // Glasses sends info TO app
|
|
124
|
+
case deviceRespondToApp = 4 // Glasses responds to app
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/// DevCfgCommandId from dev_config_protocol.proto
|
|
@@ -165,7 +165,7 @@ private struct ProtobufWriter {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
mutating func writeInt32Field(_ fieldNumber: Int, _ value: Int32) {
|
|
168
|
-
let tag = UInt64(fieldNumber << 3) | 0
|
|
168
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
169
169
|
writeVarint(tag)
|
|
170
170
|
// protobuf int32 uses varint encoding; negative values use 10 bytes
|
|
171
171
|
if value >= 0 {
|
|
@@ -176,13 +176,13 @@ private struct ProtobufWriter {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
mutating func writeInt64Field(_ fieldNumber: Int, _ value: Int64) {
|
|
179
|
-
let tag = UInt64(fieldNumber << 3) | 0
|
|
179
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
180
180
|
writeVarint(tag)
|
|
181
181
|
writeVarint(UInt64(bitPattern: value))
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
mutating func writeStringField(_ fieldNumber: Int, _ value: String) {
|
|
185
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
185
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
186
186
|
writeVarint(tag)
|
|
187
187
|
let utf8 = Array(value.utf8)
|
|
188
188
|
writeVarint(UInt64(utf8.count))
|
|
@@ -190,7 +190,7 @@ private struct ProtobufWriter {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
mutating func writeBytesField(_ fieldNumber: Int, _ value: Data) {
|
|
193
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
193
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
194
194
|
writeVarint(tag)
|
|
195
195
|
writeVarint(UInt64(value.count))
|
|
196
196
|
data.append(value)
|
|
@@ -252,7 +252,7 @@ private struct ProtobufReader {
|
|
|
252
252
|
guard let len = readVarint() else { return nil }
|
|
253
253
|
let length = Int(len)
|
|
254
254
|
guard offset + length <= data.count else { return nil }
|
|
255
|
-
let result = data[(data.startIndex + offset)..<(data.startIndex + offset + length)]
|
|
255
|
+
let result = data[(data.startIndex + offset) ..< (data.startIndex + offset + length)]
|
|
256
256
|
offset += length
|
|
257
257
|
return Data(result)
|
|
258
258
|
}
|
|
@@ -265,10 +265,10 @@ private struct ProtobufReader {
|
|
|
265
265
|
/// Skip a field value based on wire type
|
|
266
266
|
mutating func skipField(wireType: Int) {
|
|
267
267
|
switch wireType {
|
|
268
|
-
case 0: _ = readVarint()
|
|
269
|
-
case 1: offset += 8
|
|
270
|
-
case 2: _ = readBytes()
|
|
271
|
-
case 5: offset += 4
|
|
268
|
+
case 0: _ = readVarint() // varint
|
|
269
|
+
case 1: offset += 8 // 64-bit
|
|
270
|
+
case 2: _ = readBytes() // length-delimited
|
|
271
|
+
case 5: offset += 4 // 32-bit
|
|
272
272
|
default: break
|
|
273
273
|
}
|
|
274
274
|
}
|
|
@@ -280,9 +280,9 @@ private struct ProtobufReader {
|
|
|
280
280
|
while hasMore {
|
|
281
281
|
guard let (fieldNum, wireType) = readTag() else { break }
|
|
282
282
|
switch wireType {
|
|
283
|
-
case 0:
|
|
283
|
+
case 0: // varint
|
|
284
284
|
if let v = readVarint() { fields[fieldNum] = Int32(truncatingIfNeeded: v) }
|
|
285
|
-
case 2:
|
|
285
|
+
case 2: // length-delimited (submessage or bytes or string)
|
|
286
286
|
if let d = readBytes() { fields[fieldNum] = d }
|
|
287
287
|
default:
|
|
288
288
|
skipField(wireType: wireType)
|
|
@@ -304,21 +304,21 @@ private enum EvenHubProto {
|
|
|
304
304
|
content: String? = nil
|
|
305
305
|
) -> Data {
|
|
306
306
|
var w = ProtobufWriter()
|
|
307
|
-
w.writeInt32Field(1, x)
|
|
308
|
-
w.writeInt32Field(2, y)
|
|
309
|
-
w.writeInt32Field(3, width)
|
|
310
|
-
w.writeInt32Field(4, height)
|
|
311
|
-
w.writeInt32Field(5, borderWidth)
|
|
312
|
-
w.writeInt32Field(6, borderColor)
|
|
313
|
-
w.writeInt32Field(7, borderRadius)
|
|
314
|
-
w.writeInt32Field(8, paddingLength)
|
|
315
|
-
w.writeInt32Field(9, containerID)
|
|
307
|
+
w.writeInt32Field(1, x) // XPosition
|
|
308
|
+
w.writeInt32Field(2, y) // YPosition
|
|
309
|
+
w.writeInt32Field(3, width) // Width
|
|
310
|
+
w.writeInt32Field(4, height) // Height
|
|
311
|
+
w.writeInt32Field(5, borderWidth) // BorderWidth
|
|
312
|
+
w.writeInt32Field(6, borderColor) // BorderColor
|
|
313
|
+
w.writeInt32Field(7, borderRadius) // BorderRdaius (sic - typo in proto)
|
|
314
|
+
w.writeInt32Field(8, paddingLength) // PaddingLength
|
|
315
|
+
w.writeInt32Field(9, containerID) // ContainerID
|
|
316
316
|
if let name = containerName {
|
|
317
|
-
w.writeStringField(10, name)
|
|
317
|
+
w.writeStringField(10, name) // ContainerName
|
|
318
318
|
}
|
|
319
|
-
w.writeInt32Field(11, isEventCapture ? 1 : 0)
|
|
319
|
+
w.writeInt32Field(11, isEventCapture ? 1 : 0) // IsEventCapture
|
|
320
320
|
if let content = content {
|
|
321
|
-
w.writeStringField(12, content)
|
|
321
|
+
w.writeStringField(12, content) // Content
|
|
322
322
|
}
|
|
323
323
|
return w.data
|
|
324
324
|
}
|
|
@@ -329,13 +329,13 @@ private enum EvenHubProto {
|
|
|
329
329
|
containerID: Int32, containerName: String? = nil
|
|
330
330
|
) -> Data {
|
|
331
331
|
var w = ProtobufWriter()
|
|
332
|
-
w.writeInt32Field(1, x)
|
|
333
|
-
w.writeInt32Field(2, y)
|
|
334
|
-
w.writeInt32Field(3, width)
|
|
335
|
-
w.writeInt32Field(4, height)
|
|
336
|
-
w.writeInt32Field(5, containerID)
|
|
332
|
+
w.writeInt32Field(1, x) // XPosition
|
|
333
|
+
w.writeInt32Field(2, y) // YPosition
|
|
334
|
+
w.writeInt32Field(3, width) // Width
|
|
335
|
+
w.writeInt32Field(4, height) // Height
|
|
336
|
+
w.writeInt32Field(5, containerID) // ContainerID
|
|
337
337
|
if let name = containerName {
|
|
338
|
-
w.writeStringField(6, name)
|
|
338
|
+
w.writeStringField(6, name) // ContainerName
|
|
339
339
|
}
|
|
340
340
|
return w.data
|
|
341
341
|
}
|
|
@@ -347,16 +347,16 @@ private enum EvenHubProto {
|
|
|
347
347
|
mapFragmentIndex: Int32, mapFragmentPacketSize: Int32, mapRawData: Data
|
|
348
348
|
) -> Data {
|
|
349
349
|
var w = ProtobufWriter()
|
|
350
|
-
w.writeInt32Field(1, containerID)
|
|
350
|
+
w.writeInt32Field(1, containerID) // ContainerID
|
|
351
351
|
if let name = containerName {
|
|
352
|
-
w.writeStringField(2, name)
|
|
353
|
-
}
|
|
354
|
-
w.writeInt32Field(3, mapSessionId)
|
|
355
|
-
w.writeInt32Field(4, mapTotalSize)
|
|
356
|
-
w.writeInt32Field(5, compressMode)
|
|
357
|
-
w.writeInt32Field(6, mapFragmentIndex)
|
|
358
|
-
w.writeInt32Field(7, mapFragmentPacketSize)
|
|
359
|
-
w.writeBytesField(8, mapRawData)
|
|
352
|
+
w.writeStringField(2, name) // ContainerName
|
|
353
|
+
}
|
|
354
|
+
w.writeInt32Field(3, mapSessionId) // MapSessionId
|
|
355
|
+
w.writeInt32Field(4, mapTotalSize) // MapTotalSize
|
|
356
|
+
w.writeInt32Field(5, compressMode) // CompressMode
|
|
357
|
+
w.writeInt32Field(6, mapFragmentIndex) // MapFragmentIndex
|
|
358
|
+
w.writeInt32Field(7, mapFragmentPacketSize) // MapFragmentPacketSize
|
|
359
|
+
w.writeBytesField(8, mapRawData) // MapRawData
|
|
360
360
|
return w.data
|
|
361
361
|
}
|
|
362
362
|
|
|
@@ -367,13 +367,13 @@ private enum EvenHubProto {
|
|
|
367
367
|
imageContainers: [Data] = []
|
|
368
368
|
) -> Data {
|
|
369
369
|
var w = ProtobufWriter()
|
|
370
|
-
w.writeInt32Field(1, containerTotalNum)
|
|
370
|
+
w.writeInt32Field(1, containerTotalNum) // ContainerTotalNum
|
|
371
371
|
// field 2 = repeated ListContainerProperty ListObject (not used here)
|
|
372
372
|
for tc in textContainers {
|
|
373
|
-
w.writeMessageField(3, tc)
|
|
373
|
+
w.writeMessageField(3, tc) // field 3 = repeated TextObject
|
|
374
374
|
}
|
|
375
375
|
for ic in imageContainers {
|
|
376
|
-
w.writeMessageField(4, ic)
|
|
376
|
+
w.writeMessageField(4, ic) // field 4 = repeated ImageObject
|
|
377
377
|
}
|
|
378
378
|
return w.data
|
|
379
379
|
}
|
|
@@ -384,17 +384,17 @@ private enum EvenHubProto {
|
|
|
384
384
|
contentLength: Int32, content: String
|
|
385
385
|
) -> Data {
|
|
386
386
|
var w = ProtobufWriter()
|
|
387
|
-
w.writeInt32Field(1, containerID)
|
|
388
|
-
w.writeInt32Field(3, contentOffset)
|
|
389
|
-
w.writeInt32Field(4, contentLength)
|
|
390
|
-
w.writeStringField(5, content)
|
|
387
|
+
w.writeInt32Field(1, containerID) // ContainerID
|
|
388
|
+
w.writeInt32Field(3, contentOffset) // ContentOffset
|
|
389
|
+
w.writeInt32Field(4, contentLength) // ContentLength
|
|
390
|
+
w.writeStringField(5, content) // Content
|
|
391
391
|
return w.data
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
/// Build a ShutDownContaniner message (sic - typo in proto)
|
|
395
395
|
static func shutdownContainer(exitMode: Int32 = 0) -> Data {
|
|
396
396
|
var w = ProtobufWriter()
|
|
397
|
-
w.writeInt32Field(1, exitMode)
|
|
397
|
+
w.writeInt32Field(1, exitMode) // exitMode
|
|
398
398
|
return w.data
|
|
399
399
|
}
|
|
400
400
|
|
|
@@ -402,7 +402,7 @@ private enum EvenHubProto {
|
|
|
402
402
|
static func heartbeatPacket(cnt: Int32 = 0) -> Data {
|
|
403
403
|
var w = ProtobufWriter()
|
|
404
404
|
if cnt != 0 {
|
|
405
|
-
w.writeInt32Field(1, cnt)
|
|
405
|
+
w.writeInt32Field(1, cnt) // Cnt
|
|
406
406
|
}
|
|
407
407
|
return w.data
|
|
408
408
|
}
|
|
@@ -410,7 +410,7 @@ private enum EvenHubProto {
|
|
|
410
410
|
/// Build an AudioCtrCmd message
|
|
411
411
|
static func audioCtrCmd(enable: Bool) -> Data {
|
|
412
412
|
var w = ProtobufWriter()
|
|
413
|
-
w.writeInt32Field(1, enable ? 1 : 0)
|
|
413
|
+
w.writeInt32Field(1, enable ? 1 : 0) // AudoFuncEn
|
|
414
414
|
return w.data
|
|
415
415
|
}
|
|
416
416
|
|
|
@@ -421,11 +421,11 @@ private enum EvenHubProto {
|
|
|
421
421
|
appId: Int32? = nil
|
|
422
422
|
) -> Data {
|
|
423
423
|
var w = ProtobufWriter()
|
|
424
|
-
w.writeInt32Field(1, cmd.rawValue)
|
|
425
|
-
w.writeInt32Field(2, magicRandom)
|
|
426
|
-
w.writeMessageField(subFieldNumber, subMessage)
|
|
424
|
+
w.writeInt32Field(1, cmd.rawValue) // Cmd (field 1, enum)
|
|
425
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom (field 2)
|
|
426
|
+
w.writeMessageField(subFieldNumber, subMessage) // the actual command payload
|
|
427
427
|
if let appId = appId {
|
|
428
|
-
w.writeInt32Field(5, appId)
|
|
428
|
+
w.writeInt32Field(5, appId) // Associate page with a menu item appId
|
|
429
429
|
}
|
|
430
430
|
return w.data
|
|
431
431
|
}
|
|
@@ -512,17 +512,18 @@ private enum EvenHubProto {
|
|
|
512
512
|
}
|
|
513
513
|
|
|
514
514
|
// MARK: - IMU control
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
515
|
+
|
|
516
|
+
///
|
|
517
|
+
/// Wire format recovered by on-device brute-force (sample magnitude ≈ 1.0 g confirms
|
|
518
|
+
/// the decode). Shapes from even_hub_sdk@0.0.10; numeric proto tags confirmed live:
|
|
519
|
+
/// EvenHub_Cmd_List IMU command = 19
|
|
520
|
+
/// evenhub_main_msg_ctx ImuCtrlCmd slot = field 20
|
|
521
|
+
/// ImuCtrlCmd { field 1 = IMU_ReportEn (bool), field 2 = reportFrq (pacing 100…1000) }
|
|
522
|
+
/// Report path: cmd=2 (osNotifyEventToApp) → SendDeviceEvent.field13 →
|
|
523
|
+
/// Sys_ItemEvent { field 1 = eventType = 8 (IMU_DATA_REPORT),
|
|
524
|
+
/// field 3 = imuData = IMU_Report_Data }
|
|
525
|
+
/// IMU_Report_Data { field 1 = x, 2 = y, 3 = z } — each a 32-bit float (NOT double),
|
|
526
|
+
/// gravity-normalized (|v| ≈ 1 at rest).
|
|
526
527
|
static let imuCtrlSubField = 20
|
|
527
528
|
|
|
528
529
|
/// ImuReportPace pacing codes (protocol values, NOT literal Hz). Step 100, 100…1000.
|
|
@@ -533,9 +534,9 @@ private enum EvenHubProto {
|
|
|
533
534
|
/// Build an ImuCtrlCmd sub-message.
|
|
534
535
|
static func imuCtrlCmd(enable: Bool, reportFrq: Int32) -> Data {
|
|
535
536
|
var w = ProtobufWriter()
|
|
536
|
-
w.writeInt32Field(1, enable ? 1 : 0)
|
|
537
|
+
w.writeInt32Field(1, enable ? 1 : 0) // IMU_ReportEn
|
|
537
538
|
if enable {
|
|
538
|
-
w.writeInt32Field(2, reportFrq)
|
|
539
|
+
w.writeInt32Field(2, reportFrq) // reportFrq (pacing code 100…1000)
|
|
539
540
|
}
|
|
540
541
|
return w.data
|
|
541
542
|
}
|
|
@@ -547,9 +548,9 @@ private enum EvenHubProto {
|
|
|
547
548
|
) -> Data {
|
|
548
549
|
let imuMsg = imuCtrlCmd(enable: enable, reportFrq: reportFrq)
|
|
549
550
|
var w = ProtobufWriter()
|
|
550
|
-
w.writeInt32Field(1, EvenHubCmd.imuControl.rawValue)
|
|
551
|
-
w.writeInt32Field(2, magicRandom)
|
|
552
|
-
w.writeMessageField(imuCtrlSubField, imuMsg)
|
|
551
|
+
w.writeInt32Field(1, EvenHubCmd.imuControl.rawValue) // Cmd
|
|
552
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
553
|
+
w.writeMessageField(imuCtrlSubField, imuMsg) // ImuCtrlCmd slot (field 20)
|
|
553
554
|
return w.data
|
|
554
555
|
}
|
|
555
556
|
}
|
|
@@ -564,17 +565,17 @@ private enum DevSettingsProto {
|
|
|
564
565
|
// field 2 = magicRandom (int32)
|
|
565
566
|
// field 3 = authMgr (AuthMgr message)
|
|
566
567
|
var w = ProtobufWriter()
|
|
567
|
-
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue)
|
|
568
|
-
w.writeInt32Field(2, magicRandom)
|
|
568
|
+
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue) // commandId
|
|
569
|
+
w.writeInt32Field(2, magicRandom) // magicRandom
|
|
569
570
|
|
|
570
571
|
// AuthMgr sub-message:
|
|
571
572
|
// field 1 = secAuth (bool)
|
|
572
573
|
// field 2 = phoneType (enum eDevice: PHONE_IOS=3, PHONE_ANDROID=4)
|
|
573
574
|
var authW = ProtobufWriter()
|
|
574
|
-
authW.writeBoolField(1, true)
|
|
575
|
-
authW.writeInt32Field(2, 3)
|
|
575
|
+
authW.writeBoolField(1, true) // secAuth
|
|
576
|
+
authW.writeInt32Field(2, 3) // phoneType = PHONE_IOS (eDevice.PHONE_IOS=3)
|
|
576
577
|
|
|
577
|
-
w.writeMessageField(3, authW.data)
|
|
578
|
+
w.writeMessageField(3, authW.data) // authMgr
|
|
578
579
|
return w.data
|
|
579
580
|
}
|
|
580
581
|
|
|
@@ -586,8 +587,8 @@ private enum DevSettingsProto {
|
|
|
586
587
|
|
|
587
588
|
// PipeRoleChange: field 1 = asCmdRole (enum GlassesLR.RIGHT=1)
|
|
588
589
|
var roleW = ProtobufWriter()
|
|
589
|
-
roleW.writeInt32Field(1, 1)
|
|
590
|
-
w.writeMessageField(4, roleW.data)
|
|
590
|
+
roleW.writeInt32Field(1, 1) // RIGHT
|
|
591
|
+
w.writeMessageField(4, roleW.data) // roleChange (field 4 in DevCfgDataPackage)
|
|
591
592
|
return w.data
|
|
592
593
|
}
|
|
593
594
|
|
|
@@ -608,7 +609,7 @@ private enum DevSettingsProto {
|
|
|
608
609
|
let timestampSec = timestampMs / 1000
|
|
609
610
|
let tzSec = Int64(TimeZone.current.secondsFromGMT(for: timestamp))
|
|
610
611
|
tsW.writeInt32Field(1, Int32(truncatingIfNeeded: timestampSec + tzSec))
|
|
611
|
-
w.writeMessageField(128, tsW.data)
|
|
612
|
+
w.writeMessageField(128, tsW.data) // timeSync (field 128 in DevCfgDataPackage)
|
|
612
613
|
return w.data
|
|
613
614
|
}
|
|
614
615
|
|
|
@@ -648,8 +649,8 @@ private enum DevSettingsProto {
|
|
|
648
649
|
|
|
649
650
|
// BaseConnHeartBeat: empty message
|
|
650
651
|
var hbW = ProtobufWriter()
|
|
651
|
-
_ = hbW
|
|
652
|
-
w.writeMessageField(13, hbW.data)
|
|
652
|
+
_ = hbW // empty
|
|
653
|
+
w.writeMessageField(13, hbW.data) // baseHeartBeat (field 13)
|
|
653
654
|
return w.data
|
|
654
655
|
}
|
|
655
656
|
|
|
@@ -660,18 +661,18 @@ private enum DevSettingsProto {
|
|
|
660
661
|
magicRandom: Int32, connect: Bool, ringMac: Data, ringName: String = ""
|
|
661
662
|
) -> Data {
|
|
662
663
|
var w = ProtobufWriter()
|
|
663
|
-
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue)
|
|
664
|
+
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue) // commandId = RING_CONNECT_INFO (6)
|
|
664
665
|
w.writeInt32Field(2, magicRandom)
|
|
665
666
|
|
|
666
667
|
// RingInfo sub-message (field 5 in DevCfgDataPackage)
|
|
667
668
|
var ringW = ProtobufWriter()
|
|
668
|
-
ringW.writeBoolField(1, connect)
|
|
669
|
-
ringW.writeBytesField(2, ringMac)
|
|
669
|
+
ringW.writeBoolField(1, connect) // connectRing
|
|
670
|
+
ringW.writeBytesField(2, ringMac) // ringMac (6 bytes)
|
|
670
671
|
if !ringName.isEmpty {
|
|
671
|
-
ringW.writeBytesField(3, Data(ringName.utf8))
|
|
672
|
+
ringW.writeBytesField(3, Data(ringName.utf8)) // ringName
|
|
672
673
|
}
|
|
673
674
|
|
|
674
|
-
w.writeMessageField(5, ringW.data)
|
|
675
|
+
w.writeMessageField(5, ringW.data) // ringInfo (field 5)
|
|
675
676
|
return w.data
|
|
676
677
|
}
|
|
677
678
|
}
|
|
@@ -683,18 +684,18 @@ private enum G2SettingProto {
|
|
|
683
684
|
static func setBrightness(magicRandom: Int32, level: Int32, autoAdjust: Bool) -> Data {
|
|
684
685
|
// DeviceReceive_Brightness
|
|
685
686
|
var brightnessW = ProtobufWriter()
|
|
686
|
-
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0)
|
|
687
|
-
brightnessW.writeInt32Field(2, level)
|
|
687
|
+
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0) // autoAdjust
|
|
688
|
+
brightnessW.writeInt32Field(2, level) // brightnessLevel
|
|
688
689
|
|
|
689
690
|
// DeviceReceiveInfoFromAPP
|
|
690
691
|
var infoW = ProtobufWriter()
|
|
691
|
-
infoW.writeMessageField(1, brightnessW.data)
|
|
692
|
+
infoW.writeMessageField(1, brightnessW.data) // deviceReceiveBrightness (field 1)
|
|
692
693
|
|
|
693
694
|
// G2SettingPackage
|
|
694
695
|
var w = ProtobufWriter()
|
|
695
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
696
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue) // commandId
|
|
696
697
|
w.writeInt32Field(2, magicRandom)
|
|
697
|
-
w.writeMessageField(3, infoW.data)
|
|
698
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
698
699
|
return w.data
|
|
699
700
|
}
|
|
700
701
|
|
|
@@ -703,13 +704,13 @@ private enum G2SettingProto {
|
|
|
703
704
|
// DeviceReceiveRequestFromAPP - empty message triggers glasses to respond with all fields
|
|
704
705
|
var reqW = ProtobufWriter()
|
|
705
706
|
// Request brightness info type
|
|
706
|
-
reqW.writeInt32Field(1, 1)
|
|
707
|
+
reqW.writeInt32Field(1, 1) // settingInfoType = APP_REQUIRE_BASIC_SETTING
|
|
707
708
|
|
|
708
709
|
// G2SettingPackage
|
|
709
710
|
var w = ProtobufWriter()
|
|
710
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue)
|
|
711
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue) // commandId
|
|
711
712
|
w.writeInt32Field(2, magicRandom)
|
|
712
|
-
w.writeMessageField(4, reqW.data)
|
|
713
|
+
w.writeMessageField(4, reqW.data) // deviceReceiveRequestFromApp (field 4)
|
|
713
714
|
return w.data
|
|
714
715
|
}
|
|
715
716
|
|
|
@@ -717,17 +718,17 @@ private enum G2SettingProto {
|
|
|
717
718
|
static func setHeadUpSwitch(magicRandom: Int32, enabled: Bool) -> Data {
|
|
718
719
|
// DeviceReceive_Head_UP_Setting
|
|
719
720
|
var headUpW = ProtobufWriter()
|
|
720
|
-
headUpW.writeInt32Field(1, enabled ? 1 : 0)
|
|
721
|
+
headUpW.writeInt32Field(1, enabled ? 1 : 0) // headUpSwitch
|
|
721
722
|
|
|
722
723
|
// DeviceReceiveInfoFromAPP
|
|
723
724
|
var infoW = ProtobufWriter()
|
|
724
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
725
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
725
726
|
|
|
726
727
|
// G2SettingPackage
|
|
727
728
|
var w = ProtobufWriter()
|
|
728
729
|
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
729
730
|
w.writeInt32Field(2, magicRandom)
|
|
730
|
-
w.writeMessageField(3, infoW.data)
|
|
731
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
731
732
|
return w.data
|
|
732
733
|
}
|
|
733
734
|
|
|
@@ -735,11 +736,11 @@ private enum G2SettingProto {
|
|
|
735
736
|
static func setHeadUpAngle(magicRandom: Int32, angle: Int32) -> Data {
|
|
736
737
|
// DeviceReceive_Head_UP_Setting
|
|
737
738
|
var headUpW = ProtobufWriter()
|
|
738
|
-
headUpW.writeInt32Field(2, angle)
|
|
739
|
+
headUpW.writeInt32Field(2, angle) // headUpAngle (field 2)
|
|
739
740
|
|
|
740
741
|
// DeviceReceiveInfoFromAPP
|
|
741
742
|
var infoW = ProtobufWriter()
|
|
742
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
743
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
743
744
|
|
|
744
745
|
// G2SettingPackage
|
|
745
746
|
var w = ProtobufWriter()
|
|
@@ -753,11 +754,11 @@ private enum G2SettingProto {
|
|
|
753
754
|
static func setScreenHeight(magicRandom: Int32, level: Int32) -> Data {
|
|
754
755
|
// DeviceReceive_Y_Coordinate
|
|
755
756
|
var yW = ProtobufWriter()
|
|
756
|
-
yW.writeInt32Field(1, level)
|
|
757
|
+
yW.writeInt32Field(1, level) // yCoordinateLevel
|
|
757
758
|
|
|
758
759
|
// DeviceReceiveInfoFromAPP
|
|
759
760
|
var infoW = ProtobufWriter()
|
|
760
|
-
infoW.writeMessageField(2, yW.data)
|
|
761
|
+
infoW.writeMessageField(2, yW.data) // deviceReceiveYCoordinate (field 2)
|
|
761
762
|
|
|
762
763
|
// G2SettingPackage
|
|
763
764
|
var w = ProtobufWriter()
|
|
@@ -771,11 +772,11 @@ private enum G2SettingProto {
|
|
|
771
772
|
static func setScreenDepth(magicRandom: Int32, level: Int32) -> Data {
|
|
772
773
|
// DeviceReceive_X_Coordinate
|
|
773
774
|
var xW = ProtobufWriter()
|
|
774
|
-
xW.writeInt32Field(1, level)
|
|
775
|
+
xW.writeInt32Field(1, level) // xCoordinateLevel
|
|
775
776
|
|
|
776
777
|
// DeviceReceiveInfoFromAPP
|
|
777
778
|
var infoW = ProtobufWriter()
|
|
778
|
-
infoW.writeMessageField(3, xW.data)
|
|
779
|
+
infoW.writeMessageField(3, xW.data) // deviceReceiveXCoordinate (field 3)
|
|
779
780
|
|
|
780
781
|
// G2SettingPackage
|
|
781
782
|
var w = ProtobufWriter()
|
|
@@ -793,13 +794,13 @@ private enum OnboardingProto {
|
|
|
793
794
|
static func skipOnboarding(magicRandom: Int32) -> Data {
|
|
794
795
|
// OnboardingConfig: processId = FINISH (4)
|
|
795
796
|
var configW = ProtobufWriter()
|
|
796
|
-
configW.writeInt32Field(1, 4)
|
|
797
|
+
configW.writeInt32Field(1, 4) // processId = FINISH
|
|
797
798
|
|
|
798
799
|
// OnboardingDataPackage
|
|
799
800
|
var w = ProtobufWriter()
|
|
800
|
-
w.writeInt32Field(1, 1)
|
|
801
|
+
w.writeInt32Field(1, 1) // commandId = CONFIG
|
|
801
802
|
w.writeInt32Field(2, magicRandom)
|
|
802
|
-
w.writeMessageField(3, configW.data)
|
|
803
|
+
w.writeMessageField(3, configW.data) // config (field 3)
|
|
803
804
|
return w.data
|
|
804
805
|
}
|
|
805
806
|
}
|
|
@@ -819,15 +820,15 @@ private enum EvenAIProto {
|
|
|
819
820
|
// EvenAIConfig
|
|
820
821
|
var configW = ProtobufWriter()
|
|
821
822
|
if enabled {
|
|
822
|
-
configW.writeInt32Field(1, 1)
|
|
823
|
+
configW.writeInt32Field(1, 1) // voiceSwitch (omitted when off, matching the app)
|
|
823
824
|
}
|
|
824
|
-
configW.writeInt32Field(2, 32)
|
|
825
|
+
configW.writeInt32Field(2, 32) // streamSpeed (always sent, app uses 32)
|
|
825
826
|
|
|
826
827
|
// EvenAIDataPackage
|
|
827
828
|
var w = ProtobufWriter()
|
|
828
|
-
w.writeInt32Field(1, 10)
|
|
829
|
+
w.writeInt32Field(1, 10) // commandId = CONFIG
|
|
829
830
|
w.writeInt32Field(2, magicRandom)
|
|
830
|
-
w.writeMessageField(13, configW.data)
|
|
831
|
+
w.writeMessageField(13, configW.data) // config (field 13)
|
|
831
832
|
return w.data
|
|
832
833
|
}
|
|
833
834
|
|
|
@@ -837,13 +838,13 @@ private enum EvenAIProto {
|
|
|
837
838
|
/// into the glasses' AI session so the following SKILL packet has context.
|
|
838
839
|
static func aiAsk(magicRandom: Int32, text: String, streamEnable: Int32 = 0) -> Data {
|
|
839
840
|
var askW = ProtobufWriter()
|
|
840
|
-
askW.writeInt32Field(2, streamEnable)
|
|
841
|
-
askW.writeBytesField(4, Data(text.utf8))
|
|
841
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
842
|
+
askW.writeBytesField(4, Data(text.utf8)) // text
|
|
842
843
|
|
|
843
844
|
var w = ProtobufWriter()
|
|
844
|
-
w.writeInt32Field(1, 3)
|
|
845
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
845
846
|
w.writeInt32Field(2, magicRandom)
|
|
846
|
-
w.writeMessageField(5, askW.data)
|
|
847
|
+
w.writeMessageField(5, askW.data) // askInfo (field 5)
|
|
847
848
|
return w.data
|
|
848
849
|
}
|
|
849
850
|
|
|
@@ -853,12 +854,12 @@ private enum EvenAIProto {
|
|
|
853
854
|
/// status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
854
855
|
static func aiCtrl(magicRandom: Int32, status: Int32) -> Data {
|
|
855
856
|
var ctrlW = ProtobufWriter()
|
|
856
|
-
ctrlW.writeInt32Field(1, status)
|
|
857
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
857
858
|
|
|
858
859
|
var w = ProtobufWriter()
|
|
859
|
-
w.writeInt32Field(1, 1)
|
|
860
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
860
861
|
w.writeInt32Field(2, magicRandom)
|
|
861
|
-
w.writeMessageField(3, ctrlW.data)
|
|
862
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
862
863
|
return w.data
|
|
863
864
|
}
|
|
864
865
|
|
|
@@ -873,17 +874,17 @@ private enum EvenAIProto {
|
|
|
873
874
|
) -> Data {
|
|
874
875
|
// EvenAISkillInfo
|
|
875
876
|
var skillW = ProtobufWriter()
|
|
876
|
-
skillW.writeInt32Field(1, streamEnable)
|
|
877
|
-
skillW.writeInt32Field(2, skillId)
|
|
878
|
-
skillW.writeInt32Field(3, skillParam)
|
|
879
|
-
skillW.writeBytesField(4, Data(text.utf8))
|
|
880
|
-
skillW.writeInt32Field(6, fTextEnd)
|
|
877
|
+
skillW.writeInt32Field(1, streamEnable) // streamEnable
|
|
878
|
+
skillW.writeInt32Field(2, skillId) // skillId
|
|
879
|
+
skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
|
|
880
|
+
skillW.writeBytesField(4, Data(text.utf8)) // text (utterance / payload)
|
|
881
|
+
skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
|
|
881
882
|
|
|
882
883
|
// EvenAIDataPackage
|
|
883
884
|
var w = ProtobufWriter()
|
|
884
|
-
w.writeInt32Field(1, 6)
|
|
885
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
885
886
|
w.writeInt32Field(2, magicRandom)
|
|
886
|
-
w.writeMessageField(8, skillW.data)
|
|
887
|
+
w.writeMessageField(8, skillW.data) // skillInfo (field 8)
|
|
887
888
|
return w.data
|
|
888
889
|
}
|
|
889
890
|
}
|
|
@@ -898,13 +899,13 @@ private enum NotificationProto {
|
|
|
898
899
|
/// (Returned errorCode=8 NOT_SUPPORT in testing — Service 4 doesn't accept this outbound.)
|
|
899
900
|
static func iosNotification(magicRandom: Int32, appID: String, displayName: String) -> Data {
|
|
900
901
|
var iosW = ProtobufWriter()
|
|
901
|
-
iosW.writeBytesField(1, Data(appID.utf8))
|
|
902
|
-
iosW.writeBytesField(2, Data(displayName.utf8))
|
|
902
|
+
iosW.writeBytesField(1, Data(appID.utf8)) // appID
|
|
903
|
+
iosW.writeBytesField(2, Data(displayName.utf8)) // displayName
|
|
903
904
|
|
|
904
905
|
var w = ProtobufWriter()
|
|
905
|
-
w.writeInt32Field(1, 2)
|
|
906
|
+
w.writeInt32Field(1, 2) // commandId = NOTIFICATION_IOS
|
|
906
907
|
w.writeInt32Field(2, magicRandom)
|
|
907
|
-
w.writeMessageField(4, iosW.data)
|
|
908
|
+
w.writeMessageField(4, iosW.data) // IOS (field 4)
|
|
908
909
|
return w.data
|
|
909
910
|
}
|
|
910
911
|
|
|
@@ -921,15 +922,15 @@ private enum NotificationProto {
|
|
|
921
922
|
avoidDisturbEnable: Int32 = 0
|
|
922
923
|
) -> Data {
|
|
923
924
|
var ctrlW = ProtobufWriter()
|
|
924
|
-
ctrlW.writeInt32Field(1, notifEnable)
|
|
925
|
-
ctrlW.writeInt32Field(2, autoDispEnable)
|
|
926
|
-
ctrlW.writeInt32Field(3, dispTime)
|
|
927
|
-
ctrlW.writeInt32Field(5, avoidDisturbEnable)
|
|
925
|
+
ctrlW.writeInt32Field(1, notifEnable) // notifEnable
|
|
926
|
+
ctrlW.writeInt32Field(2, autoDispEnable) // autoDispEnable
|
|
927
|
+
ctrlW.writeInt32Field(3, dispTime) // dispTime (seconds)
|
|
928
|
+
ctrlW.writeInt32Field(5, avoidDisturbEnable) // avoidDisturbEnable
|
|
928
929
|
|
|
929
930
|
var w = ProtobufWriter()
|
|
930
|
-
w.writeInt32Field(1, 1)
|
|
931
|
+
w.writeInt32Field(1, 1) // commandId = NOTIFICATION_CTRL
|
|
931
932
|
w.writeInt32Field(2, magicRandom)
|
|
932
|
-
w.writeMessageField(3, ctrlW.data)
|
|
933
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
933
934
|
return w.data
|
|
934
935
|
}
|
|
935
936
|
}
|
|
@@ -947,7 +948,7 @@ private enum MenuProto {
|
|
|
947
948
|
/// G2 firmware requires minimum 5, maximum 10 menu items
|
|
948
949
|
static let MIN_MENU_SIZE = 5
|
|
949
950
|
static let MAX_MENU_SIZE = 10
|
|
950
|
-
static let MAX_NAME_LENGTH = 15
|
|
951
|
+
static let MAX_NAME_LENGTH = 15 // 17 char limit minus 2 for running indicator prefix
|
|
951
952
|
/// Placeholder appIds for padding slots (in valid Even range, unique per slot)
|
|
952
953
|
static let PLACEHOLDER_APP_IDS: [Int32] = [10535, 10536, 10537, 10538, 10539]
|
|
953
954
|
|
|
@@ -974,7 +975,7 @@ private enum MenuProto {
|
|
|
974
975
|
|
|
975
976
|
// Wire items carry either a built-in (itemType=0, no name) or third-party (itemType=1, with name)
|
|
976
977
|
struct WireItem {
|
|
977
|
-
let displayName: String?
|
|
978
|
+
let displayName: String? // nil for built-ins
|
|
978
979
|
let appId: Int32
|
|
979
980
|
let isBuiltIn: Bool
|
|
980
981
|
}
|
|
@@ -991,8 +992,8 @@ private enum MenuProto {
|
|
|
991
992
|
|
|
992
993
|
let truncated =
|
|
993
994
|
item.name.count > MAX_NAME_LENGTH
|
|
994
|
-
|
|
995
|
-
|
|
995
|
+
? String(item.name.prefix(MAX_NAME_LENGTH))
|
|
996
|
+
: item.name
|
|
996
997
|
let prefix = item.running ? "● " : ""
|
|
997
998
|
wireItems.append(
|
|
998
999
|
WireItem(displayName: prefix + truncated, appId: appId, isBuiltIn: false)
|
|
@@ -1001,7 +1002,7 @@ private enum MenuProto {
|
|
|
1001
1002
|
|
|
1002
1003
|
// Pad to MIN_MENU_SIZE with placeholder third-party items
|
|
1003
1004
|
while wireItems.count < MIN_MENU_SIZE {
|
|
1004
|
-
let idx = wireItems.count - 1
|
|
1005
|
+
let idx = wireItems.count - 1 // -1 because built-in occupies slot 0
|
|
1005
1006
|
wireItems.append(
|
|
1006
1007
|
WireItem(
|
|
1007
1008
|
displayName: " ---",
|
|
@@ -1013,27 +1014,27 @@ private enum MenuProto {
|
|
|
1013
1014
|
|
|
1014
1015
|
// MenuInfoSend
|
|
1015
1016
|
var menuW = ProtobufWriter()
|
|
1016
|
-
menuW.writeInt32Field(1, Int32(wireItems.count))
|
|
1017
|
+
menuW.writeInt32Field(1, Int32(wireItems.count)) // itemTotalNum
|
|
1017
1018
|
|
|
1018
1019
|
for item in wireItems {
|
|
1019
1020
|
var itemW = ProtobufWriter()
|
|
1020
1021
|
if item.isBuiltIn {
|
|
1021
|
-
itemW.writeInt32Field(1, 0)
|
|
1022
|
-
itemW.writeInt32Field(4, item.appId)
|
|
1022
|
+
itemW.writeInt32Field(1, 0) // itemType = 0 (built-in)
|
|
1023
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId = SID
|
|
1023
1024
|
} else {
|
|
1024
|
-
itemW.writeInt32Field(1, 1)
|
|
1025
|
-
itemW.writeInt32Field(2, 1)
|
|
1026
|
-
itemW.writeStringField(3, item.displayName ?? "")
|
|
1027
|
-
itemW.writeInt32Field(4, item.appId)
|
|
1025
|
+
itemW.writeInt32Field(1, 1) // itemType = 1 (third-party)
|
|
1026
|
+
itemW.writeInt32Field(2, 1) // iconNum = 1
|
|
1027
|
+
itemW.writeStringField(3, item.displayName ?? "") // itemName
|
|
1028
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId
|
|
1028
1029
|
}
|
|
1029
|
-
menuW.writeMessageField(2, itemW.data)
|
|
1030
|
+
menuW.writeMessageField(2, itemW.data) // repeated item (field 2)
|
|
1030
1031
|
}
|
|
1031
1032
|
|
|
1032
1033
|
// meun_main_msg_ctx
|
|
1033
1034
|
var w = ProtobufWriter()
|
|
1034
|
-
w.writeInt32Field(1, 0)
|
|
1035
|
-
w.writeInt32Field(2, magicRandom)
|
|
1036
|
-
w.writeMessageField(3, menuW.data)
|
|
1035
|
+
w.writeInt32Field(1, 0) // Cmd = APP_SEND_MENU_INFO (0)
|
|
1036
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
1037
|
+
w.writeMessageField(3, menuW.data) // sendData (field 3)
|
|
1037
1038
|
return (w.data, appIdMap)
|
|
1038
1039
|
}
|
|
1039
1040
|
}
|
|
@@ -1046,7 +1047,7 @@ private enum DashboardProto {
|
|
|
1046
1047
|
/// eDashboardCommandId values from dashboard.proto
|
|
1047
1048
|
enum CommandId: Int32 {
|
|
1048
1049
|
case dashboardRespond = 1
|
|
1049
|
-
case dashboardReceive = 2
|
|
1050
|
+
case dashboardReceive = 2 // phone → glasses widget/config push
|
|
1050
1051
|
case appRespond = 3
|
|
1051
1052
|
case appReceive = 4
|
|
1052
1053
|
}
|
|
@@ -1191,7 +1192,7 @@ private struct EvenBLETransport {
|
|
|
1191
1192
|
var offset = 0
|
|
1192
1193
|
while offset < payload.count {
|
|
1193
1194
|
let end = min(offset + maxPayload, payload.count)
|
|
1194
|
-
chunks.append(payload[offset..<end])
|
|
1195
|
+
chunks.append(payload[offset ..< end])
|
|
1195
1196
|
offset = end
|
|
1196
1197
|
}
|
|
1197
1198
|
if chunks.isEmpty {
|
|
@@ -1219,20 +1220,20 @@ private struct EvenBLETransport {
|
|
|
1219
1220
|
let payloadLen = UInt8(chunk.count + (isLast ? 2 : 0))
|
|
1220
1221
|
|
|
1221
1222
|
var packet = Data()
|
|
1222
|
-
packet.append(G2BLE.HEADER_BYTE)
|
|
1223
|
-
packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE)
|
|
1224
|
-
packet.append(syncId)
|
|
1225
|
-
packet.append(payloadLen)
|
|
1226
|
-
packet.append(totalPackets)
|
|
1227
|
-
packet.append(serialNum)
|
|
1228
|
-
packet.append(serviceId)
|
|
1229
|
-
packet.append(status)
|
|
1223
|
+
packet.append(G2BLE.HEADER_BYTE) // [0] 0xAA
|
|
1224
|
+
packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE) // [1] src+dst
|
|
1225
|
+
packet.append(syncId) // [2] syncId
|
|
1226
|
+
packet.append(payloadLen) // [3] payloadLen
|
|
1227
|
+
packet.append(totalPackets) // [4] packetTotalNum
|
|
1228
|
+
packet.append(serialNum) // [5] packetSerialNum
|
|
1229
|
+
packet.append(serviceId) // [6] serviceId
|
|
1230
|
+
packet.append(status) // [7] status
|
|
1230
1231
|
|
|
1231
1232
|
packet.append(chunk)
|
|
1232
1233
|
|
|
1233
1234
|
if isLast {
|
|
1234
|
-
packet.append(UInt8(crc & 0xFF))
|
|
1235
|
-
packet.append(UInt8((crc >> 8) & 0xFF))
|
|
1235
|
+
packet.append(UInt8(crc & 0xFF)) // CRC low
|
|
1236
|
+
packet.append(UInt8((crc >> 8) & 0xFF)) // CRC high
|
|
1236
1237
|
}
|
|
1237
1238
|
|
|
1238
1239
|
packets.append(packet)
|
|
@@ -1272,10 +1273,9 @@ private class G2SendManager {
|
|
|
1272
1273
|
// MARK: - G2 Receive Manager (multi-part reassembly)
|
|
1273
1274
|
|
|
1274
1275
|
private class G2ReceiveManager {
|
|
1275
|
-
private var partials: [String: (Data, UInt8)] = [:]
|
|
1276
|
+
private var partials: [String: (Data, UInt8)] = [:] // key -> (accumulated payload, lastSerialNum)
|
|
1276
1277
|
|
|
1277
|
-
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)?
|
|
1278
|
-
{
|
|
1278
|
+
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)? {
|
|
1279
1279
|
guard rawData.count >= 8 else { return nil }
|
|
1280
1280
|
guard rawData[0] == G2BLE.HEADER_BYTE else { return nil }
|
|
1281
1281
|
|
|
@@ -1294,7 +1294,7 @@ private class G2ReceiveManager {
|
|
|
1294
1294
|
let isLast = (serialNum == totalPackets)
|
|
1295
1295
|
let hasCrc = isLast
|
|
1296
1296
|
let payloadEnd = 8 + payloadLen - (hasCrc ? 2 : 0)
|
|
1297
|
-
let payload = rawData[8..<payloadEnd]
|
|
1297
|
+
let payload = rawData[8 ..< payloadEnd]
|
|
1298
1298
|
|
|
1299
1299
|
let syncId = rawData[2]
|
|
1300
1300
|
// Key partials by source peripheral too — left and right glasses have independent syncId counters
|
|
@@ -1372,7 +1372,7 @@ private final class ImgAckBox {
|
|
|
1372
1372
|
}
|
|
1373
1373
|
self.session = nil
|
|
1374
1374
|
self.fragment = nil
|
|
1375
|
-
|
|
1375
|
+
cont = nil
|
|
1376
1376
|
lock.unlock()
|
|
1377
1377
|
c.resume(returning: success)
|
|
1378
1378
|
return true
|
|
@@ -1391,7 +1391,7 @@ actor G2ReconnectionManager {
|
|
|
1391
1391
|
private var task: Task<Void, Never>?
|
|
1392
1392
|
private let intervalSeconds: TimeInterval
|
|
1393
1393
|
private var attempts = 0
|
|
1394
|
-
private let maxAttempts: Int
|
|
1394
|
+
private let maxAttempts: Int // -1 for unlimited
|
|
1395
1395
|
|
|
1396
1396
|
init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
|
|
1397
1397
|
self.intervalSeconds = intervalSeconds
|
|
@@ -1467,11 +1467,11 @@ class G2: NSObject, SGCManager {
|
|
|
1467
1467
|
private var pairingTimeoutTimer: DispatchWorkItem?
|
|
1468
1468
|
private var useEvenDashboard = true
|
|
1469
1469
|
private var dashboardShowing = 0
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1470
|
+
/// The 08011A00 gesture_ctrl event is ambiguous: the firmware sends it BOTH when the dashboard
|
|
1471
|
+
/// opens (it shuts our page down to take the screen) and when it closes (returns to us). When
|
|
1472
|
+
/// showDashboard() runs we set this latch; the next 08011A00 is the OPEN confirm — consume it
|
|
1473
|
+
/// WITHOUT recovering (else we rebuild our page and snatch the screen back from the dashboard).
|
|
1474
|
+
/// The following 08011A00 is the real CLOSE → recover.
|
|
1475
1475
|
private var dashboardOpening = false
|
|
1476
1476
|
// Recovery throttle: the firmware spams systemExit + dashboard-close ~1×/sec on its own.
|
|
1477
1477
|
// Coalesce so recovery can't storm — one rebuild in flight, one per RECOVERY_DEBOUNCE_MS.
|
|
@@ -1530,7 +1530,7 @@ class G2: NSObject, SGCManager {
|
|
|
1530
1530
|
private let sendManager = G2SendManager()
|
|
1531
1531
|
private let receiveManager = G2ReceiveManager()
|
|
1532
1532
|
private var foregroundObserver: NSObjectProtocol?
|
|
1533
|
-
private var startupPageCreated: Bool = false
|
|
1533
|
+
private var startupPageCreated: Bool = false // createStartUpPageContainer can only be called once
|
|
1534
1534
|
private var pageCreated: Bool = false
|
|
1535
1535
|
private var pageGeneration: UInt64 = 0
|
|
1536
1536
|
private var lastImuReportTimestamp: Int64?
|
|
@@ -1557,7 +1557,7 @@ class G2: NSObject, SGCManager {
|
|
|
1557
1557
|
/// Wakes the reconcile loop the instant a container is marked dirty, instead of waiting out the
|
|
1558
1558
|
/// idle tick. `signalDisplayDirty()` (and the ticker) yield into this; the loop drains it.
|
|
1559
1559
|
private var displayDirtySignal: AsyncStream<Void>.Continuation?
|
|
1560
|
-
private let IMG_ACK_TIMEOUT_NS: UInt64 = 2_000_000_000
|
|
1560
|
+
private let IMG_ACK_TIMEOUT_NS: UInt64 = 2_000_000_000 // 1000ms timeout (matches Dart host)
|
|
1561
1561
|
private let IMG_MAX_ATTEMPTS = 3
|
|
1562
1562
|
private var heartbeatTask: Task<Void, Never>?
|
|
1563
1563
|
private var heartbeatCounter: Int = 0
|
|
@@ -1588,6 +1588,7 @@ class G2: NSObject, SGCManager {
|
|
|
1588
1588
|
var name: String {
|
|
1589
1589
|
"img-\(id)"
|
|
1590
1590
|
}
|
|
1591
|
+
|
|
1591
1592
|
var bmpData: Data
|
|
1592
1593
|
/// Set true when `bmpData` changes and the new pixels haven't been pushed to the glasses yet.
|
|
1593
1594
|
/// The reconcile loop (see `displayReconcileTask`) is the sole sender; it clears this once the
|
|
@@ -1673,6 +1674,7 @@ class G2: NSObject, SGCManager {
|
|
|
1673
1674
|
}
|
|
1674
1675
|
return rects
|
|
1675
1676
|
}
|
|
1677
|
+
|
|
1676
1678
|
private static let defaultImgContainer = (
|
|
1677
1679
|
x: Int32(188), y: Int32(44), width: Int32(200), height: Int32(100)
|
|
1678
1680
|
)
|
|
@@ -1718,8 +1720,8 @@ class G2: NSObject, SGCManager {
|
|
|
1718
1720
|
private var rightWriteQueue: [Data] = []
|
|
1719
1721
|
private var leftDraining = false
|
|
1720
1722
|
private var rightDraining = false
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
+
/// Pace between consecutive packets (~G1's chunk pacing). Off any external callback, so the drain
|
|
1724
|
+
/// keeps making progress in the background instead of waiting for a callback iOS won't deliver.
|
|
1723
1725
|
private let writePaceNanos: UInt64 = 6_000_000
|
|
1724
1726
|
// Diagnostic: warn if a side's queue ever backs up (it shouldn't now — the drainer is always
|
|
1725
1727
|
// making progress). Rate-limited. Prefixed "BGCAP:" so it's easy to grep/strip after validation.
|
|
@@ -1757,7 +1759,7 @@ class G2: NSObject, SGCManager {
|
|
|
1757
1759
|
private func drainLoop(right: Bool) async {
|
|
1758
1760
|
while true {
|
|
1759
1761
|
guard let peripheral = right ? rightPeripheral : leftPeripheral,
|
|
1760
|
-
|
|
1762
|
+
let char = right ? rightWriteChar : leftWriteChar
|
|
1761
1763
|
else {
|
|
1762
1764
|
// No connection for this side; drop pending packets so they can't replay later.
|
|
1763
1765
|
if right { rightWriteQueue.removeAll(); rightDraining = false }
|
|
@@ -1907,44 +1909,44 @@ class G2: NSObject, SGCManager {
|
|
|
1907
1909
|
// Small delay then auth right + pipe role change + time sync
|
|
1908
1910
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1909
1911
|
|
|
1910
|
-
let authR = DevSettingsProto.authCmd(magicRandom:
|
|
1911
|
-
|
|
1912
|
+
let authR = DevSettingsProto.authCmd(magicRandom: sendManager.nextMagicRandom())
|
|
1913
|
+
sendDevSettingsCommand(authR, left: false, right: true)
|
|
1912
1914
|
|
|
1913
1915
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1914
1916
|
|
|
1915
1917
|
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1916
|
-
magicRandom:
|
|
1918
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1917
1919
|
)
|
|
1918
|
-
|
|
1920
|
+
sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1919
1921
|
|
|
1920
1922
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1921
1923
|
|
|
1922
1924
|
let timeSync = DevSettingsProto.timeSync(
|
|
1923
|
-
magicRandom:
|
|
1925
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1924
1926
|
)
|
|
1925
|
-
|
|
1927
|
+
sendDevSettingsCommand(timeSync, left: true, right: true)
|
|
1926
1928
|
|
|
1927
1929
|
// Skip onboarding on connect
|
|
1928
1930
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1929
1931
|
let onboarding = OnboardingProto.skipOnboarding(
|
|
1930
|
-
magicRandom:
|
|
1932
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1931
1933
|
)
|
|
1932
|
-
|
|
1934
|
+
sendOnboardingCommand(onboarding)
|
|
1933
1935
|
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1934
1936
|
|
|
1935
1937
|
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1936
1938
|
var gestureInitW = ProtobufWriter()
|
|
1937
1939
|
gestureInitW.writeInt32Field(1, 0)
|
|
1938
|
-
gestureInitW.writeInt32Field(2,
|
|
1939
|
-
|
|
1940
|
+
gestureInitW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1941
|
+
sendGestureCtrlCommand(gestureInitW.data)
|
|
1940
1942
|
|
|
1941
1943
|
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1942
1944
|
var uiSettW = ProtobufWriter()
|
|
1943
|
-
uiSettW.writeInt32Field(1, 2)
|
|
1944
|
-
uiSettW.writeInt32Field(2,
|
|
1945
|
-
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00]))
|
|
1946
|
-
|
|
1947
|
-
|
|
1945
|
+
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1946
|
+
uiSettW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1947
|
+
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
|
|
1948
|
+
sendToGlasses(
|
|
1949
|
+
sendManager.buildPackets(
|
|
1948
1950
|
serviceId: 0x0C, payload: uiSettW.data, reserveFlag: true
|
|
1949
1951
|
)
|
|
1950
1952
|
)
|
|
@@ -1953,30 +1955,30 @@ class G2: NSObject, SGCManager {
|
|
|
1953
1955
|
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1954
1956
|
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1955
1957
|
var dashDisplayW = ProtobufWriter()
|
|
1956
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
1957
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
1958
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
1959
|
-
dashDisplayW.writeInt32Field(4, 4)
|
|
1958
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1959
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1960
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1961
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1960
1962
|
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1961
|
-
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
1962
|
-
dashDisplayW.writeInt32Field(6,
|
|
1963
|
-
dashDisplayW.writeInt32Field(7,
|
|
1963
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1964
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1965
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1964
1966
|
|
|
1965
1967
|
var dashRecvW = ProtobufWriter()
|
|
1966
1968
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1967
1969
|
|
|
1968
1970
|
var dashPkgW = ProtobufWriter()
|
|
1969
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
1970
|
-
dashPkgW.writeInt32Field(2,
|
|
1971
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1972
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1971
1973
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1972
|
-
|
|
1974
|
+
sendDashboardCommand(dashPkgW.data)
|
|
1973
1975
|
|
|
1974
1976
|
// Disable "Hey Even" wakeword on connect
|
|
1975
1977
|
let heyEvenOff = EvenAIProto.setHeyEven(
|
|
1976
|
-
magicRandom:
|
|
1978
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
1977
1979
|
enabled: false
|
|
1978
1980
|
)
|
|
1979
|
-
|
|
1981
|
+
sendEvenAICommand(heyEvenOff)
|
|
1980
1982
|
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1981
1983
|
|
|
1982
1984
|
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5, field7={1:1})
|
|
@@ -2010,45 +2012,52 @@ class G2: NSObject, SGCManager {
|
|
|
2010
2012
|
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
2011
2013
|
|
|
2012
2014
|
// Start heartbeats after auth
|
|
2013
|
-
|
|
2015
|
+
startHeartbeats()
|
|
2014
2016
|
|
|
2015
2017
|
Task { await self.reconnectionManager.stop() }
|
|
2016
2018
|
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
2017
2019
|
|
|
2018
2020
|
// Set device_name so DeviceManager can save it for reconnection
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2021
|
+
let peripheralName = rightPeripheral?.name ?? leftPeripheral?.name
|
|
2022
|
+
if let serialNumber = G2SerialResolution.resolve(
|
|
2023
|
+
scannedSerial: peripheralName.flatMap { deviceNameToSerialNumber[$0] },
|
|
2024
|
+
requestedId: DEVICE_SEARCH_ID,
|
|
2025
|
+
persistedDeviceName: DeviceStore.shared.get("bluetooth", "device_name") as? String ?? ""
|
|
2026
|
+
) {
|
|
2023
2027
|
DeviceStore.shared.apply("bluetooth", "device_name", serialNumber)
|
|
2028
|
+
// The advertisement serial is the manufacturing serial; expose it where
|
|
2029
|
+
// the SDK status (and analytics identification) read it, not only in the
|
|
2030
|
+
// reconnection name slot. Cached reconnects skip the scan, so the
|
|
2031
|
+
// persisted name is the serial source there (see G2SerialResolution).
|
|
2032
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2024
2033
|
Bridge.log("G2: Set device_name to \(serialNumber)")
|
|
2025
2034
|
}
|
|
2026
2035
|
|
|
2027
2036
|
// Set bluetooth name and device model for Device Info page
|
|
2028
2037
|
let btName =
|
|
2029
|
-
|
|
2030
|
-
|
|
2038
|
+
rightPeripheral?.name
|
|
2039
|
+
?? leftPeripheral?.name ?? ""
|
|
2031
2040
|
DeviceStore.shared.apply("glasses", "bluetoothName", btName)
|
|
2032
2041
|
DeviceStore.shared.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
2033
2042
|
|
|
2034
|
-
|
|
2043
|
+
setFullyConnected()
|
|
2035
2044
|
|
|
2036
2045
|
// connnect a controller if we have one:
|
|
2037
|
-
|
|
2046
|
+
connectController()
|
|
2038
2047
|
|
|
2039
2048
|
// Query version + battery info from glasses
|
|
2040
|
-
|
|
2049
|
+
requestDeviceInfo()
|
|
2041
2050
|
|
|
2042
2051
|
// send dashboard menu if we have stored items
|
|
2043
|
-
|
|
2052
|
+
sendMenuApps()
|
|
2044
2053
|
|
|
2045
2054
|
// order the calendar (Schedule) widget first on the dashboard
|
|
2046
|
-
|
|
2055
|
+
setCalendarWidgetFirst()
|
|
2047
2056
|
|
|
2048
2057
|
// send calendar events
|
|
2049
2058
|
let calendarEvents =
|
|
2050
2059
|
DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
2051
|
-
|
|
2060
|
+
sendCalendarEvents(calendarEvents)
|
|
2052
2061
|
}
|
|
2053
2062
|
|
|
2054
2063
|
// MARK: - Heartbeats
|
|
@@ -2169,8 +2178,8 @@ class G2: NSObject, SGCManager {
|
|
|
2169
2178
|
)
|
|
2170
2179
|
}
|
|
2171
2180
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2181
|
+
/// Protocol witness for SGCManager.sendText — G2 renders a simple string as a
|
|
2182
|
+
/// default-positioned text wall. The positioned variant is `sendTextAt`.
|
|
2174
2183
|
func sendText(_ text: String) async {
|
|
2175
2184
|
await sendTextWall(text)
|
|
2176
2185
|
}
|
|
@@ -2222,7 +2231,8 @@ class G2: NSObject, SGCManager {
|
|
|
2222
2231
|
if let i = textContainers.firstIndex(where: {
|
|
2223
2232
|
$0.matches(
|
|
2224
2233
|
x: rx, y: ry, width: rw, height: rh, borderWidth: borderWidth,
|
|
2225
|
-
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2234
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2235
|
+
)
|
|
2226
2236
|
}) {
|
|
2227
2237
|
textContainers[i].content = content
|
|
2228
2238
|
textContainers[i].pendingSends = 1 + EVEN_HUB_RESEND_COUNT
|
|
@@ -2247,7 +2257,8 @@ class G2: NSObject, SGCManager {
|
|
|
2247
2257
|
|
|
2248
2258
|
let container = addTextContainer(
|
|
2249
2259
|
x: rx, y: ry, width: rw, height: rh, content: content, borderWidth: borderWidth,
|
|
2250
|
-
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2260
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2261
|
+
)
|
|
2251
2262
|
Bridge.log(
|
|
2252
2263
|
"G2: sendText() - added text container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page"
|
|
2253
2264
|
)
|
|
@@ -2412,7 +2423,8 @@ class G2: NSObject, SGCManager {
|
|
|
2412
2423
|
$0.matches(
|
|
2413
2424
|
x: x, y: y, width: width, height: height, borderWidth: borderWidth,
|
|
2414
2425
|
borderColor: G2.defaultTextContainer.borderColor, borderRadius: borderRadius,
|
|
2415
|
-
paddingLength: G2.defaultTextContainer.paddingLength
|
|
2426
|
+
paddingLength: G2.defaultTextContainer.paddingLength
|
|
2427
|
+
)
|
|
2416
2428
|
}) {
|
|
2417
2429
|
let cid = textContainers[i].id
|
|
2418
2430
|
// The container id may have been LRU-recycled from another element.
|
|
@@ -2481,7 +2493,7 @@ class G2: NSObject, SGCManager {
|
|
|
2481
2493
|
var tilePixels = Data(capacity: Int(t.w * t.h))
|
|
2482
2494
|
for row in 0 ..< Int(t.h) {
|
|
2483
2495
|
let start = (Int(t.dy) + row) * Int(width) + Int(t.dx)
|
|
2484
|
-
tilePixels.append(gray.subdata(in: (gray.startIndex + start)..<(gray.startIndex + start + Int(t.w))))
|
|
2496
|
+
tilePixels.append(gray.subdata(in: (gray.startIndex + start) ..< (gray.startIndex + start + Int(t.w))))
|
|
2485
2497
|
}
|
|
2486
2498
|
guard let bmp = build4BitBmp(grayscalePixels: tilePixels, width: Int(t.w), height: Int(t.h)) else {
|
|
2487
2499
|
Bridge.log("G2: drawLayoutBitmap - tile encode failed")
|
|
@@ -2659,7 +2671,7 @@ class G2: NSObject, SGCManager {
|
|
|
2659
2671
|
// "G2: sendImageData(\(containerName)) - \(fragmentCount) fragments, \(bmpData.count) bytes"
|
|
2660
2672
|
// )
|
|
2661
2673
|
|
|
2662
|
-
for
|
|
2674
|
+
for _ in 1 ... IMG_MAX_ATTEMPTS {
|
|
2663
2675
|
// One session id per WHOLE image transfer (per attempt). The glasses key their
|
|
2664
2676
|
// reassembly buffer on MapSessionId, so every fragment of this image must reuse the
|
|
2665
2677
|
// same session id with an incrementing MapFragmentIndex; the per-fragment ACK is
|
|
@@ -2676,7 +2688,7 @@ class G2: NSObject, SGCManager {
|
|
|
2676
2688
|
// }
|
|
2677
2689
|
while offset < bmpData.count {
|
|
2678
2690
|
let end = min(offset + fragmentSize, bmpData.count)
|
|
2679
|
-
let fragment = bmpData[offset..<end]
|
|
2691
|
+
let fragment = bmpData[offset ..< end]
|
|
2680
2692
|
|
|
2681
2693
|
let msg = EvenHubProto.updateImageRawDataMessage(
|
|
2682
2694
|
containerID: containerID,
|
|
@@ -2865,7 +2877,7 @@ class G2: NSObject, SGCManager {
|
|
|
2865
2877
|
// keeps being re-dirtied mid-send can't spin this pass forever (next tick picks it up).
|
|
2866
2878
|
var guardCount = 0
|
|
2867
2879
|
while pageCreated, guardCount < imageContainerIDPool.count,
|
|
2868
|
-
|
|
2880
|
+
let i = imageContainers.firstIndex(where: { $0.dirty })
|
|
2869
2881
|
{
|
|
2870
2882
|
guardCount += 1
|
|
2871
2883
|
let container = imageContainers[i]
|
|
@@ -2883,7 +2895,7 @@ class G2: NSObject, SGCManager {
|
|
|
2883
2895
|
// Only settle the flag if it's still empty — a displayBitmap during the await would
|
|
2884
2896
|
// have set new bytes, so leave it dirty for the next pass to send the real image.
|
|
2885
2897
|
if let j = imageContainers.firstIndex(where: { $0.id == container.id }),
|
|
2886
|
-
|
|
2898
|
+
imageContainers[j].bmpData.isEmpty
|
|
2887
2899
|
{
|
|
2888
2900
|
imageContainers[j].dirty = false
|
|
2889
2901
|
}
|
|
@@ -2894,7 +2906,7 @@ class G2: NSObject, SGCManager {
|
|
|
2894
2906
|
)
|
|
2895
2907
|
// Re-find by id: the array may have shifted (eviction) during the await.
|
|
2896
2908
|
if let j = imageContainers.firstIndex(where: { $0.id == container.id }),
|
|
2897
|
-
|
|
2909
|
+
imageContainers[j].bmpData == sentBytes
|
|
2898
2910
|
{
|
|
2899
2911
|
imageContainers[j].dirty = false
|
|
2900
2912
|
}
|
|
@@ -2915,7 +2927,8 @@ class G2: NSObject, SGCManager {
|
|
|
2915
2927
|
let usedIDs = Set(imageContainers.map { $0.id })
|
|
2916
2928
|
let id = imageContainerIDPool.first { !usedIDs.contains($0) } ?? imageContainerIDPool[0]
|
|
2917
2929
|
let container = ImgContainer(
|
|
2918
|
-
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData
|
|
2930
|
+
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData
|
|
2931
|
+
)
|
|
2919
2932
|
imageContainers.append(container)
|
|
2920
2933
|
return container
|
|
2921
2934
|
}
|
|
@@ -2935,7 +2948,8 @@ class G2: NSObject, SGCManager {
|
|
|
2935
2948
|
let container = TextContainer(
|
|
2936
2949
|
id: id, x: x, y: y, width: width, height: height, content: content,
|
|
2937
2950
|
borderWidth: borderWidth, borderColor: borderColor, borderRadius: borderRadius,
|
|
2938
|
-
paddingLength: paddingLength
|
|
2951
|
+
paddingLength: paddingLength
|
|
2952
|
+
)
|
|
2939
2953
|
textContainers.append(container)
|
|
2940
2954
|
return container
|
|
2941
2955
|
}
|
|
@@ -2945,18 +2959,18 @@ class G2: NSObject, SGCManager {
|
|
|
2945
2959
|
let msg = EvenHubProto.shutdownMessage()
|
|
2946
2960
|
sendEvenHubCommand(msg)
|
|
2947
2961
|
pageCreated = false
|
|
2948
|
-
try? await Task.sleep(nanoseconds: 300_000_000)// 300ms to settle
|
|
2962
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2949
2963
|
// we will automatically rebuild state when we detect the glasses shutdown:
|
|
2950
2964
|
// await rebuildState()
|
|
2951
2965
|
}
|
|
2952
2966
|
|
|
2953
|
-
|
|
2967
|
+
/// re-creates the containers and re-sends all images to the glasses:
|
|
2954
2968
|
private func rebuildState() async {
|
|
2955
2969
|
Bridge.log("G2: rebuildState()")
|
|
2956
2970
|
// recreate the containers (sets pageCreated = true; embeds text content directly):
|
|
2957
2971
|
createPageWithContainers()
|
|
2958
2972
|
|
|
2959
|
-
try? await Task.sleep(nanoseconds: 300_000_000)
|
|
2973
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2960
2974
|
// Mark every image container dirty and let the reconcile loop re-send them, one at a time.
|
|
2961
2975
|
// Doing the sends here directly is what used to race a concurrent displayBitmap and clobber
|
|
2962
2976
|
// imgAckBox; routing through the dirty flag keeps a single sender (see displayReconcileTask).
|
|
@@ -3024,7 +3038,7 @@ class G2: NSObject, SGCManager {
|
|
|
3024
3038
|
return nil
|
|
3025
3039
|
}
|
|
3026
3040
|
|
|
3027
|
-
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3
|
|
3041
|
+
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3 // 4-bit rows padded to 4 bytes
|
|
3028
3042
|
let pixelDataOffset = headerSize
|
|
3029
3043
|
|
|
3030
3044
|
let dstWidth = srcWidth * 2
|
|
@@ -3057,7 +3071,7 @@ class G2: NSObject, SGCManager {
|
|
|
3057
3071
|
dst.appendLittleEndian(UInt32(0))
|
|
3058
3072
|
|
|
3059
3073
|
// --- Color Table (same 16-entry grayscale) ---
|
|
3060
|
-
for i in 0..<16 {
|
|
3074
|
+
for i in 0 ..< 16 {
|
|
3061
3075
|
let val = UInt8(i * 17)
|
|
3062
3076
|
dst.append(contentsOf: [val, val, val, 0])
|
|
3063
3077
|
}
|
|
@@ -3065,12 +3079,12 @@ class G2: NSObject, SGCManager {
|
|
|
3065
3079
|
// --- Pixel Data (nearest-neighbor 2x upscale) ---
|
|
3066
3080
|
// BMP is bottom-up, so row 0 = bottom of image
|
|
3067
3081
|
// Each dst row maps to srcRow = dstRow / 2
|
|
3068
|
-
for dstRow in 0..<dstHeight {
|
|
3082
|
+
for dstRow in 0 ..< dstHeight {
|
|
3069
3083
|
let srcRow = dstRow / 2
|
|
3070
3084
|
let srcRowOffset = pixelDataOffset + srcRow * srcPaddedRowSize
|
|
3071
3085
|
var rowBuf = [UInt8](repeating: 0, count: dstPaddedRowSize)
|
|
3072
3086
|
|
|
3073
|
-
for dstCol in 0..<dstWidth {
|
|
3087
|
+
for dstCol in 0 ..< dstWidth {
|
|
3074
3088
|
let srcCol = dstCol / 2
|
|
3075
3089
|
|
|
3076
3090
|
// Read 4-bit nibble from source
|
|
@@ -3145,7 +3159,7 @@ class G2: NSObject, SGCManager {
|
|
|
3145
3159
|
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
3146
3160
|
|
|
3147
3161
|
guard let renderedImage = ctx.makeImage(),
|
|
3148
|
-
|
|
3162
|
+
let pixels = renderedImage.dataProvider?.data as Data?
|
|
3149
3163
|
else {
|
|
3150
3164
|
Bridge.log("G2: convertToG2Bmp - failed to get pixel data")
|
|
3151
3165
|
return nil
|
|
@@ -3168,7 +3182,7 @@ class G2: NSObject, SGCManager {
|
|
|
3168
3182
|
/// unlit, so an all-zero frame reads as blank. Used by the reconcile loop to clear a bitmap.
|
|
3169
3183
|
private func blankBmp(width: Int, height: Int) -> Data? {
|
|
3170
3184
|
guard width > 0, height > 0 else { return nil }
|
|
3171
|
-
let zeros = Data(count: width * height)
|
|
3185
|
+
let zeros = Data(count: width * height) // all-zero 8-bit grayscale = black
|
|
3172
3186
|
return build4BitBmp(grayscalePixels: zeros, width: width, height: height)
|
|
3173
3187
|
}
|
|
3174
3188
|
|
|
@@ -3176,8 +3190,8 @@ class G2: NSObject, SGCManager {
|
|
|
3176
3190
|
/// BMP rows are stored bottom-up. Each row is padded to a 4-byte boundary.
|
|
3177
3191
|
private func build4BitBmp(grayscalePixels: Data, width: Int, height: Int) -> Data? {
|
|
3178
3192
|
// 4-bit: 2 pixels per byte, rows padded to 4-byte boundary
|
|
3179
|
-
let bytesPerRow4bit = (width + 1) / 2
|
|
3180
|
-
let paddedRowSize = (bytesPerRow4bit + 3) & ~3
|
|
3193
|
+
let bytesPerRow4bit = (width + 1) / 2 // ceil(width / 2)
|
|
3194
|
+
let paddedRowSize = (bytesPerRow4bit + 3) & ~3 // pad to 4-byte boundary
|
|
3181
3195
|
let pixelDataSize = paddedRowSize * height
|
|
3182
3196
|
|
|
3183
3197
|
// BMP file header (14 bytes) + DIB header (40 bytes) + color table (16 * 4 = 64 bytes)
|
|
@@ -3187,46 +3201,46 @@ class G2: NSObject, SGCManager {
|
|
|
3187
3201
|
var bmp = Data(capacity: fileSize)
|
|
3188
3202
|
|
|
3189
3203
|
// --- BMP File Header (14 bytes) ---
|
|
3190
|
-
bmp.append(contentsOf: [0x42, 0x4D])
|
|
3191
|
-
bmp.appendLittleEndian(UInt32(fileSize))
|
|
3192
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
3193
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
3194
|
-
bmp.appendLittleEndian(UInt32(headerSize))
|
|
3204
|
+
bmp.append(contentsOf: [0x42, 0x4D]) // "BM" signature
|
|
3205
|
+
bmp.appendLittleEndian(UInt32(fileSize)) // File size
|
|
3206
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved1
|
|
3207
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved2
|
|
3208
|
+
bmp.appendLittleEndian(UInt32(headerSize)) // Pixel data offset
|
|
3195
3209
|
|
|
3196
3210
|
// --- DIB Header (BITMAPINFOHEADER, 40 bytes) ---
|
|
3197
|
-
bmp.appendLittleEndian(UInt32(40))
|
|
3198
|
-
bmp.appendLittleEndian(Int32(width))
|
|
3199
|
-
bmp.appendLittleEndian(Int32(height))
|
|
3200
|
-
bmp.appendLittleEndian(UInt16(1))
|
|
3201
|
-
bmp.appendLittleEndian(UInt16(4))
|
|
3202
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
3203
|
-
bmp.appendLittleEndian(UInt32(pixelDataSize))
|
|
3204
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
3205
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
3206
|
-
bmp.appendLittleEndian(UInt32(16))
|
|
3207
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
3211
|
+
bmp.appendLittleEndian(UInt32(40)) // DIB header size
|
|
3212
|
+
bmp.appendLittleEndian(Int32(width)) // Width
|
|
3213
|
+
bmp.appendLittleEndian(Int32(height)) // Height (positive = bottom-up)
|
|
3214
|
+
bmp.appendLittleEndian(UInt16(1)) // Color planes
|
|
3215
|
+
bmp.appendLittleEndian(UInt16(4)) // Bits per pixel (4-bit)
|
|
3216
|
+
bmp.appendLittleEndian(UInt32(0)) // Compression (none)
|
|
3217
|
+
bmp.appendLittleEndian(UInt32(pixelDataSize)) // Image size
|
|
3218
|
+
bmp.appendLittleEndian(Int32(2835)) // X pixels/meter (~72 DPI)
|
|
3219
|
+
bmp.appendLittleEndian(Int32(2835)) // Y pixels/meter
|
|
3220
|
+
bmp.appendLittleEndian(UInt32(16)) // Colors used
|
|
3221
|
+
bmp.appendLittleEndian(UInt32(0)) // Important colors (0 = all)
|
|
3208
3222
|
|
|
3209
3223
|
// --- Color Table (16 entries, 4 bytes each: B, G, R, 0) ---
|
|
3210
|
-
for i in 0..<16 {
|
|
3211
|
-
let val = UInt8(i * 17)
|
|
3212
|
-
bmp.append(contentsOf: [val, val, val, 0])
|
|
3224
|
+
for i in 0 ..< 16 {
|
|
3225
|
+
let val = UInt8(i * 17) // 0, 17, 34, ... 255 (evenly spaced grayscale)
|
|
3226
|
+
bmp.append(contentsOf: [val, val, val, 0]) // B, G, R, Reserved
|
|
3213
3227
|
}
|
|
3214
3228
|
|
|
3215
3229
|
// --- Pixel Data (bottom-up rows, 4-bit packed) ---
|
|
3216
3230
|
let rowBytes = [UInt8](repeating: 0, count: paddedRowSize)
|
|
3217
|
-
for row in 0..<height {
|
|
3231
|
+
for row in 0 ..< height {
|
|
3218
3232
|
// BMP is bottom-up: row 0 in BMP = last row of image
|
|
3219
3233
|
let srcRow = height - 1 - row
|
|
3220
3234
|
let srcOffset = srcRow * width
|
|
3221
3235
|
var rowBuf = rowBytes
|
|
3222
3236
|
|
|
3223
|
-
for col in 0..<width {
|
|
3237
|
+
for col in 0 ..< width {
|
|
3224
3238
|
let pixelIndex = srcOffset + col
|
|
3225
3239
|
guard pixelIndex < grayscalePixels.count else { continue }
|
|
3226
3240
|
|
|
3227
3241
|
// Map 8-bit grayscale (0-255) to 4-bit index (0-15)
|
|
3228
3242
|
let gray8 = grayscalePixels[pixelIndex]
|
|
3229
|
-
let index4 = gray8 >> 4
|
|
3243
|
+
let index4 = gray8 >> 4 // divide by 16
|
|
3230
3244
|
|
|
3231
3245
|
let bytePos = col / 2
|
|
3232
3246
|
if col % 2 == 0 {
|
|
@@ -3258,7 +3272,7 @@ class G2: NSObject, SGCManager {
|
|
|
3258
3272
|
let msg = EvenHubProto.shutdownMessage()
|
|
3259
3273
|
sendEvenHubCommand(msg)
|
|
3260
3274
|
pageCreated = false
|
|
3261
|
-
evenHubMicActive = false
|
|
3275
|
+
evenHubMicActive = false // dashboard takes EvenHub focus; firmware kills the mic
|
|
3262
3276
|
currentBitmapBase64 = ""
|
|
3263
3277
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
3264
3278
|
guard let self = self else { return }
|
|
@@ -3280,20 +3294,20 @@ class G2: NSObject, SGCManager {
|
|
|
3280
3294
|
|
|
3281
3295
|
func sendDashboardDisplaySettings() {
|
|
3282
3296
|
var dashDisplayW = ProtobufWriter()
|
|
3283
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
3284
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
3285
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
3286
|
-
dashDisplayW.writeInt32Field(4, 4)
|
|
3297
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
3298
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
3299
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
3300
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
3287
3301
|
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
3288
3302
|
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
3289
|
-
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat())
|
|
3290
|
-
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit())
|
|
3303
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
3304
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
3291
3305
|
|
|
3292
3306
|
var dashRecvW = ProtobufWriter()
|
|
3293
3307
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
3294
3308
|
|
|
3295
3309
|
var dashPkgW = ProtobufWriter()
|
|
3296
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
3310
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
3297
3311
|
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3298
3312
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
3299
3313
|
sendDashboardCommand(dashPkgW.data)
|
|
@@ -3360,8 +3374,8 @@ class G2: NSObject, SGCManager {
|
|
|
3360
3374
|
let total = Int32(events.count)
|
|
3361
3375
|
for (i, ev) in events.enumerated() {
|
|
3362
3376
|
guard let title = ev["title"] as? String,
|
|
3363
|
-
|
|
3364
|
-
|
|
3377
|
+
let time = ev["time"] as? String,
|
|
3378
|
+
let endTs = ev["endDate"] as? Double
|
|
3365
3379
|
else { continue }
|
|
3366
3380
|
let location = ev["location"] as? String
|
|
3367
3381
|
sendCalendarEvent(
|
|
@@ -3440,7 +3454,8 @@ class G2: NSObject, SGCManager {
|
|
|
3440
3454
|
paddingLength: c.paddingLength, containerID: c.id,
|
|
3441
3455
|
containerName: c.name, isEventCapture: false,
|
|
3442
3456
|
content: c.content
|
|
3443
|
-
)
|
|
3457
|
+
)
|
|
3458
|
+
)
|
|
3444
3459
|
}
|
|
3445
3460
|
|
|
3446
3461
|
// Page-composition dump: one line per container on every page create,
|
|
@@ -3819,7 +3834,7 @@ class G2: NSObject, SGCManager {
|
|
|
3819
3834
|
|
|
3820
3835
|
let enterPayload = EvenAIProto.aiCtrl(
|
|
3821
3836
|
magicRandom: sendManager.nextMagicRandom(),
|
|
3822
|
-
status: 2
|
|
3837
|
+
status: 2 // EVEN_AI_ENTER
|
|
3823
3838
|
)
|
|
3824
3839
|
sendEvenAICommand(enterPayload)
|
|
3825
3840
|
|
|
@@ -3833,7 +3848,7 @@ class G2: NSObject, SGCManager {
|
|
|
3833
3848
|
|
|
3834
3849
|
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
3835
3850
|
triggerSkill(
|
|
3836
|
-
3, skillParam: 1,
|
|
3851
|
+
3, skillParam: 1, // NOTIFICATION, show
|
|
3837
3852
|
text: " ",
|
|
3838
3853
|
streamEnable: 1, fTextEnd: 1
|
|
3839
3854
|
)
|
|
@@ -3866,8 +3881,8 @@ class G2: NSObject, SGCManager {
|
|
|
3866
3881
|
/// the wearer should look around until `…{status:"complete"}`.
|
|
3867
3882
|
func startCompass() {
|
|
3868
3883
|
var w = ProtobufWriter()
|
|
3869
|
-
w.writeInt32Field(1, NavigationCmd.appRequestStartUp.rawValue)
|
|
3870
|
-
w.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3884
|
+
w.writeInt32Field(1, NavigationCmd.appRequestStartUp.rawValue) // cmd
|
|
3885
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
|
|
3871
3886
|
sendNavigationCommand(w.data)
|
|
3872
3887
|
}
|
|
3873
3888
|
|
|
@@ -3943,19 +3958,19 @@ class G2: NSObject, SGCManager {
|
|
|
3943
3958
|
let widgetOrder: [UInt8] = [3, 1, 2, 4, 5]
|
|
3944
3959
|
|
|
3945
3960
|
var dashDisplayW = ProtobufWriter()
|
|
3946
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
3947
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
3948
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
3949
|
-
dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count))
|
|
3950
|
-
dashDisplayW.writeMessageField(5, Data(widgetOrder))
|
|
3951
|
-
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat())
|
|
3952
|
-
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit())
|
|
3961
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
3962
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
3963
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
3964
|
+
dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count)) // widgetDisplayCount
|
|
3965
|
+
dashDisplayW.writeMessageField(5, Data(widgetOrder)) // widgetDisplayOrder: Schedule first
|
|
3966
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
3967
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
3953
3968
|
|
|
3954
3969
|
var dashRecvW = ProtobufWriter()
|
|
3955
3970
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
3956
3971
|
|
|
3957
3972
|
var dashPkgW = ProtobufWriter()
|
|
3958
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
3973
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
3959
3974
|
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3960
3975
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
3961
3976
|
sendDashboardCommand(dashPkgW.data)
|
|
@@ -3966,7 +3981,7 @@ class G2: NSObject, SGCManager {
|
|
|
3966
3981
|
func setDashboardMenu(_ items: [[String: Any]]) {
|
|
3967
3982
|
let menuItems = items.compactMap { dict -> MenuProto.MenuItem? in
|
|
3968
3983
|
guard let name = dict["name"] as? String,
|
|
3969
|
-
|
|
3984
|
+
let packageName = dict["packageName"] as? String
|
|
3970
3985
|
else { return nil }
|
|
3971
3986
|
let running = dict["running"] as? Bool ?? false
|
|
3972
3987
|
return MenuProto.MenuItem(packageName: packageName, name: name, running: running)
|
|
@@ -4051,7 +4066,7 @@ class G2: NSObject, SGCManager {
|
|
|
4051
4066
|
func sendWifiCredentials(_: String, _: String) {}
|
|
4052
4067
|
func forgetWifiNetwork(_: String) {}
|
|
4053
4068
|
func sendHotspotState(_: Bool) {}
|
|
4054
|
-
func sendOtaStart(otaVersionUrl: String?) {}
|
|
4069
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
4055
4070
|
func sendOtaQueryStatus() {}
|
|
4056
4071
|
|
|
4057
4072
|
// MARK: - SGCManager: User Context
|
|
@@ -4132,7 +4147,7 @@ class G2: NSObject, SGCManager {
|
|
|
4132
4147
|
centralManager!.scanForPeripherals(
|
|
4133
4148
|
withServices: nil,
|
|
4134
4149
|
options: [
|
|
4135
|
-
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
4150
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false,
|
|
4136
4151
|
]
|
|
4137
4152
|
)
|
|
4138
4153
|
return true
|
|
@@ -4150,7 +4165,7 @@ class G2: NSObject, SGCManager {
|
|
|
4150
4165
|
}
|
|
4151
4166
|
|
|
4152
4167
|
guard let leftUUID = leftGlassUUID(forSN: DEVICE_SEARCH_ID),
|
|
4153
|
-
|
|
4168
|
+
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
4154
4169
|
else { return false }
|
|
4155
4170
|
|
|
4156
4171
|
let knownLeft = centralManager?.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
@@ -4191,8 +4206,8 @@ class G2: NSObject, SGCManager {
|
|
|
4191
4206
|
// a different service than our primary one, and retrieveConnectedPeripherals only
|
|
4192
4207
|
// returns peripherals whose services match.
|
|
4193
4208
|
let serviceUUIDs: [CBUUID] = [
|
|
4194
|
-
G2BLE.SERVICE_UUID,
|
|
4195
|
-
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
4209
|
+
G2BLE.SERVICE_UUID, // EvenHub: 00002760-...-0000
|
|
4210
|
+
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"), // Nordic UART
|
|
4196
4211
|
]
|
|
4197
4212
|
var devices: [CBPeripheral] = []
|
|
4198
4213
|
for svc in serviceUUIDs {
|
|
@@ -4220,8 +4235,8 @@ class G2: NSObject, SGCManager {
|
|
|
4220
4235
|
// Extract XX (the numeric ID between G2_ and _L_/_R_)
|
|
4221
4236
|
let pattern = "G2_(\\d+)_"
|
|
4222
4237
|
guard let regex = try? NSRegularExpression(pattern: pattern),
|
|
4223
|
-
|
|
4224
|
-
|
|
4238
|
+
let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
|
|
4239
|
+
let range = Range(match.range(at: 1), in: name)
|
|
4225
4240
|
else {
|
|
4226
4241
|
return nil
|
|
4227
4242
|
}
|
|
@@ -4356,7 +4371,7 @@ class G2: NSObject, SGCManager {
|
|
|
4356
4371
|
if cmd == 10, let configData = fields[13] as? Data {
|
|
4357
4372
|
var cReader = ProtobufReader(configData)
|
|
4358
4373
|
let cFields = cReader.parseFields()
|
|
4359
|
-
let voiceSwitch = cFields[1] as? Int32 ?? 0
|
|
4374
|
+
let voiceSwitch = cFields[1] as? Int32 ?? 0 // omitted = 0 = OFF
|
|
4360
4375
|
Bridge.log(
|
|
4361
4376
|
"G2: EvenAI CONFIG echo — voiceSwitch=\(voiceSwitch) (\(voiceSwitch == 1 ? "ON" : "OFF")) config=\(cFields)"
|
|
4362
4377
|
)
|
|
@@ -4391,7 +4406,8 @@ class G2: NSObject, SGCManager {
|
|
|
4391
4406
|
body: [
|
|
4392
4407
|
"heading": Int(heading),
|
|
4393
4408
|
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
4394
|
-
]
|
|
4409
|
+
]
|
|
4410
|
+
)
|
|
4395
4411
|
|
|
4396
4412
|
case NavigationCmd.osNotifyCompassCalibrateStart.rawValue:
|
|
4397
4413
|
Bridge.log("G2: compass calibration started — wearer should look around")
|
|
@@ -4411,7 +4427,6 @@ class G2: NSObject, SGCManager {
|
|
|
4411
4427
|
var reader = ProtobufReader(payload)
|
|
4412
4428
|
let fields = reader.parseFields()
|
|
4413
4429
|
|
|
4414
|
-
|
|
4415
4430
|
let payloadStr = "\(payload.map { String(format: "%02X", $0) }.joined())"
|
|
4416
4431
|
if payloadStr.contains("080C7A02100C") {
|
|
4417
4432
|
// heartbeat response
|
|
@@ -4461,7 +4476,6 @@ class G2: NSObject, SGCManager {
|
|
|
4461
4476
|
}
|
|
4462
4477
|
}
|
|
4463
4478
|
} else {
|
|
4464
|
-
|
|
4465
4479
|
// NOTE: the per-fragment image ACK is correlated inline on the BLE callback queue in
|
|
4466
4480
|
// correlateImageAck() (called from didUpdateValueFor before this is dispatched to the
|
|
4467
4481
|
// main actor) so it is never delayed behind a saturated main actor. Nothing to do here.
|
|
@@ -4494,7 +4508,7 @@ class G2: NSObject, SGCManager {
|
|
|
4494
4508
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
4495
4509
|
)
|
|
4496
4510
|
pageCreated = false
|
|
4497
|
-
evenHubMicActive = false
|
|
4511
|
+
evenHubMicActive = false // mic dies with the page
|
|
4498
4512
|
}
|
|
4499
4513
|
}
|
|
4500
4514
|
// if let errorCode = resFields[8] as? Int32 {
|
|
@@ -4512,7 +4526,7 @@ class G2: NSObject, SGCManager {
|
|
|
4512
4526
|
if cmdValue == 9 || cmdValue == 10 {
|
|
4513
4527
|
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
4514
4528
|
pageCreated = false
|
|
4515
|
-
evenHubMicActive = false
|
|
4529
|
+
evenHubMicActive = false // mic dies with the page
|
|
4516
4530
|
}
|
|
4517
4531
|
}
|
|
4518
4532
|
}
|
|
@@ -4556,8 +4570,8 @@ class G2: NSObject, SGCManager {
|
|
|
4556
4570
|
let wireType = Int(tag & 0x07)
|
|
4557
4571
|
guard wireType == 5, data.distance(from: i, to: data.endIndex) >= 4 else { break }
|
|
4558
4572
|
var bits: UInt32 = 0
|
|
4559
|
-
for b in 0..<4 {
|
|
4560
|
-
bits |= UInt32(data[data.index(i, offsetBy: b)]) << (8 * b)
|
|
4573
|
+
for b in 0 ..< 4 {
|
|
4574
|
+
bits |= UInt32(data[data.index(i, offsetBy: b)]) << (8 * b) // little-endian
|
|
4561
4575
|
}
|
|
4562
4576
|
i = data.index(i, offsetBy: 4)
|
|
4563
4577
|
let value = Float(bitPattern: bits)
|
|
@@ -4694,7 +4708,7 @@ class G2: NSObject, SGCManager {
|
|
|
4694
4708
|
// micEnabled (user intent) — recovery reads it to re-arm; clobbering it strands the mic.
|
|
4695
4709
|
if eventType == .systemExit || eventType == .abnormalExit {
|
|
4696
4710
|
pageCreated = false
|
|
4697
|
-
evenHubMicActive = false
|
|
4711
|
+
evenHubMicActive = false // firmware killed the mic with the page
|
|
4698
4712
|
}
|
|
4699
4713
|
return
|
|
4700
4714
|
}
|
|
@@ -4704,7 +4718,7 @@ class G2: NSObject, SGCManager {
|
|
|
4704
4718
|
var textReader = ProtobufReader(textData)
|
|
4705
4719
|
let textFields = textReader.parseFields()
|
|
4706
4720
|
if let eventTypeRaw = textFields[3] as? Int32,
|
|
4707
|
-
|
|
4721
|
+
let eventType = OsEventType(rawValue: eventTypeRaw)
|
|
4708
4722
|
{
|
|
4709
4723
|
guard let gestureName = mapEventTypeToGesture(eventType) else {
|
|
4710
4724
|
Bridge.log("G2: no gesture mapping for \(eventType) \(textFields)")
|
|
@@ -4746,7 +4760,7 @@ class G2: NSObject, SGCManager {
|
|
|
4746
4760
|
case .foregroundExit: return "foreground_exit"
|
|
4747
4761
|
case .systemExit: return "system_exit"
|
|
4748
4762
|
case .imuDataReport: return nil
|
|
4749
|
-
case .abnormalExit: return nil
|
|
4763
|
+
case .abnormalExit: return nil // don't report abnormal exits as gestures
|
|
4750
4764
|
}
|
|
4751
4765
|
}
|
|
4752
4766
|
|
|
@@ -4769,7 +4783,7 @@ class G2: NSObject, SGCManager {
|
|
|
4769
4783
|
|
|
4770
4784
|
// if the data is just a heartbeat, ignore it:
|
|
4771
4785
|
if let cmdValue = fields[1] as? Int32,
|
|
4772
|
-
|
|
4786
|
+
cmdValue == DevCfgCommandId.baseConnHeartBeat.rawValue
|
|
4773
4787
|
{
|
|
4774
4788
|
return
|
|
4775
4789
|
}
|
|
@@ -4793,7 +4807,7 @@ class G2: NSObject, SGCManager {
|
|
|
4793
4807
|
// Bridge.log("G2: Ring connection status: connStat=\(connStat)")
|
|
4794
4808
|
|
|
4795
4809
|
// Bridge.log("G2: RingConnectInfo: \(fields)")
|
|
4796
|
-
if let ringData = fields[5] as? Data {
|
|
4810
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
4797
4811
|
var ringReader = ProtobufReader(ringData)
|
|
4798
4812
|
let ringFields = ringReader.parseFields()
|
|
4799
4813
|
|
|
@@ -4820,10 +4834,10 @@ class G2: NSObject, SGCManager {
|
|
|
4820
4834
|
// DeviceStore.shared.apply("glasses", "controllerSearching", true)
|
|
4821
4835
|
// }
|
|
4822
4836
|
|
|
4823
|
-
if let ringData = fields[5] as? Data {
|
|
4837
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
4824
4838
|
var ringReader = ProtobufReader(ringData)
|
|
4825
4839
|
let ringFields = ringReader.parseFields()
|
|
4826
|
-
let connStatus = ringFields[4] as? Int32 ?? -1
|
|
4840
|
+
let connStatus = ringFields[4] as? Int32 ?? -1 // field 4 = connStatus
|
|
4827
4841
|
// Bridge.log(
|
|
4828
4842
|
// "G2: Ring connection status: connStatus?=\(connStatus))"
|
|
4829
4843
|
// )
|
|
@@ -4927,13 +4941,13 @@ class G2: NSObject, SGCManager {
|
|
|
4927
4941
|
|
|
4928
4942
|
// Software versions
|
|
4929
4943
|
if let leftVer = fields[5] as? Data,
|
|
4930
|
-
|
|
4944
|
+
let leftVersion = String(data: leftVer, encoding: .utf8)
|
|
4931
4945
|
{
|
|
4932
4946
|
// Bridge.log("G2: Left firmware: \(leftVersion)")
|
|
4933
4947
|
DeviceStore.shared.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
4934
4948
|
}
|
|
4935
4949
|
if let rightVer = fields[6] as? Data,
|
|
4936
|
-
|
|
4950
|
+
let rightVersion = String(data: rightVer, encoding: .utf8)
|
|
4937
4951
|
{
|
|
4938
4952
|
// Bridge.log("G2: Right firmware: \(rightVersion)")
|
|
4939
4953
|
DeviceStore.shared.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
@@ -4971,13 +4985,13 @@ class G2: NSObject, SGCManager {
|
|
|
4971
4985
|
// AppRespondToDashboard: field1=packageId, field2=flag (0=success)
|
|
4972
4986
|
if cmd == 3 {
|
|
4973
4987
|
var appRespW = ProtobufWriter()
|
|
4974
|
-
appRespW.writeInt32Field(1, packageId)
|
|
4975
|
-
appRespW.writeInt32Field(2, 0)
|
|
4988
|
+
appRespW.writeInt32Field(1, packageId) // packageId
|
|
4989
|
+
appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
|
|
4976
4990
|
|
|
4977
4991
|
var pkgW = ProtobufWriter()
|
|
4978
|
-
pkgW.writeInt32Field(1, 4)
|
|
4992
|
+
pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
|
|
4979
4993
|
pkgW.writeInt32Field(2, magicRandom)
|
|
4980
|
-
pkgW.writeMessageField(5, appRespW.data)
|
|
4994
|
+
pkgW.writeMessageField(5, appRespW.data) // field5 = appRespond
|
|
4981
4995
|
sendDashboardCommand(pkgW.data)
|
|
4982
4996
|
}
|
|
4983
4997
|
}
|
|
@@ -5015,7 +5029,7 @@ class G2: NSObject, SGCManager {
|
|
|
5015
5029
|
if data == Data([0x08, 0x01, 0x1A, 0x00]) {
|
|
5016
5030
|
Bridge.log("G2: dashboard toggle - dashboardShowing=\(dashboardShowing) opening=\(dashboardOpening)")
|
|
5017
5031
|
if dashboardOpening {
|
|
5018
|
-
dashboardOpening = false
|
|
5032
|
+
dashboardOpening = false // open confirmed; dashboard now owns the screen
|
|
5019
5033
|
return
|
|
5020
5034
|
}
|
|
5021
5035
|
dashboardShowing = 0
|
|
@@ -5072,7 +5086,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
5072
5086
|
// where the SN string starts.
|
|
5073
5087
|
|
|
5074
5088
|
// Skip "ER" prefix (2 bytes), read 14 bytes of SN
|
|
5075
|
-
let snData = data[2..<16]
|
|
5089
|
+
let snData = data[2 ..< 16]
|
|
5076
5090
|
return String(data: snData, encoding: .ascii)?
|
|
5077
5091
|
.replacingOccurrences(
|
|
5078
5092
|
of: "[\\x00-\\x1F\\x7F]", with: "", options: .regularExpression
|
|
@@ -5084,7 +5098,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
5084
5098
|
/// Returns "AA:BB:CC:DD:EE:FF" (big-endian, colon-separated).
|
|
5085
5099
|
func extractMac(from data: Data) -> String? {
|
|
5086
5100
|
guard data.count >= 22 else { return nil }
|
|
5087
|
-
let macLE = data[16..<22]
|
|
5101
|
+
let macLE = data[16 ..< 22]
|
|
5088
5102
|
return macLE.reversed().map { String(format: "%02X", $0) }.joined(separator: ":")
|
|
5089
5103
|
}
|
|
5090
5104
|
|
|
@@ -5108,13 +5122,13 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5108
5122
|
) {
|
|
5109
5123
|
guard
|
|
5110
5124
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey]
|
|
5111
|
-
|
|
5125
|
+
as? String
|
|
5112
5126
|
else { return }
|
|
5113
5127
|
|
|
5114
5128
|
// G2 glasses have "Even" prefix and "G2" in name, with _L_ or _R_ for side
|
|
5115
5129
|
guard name.contains("G2") else { return }
|
|
5116
5130
|
guard let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
5117
|
-
|
|
5131
|
+
mfgData.count >= 16
|
|
5118
5132
|
else { return }
|
|
5119
5133
|
|
|
5120
5134
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -5397,22 +5411,23 @@ extension G2: CBPeripheralDelegate {
|
|
|
5397
5411
|
// Strip the 2-byte CRC trailer on the (last == only) packet.
|
|
5398
5412
|
let payloadEnd = 8 + payloadLen - 2
|
|
5399
5413
|
guard payloadEnd >= 8, payloadEnd <= rawData.count else { return }
|
|
5400
|
-
let payload = rawData.subdata(in: (rawData.startIndex + 8)..<(rawData.startIndex + payloadEnd))
|
|
5414
|
+
let payload = rawData.subdata(in: (rawData.startIndex + 8) ..< (rawData.startIndex + payloadEnd))
|
|
5401
5415
|
|
|
5402
5416
|
var reader = ProtobufReader(payload)
|
|
5403
5417
|
let fields = reader.parseFields()
|
|
5404
|
-
guard let resData = fields[6] as? Data else { return }
|
|
5418
|
+
guard let resData = fields[6] as? Data else { return } // field 6 = ImgResCmd
|
|
5405
5419
|
var resReader = ProtobufReader(resData)
|
|
5406
5420
|
let resFields = resReader.parseFields()
|
|
5407
5421
|
guard let errorCode = resFields[8] as? Int32,
|
|
5408
|
-
|
|
5422
|
+
let ackSession = resFields[3] as? Int32
|
|
5409
5423
|
else { return }
|
|
5410
5424
|
let ackFragment = (resFields[6] as? Int32) ?? 0
|
|
5411
5425
|
Bridge.log(
|
|
5412
5426
|
"G2: img_res: session=\(ackSession) fragment=\(ackFragment) errorCode=\(errorCode) success=\(errorCode == 4)"
|
|
5413
5427
|
)
|
|
5414
5428
|
completeImageAck(
|
|
5415
|
-
session: Int(ackSession), fragmentIndex: ackFragment, success: errorCode == 4
|
|
5429
|
+
session: Int(ackSession), fragmentIndex: ackFragment, success: errorCode == 4
|
|
5430
|
+
)
|
|
5416
5431
|
}
|
|
5417
5432
|
|
|
5418
5433
|
nonisolated func peripheral(
|