@mentra/bluetooth-sdk 3.1.0-dev.98 → 3.1.1-beta.230

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.
Files changed (51) hide show
  1. package/README.md +46 -13
  2. package/android/build.gradle +2 -0
  3. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +89 -90
  4. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHost.kt +106 -0
  5. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueue.kt +111 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +139 -0
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransport.kt +49 -0
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +1 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -1
  10. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  11. package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +6 -5
  12. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +11 -1
  13. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2SerialResolution.kt +23 -0
  14. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsHostTest.kt +64 -0
  15. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsQueueTest.kt +86 -0
  16. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +148 -0
  17. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTransportTest.kt +44 -0
  18. package/android/src/test/java/com/mentra/bluetoothsdk/G2SerialResolutionTest.kt +25 -0
  19. package/android/src/test/java/com/mentra/bluetoothsdk/OtaManifestDowngradeTest.kt +8 -0
  20. package/build/generated/changelogCatalog.d.ts +4 -1
  21. package/build/generated/changelogCatalog.d.ts.map +1 -1
  22. package/build/generated/changelogCatalog.js +5 -1
  23. package/build/generated/changelogCatalog.js.map +1 -1
  24. package/build/generated/releaseMetadata.js +6 -6
  25. package/build/generated/releaseMetadata.js.map +1 -1
  26. package/ios/Source/BluetoothSdkDefaults.swift +2 -2
  27. package/ios/Source/GeneratedChangelogCatalog.swift +2 -1
  28. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  29. package/ios/Source/OtaManifest.swift +6 -5
  30. package/ios/Source/internal/BluetoothSdkAnalytics.swift +127 -76
  31. package/ios/Source/internal/BluetoothSdkAnalyticsHost.swift +91 -0
  32. package/ios/Source/internal/BluetoothSdkAnalyticsQueue.swift +117 -0
  33. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +157 -0
  34. package/ios/Source/internal/BluetoothSdkAnalyticsTransport.swift +13 -0
  35. package/ios/Source/sgcs/G2.swift +375 -360
  36. package/ios/Source/sgcs/G2SerialResolution.swift +23 -0
  37. package/ios/Tests/BluetoothSdkAnalyticsHostTests.swift +63 -0
  38. package/ios/Tests/BluetoothSdkAnalyticsQueueTests.swift +97 -0
  39. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +122 -0
  40. package/ios/Tests/BluetoothSdkAnalyticsTransportTests.swift +36 -0
  41. package/ios/Tests/G2SerialResolutionTests.swift +19 -0
  42. package/ios/Tests/OtaManifestDowngradeTests.swift +20 -0
  43. package/package.json +1 -1
  44. package/plugin/build/analyticsProps.d.ts +9 -0
  45. package/plugin/build/analyticsProps.js +34 -0
  46. package/plugin/build/index.d.ts +8 -0
  47. package/plugin/build/withAndroid.js +12 -18
  48. package/plugin/build/withIos.d.ts +1 -0
  49. package/plugin/build/withIos.js +8 -18
  50. package/src/generated/changelogCatalog.ts +5 -1
  51. package/src/generated/releaseMetadata.ts +6 -6
@@ -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
- fileprivate mutating func appendLittleEndian(_ value: UInt16) {
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
- fileprivate mutating func appendLittleEndian(_ value: UInt32) {
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
- fileprivate mutating func appendLittleEndian(_ value: Int32) {
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 // 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
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 // 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)
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 // 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
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 // OS_NOITY_EVENT_TO_APP_PACKET - touch/gesture events
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 // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
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 // 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
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 // wire type 0 = varint
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 // wire type 0 = varint
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 // wire type 2 = length-delimited
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 // wire type 2 = length-delimited
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() // varint
269
- case 1: offset += 8 // 64-bit
270
- case 2: _ = readBytes() // length-delimited
271
- case 5: offset += 4 // 32-bit
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: // varint
283
+ case 0: // varint
284
284
  if let v = readVarint() { fields[fieldNum] = Int32(truncatingIfNeeded: v) }
285
- case 2: // length-delimited (submessage or bytes or string)
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) // 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
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) // ContainerName
317
+ w.writeStringField(10, name) // ContainerName
318
318
  }
