@mentra/bluetooth-sdk 3.2.0-dev.209 → 3.2.0-dev.210
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 +45 -8
- 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/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -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/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 +1 -1
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -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 +374 -359
- 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/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 @@ enum G2NotificationProto {
|
|
|
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 @@ enum G2NotificationProto {
|
|
|
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
|
)
|
|
@@ -1723,8 +1725,8 @@ class G2: NSObject, SGCManager {
|
|
|
1723
1725
|
private var rightWriteQueue: [PendingWrite] = []
|
|
1724
1726
|
private var leftDraining = false
|
|
1725
1727
|
private var rightDraining = false
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
+
/// Pace between consecutive packets (~G1's chunk pacing). Off any external callback, so the drain
|
|
1729
|
+
/// keeps making progress in the background instead of waiting for a callback iOS won't deliver.
|
|
1728
1730
|
private let writePaceNanos: UInt64 = 6_000_000
|
|
1729
1731
|
// Diagnostic: warn if a side's queue ever backs up (it shouldn't now — the drainer is always
|
|
1730
1732
|
// making progress). Rate-limited. Prefixed "BGCAP:" so it's easy to grep/strip after validation.
|
|
@@ -1763,7 +1765,7 @@ class G2: NSObject, SGCManager {
|
|
|
1763
1765
|
private func drainLoop(right: Bool) async {
|
|
1764
1766
|
while true {
|
|
1765
1767
|
guard let peripheral = right ? rightPeripheral : leftPeripheral,
|
|
1766
|
-
|
|
1768
|
+
let char = right ? rightWriteChar : leftWriteChar
|
|
1767
1769
|
else {
|
|
1768
1770
|
// No connection for this side; drop pending packets so they can't replay later.
|
|
1769
1771
|
if right { rightWriteQueue.removeAll(); rightDraining = false }
|
|
@@ -1914,44 +1916,44 @@ class G2: NSObject, SGCManager {
|
|
|
1914
1916
|
// Small delay then auth right + pipe role change + time sync
|
|
1915
1917
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1916
1918
|
|
|
1917
|
-
let authR = DevSettingsProto.authCmd(magicRandom:
|
|
1918
|
-
|
|
1919
|
+
let authR = DevSettingsProto.authCmd(magicRandom: sendManager.nextMagicRandom())
|
|
1920
|
+
sendDevSettingsCommand(authR, left: false, right: true)
|
|
1919
1921
|
|
|
1920
1922
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1921
1923
|
|
|
1922
1924
|
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1923
|
-
magicRandom:
|
|
1925
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1924
1926
|
)
|
|
1925
|
-
|
|
1927
|
+
sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1926
1928
|
|
|
1927
1929
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1928
1930
|
|
|
1929
1931
|
let timeSync = DevSettingsProto.timeSync(
|
|
1930
|
-
magicRandom:
|
|
1932
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1931
1933
|
)
|
|
1932
|
-
|
|
1934
|
+
sendDevSettingsCommand(timeSync, left: true, right: true)
|
|
1933
1935
|
|
|
1934
1936
|
// Skip onboarding on connect
|
|
1935
1937
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1936
1938
|
let onboarding = OnboardingProto.skipOnboarding(
|
|
1937
|
-
magicRandom:
|
|
1939
|
+
magicRandom: sendManager.nextMagicRandom()
|
|
1938
1940
|
)
|
|
1939
|
-
|
|
1941
|
+
sendOnboardingCommand(onboarding)
|
|
1940
1942
|
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1941
1943
|
|
|
1942
1944
|
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1943
1945
|
var gestureInitW = ProtobufWriter()
|
|
1944
1946
|
gestureInitW.writeInt32Field(1, 0)
|
|
1945
|
-
gestureInitW.writeInt32Field(2,
|
|
1946
|
-
|
|
1947
|
+
gestureInitW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1948
|
+
sendGestureCtrlCommand(gestureInitW.data)
|
|
1947
1949
|
|
|
1948
1950
|
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1949
1951
|
var uiSettW = ProtobufWriter()
|
|
1950
|
-
uiSettW.writeInt32Field(1, 2)
|
|
1951
|
-
uiSettW.writeInt32Field(2,
|
|
1952
|
-
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00]))
|
|
1953
|
-
|
|
1954
|
-
|
|
1952
|
+
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1953
|
+
uiSettW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1954
|
+
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
|
|
1955
|
+
sendToGlasses(
|
|
1956
|
+
sendManager.buildPackets(
|
|
1955
1957
|
serviceId: 0x0C, payload: uiSettW.data, reserveFlag: true
|
|
1956
1958
|
)
|
|
1957
1959
|
)
|
|
@@ -1960,30 +1962,30 @@ class G2: NSObject, SGCManager {
|
|
|
1960
1962
|
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1961
1963
|
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1962
1964
|
var dashDisplayW = ProtobufWriter()
|
|
1963
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
1964
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
1965
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
1966
|
-
dashDisplayW.writeInt32Field(4, 4)
|
|
1965
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1966
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1967
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1968
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1967
1969
|
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1968
|
-
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
1969
|
-
dashDisplayW.writeInt32Field(6,
|
|
1970
|
-
dashDisplayW.writeInt32Field(7,
|
|
1970
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1971
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1972
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1971
1973
|
|
|
1972
1974
|
var dashRecvW = ProtobufWriter()
|
|
1973
1975
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1974
1976
|
|
|
1975
1977
|
var dashPkgW = ProtobufWriter()
|
|
1976
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
1977
|
-
dashPkgW.writeInt32Field(2,
|
|
1978
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1979
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1978
1980
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1979
|
-
|
|
1981
|
+
sendDashboardCommand(dashPkgW.data)
|
|
1980
1982
|
|
|
1981
1983
|
// Disable "Hey Even" wakeword on connect
|
|
1982
1984
|
let heyEvenOff = EvenAIProto.setHeyEven(
|
|
1983
|
-
magicRandom:
|
|
1985
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
1984
1986
|
enabled: false
|
|
1985
1987
|
)
|
|
1986
|
-
|
|
1988
|
+
sendEvenAICommand(heyEvenOff)
|
|
1987
1989
|
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1988
1990
|
|
|
1989
1991
|
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5, field7={1:1})
|
|
@@ -2017,45 +2019,52 @@ class G2: NSObject, SGCManager {
|
|
|
2017
2019
|
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
2018
2020
|
|
|
2019
2021
|
// Start heartbeats after auth
|
|
2020
|
-
|
|
2022
|
+
startHeartbeats()
|
|
2021
2023
|
|
|
2022
2024
|
Task { await self.reconnectionManager.stop() }
|
|
2023
2025
|
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
2024
2026
|
|
|
2025
2027
|
// Set device_name so DeviceManager can save it for reconnection
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2028
|
+
let peripheralName = rightPeripheral?.name ?? leftPeripheral?.name
|
|
2029
|
+
if let serialNumber = G2SerialResolution.resolve(
|
|
2030
|
+
scannedSerial: peripheralName.flatMap { deviceNameToSerialNumber[$0] },
|
|
2031
|
+
requestedId: DEVICE_SEARCH_ID,
|
|
2032
|
+
persistedDeviceName: DeviceStore.shared.get("bluetooth", "device_name") as? String ?? ""
|
|
2033
|
+
) {
|
|
2030
2034
|
DeviceStore.shared.apply("bluetooth", "device_name", serialNumber)
|
|
2035
|
+
// The advertisement serial is the manufacturing serial; expose it where
|
|
2036
|
+
// the SDK status (and analytics identification) read it, not only in the
|
|
2037
|
+
// reconnection name slot. Cached reconnects skip the scan, so the
|
|
2038
|
+
// persisted name is the serial source there (see G2SerialResolution).
|
|
2039
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
2031
2040
|
Bridge.log("G2: Set device_name to \(serialNumber)")
|
|
2032
2041
|
}
|
|
2033
2042
|
|
|
2034
2043
|
// Set bluetooth name and device model for Device Info page
|
|
2035
2044
|
let btName =
|
|
2036
|
-
|
|
2037
|
-
|
|
2045
|
+
rightPeripheral?.name
|
|
2046
|
+
?? leftPeripheral?.name ?? ""
|
|
2038
2047
|
DeviceStore.shared.apply("glasses", "bluetoothName", btName)
|
|
2039
2048
|
DeviceStore.shared.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
2040
2049
|
|
|
2041
|
-
|
|
2050
|
+
setFullyConnected()
|
|
2042
2051
|
|
|
2043
2052
|
// connnect a controller if we have one:
|
|
2044
|
-
|
|
2053
|
+
connectController()
|
|
2045
2054
|
|
|
2046
2055
|
// Query version + battery info from glasses
|
|
2047
|
-
|
|
2056
|
+
requestDeviceInfo()
|
|
2048
2057
|
|
|
2049
2058
|
// send dashboard menu if we have stored items
|
|
2050
|
-
|
|
2059
|
+
sendMenuApps()
|
|
2051
2060
|
|
|
2052
2061
|
// order the calendar (Schedule) widget first on the dashboard
|
|
2053
|
-
|
|
2062
|
+
setCalendarWidgetFirst()
|
|
2054
2063
|
|
|
2055
2064
|
// send calendar events
|
|
2056
2065
|
let calendarEvents =
|
|
2057
2066
|
DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
2058
|
-
|
|
2067
|
+
sendCalendarEvents(calendarEvents)
|
|
2059
2068
|
}
|
|
2060
2069
|
|
|
2061
2070
|
// MARK: - Heartbeats
|
|
@@ -2176,8 +2185,8 @@ class G2: NSObject, SGCManager {
|
|
|
2176
2185
|
)
|
|
2177
2186
|
}
|
|
2178
2187
|
|
|
2179
|
-
|
|
2180
|
-
|
|
2188
|
+
/// Protocol witness for SGCManager.sendText — G2 renders a simple string as a
|
|
2189
|
+
/// default-positioned text wall. The positioned variant is `sendTextAt`.
|
|
2181
2190
|
func sendText(_ text: String) async {
|
|
2182
2191
|
await sendTextWall(text)
|
|
2183
2192
|
}
|
|
@@ -2229,7 +2238,8 @@ class G2: NSObject, SGCManager {
|
|
|
2229
2238
|
if let i = textContainers.firstIndex(where: {
|
|
2230
2239
|
$0.matches(
|
|
2231
2240
|
x: rx, y: ry, width: rw, height: rh, borderWidth: borderWidth,
|
|
2232
|
-
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2241
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2242
|
+
)
|
|
2233
2243
|
}) {
|
|
2234
2244
|
textContainers[i].content = content
|
|
2235
2245
|
textContainers[i].pendingSends = 1 + EVEN_HUB_RESEND_COUNT
|
|
@@ -2254,7 +2264,8 @@ class G2: NSObject, SGCManager {
|
|
|
2254
2264
|
|
|
2255
2265
|
let container = addTextContainer(
|
|
2256
2266
|
x: rx, y: ry, width: rw, height: rh, content: content, borderWidth: borderWidth,
|
|
2257
|
-
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2267
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength
|
|
2268
|
+
)
|
|
2258
2269
|
Bridge.log(
|
|
2259
2270
|
"G2: sendText() - added text container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page"
|
|
2260
2271
|
)
|
|
@@ -2419,7 +2430,8 @@ class G2: NSObject, SGCManager {
|
|
|
2419
2430
|
$0.matches(
|
|
2420
2431
|
x: x, y: y, width: width, height: height, borderWidth: borderWidth,
|
|
2421
2432
|
borderColor: G2.defaultTextContainer.borderColor, borderRadius: borderRadius,
|
|
2422
|
-
paddingLength: G2.defaultTextContainer.paddingLength
|
|
2433
|
+
paddingLength: G2.defaultTextContainer.paddingLength
|
|
2434
|
+
)
|
|
2423
2435
|
}) {
|
|
2424
2436
|
let cid = textContainers[i].id
|
|
2425
2437
|
// The container id may have been LRU-recycled from another element.
|
|
@@ -2488,7 +2500,7 @@ class G2: NSObject, SGCManager {
|
|
|
2488
2500
|
var tilePixels = Data(capacity: Int(t.w * t.h))
|
|
2489
2501
|
for row in 0 ..< Int(t.h) {
|
|
2490
2502
|
let start = (Int(t.dy) + row) * Int(width) + Int(t.dx)
|
|
2491
|
-
tilePixels.append(gray.subdata(in: (gray.startIndex + start)..<(gray.startIndex + start + Int(t.w))))
|
|
2503
|
+
tilePixels.append(gray.subdata(in: (gray.startIndex + start) ..< (gray.startIndex + start + Int(t.w))))
|
|
2492
2504
|
}
|
|
2493
2505
|
guard let bmp = build4BitBmp(grayscalePixels: tilePixels, width: Int(t.w), height: Int(t.h)) else {
|
|
2494
2506
|
Bridge.log("G2: drawLayoutBitmap - tile encode failed")
|
|
@@ -2683,7 +2695,7 @@ class G2: NSObject, SGCManager {
|
|
|
2683
2695
|
// }
|
|
2684
2696
|
while offset < bmpData.count {
|
|
2685
2697
|
let end = min(offset + fragmentSize, bmpData.count)
|
|
2686
|
-
let fragment = bmpData[offset..<end]
|
|
2698
|
+
let fragment = bmpData[offset ..< end]
|
|
2687
2699
|
|
|
2688
2700
|
let msg = EvenHubProto.updateImageRawDataMessage(
|
|
2689
2701
|
containerID: containerID,
|
|
@@ -2872,7 +2884,7 @@ class G2: NSObject, SGCManager {
|
|
|
2872
2884
|
// keeps being re-dirtied mid-send can't spin this pass forever (next tick picks it up).
|
|
2873
2885
|
var guardCount = 0
|
|
2874
2886
|
while pageCreated, guardCount < imageContainerIDPool.count,
|
|
2875
|
-
|
|
2887
|
+
let i = imageContainers.firstIndex(where: { $0.dirty })
|
|
2876
2888
|
{
|
|
2877
2889
|
guardCount += 1
|
|
2878
2890
|
let container = imageContainers[i]
|
|
@@ -2890,7 +2902,7 @@ class G2: NSObject, SGCManager {
|
|
|
2890
2902
|
// Only settle the flag if it's still empty — a displayBitmap during the await would
|
|
2891
2903
|
// have set new bytes, so leave it dirty for the next pass to send the real image.
|
|
2892
2904
|
if let j = imageContainers.firstIndex(where: { $0.id == container.id }),
|
|
2893
|
-
|
|
2905
|
+
imageContainers[j].bmpData.isEmpty
|
|
2894
2906
|
{
|
|
2895
2907
|
imageContainers[j].dirty = false
|
|
2896
2908
|
}
|
|
@@ -2901,7 +2913,7 @@ class G2: NSObject, SGCManager {
|
|
|
2901
2913
|
)
|
|
2902
2914
|
// Re-find by id: the array may have shifted (eviction) during the await.
|
|
2903
2915
|
if let j = imageContainers.firstIndex(where: { $0.id == container.id }),
|
|
2904
|
-
|
|
2916
|
+
imageContainers[j].bmpData == sentBytes
|
|
2905
2917
|
{
|
|
2906
2918
|
imageContainers[j].dirty = false
|
|
2907
2919
|
}
|
|
@@ -2922,7 +2934,8 @@ class G2: NSObject, SGCManager {
|
|
|
2922
2934
|
let usedIDs = Set(imageContainers.map { $0.id })
|
|
2923
2935
|
let id = imageContainerIDPool.first { !usedIDs.contains($0) } ?? imageContainerIDPool[0]
|
|
2924
2936
|
let container = ImgContainer(
|
|
2925
|
-
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData
|
|
2937
|
+
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData
|
|
2938
|
+
)
|
|
2926
2939
|
imageContainers.append(container)
|
|
2927
2940
|
return container
|
|
2928
2941
|
}
|
|
@@ -2942,7 +2955,8 @@ class G2: NSObject, SGCManager {
|
|
|
2942
2955
|
let container = TextContainer(
|
|
2943
2956
|
id: id, x: x, y: y, width: width, height: height, content: content,
|
|
2944
2957
|
borderWidth: borderWidth, borderColor: borderColor, borderRadius: borderRadius,
|
|
2945
|
-
paddingLength: paddingLength
|
|
2958
|
+
paddingLength: paddingLength
|
|
2959
|
+
)
|
|
2946
2960
|
textContainers.append(container)
|
|
2947
2961
|
return container
|
|
2948
2962
|
}
|
|
@@ -2952,18 +2966,18 @@ class G2: NSObject, SGCManager {
|
|
|
2952
2966
|
let msg = EvenHubProto.shutdownMessage()
|
|
2953
2967
|
sendEvenHubCommand(msg)
|
|
2954
2968
|
pageCreated = false
|
|
2955
|
-
try? await Task.sleep(nanoseconds: 300_000_000)// 300ms to settle
|
|
2969
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2956
2970
|
// we will automatically rebuild state when we detect the glasses shutdown:
|
|
2957
2971
|
// await rebuildState()
|
|
2958
2972
|
}
|
|
2959
2973
|
|
|
2960
|
-
|
|
2974
|
+
/// re-creates the containers and re-sends all images to the glasses:
|
|
2961
2975
|
private func rebuildState() async {
|
|
2962
2976
|
Bridge.log("G2: rebuildState()")
|
|
2963
2977
|
// recreate the containers (sets pageCreated = true; embeds text content directly):
|
|
2964
2978
|
createPageWithContainers()
|
|
2965
2979
|
|
|
2966
|
-
try? await Task.sleep(nanoseconds: 300_000_000)
|
|
2980
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2967
2981
|
// Mark every image container dirty and let the reconcile loop re-send them, one at a time.
|
|
2968
2982
|
// Doing the sends here directly is what used to race a concurrent displayBitmap and clobber
|
|
2969
2983
|
// imgAckBox; routing through the dirty flag keeps a single sender (see displayReconcileTask).
|
|
@@ -3031,7 +3045,7 @@ class G2: NSObject, SGCManager {
|
|
|
3031
3045
|
return nil
|
|
3032
3046
|
}
|
|
3033
3047
|
|
|
3034
|
-
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3
|
|
3048
|
+
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3 // 4-bit rows padded to 4 bytes
|
|
3035
3049
|
let pixelDataOffset = headerSize
|
|
3036
3050
|
|
|
3037
3051
|
let dstWidth = srcWidth * 2
|
|
@@ -3064,7 +3078,7 @@ class G2: NSObject, SGCManager {
|
|
|
3064
3078
|
dst.appendLittleEndian(UInt32(0))
|
|
3065
3079
|
|
|
3066
3080
|
// --- Color Table (same 16-entry grayscale) ---
|
|
3067
|
-
for i in 0..<16 {
|
|
3081
|
+
for i in 0 ..< 16 {
|
|
3068
3082
|
let val = UInt8(i * 17)
|
|
3069
3083
|
dst.append(contentsOf: [val, val, val, 0])
|
|
3070
3084
|
}
|
|
@@ -3072,12 +3086,12 @@ class G2: NSObject, SGCManager {
|
|
|
3072
3086
|
// --- Pixel Data (nearest-neighbor 2x upscale) ---
|
|
3073
3087
|
// BMP is bottom-up, so row 0 = bottom of image
|
|
3074
3088
|
// Each dst row maps to srcRow = dstRow / 2
|
|
3075
|
-
for dstRow in 0..<dstHeight {
|
|
3089
|
+
for dstRow in 0 ..< dstHeight {
|
|
3076
3090
|
let srcRow = dstRow / 2
|
|
3077
3091
|
let srcRowOffset = pixelDataOffset + srcRow * srcPaddedRowSize
|
|
3078
3092
|
var rowBuf = [UInt8](repeating: 0, count: dstPaddedRowSize)
|
|
3079
3093
|
|
|
3080
|
-
for dstCol in 0..<dstWidth {
|
|
3094
|
+
for dstCol in 0 ..< dstWidth {
|
|
3081
3095
|
let srcCol = dstCol / 2
|
|
3082
3096
|
|
|
3083
3097
|
// Read 4-bit nibble from source
|
|
@@ -3152,7 +3166,7 @@ class G2: NSObject, SGCManager {
|
|
|
3152
3166
|
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
3153
3167
|
|
|
3154
3168
|
guard let renderedImage = ctx.makeImage(),
|
|
3155
|
-
|
|
3169
|
+
let pixels = renderedImage.dataProvider?.data as Data?
|
|
3156
3170
|
else {
|
|
3157
3171
|
Bridge.log("G2: convertToG2Bmp - failed to get pixel data")
|
|
3158
3172
|
return nil
|
|
@@ -3175,7 +3189,7 @@ class G2: NSObject, SGCManager {
|
|
|
3175
3189
|
/// unlit, so an all-zero frame reads as blank. Used by the reconcile loop to clear a bitmap.
|
|
3176
3190
|
private func blankBmp(width: Int, height: Int) -> Data? {
|
|
3177
3191
|
guard width > 0, height > 0 else { return nil }
|
|
3178
|
-
let zeros = Data(count: width * height)
|
|
3192
|
+
let zeros = Data(count: width * height) // all-zero 8-bit grayscale = black
|
|
3179
3193
|
return build4BitBmp(grayscalePixels: zeros, width: width, height: height)
|
|
3180
3194
|
}
|
|
3181
3195
|
|
|
@@ -3183,8 +3197,8 @@ class G2: NSObject, SGCManager {
|
|
|
3183
3197
|
/// BMP rows are stored bottom-up. Each row is padded to a 4-byte boundary.
|
|
3184
3198
|
private func build4BitBmp(grayscalePixels: Data, width: Int, height: Int) -> Data? {
|
|
3185
3199
|
// 4-bit: 2 pixels per byte, rows padded to 4-byte boundary
|
|
3186
|
-
let bytesPerRow4bit = (width + 1) / 2
|
|
3187
|
-
let paddedRowSize = (bytesPerRow4bit + 3) & ~3
|
|
3200
|
+
let bytesPerRow4bit = (width + 1) / 2 // ceil(width / 2)
|
|
3201
|
+
let paddedRowSize = (bytesPerRow4bit + 3) & ~3 // pad to 4-byte boundary
|
|
3188
3202
|
let pixelDataSize = paddedRowSize * height
|
|
3189
3203
|
|
|
3190
3204
|
// BMP file header (14 bytes) + DIB header (40 bytes) + color table (16 * 4 = 64 bytes)
|
|
@@ -3194,46 +3208,46 @@ class G2: NSObject, SGCManager {
|
|
|
3194
3208
|
var bmp = Data(capacity: fileSize)
|
|
3195
3209
|
|
|
3196
3210
|
// --- BMP File Header (14 bytes) ---
|
|
3197
|
-
bmp.append(contentsOf: [0x42, 0x4D])
|
|
3198
|
-
bmp.appendLittleEndian(UInt32(fileSize))
|
|
3199
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
3200
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
3201
|
-
bmp.appendLittleEndian(UInt32(headerSize))
|
|
3211
|
+
bmp.append(contentsOf: [0x42, 0x4D]) // "BM" signature
|
|
3212
|
+
bmp.appendLittleEndian(UInt32(fileSize)) // File size
|
|
3213
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved1
|
|
3214
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved2
|
|
3215
|
+
bmp.appendLittleEndian(UInt32(headerSize)) // Pixel data offset
|
|
3202
3216
|
|
|
3203
3217
|
// --- DIB Header (BITMAPINFOHEADER, 40 bytes) ---
|
|
3204
|
-
bmp.appendLittleEndian(UInt32(40))
|
|
3205
|
-
bmp.appendLittleEndian(Int32(width))
|
|
3206
|
-
bmp.appendLittleEndian(Int32(height))
|
|
3207
|
-
bmp.appendLittleEndian(UInt16(1))
|
|
3208
|
-
bmp.appendLittleEndian(UInt16(4))
|
|
3209
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
3210
|
-
bmp.appendLittleEndian(UInt32(pixelDataSize))
|
|
3211
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
3212
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
3213
|
-
bmp.appendLittleEndian(UInt32(16))
|
|
3214
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
3218
|
+
bmp.appendLittleEndian(UInt32(40)) // DIB header size
|
|
3219
|
+
bmp.appendLittleEndian(Int32(width)) // Width
|
|
3220
|
+
bmp.appendLittleEndian(Int32(height)) // Height (positive = bottom-up)
|
|
3221
|
+
bmp.appendLittleEndian(UInt16(1)) // Color planes
|
|
3222
|
+
bmp.appendLittleEndian(UInt16(4)) // Bits per pixel (4-bit)
|
|
3223
|
+
bmp.appendLittleEndian(UInt32(0)) // Compression (none)
|
|
3224
|
+
bmp.appendLittleEndian(UInt32(pixelDataSize)) // Image size
|
|
3225
|
+
bmp.appendLittleEndian(Int32(2835)) // X pixels/meter (~72 DPI)
|
|
3226
|
+
bmp.appendLittleEndian(Int32(2835)) // Y pixels/meter
|
|
3227
|
+
bmp.appendLittleEndian(UInt32(16)) // Colors used
|
|
3228
|
+
bmp.appendLittleEndian(UInt32(0)) // Important colors (0 = all)
|
|
3215
3229
|
|
|
3216
3230
|
// --- Color Table (16 entries, 4 bytes each: B, G, R, 0) ---
|
|
3217
|
-
for i in 0..<16 {
|
|
3218
|
-
let val = UInt8(i * 17)
|
|
3219
|
-
bmp.append(contentsOf: [val, val, val, 0])
|
|
3231
|
+
for i in 0 ..< 16 {
|
|
3232
|
+
let val = UInt8(i * 17) // 0, 17, 34, ... 255 (evenly spaced grayscale)
|
|
3233
|
+
bmp.append(contentsOf: [val, val, val, 0]) // B, G, R, Reserved
|
|
3220
3234
|
}
|
|
3221
3235
|
|
|
3222
3236
|
// --- Pixel Data (bottom-up rows, 4-bit packed) ---
|
|
3223
3237
|
let rowBytes = [UInt8](repeating: 0, count: paddedRowSize)
|
|
3224
|
-
for row in 0..<height {
|
|
3238
|
+
for row in 0 ..< height {
|
|
3225
3239
|
// BMP is bottom-up: row 0 in BMP = last row of image
|
|
3226
3240
|
let srcRow = height - 1 - row
|
|
3227
3241
|
let srcOffset = srcRow * width
|
|
3228
3242
|
var rowBuf = rowBytes
|
|
3229
3243
|
|
|
3230
|
-
for col in 0..<width {
|
|
3244
|
+
for col in 0 ..< width {
|
|
3231
3245
|
let pixelIndex = srcOffset + col
|
|
3232
3246
|
guard pixelIndex < grayscalePixels.count else { continue }
|
|
3233
3247
|
|
|
3234
3248
|
// Map 8-bit grayscale (0-255) to 4-bit index (0-15)
|
|
3235
3249
|
let gray8 = grayscalePixels[pixelIndex]
|
|
3236
|
-
let index4 = gray8 >> 4
|
|
3250
|
+
let index4 = gray8 >> 4 // divide by 16
|
|
3237
3251
|
|
|
3238
3252
|
let bytePos = col / 2
|
|
3239
3253
|
if col % 2 == 0 {
|
|
@@ -3265,7 +3279,7 @@ class G2: NSObject, SGCManager {
|
|
|
3265
3279
|
let msg = EvenHubProto.shutdownMessage()
|
|
3266
3280
|
sendEvenHubCommand(msg)
|
|
3267
3281
|
pageCreated = false
|
|
3268
|
-
evenHubMicActive = false
|
|
3282
|
+
evenHubMicActive = false // dashboard takes EvenHub focus; firmware kills the mic
|
|
3269
3283
|
currentBitmapBase64 = ""
|
|
3270
3284
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
3271
3285
|
guard let self = self else { return }
|
|
@@ -3287,20 +3301,20 @@ class G2: NSObject, SGCManager {
|
|
|
3287
3301
|
|
|
3288
3302
|
func sendDashboardDisplaySettings() {
|
|
3289
3303
|
var dashDisplayW = ProtobufWriter()
|
|
3290
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
3291
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
3292
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
3293
|
-
dashDisplayW.writeInt32Field(4, 4)
|
|
3304
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
3305
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
3306
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
3307
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
3294
3308
|
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
3295
3309
|
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
3296
|
-
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat())
|
|
3297
|
-
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit())
|
|
3310
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
3311
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
3298
3312
|
|
|
3299
3313
|
var dashRecvW = ProtobufWriter()
|
|
3300
3314
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
3301
3315
|
|
|
3302
3316
|
var dashPkgW = ProtobufWriter()
|
|
3303
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
3317
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
3304
3318
|
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3305
3319
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
3306
3320
|
sendDashboardCommand(dashPkgW.data)
|
|
@@ -3367,8 +3381,8 @@ class G2: NSObject, SGCManager {
|
|
|
3367
3381
|
let total = Int32(events.count)
|
|
3368
3382
|
for (i, ev) in events.enumerated() {
|
|
3369
3383
|
guard let title = ev["title"] as? String,
|
|
3370
|
-
|
|
3371
|
-
|
|
3384
|
+
let time = ev["time"] as? String,
|
|
3385
|
+
let endTs = ev["endDate"] as? Double
|
|
3372
3386
|
else { continue }
|
|
3373
3387
|
let location = ev["location"] as? String
|
|
3374
3388
|
sendCalendarEvent(
|
|
@@ -3447,7 +3461,8 @@ class G2: NSObject, SGCManager {
|
|
|
3447
3461
|
paddingLength: c.paddingLength, containerID: c.id,
|
|
3448
3462
|
containerName: c.name, isEventCapture: false,
|
|
3449
3463
|
content: c.content
|
|
3450
|
-
)
|
|
3464
|
+
)
|
|
3465
|
+
)
|
|
3451
3466
|
}
|
|
3452
3467
|
|
|
3453
3468
|
// Page-composition dump: one line per container on every page create,
|
|
@@ -3910,7 +3925,7 @@ class G2: NSObject, SGCManager {
|
|
|
3910
3925
|
|
|
3911
3926
|
let enterPayload = EvenAIProto.aiCtrl(
|
|
3912
3927
|
magicRandom: sendManager.nextMagicRandom(),
|
|
3913
|
-
status: 2
|
|
3928
|
+
status: 2 // EVEN_AI_ENTER
|
|
3914
3929
|
)
|
|
3915
3930
|
sendEvenAICommand(enterPayload)
|
|
3916
3931
|
|
|
@@ -3924,7 +3939,7 @@ class G2: NSObject, SGCManager {
|
|
|
3924
3939
|
|
|
3925
3940
|
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
3926
3941
|
triggerSkill(
|
|
3927
|
-
3, skillParam: 1,
|
|
3942
|
+
3, skillParam: 1, // NOTIFICATION, show
|
|
3928
3943
|
text: " ",
|
|
3929
3944
|
streamEnable: 1, fTextEnd: 1
|
|
3930
3945
|
)
|
|
@@ -3957,8 +3972,8 @@ class G2: NSObject, SGCManager {
|
|
|
3957
3972
|
/// the wearer should look around until `…{status:"complete"}`.
|
|
3958
3973
|
func startCompass() {
|
|
3959
3974
|
var w = ProtobufWriter()
|
|
3960
|
-
w.writeInt32Field(1, NavigationCmd.appRequestStartUp.rawValue)
|
|
3961
|
-
w.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3975
|
+
w.writeInt32Field(1, NavigationCmd.appRequestStartUp.rawValue) // cmd
|
|
3976
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
|
|
3962
3977
|
sendNavigationCommand(w.data)
|
|
3963
3978
|
}
|
|
3964
3979
|
|
|
@@ -4034,19 +4049,19 @@ class G2: NSObject, SGCManager {
|
|
|
4034
4049
|
let widgetOrder: [UInt8] = [3, 1, 2, 4, 5]
|
|
4035
4050
|
|
|
4036
4051
|
var dashDisplayW = ProtobufWriter()
|
|
4037
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
4038
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
4039
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
4040
|
-
dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count))
|
|
4041
|
-
dashDisplayW.writeMessageField(5, Data(widgetOrder))
|
|
4042
|
-
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat())
|
|
4043
|
-
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit())
|
|
4052
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
4053
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
4054
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
4055
|
+
dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count)) // widgetDisplayCount
|
|
4056
|
+
dashDisplayW.writeMessageField(5, Data(widgetOrder)) // widgetDisplayOrder: Schedule first
|
|
4057
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
4058
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
4044
4059
|
|
|
4045
4060
|
var dashRecvW = ProtobufWriter()
|
|
4046
4061
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
4047
4062
|
|
|
4048
4063
|
var dashPkgW = ProtobufWriter()
|
|
4049
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
4064
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
4050
4065
|
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
4051
4066
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
4052
4067
|
sendDashboardCommand(dashPkgW.data)
|
|
@@ -4057,7 +4072,7 @@ class G2: NSObject, SGCManager {
|
|
|
4057
4072
|
func setDashboardMenu(_ items: [[String: Any]]) {
|
|
4058
4073
|
let menuItems = items.compactMap { dict -> MenuProto.MenuItem? in
|
|
4059
4074
|
guard let name = dict["name"] as? String,
|
|
4060
|
-
|
|
4075
|
+
let packageName = dict["packageName"] as? String
|
|
4061
4076
|
else { return nil }
|
|
4062
4077
|
let running = dict["running"] as? Bool ?? false
|
|
4063
4078
|
return MenuProto.MenuItem(packageName: packageName, name: name, running: running)
|
|
@@ -4142,7 +4157,7 @@ class G2: NSObject, SGCManager {
|
|
|
4142
4157
|
func sendWifiCredentials(_: String, _: String) {}
|
|
4143
4158
|
func forgetWifiNetwork(_: String) {}
|
|
4144
4159
|
func sendHotspotState(_: Bool) {}
|
|
4145
|
-
func sendOtaStart(otaVersionUrl: String?) {}
|
|
4160
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
4146
4161
|
func sendOtaQueryStatus() {}
|
|
4147
4162
|
|
|
4148
4163
|
// MARK: - SGCManager: User Context
|
|
@@ -4217,7 +4232,7 @@ class G2: NSObject, SGCManager {
|
|
|
4217
4232
|
centralManager!.scanForPeripherals(
|
|
4218
4233
|
withServices: nil,
|
|
4219
4234
|
options: [
|
|
4220
|
-
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
4235
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false,
|
|
4221
4236
|
]
|
|
4222
4237
|
)
|
|
4223
4238
|
return true
|
|
@@ -4235,7 +4250,7 @@ class G2: NSObject, SGCManager {
|
|
|
4235
4250
|
}
|
|
4236
4251
|
|
|
4237
4252
|
guard let leftUUID = leftGlassUUID(forSN: DEVICE_SEARCH_ID),
|
|
4238
|
-
|
|
4253
|
+
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
4239
4254
|
else { return false }
|
|
4240
4255
|
|
|
4241
4256
|
let knownLeft = centralManager?.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
@@ -4276,8 +4291,8 @@ class G2: NSObject, SGCManager {
|
|
|
4276
4291
|
// a different service than our primary one, and retrieveConnectedPeripherals only
|
|
4277
4292
|
// returns peripherals whose services match.
|
|
4278
4293
|
let serviceUUIDs: [CBUUID] = [
|
|
4279
|
-
G2BLE.SERVICE_UUID,
|
|
4280
|
-
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
4294
|
+
G2BLE.SERVICE_UUID, // EvenHub: 00002760-...-0000
|
|
4295
|
+
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"), // Nordic UART
|
|
4281
4296
|
]
|
|
4282
4297
|
var devices: [CBPeripheral] = []
|
|
4283
4298
|
for svc in serviceUUIDs {
|
|
@@ -4305,8 +4320,8 @@ class G2: NSObject, SGCManager {
|
|
|
4305
4320
|
// Extract XX (the numeric ID between G2_ and _L_/_R_)
|
|
4306
4321
|
let pattern = "G2_(\\d+)_"
|
|
4307
4322
|
guard let regex = try? NSRegularExpression(pattern: pattern),
|
|
4308
|
-
|
|
4309
|
-
|
|
4323
|
+
let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
|
|
4324
|
+
let range = Range(match.range(at: 1), in: name)
|
|
4310
4325
|
else {
|
|
4311
4326
|
return nil
|
|
4312
4327
|
}
|
|
@@ -4452,7 +4467,7 @@ class G2: NSObject, SGCManager {
|
|
|
4452
4467
|
if cmd == 10, let configData = fields[13] as? Data {
|
|
4453
4468
|
var cReader = ProtobufReader(configData)
|
|
4454
4469
|
let cFields = cReader.parseFields()
|
|
4455
|
-
let voiceSwitch = cFields[1] as? Int32 ?? 0
|
|
4470
|
+
let voiceSwitch = cFields[1] as? Int32 ?? 0 // omitted = 0 = OFF
|
|
4456
4471
|
Bridge.log(
|
|
4457
4472
|
"G2: EvenAI CONFIG echo — voiceSwitch=\(voiceSwitch) (\(voiceSwitch == 1 ? "ON" : "OFF")) config=\(cFields)"
|
|
4458
4473
|
)
|
|
@@ -4487,7 +4502,8 @@ class G2: NSObject, SGCManager {
|
|
|
4487
4502
|
body: [
|
|
4488
4503
|
"heading": Int(heading),
|
|
4489
4504
|
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
4490
|
-
]
|
|
4505
|
+
]
|
|
4506
|
+
)
|
|
4491
4507
|
|
|
4492
4508
|
case NavigationCmd.osNotifyCompassCalibrateStart.rawValue:
|
|
4493
4509
|
Bridge.log("G2: compass calibration started — wearer should look around")
|
|
@@ -4507,7 +4523,6 @@ class G2: NSObject, SGCManager {
|
|
|
4507
4523
|
var reader = ProtobufReader(payload)
|
|
4508
4524
|
let fields = reader.parseFields()
|
|
4509
4525
|
|
|
4510
|
-
|
|
4511
4526
|
let payloadStr = "\(payload.map { String(format: "%02X", $0) }.joined())"
|
|
4512
4527
|
if payloadStr.contains("080C7A02100C") {
|
|
4513
4528
|
// heartbeat response
|
|
@@ -4557,7 +4572,6 @@ class G2: NSObject, SGCManager {
|
|
|
4557
4572
|
}
|
|
4558
4573
|
}
|
|
4559
4574
|
} else {
|
|
4560
|
-
|
|
4561
4575
|
// NOTE: the per-fragment image ACK is correlated inline on the BLE callback queue in
|
|
4562
4576
|
// correlateImageAck() (called from didUpdateValueFor before this is dispatched to the
|
|
4563
4577
|
// main actor) so it is never delayed behind a saturated main actor. Nothing to do here.
|
|
@@ -4590,7 +4604,7 @@ class G2: NSObject, SGCManager {
|
|
|
4590
4604
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
4591
4605
|
)
|
|
4592
4606
|
pageCreated = false
|
|
4593
|
-
evenHubMicActive = false
|
|
4607
|
+
evenHubMicActive = false // mic dies with the page
|
|
4594
4608
|
}
|
|
4595
4609
|
}
|
|
4596
4610
|
// if let errorCode = resFields[8] as? Int32 {
|
|
@@ -4608,7 +4622,7 @@ class G2: NSObject, SGCManager {
|
|
|
4608
4622
|
if cmdValue == 9 || cmdValue == 10 {
|
|
4609
4623
|
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
4610
4624
|
pageCreated = false
|
|
4611
|
-
evenHubMicActive = false
|
|
4625
|
+
evenHubMicActive = false // mic dies with the page
|
|
4612
4626
|
}
|
|
4613
4627
|
}
|
|
4614
4628
|
}
|
|
@@ -4653,8 +4667,8 @@ class G2: NSObject, SGCManager {
|
|
|
4653
4667
|
let wireType = Int(tag & 0x07)
|
|
4654
4668
|
guard wireType == 5, data.distance(from: i, to: data.endIndex) >= 4 else { break }
|
|
4655
4669
|
var bits: UInt32 = 0
|
|
4656
|
-
for b in 0..<4 {
|
|
4657
|
-
bits |= UInt32(data[data.index(i, offsetBy: b)]) << (8 * b)
|
|
4670
|
+
for b in 0 ..< 4 {
|
|
4671
|
+
bits |= UInt32(data[data.index(i, offsetBy: b)]) << (8 * b) // little-endian
|
|
4658
4672
|
}
|
|
4659
4673
|
i = data.index(i, offsetBy: 4)
|
|
4660
4674
|
let value = Float(bitPattern: bits)
|
|
@@ -4791,7 +4805,7 @@ class G2: NSObject, SGCManager {
|
|
|
4791
4805
|
// micEnabled (user intent) — recovery reads it to re-arm; clobbering it strands the mic.
|
|
4792
4806
|
if eventType == .systemExit || eventType == .abnormalExit {
|
|
4793
4807
|
pageCreated = false
|
|
4794
|
-
evenHubMicActive = false
|
|
4808
|
+
evenHubMicActive = false // firmware killed the mic with the page
|
|
4795
4809
|
}
|
|
4796
4810
|
return
|
|
4797
4811
|
}
|
|
@@ -4801,7 +4815,7 @@ class G2: NSObject, SGCManager {
|
|
|
4801
4815
|
var textReader = ProtobufReader(textData)
|
|
4802
4816
|
let textFields = textReader.parseFields()
|
|
4803
4817
|
if let eventTypeRaw = textFields[3] as? Int32,
|
|
4804
|
-
|
|
4818
|
+
let eventType = OsEventType(rawValue: eventTypeRaw)
|
|
4805
4819
|
{
|
|
4806
4820
|
guard let gestureName = mapEventTypeToGesture(eventType) else {
|
|
4807
4821
|
Bridge.log("G2: no gesture mapping for \(eventType) \(textFields)")
|
|
@@ -4843,7 +4857,7 @@ class G2: NSObject, SGCManager {
|
|
|
4843
4857
|
case .foregroundExit: return "foreground_exit"
|
|
4844
4858
|
case .systemExit: return "system_exit"
|
|
4845
4859
|
case .imuDataReport: return nil
|
|
4846
|
-
case .abnormalExit: return nil
|
|
4860
|
+
case .abnormalExit: return nil // don't report abnormal exits as gestures
|
|
4847
4861
|
}
|
|
4848
4862
|
}
|
|
4849
4863
|
|
|
@@ -4866,7 +4880,7 @@ class G2: NSObject, SGCManager {
|
|
|
4866
4880
|
|
|
4867
4881
|
// if the data is just a heartbeat, ignore it:
|
|
4868
4882
|
if let cmdValue = fields[1] as? Int32,
|
|
4869
|
-
|
|
4883
|
+
cmdValue == DevCfgCommandId.baseConnHeartBeat.rawValue
|
|
4870
4884
|
{
|
|
4871
4885
|
return
|
|
4872
4886
|
}
|
|
@@ -4890,7 +4904,7 @@ class G2: NSObject, SGCManager {
|
|
|
4890
4904
|
// Bridge.log("G2: Ring connection status: connStat=\(connStat)")
|
|
4891
4905
|
|
|
4892
4906
|
// Bridge.log("G2: RingConnectInfo: \(fields)")
|
|
4893
|
-
if let ringData = fields[5] as? Data {
|
|
4907
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
4894
4908
|
var ringReader = ProtobufReader(ringData)
|
|
4895
4909
|
let ringFields = ringReader.parseFields()
|
|
4896
4910
|
|
|
@@ -4917,10 +4931,10 @@ class G2: NSObject, SGCManager {
|
|
|
4917
4931
|
// DeviceStore.shared.apply("glasses", "controllerSearching", true)
|
|
4918
4932
|
// }
|
|
4919
4933
|
|
|
4920
|
-
if let ringData = fields[5] as? Data {
|
|
4934
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
4921
4935
|
var ringReader = ProtobufReader(ringData)
|
|
4922
4936
|
let ringFields = ringReader.parseFields()
|
|
4923
|
-
let connStatus = ringFields[4] as? Int32 ?? -1
|
|
4937
|
+
let connStatus = ringFields[4] as? Int32 ?? -1 // field 4 = connStatus
|
|
4924
4938
|
// Bridge.log(
|
|
4925
4939
|
// "G2: Ring connection status: connStatus?=\(connStatus))"
|
|
4926
4940
|
// )
|
|
@@ -5024,13 +5038,13 @@ class G2: NSObject, SGCManager {
|
|
|
5024
5038
|
|
|
5025
5039
|
// Software versions
|
|
5026
5040
|
if let leftVer = fields[5] as? Data,
|
|
5027
|
-
|
|
5041
|
+
let leftVersion = String(data: leftVer, encoding: .utf8)
|
|
5028
5042
|
{
|
|
5029
5043
|
// Bridge.log("G2: Left firmware: \(leftVersion)")
|
|
5030
5044
|
DeviceStore.shared.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
5031
5045
|
}
|
|
5032
5046
|
if let rightVer = fields[6] as? Data,
|
|
5033
|
-
|
|
5047
|
+
let rightVersion = String(data: rightVer, encoding: .utf8)
|
|
5034
5048
|
{
|
|
5035
5049
|
// Bridge.log("G2: Right firmware: \(rightVersion)")
|
|
5036
5050
|
DeviceStore.shared.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
@@ -5068,13 +5082,13 @@ class G2: NSObject, SGCManager {
|
|
|
5068
5082
|
// AppRespondToDashboard: field1=packageId, field2=flag (0=success)
|
|
5069
5083
|
if cmd == 3 {
|
|
5070
5084
|
var appRespW = ProtobufWriter()
|
|
5071
|
-
appRespW.writeInt32Field(1, packageId)
|
|
5072
|
-
appRespW.writeInt32Field(2, 0)
|
|
5085
|
+
appRespW.writeInt32Field(1, packageId) // packageId
|
|
5086
|
+
appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
|
|
5073
5087
|
|
|
5074
5088
|
var pkgW = ProtobufWriter()
|
|
5075
|
-
pkgW.writeInt32Field(1, 4)
|
|
5089
|
+
pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
|
|
5076
5090
|
pkgW.writeInt32Field(2, magicRandom)
|
|
5077
|
-
pkgW.writeMessageField(5, appRespW.data)
|
|
5091
|
+
pkgW.writeMessageField(5, appRespW.data) // field5 = appRespond
|
|
5078
5092
|
sendDashboardCommand(pkgW.data)
|
|
5079
5093
|
}
|
|
5080
5094
|
}
|
|
@@ -5112,7 +5126,7 @@ class G2: NSObject, SGCManager {
|
|
|
5112
5126
|
if data == Data([0x08, 0x01, 0x1A, 0x00]) {
|
|
5113
5127
|
Bridge.log("G2: dashboard toggle - dashboardShowing=\(dashboardShowing) opening=\(dashboardOpening)")
|
|
5114
5128
|
if dashboardOpening {
|
|
5115
|
-
dashboardOpening = false
|
|
5129
|
+
dashboardOpening = false // open confirmed; dashboard now owns the screen
|
|
5116
5130
|
return
|
|
5117
5131
|
}
|
|
5118
5132
|
dashboardShowing = 0
|
|
@@ -5169,7 +5183,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
5169
5183
|
// where the SN string starts.
|
|
5170
5184
|
|
|
5171
5185
|
// Skip "ER" prefix (2 bytes), read 14 bytes of SN
|
|
5172
|
-
let snData = data[2..<16]
|
|
5186
|
+
let snData = data[2 ..< 16]
|
|
5173
5187
|
return String(data: snData, encoding: .ascii)?
|
|
5174
5188
|
.replacingOccurrences(
|
|
5175
5189
|
of: "[\\x00-\\x1F\\x7F]", with: "", options: .regularExpression
|
|
@@ -5181,7 +5195,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
5181
5195
|
/// Returns "AA:BB:CC:DD:EE:FF" (big-endian, colon-separated).
|
|
5182
5196
|
func extractMac(from data: Data) -> String? {
|
|
5183
5197
|
guard data.count >= 22 else { return nil }
|
|
5184
|
-
let macLE = data[16..<22]
|
|
5198
|
+
let macLE = data[16 ..< 22]
|
|
5185
5199
|
return macLE.reversed().map { String(format: "%02X", $0) }.joined(separator: ":")
|
|
5186
5200
|
}
|
|
5187
5201
|
|
|
@@ -5205,13 +5219,13 @@ extension G2: CBCentralManagerDelegate {
|
|
|
5205
5219
|
) {
|
|
5206
5220
|
guard
|
|
5207
5221
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey]
|
|
5208
|
-
|
|
5222
|
+
as? String
|
|
5209
5223
|
else { return }
|
|
5210
5224
|
|
|
5211
5225
|
// G2 glasses have "Even" prefix and "G2" in name, with _L_ or _R_ for side
|
|
5212
5226
|
guard name.contains("G2") else { return }
|
|
5213
5227
|
guard let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
5214
|
-
|
|
5228
|
+
mfgData.count >= 16
|
|
5215
5229
|
else { return }
|
|
5216
5230
|
|
|
5217
5231
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -5507,22 +5521,23 @@ extension G2: CBPeripheralDelegate {
|
|
|
5507
5521
|
// Strip the 2-byte CRC trailer on the (last == only) packet.
|
|
5508
5522
|
let payloadEnd = 8 + payloadLen - 2
|
|
5509
5523
|
guard payloadEnd >= 8, payloadEnd <= rawData.count else { return }
|
|
5510
|
-
let payload = rawData.subdata(in: (rawData.startIndex + 8)..<(rawData.startIndex + payloadEnd))
|
|
5524
|
+
let payload = rawData.subdata(in: (rawData.startIndex + 8) ..< (rawData.startIndex + payloadEnd))
|
|
5511
5525
|
|
|
5512
5526
|
var reader = ProtobufReader(payload)
|
|
5513
5527
|
let fields = reader.parseFields()
|
|
5514
|
-
guard let resData = fields[6] as? Data else { return }
|
|
5528
|
+
guard let resData = fields[6] as? Data else { return } // field 6 = ImgResCmd
|
|
5515
5529
|
var resReader = ProtobufReader(resData)
|
|
5516
5530
|
let resFields = resReader.parseFields()
|
|
5517
5531
|
guard let errorCode = resFields[8] as? Int32,
|
|
5518
|
-
|
|
5532
|
+
let ackSession = resFields[3] as? Int32
|
|
5519
5533
|
else { return }
|
|
5520
5534
|
let ackFragment = (resFields[6] as? Int32) ?? 0
|
|
5521
5535
|
Bridge.log(
|
|
5522
5536
|
"G2: img_res: session=\(ackSession) fragment=\(ackFragment) errorCode=\(errorCode) success=\(errorCode == 4)"
|
|
5523
5537
|
)
|
|
5524
5538
|
completeImageAck(
|
|
5525
|
-
session: Int(ackSession), fragmentIndex: ackFragment, success: errorCode == 4
|
|
5539
|
+
session: Int(ackSession), fragmentIndex: ackFragment, success: errorCode == 4
|
|
5540
|
+
)
|
|
5526
5541
|
}
|
|
5527
5542
|
|
|
5528
5543
|
nonisolated func peripheral(
|