@mentra/bluetooth-sdk 3.2.0-dev.209 → 3.2.0-dev.213

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