319
- w.writeInt32Field(11, isEventCapture ? 1 : 0) // IsEventCapture
319
+ w.writeInt32Field(11, isEventCapture ? 1 : 0) // IsEventCapture
320
320
  if let content = content {
321
- w.writeStringField(12, content) // 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) // 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
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) // ContainerName
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) // ContainerID
350
+ w.writeInt32Field(1, containerID) // ContainerID
351
351
  if let name = containerName {
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
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) // 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) // field 3 = repeated TextObject
373
+ w.writeMessageField(3, tc) // field 3 = repeated TextObject
374
374
  }
375
375
  for ic in imageContainers {
376
- w.writeMessageField(4, ic) // field 4 = repeated ImageObject
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) // ContainerID
388
- w.writeInt32Field(3, contentOffset) // ContentOffset
389
- w.writeInt32Field(4, contentLength) // ContentLength
390
- w.writeStringField(5, content) // 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) // 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) // 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) // AudoFuncEn
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) // Cmd (field 1, enum)
425
- w.writeInt32Field(2, magicRandom) // MagicRandom (field 2)
426
- w.writeMessageField(subFieldNumber, subMessage) // the actual command payload
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) // Associate page with a menu item 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
- // Wire format recovered by on-device brute-force (sample magnitude ≈ 1.0 g confirms
517
- // the decode). Shapes from even_hub_sdk@0.0.10; numeric proto tags confirmed live:
518
- // EvenHub_Cmd_List IMU command = 19
519
- // evenhub_main_msg_ctx ImuCtrlCmd slot = field 20
520
- // ImuCtrlCmd { field 1 = IMU_ReportEn (bool), field 2 = reportFrq (pacing 100…1000) }
521
- // Report path: cmd=2 (osNotifyEventToApp) SendDeviceEvent.field13
522
- // Sys_ItemEvent { field 1 = eventType = 8 (IMU_DATA_REPORT),
523
- // field 3 = imuData = IMU_Report_Data }
524
- // IMU_Report_Data { field 1 = x, 2 = y, 3 = z } — each a 32-bit float (NOT double),
525
- // gravity-normalized (|v| 1 at rest).
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) // IMU_ReportEn
537
+ w.writeInt32Field(1, enable ? 1 : 0) // IMU_ReportEn
537
538
  if enable {
538
- w.writeInt32Field(2, reportFrq) // reportFrq (pacing code 100…1000)
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) // Cmd
551
- w.writeInt32Field(2, magicRandom) // MagicRandom
552
- w.writeMessageField(imuCtrlSubField, imuMsg) // ImuCtrlCmd slot (field 20)
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) // commandId
568
- w.writeInt32Field(2, magicRandom) // 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) // secAuth
575
- authW.writeInt32Field(2, 3) // phoneType = PHONE_IOS (eDevice.PHONE_IOS=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) // authMgr
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) // RIGHT
590
- w.writeMessageField(4, roleW.data) // roleChange (field 4 in DevCfgDataPackage)
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) // timeSync (field 128 in DevCfgDataPackage)
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 // empty
652
- w.writeMessageField(13, hbW.data) // baseHeartBeat (field 13)
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) // commandId = RING_CONNECT_INFO (6)
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) // connectRing
669
- ringW.writeBytesField(2, ringMac) // ringMac (6 bytes)
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)) // ringName
672
+ ringW.writeBytesField(3, Data(ringName.utf8)) // ringName
672
673
  }
673
674
 
674
- w.writeMessageField(5, ringW.data) // ringInfo (field 5)
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) // autoAdjust
687
- brightnessW.writeInt32Field(2, level) // brightnessLevel
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) // deviceReceiveBrightness (field 1)
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) // commandId
696
+ w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue) // commandId
696
697
  w.writeInt32Field(2, magicRandom)
697
- w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
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) // settingInfoType = APP_REQUIRE_BASIC_SETTING
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) // commandId
711
+ w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue) // commandId
711
712
  w.writeInt32Field(2, magicRandom)
712
- w.writeMessageField(4, reqW.data) // deviceReceiveRequestFromApp (field 4)
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) // headUpSwitch
721
+ headUpW.writeInt32Field(1, enabled ? 1 : 0) // headUpSwitch
721
722
 
722
723
  // DeviceReceiveInfoFromAPP
723
724
  var infoW = ProtobufWriter()
724
- infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
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) // deviceReceiveInfoFromApp (field 3)
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) // headUpAngle (field 2)
739
+ headUpW.writeInt32Field(2, angle) // headUpAngle (field 2)
739
740
 
740
741
  // DeviceReceiveInfoFromAPP
741
742
  var infoW = ProtobufWriter()
742
- infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
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) // yCoordinateLevel
757
+ yW.writeInt32Field(1, level) // yCoordinateLevel
757
758
 
758
759
  // DeviceReceiveInfoFromAPP
759
760
  var infoW = ProtobufWriter()
760
- infoW.writeMessageField(2, yW.data) // deviceReceiveYCoordinate (field 2)
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) // xCoordinateLevel
775
+ xW.writeInt32Field(1, level) // xCoordinateLevel
775
776
 
776
777
  // DeviceReceiveInfoFromAPP
777
778
  var infoW = ProtobufWriter()
778
- infoW.writeMessageField(3, xW.data) // deviceReceiveXCoordinate (field 3)
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) // processId = FINISH
797
+ configW.writeInt32Field(1, 4) // processId = FINISH
797
798
 
798
799
  // OnboardingDataPackage
799
800
  var w = ProtobufWriter()
800
- w.writeInt32Field(1, 1) // commandId = CONFIG
801
+ w.writeInt32Field(1, 1) // commandId = CONFIG
801
802
  w.writeInt32Field(2, magicRandom)
802
- w.writeMessageField(3, configW.data) // config (field 3)
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) // voiceSwitch (omitted when off, matching the app)
823
+ configW.writeInt32Field(1, 1) // voiceSwitch (omitted when off, matching the app)
823
824
  }
824
- configW.writeInt32Field(2, 32) // streamSpeed (always sent, app uses 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) // commandId = CONFIG
829
+ w.writeInt32Field(1, 10) // commandId = CONFIG
829
830
  w.writeInt32Field(2, magicRandom)
830
- w.writeMessageField(13, configW.data) // config (field 13)
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) // streamEnable
841
- askW.writeBytesField(4, Data(text.utf8)) // text
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) // commandId = ASK
845
+ w.writeInt32Field(1, 3) // commandId = ASK
845
846
  w.writeInt32Field(2, magicRandom)
846
- w.writeMessageField(5, askW.data) // askInfo (field 5)
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) // status
857
+ ctrlW.writeInt32Field(1, status) // status
857
858
 
858
859
  var w = ProtobufWriter()
859
- w.writeInt32Field(1, 1) // commandId = CTRL
860
+ w.writeInt32Field(1, 1) // commandId = CTRL
860
861
  w.writeInt32Field(2, magicRandom)
861
- w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
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) // streamEnable
877
- skillW.writeInt32Field(2, skillId) // skillId
878
- skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
879
- skillW.writeBytesField(4, Data(text.utf8)) // text (utterance / payload)
880
- skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
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) // commandId = SKILL
885
+ w.writeInt32Field(1, 6) // commandId = SKILL
885
886
  w.writeInt32Field(2, magicRandom)
886
- w.writeMessageField(8, skillW.data) // skillInfo (field 8)
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)) // appID
902
- iosW.writeBytesField(2, Data(displayName.utf8)) // displayName
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) // commandId = NOTIFICATION_IOS
906
+ w.writeInt32Field(1, 2) // commandId = NOTIFICATION_IOS
906
907
  w.writeInt32Field(2, magicRandom)
907
- w.writeMessageField(4, iosW.data) // IOS (field 4)
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) // notifEnable
925
- ctrlW.writeInt32Field(2, autoDispEnable) // autoDispEnable
926
- ctrlW.writeInt32Field(3, dispTime) // dispTime (seconds)
927
- ctrlW.writeInt32Field(5, avoidDisturbEnable) // 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) // commandId = NOTIFICATION_CTRL
931
+ w.writeInt32Field(1, 1) // commandId = NOTIFICATION_CTRL
931
932
  w.writeInt32Field(2, magicRandom)
932
- w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
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 // 17 char limit minus 2 for running indicator prefix
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? // nil for built-ins
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
- ? String(item.name.prefix(MAX_NAME_LENGTH))
995
- : item.name
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 // -1 because built-in occupies slot 0
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)) // itemTotalNum
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) // itemType = 0 (built-in)
1022
- itemW.writeInt32Field(4, item.appId) // itemAppId = SID
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) // itemType = 1 (third-party)
1025
- itemW.writeInt32Field(2, 1) // iconNum = 1
1026
- itemW.writeStringField(3, item.displayName ?? "") // itemName
1027
- itemW.writeInt32Field(4, item.appId) // itemAppId
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) // repeated item (field 2)
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) // Cmd = APP_SEND_MENU_INFO (0)
1035
- w.writeInt32Field(2, magicRandom) // MagicRandom
1036
- w.writeMessageField(3, menuW.data) // sendData (field 3)
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 // phone → glasses widget/config push
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) // [0] 0xAA
1223
- packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE) // [1] src+dst
1224
- packet.append(syncId) // [2] syncId
1225
- packet.append(payloadLen) // [3] payloadLen
1226
- packet.append(totalPackets) // [4] packetTotalNum
1227
- packet.append(serialNum) // [5] packetSerialNum
1228
- packet.append(serviceId) // [6] serviceId
1229
- packet.append(status) // [7] 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)) // CRC low
1235
- packet.append(UInt8((crc >> 8) & 0xFF)) // CRC high
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)] = [:] // key -> (accumulated payload, lastSerialNum)
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
- self.cont = nil
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 // -1 for unlimited
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
- // 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.
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 // createStartUpPageContainer can only be called once
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 // 1000ms timeout (matches Dart host)
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
- // Pace between consecutive packets (~G1's chunk pacing). Off any external callback, so the drain
1722
- // keeps making progress in the background instead of waiting for a callback iOS won't deliver.
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
- let char = right ? rightWriteChar : leftWriteChar
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: self.sendManager.nextMagicRandom())
1911
- self.sendDevSettingsCommand(authR, left: false, right: true)
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: self.sendManager.nextMagicRandom()
1918
+ magicRandom: sendManager.nextMagicRandom()
1917
1919
  )
1918
- self.sendDevSettingsCommand(roleChange, left: false, right: true)
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: self.sendManager.nextMagicRandom()
1925
+ magicRandom: sendManager.nextMagicRandom()
1924
1926
  )
1925
- self.sendDevSettingsCommand(timeSync, left: true, right: true)
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: self.sendManager.nextMagicRandom()
1932
+ magicRandom: sendManager.nextMagicRandom()
1931
1933
  )
1932
- self.sendOnboardingCommand(onboarding)
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, self.sendManager.nextMagicRandom())
1939
- self.sendGestureCtrlCommand(gestureInitW.data)
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) // cmd = DeviceReceiveRequest
1944
- uiSettW.writeInt32Field(2, self.sendManager.nextMagicRandom())
1945
- uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
1946
- self.sendToGlasses(
1947
- self.sendManager.buildPackets(
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) // displayMode
1957
- dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
1958
- dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
1959
- dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
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])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
1962
- dashDisplayW.writeInt32Field(6, self.dashboardHalfDayFormat()) // halfDayFormat
1963
- dashDisplayW.writeInt32Field(7, self.dashboardTemperatureUnit()) // temperatureUnit
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) // Dashboard_Receive
1970
- dashPkgW.writeInt32Field(2, self.sendManager.nextMagicRandom())
1971
+ dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
1972
+ dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
1971
1973
  dashPkgW.writeMessageField(4, dashRecvW.data)
1972
- self.sendDashboardCommand(dashPkgW.data)
1974
+ sendDashboardCommand(dashPkgW.data)
1973
1975
 
1974
1976
  // Disable "Hey Even" wakeword on connect
1975
1977
  let heyEvenOff = EvenAIProto.setHeyEven(
1976
- magicRandom: self.sendManager.nextMagicRandom(),
1978
+ magicRandom: sendManager.nextMagicRandom(),
1977
1979
  enabled: false
1978
1980
  )
1979
- self.sendEvenAICommand(heyEvenOff)
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
- self.startHeartbeats()
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
- if let peripheralName = self.rightPeripheral?.name
2020
- ?? self.leftPeripheral?.name,
2021
- let serialNumber = self.deviceNameToSerialNumber[peripheralName]
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
- self.rightPeripheral?.name
2030
- ?? self.leftPeripheral?.name ?? ""
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
- self.setFullyConnected()
2043
+ setFullyConnected()
2035
2044
 
2036
2045
  // connnect a controller if we have one:
2037
- self.connectController()
2046
+ connectController()
2038
2047
 
2039
2048
  // Query version + battery info from glasses
2040
- self.requestDeviceInfo()
2049
+ requestDeviceInfo()
2041
2050
 
2042
2051
  // send dashboard menu if we have stored items
2043
- self.sendMenuApps()
2052
+ sendMenuApps()
2044
2053
 
2045
2054
  // order the calendar (Schedule) widget first on the dashboard
2046
- self.setCalendarWidgetFirst()
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
- self.sendCalendarEvents(calendarEvents)
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
- // Protocol witness for SGCManager.sendText — G2 renders a simple string as a
2173
- // default-positioned text wall. The positioned variant is `sendTextAt`.
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 attempt in 1...IMG_MAX_ATTEMPTS {
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
- let i = imageContainers.firstIndex(where: { $0.dirty })
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
- imageContainers[j].bmpData.isEmpty
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
- imageContainers[j].bmpData == sentBytes
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
- // re-creates the containers and re-sends all images to the glasses:
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) // 300ms to settle
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 // 4-bit rows padded to 4 bytes
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
- let pixels = renderedImage.dataProvider?.data as Data?
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) // all-zero 8-bit grayscale = black
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 // ceil(width / 2)
3180
- let paddedRowSize = (bytesPerRow4bit + 3) & ~3 // pad to 4-byte boundary
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]) // "BM" signature
3191
- bmp.appendLittleEndian(UInt32(fileSize)) // File size
3192
- bmp.appendLittleEndian(UInt16(0)) // Reserved1
3193
- bmp.appendLittleEndian(UInt16(0)) // Reserved2
3194
- bmp.appendLittleEndian(UInt32(headerSize)) // Pixel data offset
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)) // DIB header size
3198
- bmp.appendLittleEndian(Int32(width)) // Width
3199
- bmp.appendLittleEndian(Int32(height)) // Height (positive = bottom-up)
3200
- bmp.appendLittleEndian(UInt16(1)) // Color planes
3201
- bmp.appendLittleEndian(UInt16(4)) // Bits per pixel (4-bit)
3202
- bmp.appendLittleEndian(UInt32(0)) // Compression (none)
3203
- bmp.appendLittleEndian(UInt32(pixelDataSize)) // Image size
3204
- bmp.appendLittleEndian(Int32(2835)) // X pixels/meter (~72 DPI)
3205
- bmp.appendLittleEndian(Int32(2835)) // Y pixels/meter
3206
- bmp.appendLittleEndian(UInt32(16)) // Colors used
3207
- bmp.appendLittleEndian(UInt32(0)) // Important colors (0 = all)
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) // 0, 17, 34, ... 255 (evenly spaced grayscale)
3212
- bmp.append(contentsOf: [val, val, val, 0]) // B, G, R, Reserved
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 // divide by 16
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 // dashboard takes EvenHub focus; firmware kills the mic
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) // displayMode
3284
- dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
3285
- dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
3286
- dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
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()) // halfDayFormat
3290
- dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
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) // Dashboard_Receive
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
- let time = ev["time"] as? String,
3364
- let endTs = ev["endDate"] as? Double
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 // EVEN_AI_ENTER
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, // NOTIFICATION, show
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) // cmd
3870
- w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
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) // displayMode
3947
- dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
3948
- dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
3949
- dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count)) // widgetDisplayCount
3950
- dashDisplayW.writeMessageField(5, Data(widgetOrder)) // widgetDisplayOrder: Schedule first
3951
- dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
3952
- dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
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) // Dashboard_Receive
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
- let packageName = dict["packageName"] as? String
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
- let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
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, // EvenHub: 00002760-...-0000
4195
- CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"), // Nordic UART
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
- let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
4224
- let range = Range(match.range(at: 1), in: name)
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 // omitted = 0 = OFF
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 // mic dies with the page
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 // mic dies with the page
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) // little-endian
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 // firmware killed the mic with the page
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
- let eventType = OsEventType(rawValue: eventTypeRaw)
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 // don't report abnormal exits as gestures
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
- cmdValue == DevCfgCommandId.baseConnHeartBeat.rawValue
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 { // field 5 = ringInfo
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 { // field 5 = ringInfo
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 // field 4 = connStatus
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
- let leftVersion = String(data: leftVer, encoding: .utf8)
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
- let rightVersion = String(data: rightVer, encoding: .utf8)
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) // packageId
4975
- appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
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) // commandId = APP_RECEIVE
4992
+ pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
4979
4993
  pkgW.writeInt32Field(2, magicRandom)
4980
- pkgW.writeMessageField(5, appRespW.data) // field5 = appRespond
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 // open confirmed; dashboard now owns the screen
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
- as? String
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
- mfgData.count >= 16
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 } // field 6 = ImgResCmd
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
- let ackSession = resFields[3] as? Int32
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(