@mentra/bluetooth-sdk 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +39 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +22 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +829 -643
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +40 -64
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -1
- package/build/BluetoothSdk.types.d.ts +12 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +1 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +14 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +3 -0
- package/ios/Source/Bridge.swift +10 -0
- package/ios/Source/DeviceManager.swift +24 -21
- package/ios/Source/DeviceStore.swift +8 -2
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/sgcs/G1.swift +3 -3
- package/ios/Source/sgcs/G2.swift +1022 -640
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +2 -2
- package/ios/Source/sgcs/MentraNex.swift +215 -85
- package/ios/Source/sgcs/SGCManager.swift +13 -4
- package/ios/Source/sgcs/Simulated.swift +2 -2
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +13 -1
- package/src/_private/BluetoothSdkModule.ts +22 -3
- package/src/index.ts +3 -0
package/ios/Source/sgcs/G2.swift
CHANGED
|
@@ -13,18 +13,18 @@ import UIKit
|
|
|
13
13
|
|
|
14
14
|
// MARK: - Data Little-Endian Helpers (for BMP construction)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
mutating func appendLittleEndian(_ value: UInt16) {
|
|
16
|
+
extension Data {
|
|
17
|
+
fileprivate mutating func appendLittleEndian(_ value: UInt16) {
|
|
18
18
|
var v = value.littleEndian
|
|
19
19
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
mutating func appendLittleEndian(_ value: UInt32) {
|
|
22
|
+
fileprivate mutating func appendLittleEndian(_ value: UInt32) {
|
|
23
23
|
var v = value.littleEndian
|
|
24
24
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
mutating func appendLittleEndian(_ value: Int32) {
|
|
27
|
+
fileprivate mutating func appendLittleEndian(_ value: Int32) {
|
|
28
28
|
var v = value.littleEndian
|
|
29
29
|
Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) }
|
|
30
30
|
}
|
|
@@ -51,32 +51,47 @@ private enum G2BLE {
|
|
|
51
51
|
|
|
52
52
|
/// Service IDs from service_id_def.proto
|
|
53
53
|
private enum ServiceID: UInt8 {
|
|
54
|
-
case dashboard = 1
|
|
55
|
-
case menu = 3
|
|
56
|
-
case notification = 4
|
|
57
|
-
case evenAI = 7
|
|
58
|
-
case
|
|
59
|
-
case
|
|
60
|
-
case
|
|
61
|
-
case
|
|
62
|
-
case
|
|
63
|
-
case
|
|
54
|
+
case dashboard = 1 // 0x01 - UI_BACKGROUND_DASHBOARD_APP_ID
|
|
55
|
+
case menu = 3 // 0x03 - UI_FOREGROUND_MEUN_ID (typo is intentional — matches Even's proto)
|
|
56
|
+
case notification = 4 // 0x04 - UI_FOREGROUND_NOTIFICATION_ID
|
|
57
|
+
case evenAI = 7 // 0x07 - UI_FOREGROUND_EVEN_AI_ID
|
|
58
|
+
case navigation = 8 // 0x08 - UI_BACKGROUND_NAVIGATION_ID (compass/heading lives here)
|
|
59
|
+
case g2Setting = 9 // 0x09 - UI_SETTING_APP_ID
|
|
60
|
+
case gestureCtrl = 13 // 0x0D - gesture_ctrl lifecycle signals
|
|
61
|
+
case onboarding = 16 // 0x10 - UI_ONBOARDING_APP_ID
|
|
62
|
+
case deviceSettings = 128 // 0x80 - UX_DEVICE_SETTINGS_APP_ID
|
|
63
|
+
case evenHubCtrl = 129 // 0x81 - EvenHub CTRL channel (init/registration)
|
|
64
|
+
case evenHub = 224 // 0xE0 - UI_BACKGROUND_EVENHUB_APP_ID
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/// EvenHub command IDs from EvenHub.proto
|
|
67
68
|
private enum EvenHubCmd: Int32 {
|
|
68
|
-
case createStartupPage = 0
|
|
69
|
-
case updateImageRawData = 3
|
|
70
|
-
case updateTextData = 5
|
|
71
|
-
case rebuildPage = 7
|
|
72
|
-
case shutdownPage = 9
|
|
73
|
-
case heartbeat = 12
|
|
74
|
-
case audioControl = 15
|
|
69
|
+
case createStartupPage = 0 // APP_REQUEST_CREATE_STARTUP_PAGE_PACKET
|
|
70
|
+
case updateImageRawData = 3 // APP_UPDATE_IMAGE_RAW_DATA_PACKET
|
|
71
|
+
case updateTextData = 5 // APP_UPDATE_TEXT_DATA_PACKET
|
|
72
|
+
case rebuildPage = 7 // APP_REQUEST_REBUILD_PAGE_PACKET
|
|
73
|
+
case shutdownPage = 9 // APP_REQUEST_SHUTDOWN_PAGE_PACKET
|
|
74
|
+
case heartbeat = 12 // APP_REQUEST_HEARTBEAT_PACKET
|
|
75
|
+
case audioControl = 15 // APP_REQUEST_AUDIO_CTR_PACKET
|
|
76
|
+
case imuControl = 19 // APP_REQUEST_IMU_CTR_PACKET (confirmed via on-device brute-force)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Navigation_Cmd_list from navigation.proto (service 0x08)
|
|
80
|
+
private enum NavigationCmd: Int32 {
|
|
81
|
+
case appSendHeartbeat = 0 // APP_SEND_HEARTBEAT_CMD
|
|
82
|
+
case appRequestStartUp = 5 // APP_REQUEST_START_UP — begin navigation/compass session
|
|
83
|
+
case appSendBasicInfo = 7 // APP_SEND_BASIC_INFO
|
|
84
|
+
case appRequestExit = 12 // APP_REQUEST_EXIT
|
|
85
|
+
case osNotifyExit = 13 // OS_NOTIFY_EXIT
|
|
86
|
+
case osNotifyReviewChanged = 14 // OS_NOTIFY_REVIEW_CHANGED
|
|
87
|
+
case osNotifyCompassChanged = 15 // OS_NOTIFY_COMPASS_CHANGED — heading update
|
|
88
|
+
case osNotifyCompassCalibrateStart = 16 // OS_NOTIFY_COMPASS_CALIBRATE_STRAT (sic)
|
|
89
|
+
case osNotifyCompassCalibrateComplete = 17 // OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
/// EvenHub response command IDs (from glasses → phone)
|
|
78
93
|
private enum EvenHubResponseCmd: Int32 {
|
|
79
|
-
case osNotifyEventToApp = 2
|
|
94
|
+
case osNotifyEventToApp = 2 // OS_NOITY_EVENT_TO_APP_PACKET - touch/gesture events
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
/// OsEventTypeList from EvenHub.proto
|
|
@@ -89,15 +104,16 @@ private enum OsEventType: Int32 {
|
|
|
89
104
|
case foregroundExit = 5
|
|
90
105
|
case abnormalExit = 6
|
|
91
106
|
case systemExit = 7
|
|
107
|
+
case imuDataReport = 8 // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
/// g2_settingCommandId from g2_setting.proto
|
|
95
111
|
private enum G2SettingCommandId: Int32 {
|
|
96
112
|
case none = 0
|
|
97
|
-
case deviceReceiveInfo = 1
|
|
98
|
-
case deviceReceiveRequest = 2
|
|
99
|
-
case deviceSendToApp = 3
|
|
100
|
-
case deviceRespondToApp = 4
|
|
113
|
+
case deviceReceiveInfo = 1 // Send settings TO glasses
|
|
114
|
+
case deviceReceiveRequest = 2 // Request info FROM glasses
|
|
115
|
+
case deviceSendToApp = 3 // Glasses sends info TO app
|
|
116
|
+
case deviceRespondToApp = 4 // Glasses responds to app
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
/// DevCfgCommandId from dev_config_protocol.proto
|
|
@@ -141,7 +157,7 @@ private struct ProtobufWriter {
|
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
mutating func writeInt32Field(_ fieldNumber: Int, _ value: Int32) {
|
|
144
|
-
let tag = UInt64(fieldNumber << 3) | 0
|
|
160
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
145
161
|
writeVarint(tag)
|
|
146
162
|
// protobuf int32 uses varint encoding; negative values use 10 bytes
|
|
147
163
|
if value >= 0 {
|
|
@@ -152,13 +168,13 @@ private struct ProtobufWriter {
|
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
mutating func writeInt64Field(_ fieldNumber: Int, _ value: Int64) {
|
|
155
|
-
let tag = UInt64(fieldNumber << 3) | 0
|
|
171
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
156
172
|
writeVarint(tag)
|
|
157
173
|
writeVarint(UInt64(bitPattern: value))
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
mutating func writeStringField(_ fieldNumber: Int, _ value: String) {
|
|
161
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
177
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
162
178
|
writeVarint(tag)
|
|
163
179
|
let utf8 = Array(value.utf8)
|
|
164
180
|
writeVarint(UInt64(utf8.count))
|
|
@@ -166,7 +182,7 @@ private struct ProtobufWriter {
|
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
mutating func writeBytesField(_ fieldNumber: Int, _ value: Data) {
|
|
169
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
185
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
170
186
|
writeVarint(tag)
|
|
171
187
|
writeVarint(UInt64(value.count))
|
|
172
188
|
data.append(value)
|
|
@@ -228,7 +244,7 @@ private struct ProtobufReader {
|
|
|
228
244
|
guard let len = readVarint() else { return nil }
|
|
229
245
|
let length = Int(len)
|
|
230
246
|
guard offset + length <= data.count else { return nil }
|
|
231
|
-
let result = data[(data.startIndex + offset)
|
|
247
|
+
let result = data[(data.startIndex + offset)..<(data.startIndex + offset + length)]
|
|
232
248
|
offset += length
|
|
233
249
|
return Data(result)
|
|
234
250
|
}
|
|
@@ -241,10 +257,10 @@ private struct ProtobufReader {
|
|
|
241
257
|
/// Skip a field value based on wire type
|
|
242
258
|
mutating func skipField(wireType: Int) {
|
|
243
259
|
switch wireType {
|
|
244
|
-
case 0: _ = readVarint()
|
|
245
|
-
case 1: offset += 8
|
|
246
|
-
case 2: _ = readBytes()
|
|
247
|
-
case 5: offset += 4
|
|
260
|
+
case 0: _ = readVarint() // varint
|
|
261
|
+
case 1: offset += 8 // 64-bit
|
|
262
|
+
case 2: _ = readBytes() // length-delimited
|
|
263
|
+
case 5: offset += 4 // 32-bit
|
|
248
264
|
default: break
|
|
249
265
|
}
|
|
250
266
|
}
|
|
@@ -256,9 +272,9 @@ private struct ProtobufReader {
|
|
|
256
272
|
while hasMore {
|
|
257
273
|
guard let (fieldNum, wireType) = readTag() else { break }
|
|
258
274
|
switch wireType {
|
|
259
|
-
case 0:
|
|
275
|
+
case 0: // varint
|
|
260
276
|
if let v = readVarint() { fields[fieldNum] = Int32(truncatingIfNeeded: v) }
|
|
261
|
-
case 2:
|
|
277
|
+
case 2: // length-delimited (submessage or bytes or string)
|
|
262
278
|
if let d = readBytes() { fields[fieldNum] = d }
|
|
263
279
|
default:
|
|
264
280
|
skipField(wireType: wireType)
|
|
@@ -280,21 +296,21 @@ private enum EvenHubProto {
|
|
|
280
296
|
content: String? = nil
|
|
281
297
|
) -> Data {
|
|
282
298
|
var w = ProtobufWriter()
|
|
283
|
-
w.writeInt32Field(1, x)
|
|
284
|
-
w.writeInt32Field(2, y)
|
|
285
|
-
w.writeInt32Field(3, width)
|
|
286
|
-
w.writeInt32Field(4, height)
|
|
287
|
-
w.writeInt32Field(5, borderWidth)
|
|
288
|
-
w.writeInt32Field(6, borderColor)
|
|
289
|
-
w.writeInt32Field(7, borderRadius)
|
|
290
|
-
w.writeInt32Field(8, paddingLength)
|
|
291
|
-
w.writeInt32Field(9, containerID)
|
|
299
|
+
w.writeInt32Field(1, x) // XPosition
|
|
300
|
+
w.writeInt32Field(2, y) // YPosition
|
|
301
|
+
w.writeInt32Field(3, width) // Width
|
|
302
|
+
w.writeInt32Field(4, height) // Height
|
|
303
|
+
w.writeInt32Field(5, borderWidth) // BorderWidth
|
|
304
|
+
w.writeInt32Field(6, borderColor) // BorderColor
|
|
305
|
+
w.writeInt32Field(7, borderRadius) // BorderRdaius (sic - typo in proto)
|
|
306
|
+
w.writeInt32Field(8, paddingLength) // PaddingLength
|
|
307
|
+
w.writeInt32Field(9, containerID) // ContainerID
|
|
292
308
|
if let name = containerName {
|
|
293
|
-
w.writeStringField(10, name)
|
|
309
|
+
w.writeStringField(10, name) // ContainerName
|
|
294
310
|
}
|
|
295
|
-
w.writeInt32Field(11, isEventCapture ? 1 : 0)
|
|
311
|
+
w.writeInt32Field(11, isEventCapture ? 1 : 0) // IsEventCapture
|
|
296
312
|
if let content = content {
|
|
297
|
-
w.writeStringField(12, content)
|
|
313
|
+
w.writeStringField(12, content) // Content
|
|
298
314
|
}
|
|
299
315
|
return w.data
|
|
300
316
|
}
|
|
@@ -305,13 +321,13 @@ private enum EvenHubProto {
|
|
|
305
321
|
containerID: Int32, containerName: String? = nil
|
|
306
322
|
) -> Data {
|
|
307
323
|
var w = ProtobufWriter()
|
|
308
|
-
w.writeInt32Field(1, x)
|
|
309
|
-
w.writeInt32Field(2, y)
|
|
310
|
-
w.writeInt32Field(3, width)
|
|
311
|
-
w.writeInt32Field(4, height)
|
|
312
|
-
w.writeInt32Field(5, containerID)
|
|
324
|
+
w.writeInt32Field(1, x) // XPosition
|
|
325
|
+
w.writeInt32Field(2, y) // YPosition
|
|
326
|
+
w.writeInt32Field(3, width) // Width
|
|
327
|
+
w.writeInt32Field(4, height) // Height
|
|
328
|
+
w.writeInt32Field(5, containerID) // ContainerID
|
|
313
329
|
if let name = containerName {
|
|
314
|
-
w.writeStringField(6, name)
|
|
330
|
+
w.writeStringField(6, name) // ContainerName
|
|
315
331
|
}
|
|
316
332
|
return w.data
|
|
317
333
|
}
|
|
@@ -323,16 +339,16 @@ private enum EvenHubProto {
|
|
|
323
339
|
mapFragmentIndex: Int32, mapFragmentPacketSize: Int32, mapRawData: Data
|
|
324
340
|
) -> Data {
|
|
325
341
|
var w = ProtobufWriter()
|
|
326
|
-
w.writeInt32Field(1, containerID)
|
|
342
|
+
w.writeInt32Field(1, containerID) // ContainerID
|
|
327
343
|
if let name = containerName {
|
|
328
|
-
w.writeStringField(2, name)
|
|
329
|
-
}
|
|
330
|
-
w.writeInt32Field(3, mapSessionId)
|
|
331
|
-
w.writeInt32Field(4, mapTotalSize)
|
|
332
|
-
w.writeInt32Field(5, compressMode)
|
|
333
|
-
w.writeInt32Field(6, mapFragmentIndex)
|
|
334
|
-
w.writeInt32Field(7, mapFragmentPacketSize)
|
|
335
|
-
w.writeBytesField(8, mapRawData)
|
|
344
|
+
w.writeStringField(2, name) // ContainerName
|
|
345
|
+
}
|
|
346
|
+
w.writeInt32Field(3, mapSessionId) // MapSessionId
|
|
347
|
+
w.writeInt32Field(4, mapTotalSize) // MapTotalSize
|
|
348
|
+
w.writeInt32Field(5, compressMode) // CompressMode
|
|
349
|
+
w.writeInt32Field(6, mapFragmentIndex) // MapFragmentIndex
|
|
350
|
+
w.writeInt32Field(7, mapFragmentPacketSize) // MapFragmentPacketSize
|
|
351
|
+
w.writeBytesField(8, mapRawData) // MapRawData
|
|
336
352
|
return w.data
|
|
337
353
|
}
|
|
338
354
|
|
|
@@ -343,13 +359,13 @@ private enum EvenHubProto {
|
|
|
343
359
|
imageContainers: [Data] = []
|
|
344
360
|
) -> Data {
|
|
345
361
|
var w = ProtobufWriter()
|
|
346
|
-
w.writeInt32Field(1, containerTotalNum)
|
|
362
|
+
w.writeInt32Field(1, containerTotalNum) // ContainerTotalNum
|
|
347
363
|
// field 2 = repeated ListContainerProperty ListObject (not used here)
|
|
348
364
|
for tc in textContainers {
|
|
349
|
-
w.writeMessageField(3, tc)
|
|
365
|
+
w.writeMessageField(3, tc) // field 3 = repeated TextObject
|
|
350
366
|
}
|
|
351
367
|
for ic in imageContainers {
|
|
352
|
-
w.writeMessageField(4, ic)
|
|
368
|
+
w.writeMessageField(4, ic) // field 4 = repeated ImageObject
|
|
353
369
|
}
|
|
354
370
|
return w.data
|
|
355
371
|
}
|
|
@@ -360,17 +376,17 @@ private enum EvenHubProto {
|
|
|
360
376
|
contentLength: Int32, content: String
|
|
361
377
|
) -> Data {
|
|
362
378
|
var w = ProtobufWriter()
|
|
363
|
-
w.writeInt32Field(1, containerID)
|
|
364
|
-
w.writeInt32Field(3, contentOffset)
|
|
365
|
-
w.writeInt32Field(4, contentLength)
|
|
366
|
-
w.writeStringField(5, content)
|
|
379
|
+
w.writeInt32Field(1, containerID) // ContainerID
|
|
380
|
+
w.writeInt32Field(3, contentOffset) // ContentOffset
|
|
381
|
+
w.writeInt32Field(4, contentLength) // ContentLength
|
|
382
|
+
w.writeStringField(5, content) // Content
|
|
367
383
|
return w.data
|
|
368
384
|
}
|
|
369
385
|
|
|
370
386
|
/// Build a ShutDownContaniner message (sic - typo in proto)
|
|
371
387
|
static func shutdownContainer(exitMode: Int32 = 0) -> Data {
|
|
372
388
|
var w = ProtobufWriter()
|
|
373
|
-
w.writeInt32Field(1, exitMode)
|
|
389
|
+
w.writeInt32Field(1, exitMode) // exitMode
|
|
374
390
|
return w.data
|
|
375
391
|
}
|
|
376
392
|
|
|
@@ -378,7 +394,7 @@ private enum EvenHubProto {
|
|
|
378
394
|
static func heartbeatPacket(cnt: Int32 = 0) -> Data {
|
|
379
395
|
var w = ProtobufWriter()
|
|
380
396
|
if cnt != 0 {
|
|
381
|
-
w.writeInt32Field(1, cnt)
|
|
397
|
+
w.writeInt32Field(1, cnt) // Cnt
|
|
382
398
|
}
|
|
383
399
|
return w.data
|
|
384
400
|
}
|
|
@@ -386,7 +402,7 @@ private enum EvenHubProto {
|
|
|
386
402
|
/// Build an AudioCtrCmd message
|
|
387
403
|
static func audioCtrCmd(enable: Bool) -> Data {
|
|
388
404
|
var w = ProtobufWriter()
|
|
389
|
-
w.writeInt32Field(1, enable ? 1 : 0)
|
|
405
|
+
w.writeInt32Field(1, enable ? 1 : 0) // AudoFuncEn
|
|
390
406
|
return w.data
|
|
391
407
|
}
|
|
392
408
|
|
|
@@ -397,11 +413,11 @@ private enum EvenHubProto {
|
|
|
397
413
|
appId: Int32? = nil
|
|
398
414
|
) -> Data {
|
|
399
415
|
var w = ProtobufWriter()
|
|
400
|
-
w.writeInt32Field(1, cmd.rawValue)
|
|
401
|
-
w.writeInt32Field(2, magicRandom)
|
|
402
|
-
w.writeMessageField(subFieldNumber, subMessage)
|
|
416
|
+
w.writeInt32Field(1, cmd.rawValue) // Cmd (field 1, enum)
|
|
417
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom (field 2)
|
|
418
|
+
w.writeMessageField(subFieldNumber, subMessage) // the actual command payload
|
|
403
419
|
if let appId = appId {
|
|
404
|
-
w.writeInt32Field(5, appId)
|
|
420
|
+
w.writeInt32Field(5, appId) // Associate page with a menu item appId
|
|
405
421
|
}
|
|
406
422
|
return w.data
|
|
407
423
|
}
|
|
@@ -486,6 +502,48 @@ private enum EvenHubProto {
|
|
|
486
502
|
cmd: .audioControl, subFieldNumber: 18, subMessage: audioMsg, magicRandom: magicRandom
|
|
487
503
|
)
|
|
488
504
|
}
|
|
505
|
+
|
|
506
|
+
// MARK: - IMU control
|
|
507
|
+
//
|
|
508
|
+
// Wire format recovered by on-device brute-force (sample magnitude ≈ 1.0 g confirms
|
|
509
|
+
// the decode). Shapes from even_hub_sdk@0.0.10; numeric proto tags confirmed live:
|
|
510
|
+
// EvenHub_Cmd_List IMU command = 19
|
|
511
|
+
// evenhub_main_msg_ctx ImuCtrlCmd slot = field 20
|
|
512
|
+
// ImuCtrlCmd { field 1 = IMU_ReportEn (bool), field 2 = reportFrq (pacing 100…1000) }
|
|
513
|
+
// Report path: cmd=2 (osNotifyEventToApp) → SendDeviceEvent.field13 →
|
|
514
|
+
// Sys_ItemEvent { field 1 = eventType = 8 (IMU_DATA_REPORT),
|
|
515
|
+
// field 3 = imuData = IMU_Report_Data }
|
|
516
|
+
// IMU_Report_Data { field 1 = x, 2 = y, 3 = z } — each a 32-bit float (NOT double),
|
|
517
|
+
// gravity-normalized (|v| ≈ 1 at rest).
|
|
518
|
+
static let imuCtrlSubField = 20
|
|
519
|
+
|
|
520
|
+
/// ImuReportPace pacing codes (protocol values, NOT literal Hz). Step 100, 100…1000.
|
|
521
|
+
static let imuPaceP100: Int32 = 100
|
|
522
|
+
static let imuPaceP500: Int32 = 500
|
|
523
|
+
static let imuPaceP1000: Int32 = 1000
|
|
524
|
+
|
|
525
|
+
/// Build an ImuCtrlCmd sub-message.
|
|
526
|
+
static func imuCtrlCmd(enable: Bool, reportFrq: Int32) -> Data {
|
|
527
|
+
var w = ProtobufWriter()
|
|
528
|
+
w.writeInt32Field(1, enable ? 1 : 0) // IMU_ReportEn
|
|
529
|
+
if enable {
|
|
530
|
+
w.writeInt32Field(2, reportFrq) // reportFrq (pacing code 100…1000)
|
|
531
|
+
}
|
|
532
|
+
return w.data
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// Build a full evenhub_main_msg_ctx that enables/disables IMU reporting.
|
|
536
|
+
/// `reportFrq` is an ImuReportPace pacing code; ignored when disabling.
|
|
537
|
+
static func imuControlMessage(
|
|
538
|
+
enable: Bool, reportFrq: Int32 = imuPaceP100, magicRandom: Int32 = 0
|
|
539
|
+
) -> Data {
|
|
540
|
+
let imuMsg = imuCtrlCmd(enable: enable, reportFrq: reportFrq)
|
|
541
|
+
var w = ProtobufWriter()
|
|
542
|
+
w.writeInt32Field(1, EvenHubCmd.imuControl.rawValue) // Cmd
|
|
543
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
544
|
+
w.writeMessageField(imuCtrlSubField, imuMsg) // ImuCtrlCmd slot (field 20)
|
|
545
|
+
return w.data
|
|
546
|
+
}
|
|
489
547
|
}
|
|
490
548
|
|
|
491
549
|
// MARK: - DevSettings Auth Protobuf Builders
|
|
@@ -498,17 +556,17 @@ private enum DevSettingsProto {
|
|
|
498
556
|
// field 2 = magicRandom (int32)
|
|
499
557
|
// field 3 = authMgr (AuthMgr message)
|
|
500
558
|
var w = ProtobufWriter()
|
|
501
|
-
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue)
|
|
502
|
-
w.writeInt32Field(2, magicRandom)
|
|
559
|
+
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue) // commandId
|
|
560
|
+
w.writeInt32Field(2, magicRandom) // magicRandom
|
|
503
561
|
|
|
504
562
|
// AuthMgr sub-message:
|
|
505
563
|
// field 1 = secAuth (bool)
|
|
506
564
|
// field 2 = phoneType (enum eDevice: PHONE_IOS=3, PHONE_ANDROID=4)
|
|
507
565
|
var authW = ProtobufWriter()
|
|
508
|
-
authW.writeBoolField(1, true)
|
|
509
|
-
authW.writeInt32Field(2, 3)
|
|
566
|
+
authW.writeBoolField(1, true) // secAuth
|
|
567
|
+
authW.writeInt32Field(2, 3) // phoneType = PHONE_IOS (eDevice.PHONE_IOS=3)
|
|
510
568
|
|
|
511
|
-
w.writeMessageField(3, authW.data)
|
|
569
|
+
w.writeMessageField(3, authW.data) // authMgr
|
|
512
570
|
return w.data
|
|
513
571
|
}
|
|
514
572
|
|
|
@@ -520,8 +578,8 @@ private enum DevSettingsProto {
|
|
|
520
578
|
|
|
521
579
|
// PipeRoleChange: field 1 = asCmdRole (enum GlassesLR.RIGHT=1)
|
|
522
580
|
var roleW = ProtobufWriter()
|
|
523
|
-
roleW.writeInt32Field(1, 1)
|
|
524
|
-
w.writeMessageField(4, roleW.data)
|
|
581
|
+
roleW.writeInt32Field(1, 1) // RIGHT
|
|
582
|
+
w.writeMessageField(4, roleW.data) // roleChange (field 4 in DevCfgDataPackage)
|
|
525
583
|
return w.data
|
|
526
584
|
}
|
|
527
585
|
|
|
@@ -538,7 +596,7 @@ private enum DevSettingsProto {
|
|
|
538
596
|
let nowSec = Int64(Date().timeIntervalSince1970)
|
|
539
597
|
let tzSec = Int64(TimeZone.current.secondsFromGMT())
|
|
540
598
|
tsW.writeInt32Field(1, Int32(truncatingIfNeeded: nowSec + tzSec))
|
|
541
|
-
w.writeMessageField(128, tsW.data)
|
|
599
|
+
w.writeMessageField(128, tsW.data) // timeSync (field 128 in DevCfgDataPackage)
|
|
542
600
|
return w.data
|
|
543
601
|
}
|
|
544
602
|
|
|
@@ -578,8 +636,8 @@ private enum DevSettingsProto {
|
|
|
578
636
|
|
|
579
637
|
// BaseConnHeartBeat: empty message
|
|
580
638
|
var hbW = ProtobufWriter()
|
|
581
|
-
_ = hbW
|
|
582
|
-
w.writeMessageField(13, hbW.data)
|
|
639
|
+
_ = hbW // empty
|
|
640
|
+
w.writeMessageField(13, hbW.data) // baseHeartBeat (field 13)
|
|
583
641
|
return w.data
|
|
584
642
|
}
|
|
585
643
|
|
|
@@ -590,18 +648,18 @@ private enum DevSettingsProto {
|
|
|
590
648
|
magicRandom: Int32, connect: Bool, ringMac: Data, ringName: String = ""
|
|
591
649
|
) -> Data {
|
|
592
650
|
var w = ProtobufWriter()
|
|
593
|
-
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue)
|
|
651
|
+
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue) // commandId = RING_CONNECT_INFO (6)
|
|
594
652
|
w.writeInt32Field(2, magicRandom)
|
|
595
653
|
|
|
596
654
|
// RingInfo sub-message (field 5 in DevCfgDataPackage)
|
|
597
655
|
var ringW = ProtobufWriter()
|
|
598
|
-
ringW.writeBoolField(1, connect)
|
|
599
|
-
ringW.writeBytesField(2, ringMac)
|
|
656
|
+
ringW.writeBoolField(1, connect) // connectRing
|
|
657
|
+
ringW.writeBytesField(2, ringMac) // ringMac (6 bytes)
|
|
600
658
|
if !ringName.isEmpty {
|
|
601
|
-
ringW.writeBytesField(3, Data(ringName.utf8))
|
|
659
|
+
ringW.writeBytesField(3, Data(ringName.utf8)) // ringName
|
|
602
660
|
}
|
|
603
661
|
|
|
604
|
-
w.writeMessageField(5, ringW.data)
|
|
662
|
+
w.writeMessageField(5, ringW.data) // ringInfo (field 5)
|
|
605
663
|
return w.data
|
|
606
664
|
}
|
|
607
665
|
}
|
|
@@ -613,18 +671,18 @@ private enum G2SettingProto {
|
|
|
613
671
|
static func setBrightness(magicRandom: Int32, level: Int32, autoAdjust: Bool) -> Data {
|
|
614
672
|
// DeviceReceive_Brightness
|
|
615
673
|
var brightnessW = ProtobufWriter()
|
|
616
|
-
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0)
|
|
617
|
-
brightnessW.writeInt32Field(2, level)
|
|
674
|
+
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0) // autoAdjust
|
|
675
|
+
brightnessW.writeInt32Field(2, level) // brightnessLevel
|
|
618
676
|
|
|
619
677
|
// DeviceReceiveInfoFromAPP
|
|
620
678
|
var infoW = ProtobufWriter()
|
|
621
|
-
infoW.writeMessageField(1, brightnessW.data)
|
|
679
|
+
infoW.writeMessageField(1, brightnessW.data) // deviceReceiveBrightness (field 1)
|
|
622
680
|
|
|
623
681
|
// G2SettingPackage
|
|
624
682
|
var w = ProtobufWriter()
|
|
625
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
683
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue) // commandId
|
|
626
684
|
w.writeInt32Field(2, magicRandom)
|
|
627
|
-
w.writeMessageField(3, infoW.data)
|
|
685
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
628
686
|
return w.data
|
|
629
687
|
}
|
|
630
688
|
|
|
@@ -633,13 +691,13 @@ private enum G2SettingProto {
|
|
|
633
691
|
// DeviceReceiveRequestFromAPP - empty message triggers glasses to respond with all fields
|
|
634
692
|
var reqW = ProtobufWriter()
|
|
635
693
|
// Request brightness info type
|
|
636
|
-
reqW.writeInt32Field(1, 1)
|
|
694
|
+
reqW.writeInt32Field(1, 1) // settingInfoType = APP_REQUIRE_BASIC_SETTING
|
|
637
695
|
|
|
638
696
|
// G2SettingPackage
|
|
639
697
|
var w = ProtobufWriter()
|
|
640
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue)
|
|
698
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue) // commandId
|
|
641
699
|
w.writeInt32Field(2, magicRandom)
|
|
642
|
-
w.writeMessageField(4, reqW.data)
|
|
700
|
+
w.writeMessageField(4, reqW.data) // deviceReceiveRequestFromApp (field 4)
|
|
643
701
|
return w.data
|
|
644
702
|
}
|
|
645
703
|
|
|
@@ -647,17 +705,17 @@ private enum G2SettingProto {
|
|
|
647
705
|
static func setHeadUpSwitch(magicRandom: Int32, enabled: Bool) -> Data {
|
|
648
706
|
// DeviceReceive_Head_UP_Setting
|
|
649
707
|
var headUpW = ProtobufWriter()
|
|
650
|
-
headUpW.writeInt32Field(1, enabled ? 1 : 0)
|
|
708
|
+
headUpW.writeInt32Field(1, enabled ? 1 : 0) // headUpSwitch
|
|
651
709
|
|
|
652
710
|
// DeviceReceiveInfoFromAPP
|
|
653
711
|
var infoW = ProtobufWriter()
|
|
654
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
712
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
655
713
|
|
|
656
714
|
// G2SettingPackage
|
|
657
715
|
var w = ProtobufWriter()
|
|
658
716
|
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
659
717
|
w.writeInt32Field(2, magicRandom)
|
|
660
|
-
w.writeMessageField(3, infoW.data)
|
|
718
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
661
719
|
return w.data
|
|
662
720
|
}
|
|
663
721
|
|
|
@@ -665,11 +723,11 @@ private enum G2SettingProto {
|
|
|
665
723
|
static func setHeadUpAngle(magicRandom: Int32, angle: Int32) -> Data {
|
|
666
724
|
// DeviceReceive_Head_UP_Setting
|
|
667
725
|
var headUpW = ProtobufWriter()
|
|
668
|
-
headUpW.writeInt32Field(2, angle)
|
|
726
|
+
headUpW.writeInt32Field(2, angle) // headUpAngle (field 2)
|
|
669
727
|
|
|
670
728
|
// DeviceReceiveInfoFromAPP
|
|
671
729
|
var infoW = ProtobufWriter()
|
|
672
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
730
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
673
731
|
|
|
674
732
|
// G2SettingPackage
|
|
675
733
|
var w = ProtobufWriter()
|
|
@@ -683,11 +741,11 @@ private enum G2SettingProto {
|
|
|
683
741
|
static func setScreenHeight(magicRandom: Int32, level: Int32) -> Data {
|
|
684
742
|
// DeviceReceive_Y_Coordinate
|
|
685
743
|
var yW = ProtobufWriter()
|
|
686
|
-
yW.writeInt32Field(1, level)
|
|
744
|
+
yW.writeInt32Field(1, level) // yCoordinateLevel
|
|
687
745
|
|
|
688
746
|
// DeviceReceiveInfoFromAPP
|
|
689
747
|
var infoW = ProtobufWriter()
|
|
690
|
-
infoW.writeMessageField(2, yW.data)
|
|
748
|
+
infoW.writeMessageField(2, yW.data) // deviceReceiveYCoordinate (field 2)
|
|
691
749
|
|
|
692
750
|
// G2SettingPackage
|
|
693
751
|
var w = ProtobufWriter()
|
|
@@ -701,11 +759,11 @@ private enum G2SettingProto {
|
|
|
701
759
|
static func setScreenDepth(magicRandom: Int32, level: Int32) -> Data {
|
|
702
760
|
// DeviceReceive_X_Coordinate
|
|
703
761
|
var xW = ProtobufWriter()
|
|
704
|
-
xW.writeInt32Field(1, level)
|
|
762
|
+
xW.writeInt32Field(1, level) // xCoordinateLevel
|
|
705
763
|
|
|
706
764
|
// DeviceReceiveInfoFromAPP
|
|
707
765
|
var infoW = ProtobufWriter()
|
|
708
|
-
infoW.writeMessageField(3, xW.data)
|
|
766
|
+
infoW.writeMessageField(3, xW.data) // deviceReceiveXCoordinate (field 3)
|
|
709
767
|
|
|
710
768
|
// G2SettingPackage
|
|
711
769
|
var w = ProtobufWriter()
|
|
@@ -723,13 +781,13 @@ private enum OnboardingProto {
|
|
|
723
781
|
static func skipOnboarding(magicRandom: Int32) -> Data {
|
|
724
782
|
// OnboardingConfig: processId = FINISH (4)
|
|
725
783
|
var configW = ProtobufWriter()
|
|
726
|
-
configW.writeInt32Field(1, 4)
|
|
784
|
+
configW.writeInt32Field(1, 4) // processId = FINISH
|
|
727
785
|
|
|
728
786
|
// OnboardingDataPackage
|
|
729
787
|
var w = ProtobufWriter()
|
|
730
|
-
w.writeInt32Field(1, 1)
|
|
788
|
+
w.writeInt32Field(1, 1) // commandId = CONFIG
|
|
731
789
|
w.writeInt32Field(2, magicRandom)
|
|
732
|
-
w.writeMessageField(3, configW.data)
|
|
790
|
+
w.writeMessageField(3, configW.data) // config (field 3)
|
|
733
791
|
return w.data
|
|
734
792
|
}
|
|
735
793
|
}
|
|
@@ -737,19 +795,27 @@ private enum OnboardingProto {
|
|
|
737
795
|
// MARK: - EvenAI Protobuf Builders (even_ai.proto, service ID 7)
|
|
738
796
|
|
|
739
797
|
private enum EvenAIProto {
|
|
740
|
-
/// EvenAIDataPackage with CONFIG command to toggle Hey Even wakeword
|
|
741
|
-
/// voiceSwitch: 0 = OFF, 1 = ON
|
|
798
|
+
/// EvenAIDataPackage with CONFIG command to toggle Hey Even wakeword.
|
|
799
|
+
/// voiceSwitch: 0 = OFF, 1 = ON.
|
|
800
|
+
///
|
|
801
|
+
/// Wire format confirmed by sniffing the official app toggling the setting:
|
|
802
|
+
/// EvenAIConfig (field 13) = { f1=voiceSwitch, f2=32 }
|
|
803
|
+
/// The app OMITS f1 when disabling (proto3 zero) and sends f2=32 (0x20), NOT 80.
|
|
804
|
+
/// Observed echoes: ON → 6A04 08 01 10 20 ({f1:1, f2:32})
|
|
805
|
+
/// OFF → 6A02 10 20 ({f2:32})
|
|
742
806
|
static func setHeyEven(magicRandom: Int32, enabled: Bool) -> Data {
|
|
743
807
|
// EvenAIConfig
|
|
744
808
|
var configW = ProtobufWriter()
|
|
745
|
-
|
|
746
|
-
|
|
809
|
+
if enabled {
|
|
810
|
+
configW.writeInt32Field(1, 1) // voiceSwitch (omitted when off, matching the app)
|
|
811
|
+
}
|
|
812
|
+
configW.writeInt32Field(2, 32) // streamSpeed (always sent, app uses 32)
|
|
747
813
|
|
|
748
814
|
// EvenAIDataPackage
|
|
749
815
|
var w = ProtobufWriter()
|
|
750
|
-
w.writeInt32Field(1, 10)
|
|
816
|
+
w.writeInt32Field(1, 10) // commandId = CONFIG
|
|
751
817
|
w.writeInt32Field(2, magicRandom)
|
|
752
|
-
w.writeMessageField(13, configW.data)
|
|
818
|
+
w.writeMessageField(13, configW.data) // config (field 13)
|
|
753
819
|
return w.data
|
|
754
820
|
}
|
|
755
821
|
|
|
@@ -759,13 +825,13 @@ private enum EvenAIProto {
|
|
|
759
825
|
/// into the glasses' AI session so the following SKILL packet has context.
|
|
760
826
|
static func aiAsk(magicRandom: Int32, text: String, streamEnable: Int32 = 0) -> Data {
|
|
761
827
|
var askW = ProtobufWriter()
|
|
762
|
-
askW.writeInt32Field(2, streamEnable)
|
|
763
|
-
askW.writeBytesField(4, Data(text.utf8))
|
|
828
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
829
|
+
askW.writeBytesField(4, Data(text.utf8)) // text
|
|
764
830
|
|
|
765
831
|
var w = ProtobufWriter()
|
|
766
|
-
w.writeInt32Field(1, 3)
|
|
832
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
767
833
|
w.writeInt32Field(2, magicRandom)
|
|
768
|
-
w.writeMessageField(5, askW.data)
|
|
834
|
+
w.writeMessageField(5, askW.data) // askInfo (field 5)
|
|
769
835
|
return w.data
|
|
770
836
|
}
|
|
771
837
|
|
|
@@ -775,12 +841,12 @@ private enum EvenAIProto {
|
|
|
775
841
|
/// status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
776
842
|
static func aiCtrl(magicRandom: Int32, status: Int32) -> Data {
|
|
777
843
|
var ctrlW = ProtobufWriter()
|
|
778
|
-
ctrlW.writeInt32Field(1, status)
|
|
844
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
779
845
|
|
|
780
846
|
var w = ProtobufWriter()
|
|
781
|
-
w.writeInt32Field(1, 1)
|
|
847
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
782
848
|
w.writeInt32Field(2, magicRandom)
|
|
783
|
-
w.writeMessageField(3, ctrlW.data)
|
|
849
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
784
850
|
return w.data
|
|
785
851
|
}
|
|
786
852
|
|
|
@@ -795,17 +861,17 @@ private enum EvenAIProto {
|
|
|
795
861
|
) -> Data {
|
|
796
862
|
// EvenAISkillInfo
|
|
797
863
|
var skillW = ProtobufWriter()
|
|
798
|
-
skillW.writeInt32Field(1, streamEnable)
|
|
799
|
-
skillW.writeInt32Field(2, skillId)
|
|
800
|
-
skillW.writeInt32Field(3, skillParam)
|
|
801
|
-
skillW.writeBytesField(4, Data(text.utf8))
|
|
802
|
-
skillW.writeInt32Field(6, fTextEnd)
|
|
864
|
+
skillW.writeInt32Field(1, streamEnable) // streamEnable
|
|
865
|
+
skillW.writeInt32Field(2, skillId) // skillId
|
|
866
|
+
skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
|
|
867
|
+
skillW.writeBytesField(4, Data(text.utf8)) // text (utterance / payload)
|
|
868
|
+
skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
|
|
803
869
|
|
|
804
870
|
// EvenAIDataPackage
|
|
805
871
|
var w = ProtobufWriter()
|
|
806
|
-
w.writeInt32Field(1, 6)
|
|
872
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
807
873
|
w.writeInt32Field(2, magicRandom)
|
|
808
|
-
w.writeMessageField(8, skillW.data)
|
|
874
|
+
w.writeMessageField(8, skillW.data) // skillInfo (field 8)
|
|
809
875
|
return w.data
|
|
810
876
|
}
|
|
811
877
|
}
|
|
@@ -820,13 +886,13 @@ private enum NotificationProto {
|
|
|
820
886
|
/// (Returned errorCode=8 NOT_SUPPORT in testing — Service 4 doesn't accept this outbound.)
|
|
821
887
|
static func iosNotification(magicRandom: Int32, appID: String, displayName: String) -> Data {
|
|
822
888
|
var iosW = ProtobufWriter()
|
|
823
|
-
iosW.writeBytesField(1, Data(appID.utf8))
|
|
824
|
-
iosW.writeBytesField(2, Data(displayName.utf8))
|
|
889
|
+
iosW.writeBytesField(1, Data(appID.utf8)) // appID
|
|
890
|
+
iosW.writeBytesField(2, Data(displayName.utf8)) // displayName
|
|
825
891
|
|
|
826
892
|
var w = ProtobufWriter()
|
|
827
|
-
w.writeInt32Field(1, 2)
|
|
893
|
+
w.writeInt32Field(1, 2) // commandId = NOTIFICATION_IOS
|
|
828
894
|
w.writeInt32Field(2, magicRandom)
|
|
829
|
-
w.writeMessageField(4, iosW.data)
|
|
895
|
+
w.writeMessageField(4, iosW.data) // IOS (field 4)
|
|
830
896
|
return w.data
|
|
831
897
|
}
|
|
832
898
|
|
|
@@ -843,15 +909,15 @@ private enum NotificationProto {
|
|
|
843
909
|
avoidDisturbEnable: Int32 = 0
|
|
844
910
|
) -> Data {
|
|
845
911
|
var ctrlW = ProtobufWriter()
|
|
846
|
-
ctrlW.writeInt32Field(1, notifEnable)
|
|
847
|
-
ctrlW.writeInt32Field(2, autoDispEnable)
|
|
848
|
-
ctrlW.writeInt32Field(3, dispTime)
|
|
849
|
-
ctrlW.writeInt32Field(5, avoidDisturbEnable)
|
|
912
|
+
ctrlW.writeInt32Field(1, notifEnable) // notifEnable
|
|
913
|
+
ctrlW.writeInt32Field(2, autoDispEnable) // autoDispEnable
|
|
914
|
+
ctrlW.writeInt32Field(3, dispTime) // dispTime (seconds)
|
|
915
|
+
ctrlW.writeInt32Field(5, avoidDisturbEnable) // avoidDisturbEnable
|
|
850
916
|
|
|
851
917
|
var w = ProtobufWriter()
|
|
852
|
-
w.writeInt32Field(1, 1)
|
|
918
|
+
w.writeInt32Field(1, 1) // commandId = NOTIFICATION_CTRL
|
|
853
919
|
w.writeInt32Field(2, magicRandom)
|
|
854
|
-
w.writeMessageField(3, ctrlW.data)
|
|
920
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
855
921
|
return w.data
|
|
856
922
|
}
|
|
857
923
|
}
|
|
@@ -869,7 +935,7 @@ private enum MenuProto {
|
|
|
869
935
|
/// G2 firmware requires minimum 5, maximum 10 menu items
|
|
870
936
|
static let MIN_MENU_SIZE = 5
|
|
871
937
|
static let MAX_MENU_SIZE = 10
|
|
872
|
-
static let MAX_NAME_LENGTH = 15
|
|
938
|
+
static let MAX_NAME_LENGTH = 15 // 17 char limit minus 2 for running indicator prefix
|
|
873
939
|
/// Placeholder appIds for padding slots (in valid Even range, unique per slot)
|
|
874
940
|
static let PLACEHOLDER_APP_IDS: [Int32] = [10535, 10536, 10537, 10538, 10539]
|
|
875
941
|
|
|
@@ -896,7 +962,7 @@ private enum MenuProto {
|
|
|
896
962
|
|
|
897
963
|
// Wire items carry either a built-in (itemType=0, no name) or third-party (itemType=1, with name)
|
|
898
964
|
struct WireItem {
|
|
899
|
-
let displayName: String?
|
|
965
|
+
let displayName: String? // nil for built-ins
|
|
900
966
|
let appId: Int32
|
|
901
967
|
let isBuiltIn: Bool
|
|
902
968
|
}
|
|
@@ -913,8 +979,8 @@ private enum MenuProto {
|
|
|
913
979
|
|
|
914
980
|
let truncated =
|
|
915
981
|
item.name.count > MAX_NAME_LENGTH
|
|
916
|
-
|
|
917
|
-
|
|
982
|
+
? String(item.name.prefix(MAX_NAME_LENGTH))
|
|
983
|
+
: item.name
|
|
918
984
|
let prefix = item.running ? "● " : ""
|
|
919
985
|
wireItems.append(
|
|
920
986
|
WireItem(displayName: prefix + truncated, appId: appId, isBuiltIn: false)
|
|
@@ -923,7 +989,7 @@ private enum MenuProto {
|
|
|
923
989
|
|
|
924
990
|
// Pad to MIN_MENU_SIZE with placeholder third-party items
|
|
925
991
|
while wireItems.count < MIN_MENU_SIZE {
|
|
926
|
-
let idx = wireItems.count - 1
|
|
992
|
+
let idx = wireItems.count - 1 // -1 because built-in occupies slot 0
|
|
927
993
|
wireItems.append(
|
|
928
994
|
WireItem(
|
|
929
995
|
displayName: " ---",
|
|
@@ -935,27 +1001,27 @@ private enum MenuProto {
|
|
|
935
1001
|
|
|
936
1002
|
// MenuInfoSend
|
|
937
1003
|
var menuW = ProtobufWriter()
|
|
938
|
-
menuW.writeInt32Field(1, Int32(wireItems.count))
|
|
1004
|
+
menuW.writeInt32Field(1, Int32(wireItems.count)) // itemTotalNum
|
|
939
1005
|
|
|
940
1006
|
for item in wireItems {
|
|
941
1007
|
var itemW = ProtobufWriter()
|
|
942
1008
|
if item.isBuiltIn {
|
|
943
|
-
itemW.writeInt32Field(1, 0)
|
|
944
|
-
itemW.writeInt32Field(4, item.appId)
|
|
1009
|
+
itemW.writeInt32Field(1, 0) // itemType = 0 (built-in)
|
|
1010
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId = SID
|
|
945
1011
|
} else {
|
|
946
|
-
itemW.writeInt32Field(1, 1)
|
|
947
|
-
itemW.writeInt32Field(2, 1)
|
|
948
|
-
itemW.writeStringField(3, item.displayName ?? "")
|
|
949
|
-
itemW.writeInt32Field(4, item.appId)
|
|
1012
|
+
itemW.writeInt32Field(1, 1) // itemType = 1 (third-party)
|
|
1013
|
+
itemW.writeInt32Field(2, 1) // iconNum = 1
|
|
1014
|
+
itemW.writeStringField(3, item.displayName ?? "") // itemName
|
|
1015
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId
|
|
950
1016
|
}
|
|
951
|
-
menuW.writeMessageField(2, itemW.data)
|
|
1017
|
+
menuW.writeMessageField(2, itemW.data) // repeated item (field 2)
|
|
952
1018
|
}
|
|
953
1019
|
|
|
954
1020
|
// meun_main_msg_ctx
|
|
955
1021
|
var w = ProtobufWriter()
|
|
956
|
-
w.writeInt32Field(1, 0)
|
|
957
|
-
w.writeInt32Field(2, magicRandom)
|
|
958
|
-
w.writeMessageField(3, menuW.data)
|
|
1022
|
+
w.writeInt32Field(1, 0) // Cmd = APP_SEND_MENU_INFO (0)
|
|
1023
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
1024
|
+
w.writeMessageField(3, menuW.data) // sendData (field 3)
|
|
959
1025
|
return (w.data, appIdMap)
|
|
960
1026
|
}
|
|
961
1027
|
}
|
|
@@ -968,7 +1034,7 @@ private enum DashboardProto {
|
|
|
968
1034
|
/// eDashboardCommandId values from dashboard.proto
|
|
969
1035
|
enum CommandId: Int32 {
|
|
970
1036
|
case dashboardRespond = 1
|
|
971
|
-
case dashboardReceive = 2
|
|
1037
|
+
case dashboardReceive = 2 // phone → glasses widget/config push
|
|
972
1038
|
case appRespond = 3
|
|
973
1039
|
case appReceive = 4
|
|
974
1040
|
}
|
|
@@ -1113,7 +1179,7 @@ private struct EvenBLETransport {
|
|
|
1113
1179
|
var offset = 0
|
|
1114
1180
|
while offset < payload.count {
|
|
1115
1181
|
let end = min(offset + maxPayload, payload.count)
|
|
1116
|
-
chunks.append(payload[offset
|
|
1182
|
+
chunks.append(payload[offset..<end])
|
|
1117
1183
|
offset = end
|
|
1118
1184
|
}
|
|
1119
1185
|
if chunks.isEmpty {
|
|
@@ -1141,20 +1207,20 @@ private struct EvenBLETransport {
|
|
|
1141
1207
|
let payloadLen = UInt8(chunk.count + (isLast ? 2 : 0))
|
|
1142
1208
|
|
|
1143
1209
|
var packet = Data()
|
|
1144
|
-
packet.append(G2BLE.HEADER_BYTE)
|
|
1145
|
-
packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE)
|
|
1146
|
-
packet.append(syncId)
|
|
1147
|
-
packet.append(payloadLen)
|
|
1148
|
-
packet.append(totalPackets)
|
|
1149
|
-
packet.append(serialNum)
|
|
1150
|
-
packet.append(serviceId)
|
|
1151
|
-
packet.append(status)
|
|
1210
|
+
packet.append(G2BLE.HEADER_BYTE) // [0] 0xAA
|
|
1211
|
+
packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE) // [1] src+dst
|
|
1212
|
+
packet.append(syncId) // [2] syncId
|
|
1213
|
+
packet.append(payloadLen) // [3] payloadLen
|
|
1214
|
+
packet.append(totalPackets) // [4] packetTotalNum
|
|
1215
|
+
packet.append(serialNum) // [5] packetSerialNum
|
|
1216
|
+
packet.append(serviceId) // [6] serviceId
|
|
1217
|
+
packet.append(status) // [7] status
|
|
1152
1218
|
|
|
1153
1219
|
packet.append(chunk)
|
|
1154
1220
|
|
|
1155
1221
|
if isLast {
|
|
1156
|
-
packet.append(UInt8(crc & 0xFF))
|
|
1157
|
-
packet.append(UInt8((crc >> 8) & 0xFF))
|
|
1222
|
+
packet.append(UInt8(crc & 0xFF)) // CRC low
|
|
1223
|
+
packet.append(UInt8((crc >> 8) & 0xFF)) // CRC high
|
|
1158
1224
|
}
|
|
1159
1225
|
|
|
1160
1226
|
packets.append(packet)
|
|
@@ -1194,9 +1260,10 @@ private class G2SendManager {
|
|
|
1194
1260
|
// MARK: - G2 Receive Manager (multi-part reassembly)
|
|
1195
1261
|
|
|
1196
1262
|
private class G2ReceiveManager {
|
|
1197
|
-
private var partials: [String: (Data, UInt8)] = [:]
|
|
1263
|
+
private var partials: [String: (Data, UInt8)] = [:] // key -> (accumulated payload, lastSerialNum)
|
|
1198
1264
|
|
|
1199
|
-
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)?
|
|
1265
|
+
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)?
|
|
1266
|
+
{
|
|
1200
1267
|
guard rawData.count >= 8 else { return nil }
|
|
1201
1268
|
guard rawData[0] == G2BLE.HEADER_BYTE else { return nil }
|
|
1202
1269
|
|
|
@@ -1215,7 +1282,7 @@ private class G2ReceiveManager {
|
|
|
1215
1282
|
let isLast = (serialNum == totalPackets)
|
|
1216
1283
|
let hasCrc = isLast
|
|
1217
1284
|
let payloadEnd = 8 + payloadLen - (hasCrc ? 2 : 0)
|
|
1218
|
-
let payload = rawData[8
|
|
1285
|
+
let payload = rawData[8..<payloadEnd]
|
|
1219
1286
|
|
|
1220
1287
|
let syncId = rawData[2]
|
|
1221
1288
|
// Key partials by source peripheral too — left and right glasses have independent syncId counters
|
|
@@ -1262,7 +1329,7 @@ actor G2ReconnectionManager {
|
|
|
1262
1329
|
private var task: Task<Void, Never>?
|
|
1263
1330
|
private let intervalSeconds: TimeInterval
|
|
1264
1331
|
private var attempts = 0
|
|
1265
|
-
private let maxAttempts: Int
|
|
1332
|
+
private let maxAttempts: Int // -1 for unlimited
|
|
1266
1333
|
|
|
1267
1334
|
init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
|
|
1268
1335
|
self.intervalSeconds = intervalSeconds
|
|
@@ -1348,12 +1415,18 @@ class G2: NSObject, SGCManager {
|
|
|
1348
1415
|
/// Maps serial number -> peripheral UUID string. Persisted across forget() so previously
|
|
1349
1416
|
/// paired devices can reconnect quickly without a fresh scan.
|
|
1350
1417
|
private var leftGlassUUIDMap: [String: String] {
|
|
1351
|
-
get {
|
|
1418
|
+
get {
|
|
1419
|
+
UserDefaults.standard.dictionary(forKey: "g2_leftGlassUUIDMap") as? [String: String]
|
|
1420
|
+
?? [:]
|
|
1421
|
+
}
|
|
1352
1422
|
set { UserDefaults.standard.set(newValue, forKey: "g2_leftGlassUUIDMap") }
|
|
1353
1423
|
}
|
|
1354
1424
|
|
|
1355
1425
|
private var rightGlassUUIDMap: [String: String] {
|
|
1356
|
-
get {
|
|
1426
|
+
get {
|
|
1427
|
+
UserDefaults.standard.dictionary(forKey: "g2_rightGlassUUIDMap") as? [String: String]
|
|
1428
|
+
?? [:]
|
|
1429
|
+
}
|
|
1357
1430
|
set { UserDefaults.standard.set(newValue, forKey: "g2_rightGlassUUIDMap") }
|
|
1358
1431
|
}
|
|
1359
1432
|
|
|
@@ -1384,9 +1457,8 @@ class G2: NSObject, SGCManager {
|
|
|
1384
1457
|
private let sendManager = G2SendManager()
|
|
1385
1458
|
private let receiveManager = G2ReceiveManager()
|
|
1386
1459
|
private var foregroundObserver: NSObjectProtocol?
|
|
1387
|
-
private var startupPageCreated: Bool = false
|
|
1460
|
+
private var startupPageCreated: Bool = false // createStartUpPageContainer can only be called once
|
|
1388
1461
|
private var pageCreated: Bool = false
|
|
1389
|
-
private var pageHasTextContainer: Bool = false // tracks if current page has a text container
|
|
1390
1462
|
private var currentTextContent: String = ""
|
|
1391
1463
|
private var currentBitmapBase64: String = ""
|
|
1392
1464
|
private var textContainerID: Int32 = 1
|
|
@@ -1409,6 +1481,7 @@ class G2: NSObject, SGCManager {
|
|
|
1409
1481
|
/// Set to the first menu item's appId so glasses know our page belongs to the menu
|
|
1410
1482
|
private var activeMenuAppId: Int32?
|
|
1411
1483
|
private var lastClickTimestamp: Int64?
|
|
1484
|
+
private var lastEvenHubResponseTimestamp: Int64?
|
|
1412
1485
|
private var lastMenuSelectTimestamp: Int64?
|
|
1413
1486
|
private var lastGestureCtrlTimestamp: Int64?
|
|
1414
1487
|
|
|
@@ -1419,19 +1492,56 @@ class G2: NSObject, SGCManager {
|
|
|
1419
1492
|
let y: Int32
|
|
1420
1493
|
let width: Int32
|
|
1421
1494
|
let height: Int32
|
|
1422
|
-
var name: String {
|
|
1495
|
+
var name: String {
|
|
1496
|
+
"img-\(id)"
|
|
1497
|
+
}
|
|
1498
|
+
var bmpData: Data
|
|
1499
|
+
|
|
1423
1500
|
func matches(x: Int32, y: Int32, width: Int32, height: Int32) -> Bool {
|
|
1424
1501
|
self.x == x && self.y == y && self.width == width && self.height == height
|
|
1425
1502
|
}
|
|
1426
1503
|
}
|
|
1427
1504
|
|
|
1505
|
+
private struct TextContainer: Equatable {
|
|
1506
|
+
let id: Int32
|
|
1507
|
+
let x: Int32
|
|
1508
|
+
let y: Int32
|
|
1509
|
+
let width: Int32
|
|
1510
|
+
let height: Int32
|
|
1511
|
+
var content: String
|
|
1512
|
+
let borderWidth: Int32
|
|
1513
|
+
let borderColor: Int32
|
|
1514
|
+
let borderRadius: Int32
|
|
1515
|
+
let paddingLength: Int32
|
|
1516
|
+
var name: String {
|
|
1517
|
+
"text-\(id)"
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
func matches(
|
|
1521
|
+
x: Int32, y: Int32, width: Int32, height: Int32, borderWidth: Int32, borderColor: Int32,
|
|
1522
|
+
borderRadius: Int32, paddingLength: Int32
|
|
1523
|
+
) -> Bool {
|
|
1524
|
+
self.x == x && self.y == y && self.width == width && self.height == height
|
|
1525
|
+
&& self.borderWidth == borderWidth && self.borderColor == borderColor
|
|
1526
|
+
&& self.borderRadius == borderRadius && self.paddingLength == paddingLength
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1428
1530
|
/// Live list of image containers on the page, ordered oldest→newest (for LRU eviction).
|
|
1429
1531
|
/// The page may hold at most 4 image containers (IDs from the pool below).
|
|
1430
1532
|
private var imageContainers: [ImgContainer] = []
|
|
1533
|
+
private var textContainers: [TextContainer] = []
|
|
1431
1534
|
/// Fixed pool of container IDs the page protocol expects.
|
|
1432
1535
|
private let imageContainerIDPool: [Int32] = [10, 11, 12, 13]
|
|
1536
|
+
private let textContainerIDPool: [Int32] = [1, 2, 3, 4, 5, 6]
|
|
1433
1537
|
/// Default container seeded into every fresh page: 100x100 in the top-left.
|
|
1434
|
-
private static let defaultImgContainer = (
|
|
1538
|
+
private static let defaultImgContainer = (
|
|
1539
|
+
x: Int32(188), y: Int32(44), width: Int32(200), height: Int32(100)
|
|
1540
|
+
)
|
|
1541
|
+
private static let defaultTextContainer = (
|
|
1542
|
+
x: Int32(0), y: Int32(0), width: Int32(576), height: Int32(288), borderWidth: Int32(0),
|
|
1543
|
+
borderColor: Int32(0), borderRadius: Int32(0), paddingLength: Int32(4)
|
|
1544
|
+
)
|
|
1435
1545
|
|
|
1436
1546
|
@Published var aiListening: Bool = false
|
|
1437
1547
|
|
|
@@ -1455,6 +1565,7 @@ class G2: NSObject, SGCManager {
|
|
|
1455
1565
|
// MARK: - BLE Sending
|
|
1456
1566
|
|
|
1457
1567
|
private func sendToGlasses(_ packets: [Data], left: Bool = false, right: Bool = true) {
|
|
1568
|
+
// Bridge.log("G2: sendToGlasses() - sending \(packets.count) packets first byte: \(packets[0][0])")
|
|
1458
1569
|
for packet in packets {
|
|
1459
1570
|
if right, let char = rightWriteChar, let peripheral = rightPeripheral {
|
|
1460
1571
|
peripheral.writeValue(packet, for: char, type: .withoutResponse)
|
|
@@ -1482,6 +1593,15 @@ class G2: NSObject, SGCManager {
|
|
|
1482
1593
|
sendToGlasses(packets, left: left, right: right)
|
|
1483
1594
|
}
|
|
1484
1595
|
|
|
1596
|
+
private func sendNavigationCommand(_ payload: Data) {
|
|
1597
|
+
let packets = sendManager.buildPackets(
|
|
1598
|
+
serviceId: ServiceID.navigation.rawValue,
|
|
1599
|
+
payload: payload,
|
|
1600
|
+
reserveFlag: true
|
|
1601
|
+
)
|
|
1602
|
+
sendToGlasses(packets)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1485
1605
|
private func sendG2SettingCommand(_ payload: Data) {
|
|
1486
1606
|
let packets = sendManager.buildPackets(
|
|
1487
1607
|
serviceId: ServiceID.g2Setting.rawValue,
|
|
@@ -1569,7 +1689,7 @@ class G2: NSObject, SGCManager {
|
|
|
1569
1689
|
sendDevSettingsCommand(authR, left: false, right: true)
|
|
1570
1690
|
}
|
|
1571
1691
|
|
|
1572
|
-
private func runAuthSequence() {
|
|
1692
|
+
private func runAuthSequence() async {
|
|
1573
1693
|
Bridge.log("G2: Running auth sequence")
|
|
1574
1694
|
|
|
1575
1695
|
// Auth to left side
|
|
@@ -1579,206 +1699,150 @@ class G2: NSObject, SGCManager {
|
|
|
1579
1699
|
}
|
|
1580
1700
|
|
|
1581
1701
|
// Small delay then auth right + pipe role change + time sync
|
|
1582
|
-
|
|
1583
|
-
guard let self = self else { return }
|
|
1702
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1584
1703
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1704
|
+
let authR = DevSettingsProto.authCmd(magicRandom: self.sendManager.nextMagicRandom())
|
|
1705
|
+
self.sendDevSettingsCommand(authR, left: false, right: true)
|
|
1587
1706
|
|
|
1588
|
-
|
|
1589
|
-
guard let self = self else { return }
|
|
1707
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1590
1708
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1709
|
+
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1710
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
1711
|
+
)
|
|
1712
|
+
self.sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1595
1713
|
|
|
1596
|
-
|
|
1597
|
-
guard let self = self else { return }
|
|
1714
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1598
1715
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1716
|
+
let timeSync = DevSettingsProto.timeSync(
|
|
1717
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
1718
|
+
)
|
|
1719
|
+
self.sendDevSettingsCommand(timeSync)
|
|
1603
1720
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1721
|
+
// Skip onboarding on connect
|
|
1722
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1723
|
+
let onboarding = OnboardingProto.skipOnboarding(
|
|
1724
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
1725
|
+
)
|
|
1726
|
+
self.sendOnboardingCommand(onboarding)
|
|
1727
|
+
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1728
|
+
|
|
1729
|
+
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1730
|
+
var gestureInitW = ProtobufWriter()
|
|
1731
|
+
gestureInitW.writeInt32Field(1, 0)
|
|
1732
|
+
gestureInitW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1733
|
+
self.sendGestureCtrlCommand(gestureInitW.data)
|
|
1734
|
+
|
|
1735
|
+
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1736
|
+
var uiSettW = ProtobufWriter()
|
|
1737
|
+
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1738
|
+
uiSettW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1739
|
+
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
|
|
1740
|
+
self.sendToGlasses(
|
|
1741
|
+
self.sendManager.buildPackets(
|
|
1742
|
+
serviceId: 0x0C, payload: uiSettW.data, reserveFlag: true
|
|
1743
|
+
)
|
|
1744
|
+
)
|
|
1612
1745
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
// = field 9 (universe), {1:0, 2:0, 3:1, 4:0, 5:1}
|
|
1626
|
-
var univW = ProtobufWriter()
|
|
1627
|
-
univW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1628
|
-
univW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1629
|
-
univW.writeMessageField(
|
|
1630
|
-
3,
|
|
1631
|
-
Data([
|
|
1632
|
-
0x4A, 0x0A, // field 9, length 10
|
|
1633
|
-
0x08, 0x00, // unitFormat=0
|
|
1634
|
-
0x10, 0x00, // distanceUnit=0
|
|
1635
|
-
0x18, UInt8(self.dashboardHalfDayFormat()), // timeFormat / halfDayFormat
|
|
1636
|
-
0x20, 0x00, // dateFormat=0
|
|
1637
|
-
0x28, UInt8(self.dashboardTemperatureUnit()), // temperatureUnit
|
|
1638
|
-
])
|
|
1639
|
-
)
|
|
1640
|
-
self.sendG2SettingCommand(univW.data)
|
|
1641
|
-
|
|
1642
|
-
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1643
|
-
var gestureInitW = ProtobufWriter()
|
|
1644
|
-
gestureInitW.writeInt32Field(1, 0)
|
|
1645
|
-
gestureInitW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1646
|
-
self.sendGestureCtrlCommand(gestureInitW.data)
|
|
1647
|
-
|
|
1648
|
-
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1649
|
-
var uiSettW = ProtobufWriter()
|
|
1650
|
-
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1651
|
-
uiSettW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1652
|
-
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
|
|
1653
|
-
self.sendToGlasses(
|
|
1654
|
-
self.sendManager.buildPackets(
|
|
1655
|
-
serviceId: 0x0C, payload: uiSettW.data, reserveFlag: true
|
|
1656
|
-
)
|
|
1657
|
-
)
|
|
1746
|
+
// 6. Dashboard init (0x01) — display settings
|
|
1747
|
+
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1748
|
+
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1749
|
+
var dashDisplayW = ProtobufWriter()
|
|
1750
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1751
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1752
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1753
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1754
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1755
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1756
|
+
dashDisplayW.writeInt32Field(6, self.dashboardHalfDayFormat()) // halfDayFormat
|
|
1757
|
+
dashDisplayW.writeInt32Field(7, self.dashboardTemperatureUnit()) // temperatureUnit
|
|
1658
1758
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
// teleW.writeInt32Field(1, 1)
|
|
1662
|
-
// teleW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1663
|
-
// teleW.writeMessageField(3, Data([0x08, 0x04])) // {1:4}
|
|
1664
|
-
// self.sendToGlasses(
|
|
1665
|
-
// self.sendManager.buildPackets(
|
|
1666
|
-
// serviceId: 0x10, payload: teleW.data, reserveFlag: true
|
|
1667
|
-
// )
|
|
1668
|
-
// )
|
|
1669
|
-
|
|
1670
|
-
// // 4. EvenHub CTRL on service 0x81 (cmd=1, empty field3)
|
|
1671
|
-
// var ehCtrlW = ProtobufWriter()
|
|
1672
|
-
// ehCtrlW.writeInt32Field(1, 1)
|
|
1673
|
-
// ehCtrlW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1674
|
-
// ehCtrlW.writeMessageField(3, Data())
|
|
1675
|
-
// self.sendEvenHubCtrlCommand(ehCtrlW.data)
|
|
1676
|
-
|
|
1677
|
-
// // 5. calendar (0x04) — config
|
|
1678
|
-
// var calW = ProtobufWriter()
|
|
1679
|
-
// calW.writeInt32Field(1, 1)
|
|
1680
|
-
// calW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1681
|
-
// calW.writeMessageField(
|
|
1682
|
-
// 3, Data([0x08, 0x01, 0x10, 0x01, 0x18, 0x05, 0x28, 0x01])
|
|
1683
|
-
// )
|
|
1684
|
-
// self.sendToGlasses(
|
|
1685
|
-
// self.sendManager.buildPackets(
|
|
1686
|
-
// serviceId: 0x04, payload: calW.data, reserveFlag: true
|
|
1687
|
-
// )
|
|
1688
|
-
// )
|
|
1689
|
-
|
|
1690
|
-
// 6. Dashboard init (0x01) — display settings
|
|
1691
|
-
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1692
|
-
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1693
|
-
var dashDisplayW = ProtobufWriter()
|
|
1694
|
-
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1695
|
-
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1696
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1697
|
-
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1698
|
-
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1699
|
-
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1700
|
-
dashDisplayW.writeInt32Field(6, self.dashboardHalfDayFormat()) // halfDayFormat
|
|
1701
|
-
dashDisplayW.writeInt32Field(7, self.dashboardTemperatureUnit()) // temperatureUnit
|
|
1702
|
-
|
|
1703
|
-
var dashRecvW = ProtobufWriter()
|
|
1704
|
-
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1705
|
-
|
|
1706
|
-
var dashPkgW = ProtobufWriter()
|
|
1707
|
-
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1708
|
-
dashPkgW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1709
|
-
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1710
|
-
self.sendDashboardCommand(dashPkgW.data)
|
|
1711
|
-
|
|
1712
|
-
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5, field7={1:1})
|
|
1713
|
-
// var dashNewsReqW = ProtobufWriter()
|
|
1714
|
-
// dashNewsReqW.writeInt32Field(1, 5) // REQUEST_NEWS_INFO
|
|
1715
|
-
// dashNewsReqW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1716
|
-
// dashNewsReqW.writeMessageField(7, Data([0x08, 0x01])) // {1:1}
|
|
1717
|
-
// self.sendDashboardCommand(dashNewsReqW.data)
|
|
1718
|
-
|
|
1719
|
-
// // 8. Gesture control list via g2_setting
|
|
1720
|
-
// var gestListW = ProtobufWriter()
|
|
1721
|
-
// gestListW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1722
|
-
// gestListW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1723
|
-
// // field 3 with field 10 (gestureControlList): 3 items, all app_unable
|
|
1724
|
-
// let gestureCtrlPayload = Data([
|
|
1725
|
-
// 0x52, 0x18, // field 10, length 24
|
|
1726
|
-
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, // item 1
|
|
1727
|
-
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x01, 0x18, 0x00, // item 2
|
|
1728
|
-
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x02, 0x18, 0x00, // item 3
|
|
1729
|
-
// ])
|
|
1730
|
-
// gestListW.writeMessageField(3, gestureCtrlPayload)
|
|
1731
|
-
// self.sendG2SettingCommand(gestListW.data)
|
|
1732
|
-
|
|
1733
|
-
// // 9. Dashboard APP_REQUEST_NEWS_INFO (cmd=7, field9={1:1})
|
|
1734
|
-
// var dashAppNewsW = ProtobufWriter()
|
|
1735
|
-
// dashAppNewsW.writeInt32Field(1, 7) // APP_REQUEST_NEWS_INFO
|
|
1736
|
-
// dashAppNewsW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1737
|
-
// dashAppNewsW.writeMessageField(9, Data([0x08, 0x01])) // {1:1}
|
|
1738
|
-
// self.sendDashboardCommand(dashAppNewsW.data)
|
|
1739
|
-
|
|
1740
|
-
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1741
|
-
}
|
|
1759
|
+
var dashRecvW = ProtobufWriter()
|
|
1760
|
+
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1742
1761
|
|
|
1743
|
-
|
|
1744
|
-
|
|
1762
|
+
var dashPkgW = ProtobufWriter()
|
|
1763
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1764
|
+
dashPkgW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1765
|
+
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1766
|
+
self.sendDashboardCommand(dashPkgW.data)
|
|
1745
1767
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1768
|
+
// Disable "Hey Even" wakeword on connect
|
|
1769
|
+
let heyEvenOff = EvenAIProto.setHeyEven(
|
|
1770
|
+
magicRandom: self.sendManager.nextMagicRandom(),
|
|
1771
|
+
enabled: false
|
|
1772
|
+
)
|
|
1773
|
+
self.sendEvenAICommand(heyEvenOff)
|
|
1774
|
+
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1775
|
+
|
|
1776
|
+
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5, field7={1:1})
|
|
1777
|
+
// var dashNewsReqW = ProtobufWriter()
|
|
1778
|
+
// dashNewsReqW.writeInt32Field(1, 5) // REQUEST_NEWS_INFO
|
|
1779
|
+
// dashNewsReqW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1780
|
+
// dashNewsReqW.writeMessageField(7, Data([0x08, 0x01])) // {1:1}
|
|
1781
|
+
// self.sendDashboardCommand(dashNewsReqW.data)
|
|
1782
|
+
|
|
1783
|
+
// // 8. Gesture control list via g2_setting
|
|
1784
|
+
// var gestListW = ProtobufWriter()
|
|
1785
|
+
// gestListW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1786
|
+
// gestListW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1787
|
+
// // field 3 with field 10 (gestureControlList): 3 items, all app_unable
|
|
1788
|
+
// let gestureCtrlPayload = Data([
|
|
1789
|
+
// 0x52, 0x18, // field 10, length 24
|
|
1790
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, // item 1
|
|
1791
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x01, 0x18, 0x00, // item 2
|
|
1792
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x02, 0x18, 0x00, // item 3
|
|
1793
|
+
// ])
|
|
1794
|
+
// gestListW.writeMessageField(3, gestureCtrlPayload)
|
|
1795
|
+
// self.sendG2SettingCommand(gestListW.data)
|
|
1796
|
+
|
|
1797
|
+
// // 9. Dashboard APP_REQUEST_NEWS_INFO (cmd=7, field9={1:1})
|
|
1798
|
+
// var dashAppNewsW = ProtobufWriter()
|
|
1799
|
+
// dashAppNewsW.writeInt32Field(1, 7) // APP_REQUEST_NEWS_INFO
|
|
1800
|
+
// dashAppNewsW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1801
|
+
// dashAppNewsW.writeMessageField(9, Data([0x08, 0x01])) // {1:1}
|
|
1802
|
+
// self.sendDashboardCommand(dashAppNewsW.data)
|
|
1803
|
+
|
|
1804
|
+
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1805
|
+
|
|
1806
|
+
// Start heartbeats after auth
|
|
1807
|
+
self.startHeartbeats()
|
|
1808
|
+
|
|
1809
|
+
Task { await self.reconnectionManager.stop() }
|
|
1810
|
+
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
1811
|
+
|
|
1812
|
+
// Set device_name so DeviceManager can save it for reconnection
|
|
1813
|
+
if let peripheralName = self.rightPeripheral?.name
|
|
1814
|
+
?? self.leftPeripheral?.name,
|
|
1815
|
+
let serialNumber = self.deviceNameToSerialNumber[peripheralName]
|
|
1816
|
+
{
|
|
1817
|
+
DeviceStore.shared.apply("bluetooth", "device_name", serialNumber)
|
|
1818
|
+
Bridge.log("G2: Set device_name to \(serialNumber)")
|
|
1819
|
+
}
|
|
1748
1820
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
Bridge.log("G2: Set device_name to \(serialNumber)")
|
|
1756
|
-
}
|
|
1821
|
+
// Set bluetooth name and device model for Device Info page
|
|
1822
|
+
let btName =
|
|
1823
|
+
self.rightPeripheral?.name
|
|
1824
|
+
?? self.leftPeripheral?.name ?? ""
|
|
1825
|
+
DeviceStore.shared.apply("glasses", "bluetoothName", btName)
|
|
1826
|
+
DeviceStore.shared.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
1757
1827
|
|
|
1758
|
-
|
|
1759
|
-
let btName =
|
|
1760
|
-
self.rightPeripheral?.name
|
|
1761
|
-
?? self.leftPeripheral?.name ?? ""
|
|
1762
|
-
DeviceStore.shared.apply("glasses", "bluetoothName", btName)
|
|
1763
|
-
DeviceStore.shared.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
1828
|
+
self.setFullyConnected()
|
|
1764
1829
|
|
|
1765
|
-
|
|
1830
|
+
// connnect a controller if we have one:
|
|
1831
|
+
self.connectController()
|
|
1766
1832
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1833
|
+
// Query version + battery info from glasses
|
|
1834
|
+
self.requestDeviceInfo()
|
|
1769
1835
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1836
|
+
// send dashboard menu if we have stored items
|
|
1837
|
+
self.sendMenuApps()
|
|
1772
1838
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1839
|
+
// order the calendar (Schedule) widget first on the dashboard
|
|
1840
|
+
self.setCalendarWidgetFirst()
|
|
1775
1841
|
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1842
|
+
// send calendar events
|
|
1843
|
+
let calendarEvents =
|
|
1844
|
+
DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
1845
|
+
self.sendCalendarEvents(calendarEvents)
|
|
1782
1846
|
}
|
|
1783
1847
|
|
|
1784
1848
|
// MARK: - Heartbeats
|
|
@@ -1787,7 +1851,7 @@ class G2: NSObject, SGCManager {
|
|
|
1787
1851
|
heartbeatTask?.cancel()
|
|
1788
1852
|
heartbeatTask = Task { [weak self] in
|
|
1789
1853
|
while !Task.isCancelled {
|
|
1790
|
-
try? await Task.sleep(nanoseconds:
|
|
1854
|
+
try? await Task.sleep(nanoseconds: 10_000_000_000)
|
|
1791
1855
|
guard !Task.isCancelled else { break }
|
|
1792
1856
|
await MainActor.run {
|
|
1793
1857
|
self?.sendEvenHubHeartbeat()
|
|
@@ -1862,55 +1926,112 @@ class G2: NSObject, SGCManager {
|
|
|
1862
1926
|
|
|
1863
1927
|
// MARK: - SGCManager: Display Control
|
|
1864
1928
|
|
|
1865
|
-
func sendTextWall(_ text: String) {
|
|
1929
|
+
func sendTextWall(_ text: String) async {
|
|
1930
|
+
await sendText(
|
|
1931
|
+
text,
|
|
1932
|
+
x: G2.defaultTextContainer.x,
|
|
1933
|
+
y: G2.defaultTextContainer.y,
|
|
1934
|
+
width: G2.defaultTextContainer.width,
|
|
1935
|
+
height: G2.defaultTextContainer.height,
|
|
1936
|
+
borderWidth: G2.defaultTextContainer.borderWidth,
|
|
1937
|
+
borderColor: G2.defaultTextContainer.borderColor,
|
|
1938
|
+
borderRadius: G2.defaultTextContainer.borderRadius,
|
|
1939
|
+
paddingLength: G2.defaultTextContainer.paddingLength
|
|
1940
|
+
)
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
func sendText(
|
|
1944
|
+
_ text: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil, height: Int32? = nil,
|
|
1945
|
+
borderWidth: Int32? = nil, borderColor: Int32? = nil, borderRadius: Int32? = nil,
|
|
1946
|
+
paddingLength: Int32? = nil
|
|
1947
|
+
) async {
|
|
1866
1948
|
// Bridge.log("G2: sendTextWall(\(text.prefix(50))...)")
|
|
1867
1949
|
|
|
1868
1950
|
// ignore events while the ER dashboard is open:
|
|
1869
|
-
let useNativeDashboard =
|
|
1951
|
+
let useNativeDashboard =
|
|
1952
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
1870
1953
|
if useNativeDashboard && dashboardShowing > 0 {
|
|
1871
1954
|
return
|
|
1872
1955
|
}
|
|
1873
1956
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1957
|
+
let rx = x ?? G2.defaultTextContainer.x
|
|
1958
|
+
let ry = y ?? G2.defaultTextContainer.y
|
|
1959
|
+
let rw = width ?? G2.defaultTextContainer.width
|
|
1960
|
+
let rh = height ?? G2.defaultTextContainer.height
|
|
1961
|
+
let borderWidth = G2.defaultTextContainer.borderWidth
|
|
1962
|
+
let borderColor = G2.defaultTextContainer.borderColor
|
|
1963
|
+
let borderRadius = G2.defaultTextContainer.borderRadius
|
|
1964
|
+
let paddingLength = G2.defaultTextContainer.paddingLength
|
|
1965
|
+
let content = text.isEmpty ? " " : text
|
|
1966
|
+
|
|
1967
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
1968
|
+
var container: TextContainer
|
|
1969
|
+
if let i = textContainers.firstIndex(where: {
|
|
1970
|
+
$0.matches(
|
|
1971
|
+
x: rx, y: ry, width: rw, height: rh, borderWidth: borderWidth,
|
|
1972
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength)
|
|
1973
|
+
}) {
|
|
1974
|
+
textContainers[i].content = content
|
|
1975
|
+
container = textContainers[i]
|
|
1976
|
+
Bridge.log(
|
|
1977
|
+
"G2: sendText() - reusing container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh)"
|
|
1978
|
+
)
|
|
1979
|
+
if !pageCreated {
|
|
1980
|
+
await rebuildPage()
|
|
1981
|
+
return
|
|
1982
|
+
}
|
|
1983
|
+
let msg = EvenHubProto.updateTextMessage(
|
|
1984
|
+
containerID: container.id,
|
|
1985
|
+
contentOffset: 0,
|
|
1986
|
+
contentLength: Int32(text.utf8.count),
|
|
1987
|
+
content: container.content
|
|
1988
|
+
)
|
|
1989
|
+
queueEvenHubCommand(msg)
|
|
1876
1990
|
return
|
|
1877
1991
|
}
|
|
1878
1992
|
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
}
|
|
1993
|
+
container = addTextContainer(
|
|
1994
|
+
x: rx, y: ry, width: rw, height: rh, content: content, borderWidth: borderWidth,
|
|
1995
|
+
borderColor: borderColor, borderRadius: borderRadius, paddingLength: paddingLength)
|
|
1996
|
+
Bridge.log(
|
|
1997
|
+
"G2: sendText() - added text container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page"
|
|
1998
|
+
)
|
|
1999
|
+
await rebuildPage()
|
|
1887
2000
|
}
|
|
1888
2001
|
|
|
1889
|
-
func sendDoubleTextWall(_ top: String, _ bottom: String) {
|
|
2002
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
2003
|
+
Bridge.log("G2: sendDoubleTextWall() - top: \(top), bottom: \(bottom)")
|
|
1890
2004
|
// G2 doesn't have native double text wall, combine them
|
|
1891
|
-
let combined = top + "\n" + bottom
|
|
1892
|
-
sendTextWall(combined)
|
|
2005
|
+
let combined = top + "\n\n" + bottom
|
|
2006
|
+
await sendTextWall(combined)
|
|
1893
2007
|
}
|
|
1894
2008
|
|
|
1895
2009
|
func clearDisplay() {
|
|
1896
2010
|
Bridge.log("G2: clearDisplay()")
|
|
1897
2011
|
// Don't shutdown the EvenHub page — that kills audio streaming too.
|
|
1898
2012
|
// Instead, just clear the text content by sending a space.
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2013
|
+
|
|
2014
|
+
if !pageCreated {
|
|
2015
|
+
Bridge.log("G2: clearDisplay() - page not created")
|
|
2016
|
+
createPageWithContainers()
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// reset the content of all text containers to empty:
|
|
2020
|
+
for i in textContainers.indices {
|
|
2021
|
+
textContainers[i].content = " "
|
|
2022
|
+
}
|
|
2023
|
+
for i in imageContainers.indices {
|
|
2024
|
+
imageContainers[i].bmpData = Data()
|
|
1907
2025
|
}
|
|
2026
|
+
// shutdown the page and then recreate the containers without the content:
|
|
2027
|
+
let msg = EvenHubProto.shutdownMessage()
|
|
2028
|
+
sendEvenHubCommand(msg)
|
|
2029
|
+
createPageWithContainers()
|
|
2030
|
+
restartMicIfAlreadyEnabled()
|
|
1908
2031
|
}
|
|
1909
2032
|
|
|
1910
2033
|
/// Send BMP data to an image container via fragmented updateImageRawData
|
|
1911
|
-
private func sendImageData(containerID: Int32, containerName: String, bmpData: Data) async
|
|
1912
|
-
-> Bool
|
|
1913
|
-
{
|
|
2034
|
+
private func sendImageData(containerID: Int32, containerName: String, bmpData: Data) async {
|
|
1914
2035
|
let fragmentSize = 4096
|
|
1915
2036
|
imageSessionCounter += 1
|
|
1916
2037
|
let sessionId = imageSessionCounter
|
|
@@ -1918,9 +2039,13 @@ class G2: NSObject, SGCManager {
|
|
|
1918
2039
|
var fragmentIndex: Int32 = 0
|
|
1919
2040
|
var offset = 0
|
|
1920
2041
|
|
|
2042
|
+
Bridge.log(
|
|
2043
|
+
"G2: sendImageData(\(containerName)) - \(fragmentIndex) fragments, \(bmpData.count) bytes"
|
|
2044
|
+
)
|
|
2045
|
+
|
|
1921
2046
|
while offset < bmpData.count {
|
|
1922
2047
|
let end = min(offset + fragmentSize, bmpData.count)
|
|
1923
|
-
let fragment = bmpData[offset
|
|
2048
|
+
let fragment = bmpData[offset..<end]
|
|
1924
2049
|
|
|
1925
2050
|
let msg = EvenHubProto.updateImageRawDataMessage(
|
|
1926
2051
|
containerID: containerID,
|
|
@@ -1936,13 +2061,8 @@ class G2: NSObject, SGCManager {
|
|
|
1936
2061
|
|
|
1937
2062
|
fragmentIndex += 1
|
|
1938
2063
|
offset = end
|
|
1939
|
-
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
2064
|
+
try? await Task.sleep(nanoseconds: 200_000_000) // 200ms between fragments
|
|
1940
2065
|
}
|
|
1941
|
-
|
|
1942
|
-
Bridge.log(
|
|
1943
|
-
"G2: sendImageData(\(containerName)) - \(fragmentIndex) fragments, \(bmpData.count) bytes"
|
|
1944
|
-
)
|
|
1945
|
-
return true
|
|
1946
2066
|
}
|
|
1947
2067
|
|
|
1948
2068
|
/// Display a bitmap inside a positioned image container.
|
|
@@ -1954,65 +2074,67 @@ class G2: NSObject, SGCManager {
|
|
|
1954
2074
|
/// the page is rebuilt before the image is sent.
|
|
1955
2075
|
///
|
|
1956
2076
|
/// Omitted params default to a 100x100 container in the top-left corner.
|
|
1957
|
-
func displayBitmap(
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2077
|
+
func displayBitmap(
|
|
2078
|
+
base64ImageData: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil,
|
|
2079
|
+
height: Int32? = nil
|
|
2080
|
+
) async -> Bool {
|
|
1961
2081
|
let rx = x ?? G2.defaultImgContainer.x
|
|
1962
2082
|
let ry = y ?? G2.defaultImgContainer.y
|
|
1963
2083
|
let rw = width ?? G2.defaultImgContainer.width
|
|
1964
2084
|
let rh = height ?? G2.defaultImgContainer.height
|
|
1965
2085
|
|
|
1966
|
-
|
|
1967
|
-
|
|
2086
|
+
// ignore events while the ER dashboard is open:
|
|
2087
|
+
let useNativeDashboard =
|
|
2088
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
2089
|
+
if useNativeDashboard && dashboardShowing > 0 {
|
|
1968
2090
|
return false
|
|
1969
2091
|
}
|
|
1970
2092
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
ImgContainer(
|
|
1975
|
-
id: imageContainerIDPool[0],
|
|
1976
|
-
x: G2.defaultImgContainer.x, y: G2.defaultImgContainer.y,
|
|
1977
|
-
width: G2.defaultImgContainer.width, height: G2.defaultImgContainer.height
|
|
1978
|
-
)
|
|
1979
|
-
]
|
|
1980
|
-
createPageWithText("")
|
|
1981
|
-
Bridge.log("G2: displayBitmap() - startup page created, waiting 1s before sending image data...")
|
|
1982
|
-
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
1986
|
-
let container: ImgContainer
|
|
1987
|
-
if let existing = imageContainers.first(where: { $0.matches(x: rx, y: ry, width: rw, height: rh) }) {
|
|
1988
|
-
container = existing
|
|
1989
|
-
Bridge.log("G2: displayBitmap() - reusing container \(existing.id) for rect \(rx),\(ry) \(rw)x\(rh)")
|
|
1990
|
-
} else {
|
|
1991
|
-
container = addImageContainer(x: rx, y: ry, width: rw, height: rh)
|
|
1992
|
-
Bridge.log("G2: displayBitmap() - added container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page")
|
|
1993
|
-
rebuildPage()
|
|
1994
|
-
try? await Task.sleep(nanoseconds: 1_000_000_000) // settle before sending image data
|
|
2093
|
+
guard let rawData = Data(base64Encoded: base64ImageData) else {
|
|
2094
|
+
Bridge.log("G2: failed to decode base64")
|
|
2095
|
+
return false
|
|
1995
2096
|
}
|
|
1996
2097
|
|
|
1997
|
-
guard
|
|
2098
|
+
guard
|
|
2099
|
+
let bmpData = convertToG2Bmp(rawData, containerWidth: Int(rw), containerHeight: Int(rh))
|
|
1998
2100
|
else {
|
|
1999
|
-
Bridge.log("G2:
|
|
2101
|
+
Bridge.log("G2: failed to convert image to BMP")
|
|
2000
2102
|
return false
|
|
2001
2103
|
}
|
|
2002
2104
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2105
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
2106
|
+
var container: ImgContainer
|
|
2107
|
+
if let i = imageContainers.firstIndex(where: {
|
|
2108
|
+
$0.matches(x: rx, y: ry, width: rw, height: rh)
|
|
2109
|
+
}) {
|
|
2110
|
+
imageContainers[i].bmpData = bmpData
|
|
2111
|
+
container = imageContainers[i]
|
|
2112
|
+
Bridge.log(
|
|
2113
|
+
"G2: displayBitmap() - reusing container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh)"
|
|
2114
|
+
)
|
|
2115
|
+
if !pageCreated {
|
|
2116
|
+
await rebuildPage()
|
|
2117
|
+
return true
|
|
2118
|
+
}
|
|
2119
|
+
await sendImageData(
|
|
2120
|
+
containerID: container.id, containerName: container.name, bmpData: container.bmpData
|
|
2121
|
+
)
|
|
2122
|
+
return true
|
|
2123
|
+
} else {
|
|
2124
|
+
container = addImageContainer(x: rx, y: ry, width: rw, height: rh, bmpData: bmpData)
|
|
2125
|
+
Bridge.log(
|
|
2126
|
+
"G2: displayBitmap() - added container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page"
|
|
2127
|
+
)
|
|
2128
|
+
await rebuildPage()
|
|
2008
2129
|
}
|
|
2009
|
-
|
|
2010
|
-
return success
|
|
2130
|
+
return true
|
|
2011
2131
|
}
|
|
2012
2132
|
|
|
2013
2133
|
/// Add a new image container for `rect`, evicting the oldest when the list is full (max 4).
|
|
2014
2134
|
/// Returns the newly tracked container (with an assigned ID from the pool).
|
|
2015
|
-
private func addImageContainer(x: Int32, y: Int32, width: Int32, height: Int32
|
|
2135
|
+
private func addImageContainer(x: Int32, y: Int32, width: Int32, height: Int32, bmpData: Data)
|
|
2136
|
+
-> ImgContainer
|
|
2137
|
+
{
|
|
2016
2138
|
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2017
2139
|
if imageContainers.count >= imageContainerIDPool.count {
|
|
2018
2140
|
let evicted = imageContainers.removeFirst()
|
|
@@ -2021,14 +2143,70 @@ class G2: NSObject, SGCManager {
|
|
|
2021
2143
|
// Pick the lowest free ID from the pool.
|
|
2022
2144
|
let usedIDs = Set(imageContainers.map { $0.id })
|
|
2023
2145
|
let id = imageContainerIDPool.first { !usedIDs.contains($0) } ?? imageContainerIDPool[0]
|
|
2024
|
-
let container = ImgContainer(
|
|
2146
|
+
let container = ImgContainer(
|
|
2147
|
+
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData)
|
|
2025
2148
|
imageContainers.append(container)
|
|
2026
2149
|
return container
|
|
2027
2150
|
}
|
|
2028
2151
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2152
|
+
private func addTextContainer(
|
|
2153
|
+
x: Int32, y: Int32, width: Int32, height: Int32, content: String, borderWidth: Int32,
|
|
2154
|
+
borderColor: Int32, borderRadius: Int32, paddingLength: Int32
|
|
2155
|
+
) -> TextContainer {
|
|
2156
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2157
|
+
if textContainers.count >= textContainerIDPool.count {
|
|
2158
|
+
let evicted = textContainers.removeFirst()
|
|
2159
|
+
Bridge.log("G2: evicting oldest text container \(evicted.id)")
|
|
2160
|
+
}
|
|
2161
|
+
// Pick the lowest free ID from the pool.
|
|
2162
|
+
let usedIDs = Set(textContainers.map { $0.id })
|
|
2163
|
+
let id = textContainerIDPool.first { !usedIDs.contains($0) } ?? textContainerIDPool[0]
|
|
2164
|
+
let container = TextContainer(
|
|
2165
|
+
id: id, x: x, y: y, width: width, height: height, content: content,
|
|
2166
|
+
borderWidth: borderWidth, borderColor: borderColor, borderRadius: borderRadius,
|
|
2167
|
+
paddingLength: paddingLength)
|
|
2168
|
+
textContainers.append(container)
|
|
2169
|
+
return container
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/// shutdown and rebuild everything, re-sends all data to the glasses:
|
|
2173
|
+
private func rebuildPage() async {
|
|
2174
|
+
let msg = EvenHubProto.shutdownMessage()
|
|
2175
|
+
sendEvenHubCommand(msg)
|
|
2176
|
+
pageCreated = false
|
|
2177
|
+
await rebuildState()
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// re-creates the containers and sends all images and text again to the glasses:
|
|
2181
|
+
private func rebuildState() async {
|
|
2182
|
+
Bridge.log("G2: rebuildState()")
|
|
2183
|
+
// recreate the containers:
|
|
2184
|
+
createPageWithContainers()
|
|
2185
|
+
|
|
2186
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms to settle
|
|
2187
|
+
// send any image containers we have
|
|
2188
|
+
// go through each container and send the data:
|
|
2189
|
+
for container in imageContainers {
|
|
2190
|
+
Bridge.log(
|
|
2191
|
+
"G2: rebuildState() - sending image data to container \(container.id), \(container.bmpData.count) bytes"
|
|
2192
|
+
)
|
|
2193
|
+
await sendImageData(
|
|
2194
|
+
containerID: container.id, containerName: container.name, bmpData: container.bmpData
|
|
2195
|
+
)
|
|
2196
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms between containers
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// go through each text container and send the data:
|
|
2200
|
+
// disabled because text containers are initialized with their content:
|
|
2201
|
+
// for container in textContainers {
|
|
2202
|
+
// let msg = EvenHubProto.updateTextMessage(
|
|
2203
|
+
// containerID: container.id,
|
|
2204
|
+
// contentOffset: 0,
|
|
2205
|
+
// contentLength: Int32(container.content.utf8.count),
|
|
2206
|
+
// content: container.content
|
|
2207
|
+
// )
|
|
2208
|
+
// sendEvenHubCommand(msg)
|
|
2209
|
+
// }
|
|
2032
2210
|
}
|
|
2033
2211
|
|
|
2034
2212
|
/// Upscale BMP pixel data by 2x (200x100 → 400x200) using nearest-neighbor
|
|
@@ -2041,7 +2219,7 @@ class G2: NSObject, SGCManager {
|
|
|
2041
2219
|
return nil
|
|
2042
2220
|
}
|
|
2043
2221
|
|
|
2044
|
-
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3
|
|
2222
|
+
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3 // 4-bit rows padded to 4 bytes
|
|
2045
2223
|
let pixelDataOffset = headerSize
|
|
2046
2224
|
|
|
2047
2225
|
let dstWidth = srcWidth * 2
|
|
@@ -2074,7 +2252,7 @@ class G2: NSObject, SGCManager {
|
|
|
2074
2252
|
dst.appendLittleEndian(UInt32(0))
|
|
2075
2253
|
|
|
2076
2254
|
// --- Color Table (same 16-entry grayscale) ---
|
|
2077
|
-
for i in 0
|
|
2255
|
+
for i in 0..<16 {
|
|
2078
2256
|
let val = UInt8(i * 17)
|
|
2079
2257
|
dst.append(contentsOf: [val, val, val, 0])
|
|
2080
2258
|
}
|
|
@@ -2082,12 +2260,12 @@ class G2: NSObject, SGCManager {
|
|
|
2082
2260
|
// --- Pixel Data (nearest-neighbor 2x upscale) ---
|
|
2083
2261
|
// BMP is bottom-up, so row 0 = bottom of image
|
|
2084
2262
|
// Each dst row maps to srcRow = dstRow / 2
|
|
2085
|
-
for dstRow in 0
|
|
2263
|
+
for dstRow in 0..<dstHeight {
|
|
2086
2264
|
let srcRow = dstRow / 2
|
|
2087
2265
|
let srcRowOffset = pixelDataOffset + srcRow * srcPaddedRowSize
|
|
2088
2266
|
var rowBuf = [UInt8](repeating: 0, count: dstPaddedRowSize)
|
|
2089
2267
|
|
|
2090
|
-
for dstCol in 0
|
|
2268
|
+
for dstCol in 0..<dstWidth {
|
|
2091
2269
|
let srcCol = dstCol / 2
|
|
2092
2270
|
|
|
2093
2271
|
// Read 4-bit nibble from source
|
|
@@ -2136,9 +2314,9 @@ class G2: NSObject, SGCManager {
|
|
|
2136
2314
|
let offsetX = (containerWidth - scaledW) / 2
|
|
2137
2315
|
let offsetY = (containerHeight - scaledH) / 2
|
|
2138
2316
|
|
|
2139
|
-
Bridge.log(
|
|
2140
|
-
|
|
2141
|
-
)
|
|
2317
|
+
// Bridge.log(
|
|
2318
|
+
// "G2: convertToG2Bmp - input \(srcWidth)x\(srcHeight) → scaled \(scaledW)x\(scaledH) in \(containerWidth)x\(containerHeight)"
|
|
2319
|
+
// )
|
|
2142
2320
|
|
|
2143
2321
|
// Render to 8-bit grayscale at the CONTAINER size (not scaled size)
|
|
2144
2322
|
guard
|
|
@@ -2162,7 +2340,7 @@ class G2: NSObject, SGCManager {
|
|
|
2162
2340
|
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
2163
2341
|
|
|
2164
2342
|
guard let renderedImage = ctx.makeImage(),
|
|
2165
|
-
|
|
2343
|
+
let pixels = renderedImage.dataProvider?.data as Data?
|
|
2166
2344
|
else {
|
|
2167
2345
|
Bridge.log("G2: convertToG2Bmp - failed to get pixel data")
|
|
2168
2346
|
return nil
|
|
@@ -2184,8 +2362,8 @@ class G2: NSObject, SGCManager {
|
|
|
2184
2362
|
/// BMP rows are stored bottom-up. Each row is padded to a 4-byte boundary.
|
|
2185
2363
|
private func build4BitBmp(grayscalePixels: Data, width: Int, height: Int) -> Data? {
|
|
2186
2364
|
// 4-bit: 2 pixels per byte, rows padded to 4-byte boundary
|
|
2187
|
-
let bytesPerRow4bit = (width + 1) / 2
|
|
2188
|
-
let paddedRowSize = (bytesPerRow4bit + 3) & ~3
|
|
2365
|
+
let bytesPerRow4bit = (width + 1) / 2 // ceil(width / 2)
|
|
2366
|
+
let paddedRowSize = (bytesPerRow4bit + 3) & ~3 // pad to 4-byte boundary
|
|
2189
2367
|
let pixelDataSize = paddedRowSize * height
|
|
2190
2368
|
|
|
2191
2369
|
// BMP file header (14 bytes) + DIB header (40 bytes) + color table (16 * 4 = 64 bytes)
|
|
@@ -2195,46 +2373,46 @@ class G2: NSObject, SGCManager {
|
|
|
2195
2373
|
var bmp = Data(capacity: fileSize)
|
|
2196
2374
|
|
|
2197
2375
|
// --- BMP File Header (14 bytes) ---
|
|
2198
|
-
bmp.append(contentsOf: [0x42, 0x4D])
|
|
2199
|
-
bmp.appendLittleEndian(UInt32(fileSize))
|
|
2200
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
2201
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
2202
|
-
bmp.appendLittleEndian(UInt32(headerSize))
|
|
2376
|
+
bmp.append(contentsOf: [0x42, 0x4D]) // "BM" signature
|
|
2377
|
+
bmp.appendLittleEndian(UInt32(fileSize)) // File size
|
|
2378
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved1
|
|
2379
|
+
bmp.appendLittleEndian(UInt16(0)) // Reserved2
|
|
2380
|
+
bmp.appendLittleEndian(UInt32(headerSize)) // Pixel data offset
|
|
2203
2381
|
|
|
2204
2382
|
// --- DIB Header (BITMAPINFOHEADER, 40 bytes) ---
|
|
2205
|
-
bmp.appendLittleEndian(UInt32(40))
|
|
2206
|
-
bmp.appendLittleEndian(Int32(width))
|
|
2207
|
-
bmp.appendLittleEndian(Int32(height))
|
|
2208
|
-
bmp.appendLittleEndian(UInt16(1))
|
|
2209
|
-
bmp.appendLittleEndian(UInt16(4))
|
|
2210
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
2211
|
-
bmp.appendLittleEndian(UInt32(pixelDataSize))
|
|
2212
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
2213
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
2214
|
-
bmp.appendLittleEndian(UInt32(16))
|
|
2215
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
2383
|
+
bmp.appendLittleEndian(UInt32(40)) // DIB header size
|
|
2384
|
+
bmp.appendLittleEndian(Int32(width)) // Width
|
|
2385
|
+
bmp.appendLittleEndian(Int32(height)) // Height (positive = bottom-up)
|
|
2386
|
+
bmp.appendLittleEndian(UInt16(1)) // Color planes
|
|
2387
|
+
bmp.appendLittleEndian(UInt16(4)) // Bits per pixel (4-bit)
|
|
2388
|
+
bmp.appendLittleEndian(UInt32(0)) // Compression (none)
|
|
2389
|
+
bmp.appendLittleEndian(UInt32(pixelDataSize)) // Image size
|
|
2390
|
+
bmp.appendLittleEndian(Int32(2835)) // X pixels/meter (~72 DPI)
|
|
2391
|
+
bmp.appendLittleEndian(Int32(2835)) // Y pixels/meter
|
|
2392
|
+
bmp.appendLittleEndian(UInt32(16)) // Colors used
|
|
2393
|
+
bmp.appendLittleEndian(UInt32(0)) // Important colors (0 = all)
|
|
2216
2394
|
|
|
2217
2395
|
// --- Color Table (16 entries, 4 bytes each: B, G, R, 0) ---
|
|
2218
|
-
for i in 0
|
|
2219
|
-
let val = UInt8(i * 17)
|
|
2220
|
-
bmp.append(contentsOf: [val, val, val, 0])
|
|
2396
|
+
for i in 0..<16 {
|
|
2397
|
+
let val = UInt8(i * 17) // 0, 17, 34, ... 255 (evenly spaced grayscale)
|
|
2398
|
+
bmp.append(contentsOf: [val, val, val, 0]) // B, G, R, Reserved
|
|
2221
2399
|
}
|
|
2222
2400
|
|
|
2223
2401
|
// --- Pixel Data (bottom-up rows, 4-bit packed) ---
|
|
2224
2402
|
let rowBytes = [UInt8](repeating: 0, count: paddedRowSize)
|
|
2225
|
-
for row in 0
|
|
2403
|
+
for row in 0..<height {
|
|
2226
2404
|
// BMP is bottom-up: row 0 in BMP = last row of image
|
|
2227
2405
|
let srcRow = height - 1 - row
|
|
2228
2406
|
let srcOffset = srcRow * width
|
|
2229
2407
|
var rowBuf = rowBytes
|
|
2230
2408
|
|
|
2231
|
-
for col in 0
|
|
2409
|
+
for col in 0..<width {
|
|
2232
2410
|
let pixelIndex = srcOffset + col
|
|
2233
2411
|
guard pixelIndex < grayscalePixels.count else { continue }
|
|
2234
2412
|
|
|
2235
2413
|
// Map 8-bit grayscale (0-255) to 4-bit index (0-15)
|
|
2236
2414
|
let gray8 = grayscalePixels[pixelIndex]
|
|
2237
|
-
let index4 = gray8 >> 4
|
|
2415
|
+
let index4 = gray8 >> 4 // divide by 16
|
|
2238
2416
|
|
|
2239
2417
|
let bytePos = col / 2
|
|
2240
2418
|
if col % 2 == 0 {
|
|
@@ -2248,9 +2426,9 @@ class G2: NSObject, SGCManager {
|
|
|
2248
2426
|
bmp.append(contentsOf: rowBuf)
|
|
2249
2427
|
}
|
|
2250
2428
|
|
|
2251
|
-
Bridge.log(
|
|
2252
|
-
|
|
2253
|
-
)
|
|
2429
|
+
// Bridge.log(
|
|
2430
|
+
// "G2: build4BitBmp - \(bmp.count) bytes (header=\(headerSize), pixels=\(pixelDataSize), rows=\(paddedRowSize)x\(height))"
|
|
2431
|
+
// )
|
|
2254
2432
|
return bmp
|
|
2255
2433
|
}
|
|
2256
2434
|
|
|
@@ -2263,8 +2441,6 @@ class G2: NSObject, SGCManager {
|
|
|
2263
2441
|
let msg = EvenHubProto.shutdownMessage()
|
|
2264
2442
|
sendEvenHubCommand(msg)
|
|
2265
2443
|
pageCreated = false
|
|
2266
|
-
pageHasTextContainer = false
|
|
2267
|
-
currentTextContent = ""
|
|
2268
2444
|
currentBitmapBase64 = ""
|
|
2269
2445
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
2270
2446
|
guard let self = self else { return }
|
|
@@ -2286,20 +2462,20 @@ class G2: NSObject, SGCManager {
|
|
|
2286
2462
|
|
|
2287
2463
|
func sendDashboardDisplaySettings() {
|
|
2288
2464
|
var dashDisplayW = ProtobufWriter()
|
|
2289
|
-
dashDisplayW.writeInt32Field(1, 4)
|
|
2290
|
-
dashDisplayW.writeInt32Field(2, 3)
|
|
2291
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3]))
|
|
2292
|
-
dashDisplayW.writeInt32Field(4, 4)
|
|
2465
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
2466
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
2467
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
2468
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
2293
2469
|
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
2294
2470
|
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
2295
|
-
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat())
|
|
2296
|
-
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit())
|
|
2471
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
2472
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
2297
2473
|
|
|
2298
2474
|
var dashRecvW = ProtobufWriter()
|
|
2299
2475
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
2300
2476
|
|
|
2301
2477
|
var dashPkgW = ProtobufWriter()
|
|
2302
|
-
dashPkgW.writeInt32Field(1, 2)
|
|
2478
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
2303
2479
|
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2304
2480
|
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
2305
2481
|
sendDashboardCommand(dashPkgW.data)
|
|
@@ -2366,8 +2542,8 @@ class G2: NSObject, SGCManager {
|
|
|
2366
2542
|
let total = Int32(events.count)
|
|
2367
2543
|
for (i, ev) in events.enumerated() {
|
|
2368
2544
|
guard let title = ev["title"] as? String,
|
|
2369
|
-
|
|
2370
|
-
|
|
2545
|
+
let time = ev["time"] as? String,
|
|
2546
|
+
let endTs = ev["endDate"] as? Double
|
|
2371
2547
|
else { continue }
|
|
2372
2548
|
let location = ev["location"] as? String
|
|
2373
2549
|
sendCalendarEvent(
|
|
@@ -2420,14 +2596,36 @@ class G2: NSObject, SGCManager {
|
|
|
2420
2596
|
|
|
2421
2597
|
// MARK: - Private Display Helpers
|
|
2422
2598
|
|
|
2423
|
-
private func
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2599
|
+
private func createPageWithContainers() {
|
|
2600
|
+
// build the page's text containers from the live tracked list.
|
|
2601
|
+
// iterate by index not using map:
|
|
2602
|
+
var textContainerProps: [Data] = []
|
|
2603
|
+
for (index, c) in textContainers.enumerated() {
|
|
2604
|
+
textContainerProps.append(
|
|
2605
|
+
EvenHubProto.textContainerProperty(
|
|
2606
|
+
x: c.x, y: c.y, width: c.width, height: c.height,
|
|
2607
|
+
borderWidth: c.borderWidth, borderColor: c.borderColor,
|
|
2608
|
+
borderRadius: c.borderRadius,
|
|
2609
|
+
paddingLength: c.paddingLength, containerID: c.id,
|
|
2610
|
+
containerName: c.name, isEventCapture: index == 0, // the first container is the event capture container
|
|
2611
|
+
content: c.content
|
|
2612
|
+
))
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// iterate all image containers, remove any entrys with duplicate id's, and ensure the ids in the imageContainerIDPool is up-to-date:
|
|
2616
|
+
var seenIDs = Set<Int32>()
|
|
2617
|
+
imageContainers = imageContainers.filter { c in
|
|
2618
|
+
guard !c.bmpData.isEmpty else {
|
|
2619
|
+
Bridge.log("G2: removing empty image container \(c.id)")
|
|
2620
|
+
return false
|
|
2621
|
+
}
|
|
2622
|
+
guard !seenIDs.contains(c.id) else {
|
|
2623
|
+
Bridge.log("G2: removing duplicate image container \(c.id)")
|
|
2624
|
+
return false
|
|
2625
|
+
}
|
|
2626
|
+
seenIDs.insert(c.id)
|
|
2627
|
+
return imageContainerIDPool.contains(c.id)
|
|
2628
|
+
}
|
|
2431
2629
|
|
|
2432
2630
|
// Build the page's image containers from the live tracked list.
|
|
2433
2631
|
let imageContainerProps: [Data] = imageContainers.map { c in
|
|
@@ -2438,19 +2636,18 @@ class G2: NSObject, SGCManager {
|
|
|
2438
2636
|
}
|
|
2439
2637
|
|
|
2440
2638
|
let msg: Data
|
|
2441
|
-
if !
|
|
2442
|
-
Bridge.log("G2:
|
|
2639
|
+
if !pageCreated {
|
|
2640
|
+
Bridge.log("G2: createPageWithContainers() - using createPageMessage (first time)")
|
|
2443
2641
|
msg = EvenHubProto.createPageMessage(
|
|
2444
|
-
textContainers:
|
|
2642
|
+
textContainers: textContainerProps,
|
|
2445
2643
|
imageContainers: imageContainerProps,
|
|
2446
2644
|
magicRandom: sendManager.nextMagicRandom(),
|
|
2447
2645
|
appId: activeMenuAppId
|
|
2448
2646
|
)
|
|
2449
|
-
startupPageCreated = true
|
|
2450
2647
|
} else {
|
|
2451
|
-
Bridge.log("G2:
|
|
2648
|
+
Bridge.log("G2: createPageWithContainers() - using rebuildPageMessage")
|
|
2452
2649
|
msg = EvenHubProto.rebuildPageMessage(
|
|
2453
|
-
textContainers:
|
|
2650
|
+
textContainers: textContainerProps,
|
|
2454
2651
|
imageContainers: imageContainerProps,
|
|
2455
2652
|
magicRandom: sendManager.nextMagicRandom(),
|
|
2456
2653
|
appId: activeMenuAppId
|
|
@@ -2458,21 +2655,6 @@ class G2: NSObject, SGCManager {
|
|
|
2458
2655
|
}
|
|
2459
2656
|
sendEvenHubCommand(msg)
|
|
2460
2657
|
pageCreated = true
|
|
2461
|
-
pageHasTextContainer = true
|
|
2462
|
-
currentTextContent = text
|
|
2463
|
-
currentBitmapBase64 = ""
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
private func updateText(_ text: String) {
|
|
2467
|
-
let msg = EvenHubProto.updateTextMessage(
|
|
2468
|
-
containerID: textContainerID,
|
|
2469
|
-
contentOffset: 0,
|
|
2470
|
-
contentLength: Int32(text.utf8.count),
|
|
2471
|
-
content: text
|
|
2472
|
-
)
|
|
2473
|
-
queueEvenHubCommand(msg)
|
|
2474
|
-
currentTextContent = text
|
|
2475
|
-
currentBitmapBase64 = ""
|
|
2476
2658
|
}
|
|
2477
2659
|
|
|
2478
2660
|
private func queueEvenHubCommand(_ payload: Data) {
|
|
@@ -2501,6 +2683,13 @@ class G2: NSObject, SGCManager {
|
|
|
2501
2683
|
sendEvenHubCommand(toSend)
|
|
2502
2684
|
}
|
|
2503
2685
|
|
|
2686
|
+
private func restartMicIfAlreadyEnabled() {
|
|
2687
|
+
let currentEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
2688
|
+
if currentEnabled {
|
|
2689
|
+
restartMic()
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2504
2693
|
func restartMic() {
|
|
2505
2694
|
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2506
2695
|
DeviceStore.shared.apply("glasses", "micEnabled", true)
|
|
@@ -2508,13 +2697,14 @@ class G2: NSObject, SGCManager {
|
|
|
2508
2697
|
sendEvenHubCommand(msg)
|
|
2509
2698
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
2510
2699
|
guard let self = self else { return }
|
|
2511
|
-
let useNativeDashboard =
|
|
2512
|
-
|
|
2700
|
+
let useNativeDashboard =
|
|
2701
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
2702
|
+
// Bridge.log("G2: setMicEnabled - useNativeDashboard=\(useNativeDashboard), dashboardShowing=\(dashboardShowing)")
|
|
2513
2703
|
if useNativeDashboard && dashboardShowing > 0 {
|
|
2514
2704
|
return
|
|
2515
2705
|
}
|
|
2516
|
-
if !pageCreated
|
|
2517
|
-
DeviceManager.shared.sendCurrentState()
|
|
2706
|
+
if !pageCreated {
|
|
2707
|
+
DeviceManager.shared.sendCurrentState() // should re-create the page if needed
|
|
2518
2708
|
}
|
|
2519
2709
|
let msg = EvenHubProto.audioControlMessage(enable: true)
|
|
2520
2710
|
self.sendEvenHubCommand(msg)
|
|
@@ -2601,7 +2791,6 @@ class G2: NSObject, SGCManager {
|
|
|
2601
2791
|
rightAuthenticated = false
|
|
2602
2792
|
startupPageCreated = false
|
|
2603
2793
|
pageCreated = false
|
|
2604
|
-
pageHasTextContainer = false
|
|
2605
2794
|
dashboardShowing = 0
|
|
2606
2795
|
heartbeatCounter = 0
|
|
2607
2796
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
@@ -2707,7 +2896,9 @@ class G2: NSObject, SGCManager {
|
|
|
2707
2896
|
_ skillId: Int32, skillParam: Int32 = 0,
|
|
2708
2897
|
text: String = "", streamEnable: Int32 = 1, fTextEnd: Int32 = 1
|
|
2709
2898
|
) {
|
|
2710
|
-
Bridge.log(
|
|
2899
|
+
Bridge.log(
|
|
2900
|
+
"G2: triggerSkill(\(skillId), skillParam=\(skillParam), text=\"\(text)\", streamEnable=\(streamEnable), fTextEnd=\(fTextEnd))"
|
|
2901
|
+
)
|
|
2711
2902
|
let payload = EvenAIProto.triggerSkill(
|
|
2712
2903
|
magicRandom: sendManager.nextMagicRandom(),
|
|
2713
2904
|
skillId: skillId,
|
|
@@ -2726,64 +2917,109 @@ class G2: NSObject, SGCManager {
|
|
|
2726
2917
|
/// 3. SKILL{skillId=NOTIFICATION, skillParam=show, ...} — dispatches the intent
|
|
2727
2918
|
/// The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2728
2919
|
/// supply the session context that lets the glasses act on the SKILL.
|
|
2729
|
-
func showNotificationsPanel() {
|
|
2920
|
+
func showNotificationsPanel() async {
|
|
2730
2921
|
Bridge.log("G2: showNotificationsPanel()")
|
|
2731
|
-
Task { @MainActor [weak self] in
|
|
2732
|
-
guard let self = self else { return }
|
|
2733
2922
|
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2923
|
+
let enterPayload = EvenAIProto.aiCtrl(
|
|
2924
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2925
|
+
status: 2 // EVEN_AI_ENTER
|
|
2926
|
+
)
|
|
2927
|
+
sendEvenAICommand(enterPayload)
|
|
2739
2928
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2929
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
2930
|
+
let askPayload = EvenAIProto.aiAsk(
|
|
2931
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2932
|
+
text: " ",
|
|
2933
|
+
streamEnable: 0
|
|
2934
|
+
)
|
|
2935
|
+
sendEvenAICommand(askPayload)
|
|
2747
2936
|
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
}
|
|
2937
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
2938
|
+
triggerSkill(
|
|
2939
|
+
3, skillParam: 1, // NOTIFICATION, show
|
|
2940
|
+
text: " ",
|
|
2941
|
+
streamEnable: 1, fTextEnd: 1
|
|
2942
|
+
)
|
|
2755
2943
|
}
|
|
2756
2944
|
|
|
2757
2945
|
func dbg1() {
|
|
2758
|
-
|
|
2946
|
+
setCalendarWidgetFirst()
|
|
2947
|
+
// toggleHeyEven()
|
|
2759
2948
|
}
|
|
2760
2949
|
|
|
2950
|
+
private var compassRunning = false
|
|
2951
|
+
|
|
2761
2952
|
func dbg2() {
|
|
2762
|
-
|
|
2953
|
+
// compassRunning.toggle()
|
|
2954
|
+
// Bridge.log("G2: dbg2() — \(compassRunning ? "start" : "stop") compass")
|
|
2955
|
+
// if compassRunning {
|
|
2956
|
+
// startCompass()
|
|
2957
|
+
// } else {
|
|
2958
|
+
// stopCompass()
|
|
2959
|
+
// }
|
|
2960
|
+
}
|
|
2763
2961
|
|
|
2764
|
-
|
|
2962
|
+
/// Start a navigation session so the glasses stream compass heading via
|
|
2963
|
+
/// OS_NOTIFY_COMPASS_CHANGED — surfaced as `CompassHeadingEvent { heading: 0…359 }`
|
|
2964
|
+
/// in handleNavigationResponse.
|
|
2965
|
+
///
|
|
2966
|
+
/// If the magnetometer needs calibration, the glasses emit
|
|
2967
|
+
/// OS_NOTIFY_COMPASS_CALIBRATE_STRAT (→ `CompassCalibrationEvent {status:"start"}`);
|
|
2968
|
+
/// the wearer should look around until `…{status:"complete"}`.
|
|
2969
|
+
func startCompass() {
|
|
2970
|
+
var w = ProtobufWriter()
|
|
2971
|
+
w.writeInt32Field(1, NavigationCmd.appRequestStartUp.rawValue) // cmd
|
|
2972
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
|
|
2973
|
+
sendNavigationCommand(w.data)
|
|
2974
|
+
}
|
|
2765
2975
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2976
|
+
/// Stop the navigation/compass session (ends heading streaming).
|
|
2977
|
+
func stopCompass() {
|
|
2978
|
+
var w = ProtobufWriter()
|
|
2979
|
+
w.writeInt32Field(1, NavigationCmd.appRequestExit.rawValue)
|
|
2980
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2981
|
+
sendNavigationCommand(w.data)
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
func setImuEnabled(_ enabled: Bool) async {
|
|
2985
|
+
await setImuEnabled(enabled, reportFrq: EvenHubProto.imuPaceP100)
|
|
2986
|
+
}
|
|
2773
2987
|
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2988
|
+
/// Enable or disable IMU motion reporting on the glasses.
|
|
2989
|
+
///
|
|
2990
|
+
/// When enabled, the glasses continuously push `IMU_Report_Data { x, y, z }` (32-bit
|
|
2991
|
+
/// floats, gravity-normalized) via the EvenHub notify path; these surface in
|
|
2992
|
+
/// `handleTouchEvent` as a Sys_ItemEvent with `eventType == IMU_DATA_REPORT (8)` and
|
|
2993
|
+
/// are emitted through `Bridge.sendAccelEvent` (a single accelerometer reading;
|
|
2994
|
+
/// a richer combined IMU event covering gyro + magnetometer is future work).
|
|
2995
|
+
///
|
|
2996
|
+
/// - Parameters:
|
|
2997
|
+
/// - enabled: `true` to start streaming, `false` to stop.
|
|
2998
|
+
/// - reportFrq: ImuReportPace pacing code (100…1000, step 100 — protocol codes, not
|
|
2999
|
+
/// Hz). Ignored when disabling.
|
|
3000
|
+
func setImuEnabled(_ enabled: Bool, reportFrq: Int32 = EvenHubProto.imuPaceP100) async {
|
|
3001
|
+
Bridge.log("G2: setImuEnabled(\(enabled), frq=\(reportFrq))")
|
|
3002
|
+
|
|
3003
|
+
// IMU requires an active EvenHub page (same prerequisite as the mic).
|
|
3004
|
+
if enabled && !pageCreated {
|
|
3005
|
+
await rebuildState()
|
|
3006
|
+
}
|
|
2779
3007
|
|
|
2780
|
-
|
|
3008
|
+
let send = { [weak self] in
|
|
3009
|
+
guard let self = self else { return }
|
|
3010
|
+
let msg = EvenHubProto.imuControlMessage(
|
|
3011
|
+
enable: enabled, reportFrq: reportFrq,
|
|
3012
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
3013
|
+
)
|
|
3014
|
+
self.sendEvenHubCommand(msg)
|
|
3015
|
+
}
|
|
2781
3016
|
|
|
2782
|
-
//
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
3017
|
+
// If we just asked for a page, give it a moment to be created first.
|
|
3018
|
+
if enabled, !pageCreated {
|
|
3019
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: send)
|
|
3020
|
+
} else {
|
|
3021
|
+
send()
|
|
3022
|
+
}
|
|
2787
3023
|
}
|
|
2788
3024
|
|
|
2789
3025
|
// MARK: - SGCManager: Device Control
|
|
@@ -2812,10 +3048,41 @@ class G2: NSObject, SGCManager {
|
|
|
2812
3048
|
requestDeviceInfo()
|
|
2813
3049
|
}
|
|
2814
3050
|
|
|
3051
|
+
/// Reorder the dashboard widgets so the calendar (Schedule) widget appears first.
|
|
3052
|
+
///
|
|
3053
|
+
/// Sends a Dashboard_Receive (service 0x01) display-settings push with
|
|
3054
|
+
/// `widgetDisplayOrder` led by WidgetType 3 (Schedule). The remaining widgets
|
|
3055
|
+
/// keep their default relative order.
|
|
3056
|
+
/// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
3057
|
+
func setCalendarWidgetFirst() {
|
|
3058
|
+
// Schedule (calendar) first, then the rest in their default order.
|
|
3059
|
+
let widgetOrder: [UInt8] = [3, 1, 2, 4, 5]
|
|
3060
|
+
|
|
3061
|
+
var dashDisplayW = ProtobufWriter()
|
|
3062
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
3063
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
3064
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
3065
|
+
dashDisplayW.writeInt32Field(4, Int32(widgetOrder.count)) // widgetDisplayCount
|
|
3066
|
+
dashDisplayW.writeMessageField(5, Data(widgetOrder)) // widgetDisplayOrder: Schedule first
|
|
3067
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
3068
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
3069
|
+
|
|
3070
|
+
var dashRecvW = ProtobufWriter()
|
|
3071
|
+
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
3072
|
+
|
|
3073
|
+
var dashPkgW = ProtobufWriter()
|
|
3074
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
3075
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
3076
|
+
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
3077
|
+
sendDashboardCommand(dashPkgW.data)
|
|
3078
|
+
|
|
3079
|
+
Bridge.log("G2: setCalendarWidgetFirst — widgetDisplayOrder \(widgetOrder)")
|
|
3080
|
+
}
|
|
3081
|
+
|
|
2815
3082
|
func setDashboardMenu(_ items: [[String: Any]]) {
|
|
2816
3083
|
let menuItems = items.compactMap { dict -> MenuProto.MenuItem? in
|
|
2817
3084
|
guard let name = dict["name"] as? String,
|
|
2818
|
-
|
|
3085
|
+
let packageName = dict["packageName"] as? String
|
|
2819
3086
|
else { return nil }
|
|
2820
3087
|
let running = dict["running"] as? Bool ?? false
|
|
2821
3088
|
return MenuProto.MenuItem(packageName: packageName, name: name, running: running)
|
|
@@ -2875,7 +3142,8 @@ class G2: NSObject, SGCManager {
|
|
|
2875
3142
|
|
|
2876
3143
|
func requestPhoto(
|
|
2877
3144
|
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
2878
|
-
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?,
|
|
3145
|
+
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?,
|
|
3146
|
+
iso _: Int?
|
|
2879
3147
|
) {}
|
|
2880
3148
|
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
2881
3149
|
func startStream(_: [String: Any]) {}
|
|
@@ -2976,7 +3244,7 @@ class G2: NSObject, SGCManager {
|
|
|
2976
3244
|
centralManager!.scanForPeripherals(
|
|
2977
3245
|
withServices: nil,
|
|
2978
3246
|
options: [
|
|
2979
|
-
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
3247
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
2980
3248
|
]
|
|
2981
3249
|
)
|
|
2982
3250
|
return true
|
|
@@ -2994,7 +3262,7 @@ class G2: NSObject, SGCManager {
|
|
|
2994
3262
|
}
|
|
2995
3263
|
|
|
2996
3264
|
guard let leftUUID = leftGlassUUID(forSN: DEVICE_SEARCH_ID),
|
|
2997
|
-
|
|
3265
|
+
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
2998
3266
|
else { return false }
|
|
2999
3267
|
|
|
3000
3268
|
let knownLeft = centralManager?.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
@@ -3035,8 +3303,8 @@ class G2: NSObject, SGCManager {
|
|
|
3035
3303
|
// a different service than our primary one, and retrieveConnectedPeripherals only
|
|
3036
3304
|
// returns peripherals whose services match.
|
|
3037
3305
|
let serviceUUIDs: [CBUUID] = [
|
|
3038
|
-
G2BLE.SERVICE_UUID,
|
|
3039
|
-
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
3306
|
+
G2BLE.SERVICE_UUID, // EvenHub: 00002760-...-0000
|
|
3307
|
+
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"), // Nordic UART
|
|
3040
3308
|
]
|
|
3041
3309
|
var devices: [CBPeripheral] = []
|
|
3042
3310
|
for svc in serviceUUIDs {
|
|
@@ -3064,8 +3332,8 @@ class G2: NSObject, SGCManager {
|
|
|
3064
3332
|
// Extract XX (the numeric ID between G2_ and _L_/_R_)
|
|
3065
3333
|
let pattern = "G2_(\\d+)_"
|
|
3066
3334
|
guard let regex = try? NSRegularExpression(pattern: pattern),
|
|
3067
|
-
|
|
3068
|
-
|
|
3335
|
+
let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
|
|
3336
|
+
let range = Range(match.range(at: 1), in: name)
|
|
3069
3337
|
else {
|
|
3070
3338
|
return nil
|
|
3071
3339
|
}
|
|
@@ -3074,7 +3342,7 @@ class G2: NSObject, SGCManager {
|
|
|
3074
3342
|
|
|
3075
3343
|
// MARK: - Incoming Data Handling
|
|
3076
3344
|
|
|
3077
|
-
private func handleNotifyData(_ data: Data, from peripheral: CBPeripheral) {
|
|
3345
|
+
private func handleNotifyData(_ data: Data, from peripheral: CBPeripheral) async {
|
|
3078
3346
|
// Distinguish left vs right peripheral so multi-packet reassembly doesn't collide
|
|
3079
3347
|
let sourceKey = peripheral === leftPeripheral ? "L" : "R"
|
|
3080
3348
|
guard let result = receiveManager.handlePacket(data, sourceKey: sourceKey) else { return }
|
|
@@ -3095,16 +3363,79 @@ class G2: NSObject, SGCManager {
|
|
|
3095
3363
|
case ServiceID.dashboard.rawValue:
|
|
3096
3364
|
handleDashboardResponse(result.payload)
|
|
3097
3365
|
case ServiceID.gestureCtrl.rawValue:
|
|
3098
|
-
handleGestureCtrl(result.payload)
|
|
3366
|
+
await handleGestureCtrl(result.payload)
|
|
3367
|
+
case ServiceID.navigation.rawValue:
|
|
3368
|
+
handleNavigationResponse(result.payload)
|
|
3369
|
+
case ServiceID.evenAI.rawValue:
|
|
3370
|
+
handleEvenAIResponse(result.payload)
|
|
3099
3371
|
case ServiceID.evenHubCtrl.rawValue:
|
|
3100
3372
|
handleEvenHubCtrlResponse(result.payload)
|
|
3101
3373
|
default:
|
|
3102
3374
|
Bridge.log(
|
|
3103
|
-
"G2: Unhandled service \(result.serviceId) (\(result.payload.count) bytes): \(result.payload.
|
|
3375
|
+
"G2: Unhandled service \(result.serviceId) (\(result.payload.count) bytes): \(result.payload.map { String(format: "%02X", $0) }.joined())"
|
|
3376
|
+
)
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
/// EvenAI service (0x07). Logs the decoded EvenAIDataPackage so we can read the
|
|
3381
|
+
/// CONFIG (Hey Even) echo: commandId=10 (CONFIG), config sub-message in field 13.
|
|
3382
|
+
private func handleEvenAIResponse(_ payload: Data) {
|
|
3383
|
+
var reader = ProtobufReader(payload)
|
|
3384
|
+
let fields = reader.parseFields()
|
|
3385
|
+
let cmd = fields[1] as? Int32 ?? -1
|
|
3386
|
+
if cmd == 10, let configData = fields[13] as? Data {
|
|
3387
|
+
var cReader = ProtobufReader(configData)
|
|
3388
|
+
let cFields = cReader.parseFields()
|
|
3389
|
+
let voiceSwitch = cFields[1] as? Int32 ?? 0 // omitted = 0 = OFF
|
|
3390
|
+
Bridge.log(
|
|
3391
|
+
"G2: EvenAI CONFIG echo — voiceSwitch=\(voiceSwitch) (\(voiceSwitch == 1 ? "ON" : "OFF")) config=\(cFields)"
|
|
3392
|
+
)
|
|
3393
|
+
} else {
|
|
3394
|
+
Bridge.log(
|
|
3395
|
+
"G2: EvenAI cmd=\(cmd) fields=\(Array(fields.keys).sorted()) raw=\(payload.map { String(format: "%02X", $0) }.joined())"
|
|
3104
3396
|
)
|
|
3105
3397
|
}
|
|
3106
3398
|
}
|
|
3107
3399
|
|
|
3400
|
+
/// Navigation service (0x08).
|
|
3401
|
+
///
|
|
3402
|
+
/// OS_NOTIFY_COMPASS_CHANGED (15) carries the magnetometer heading in
|
|
3403
|
+
/// compass_info_msg (field 10) → field 1, as whole degrees 0…359. (The proto names
|
|
3404
|
+
/// that field `compassIndex`, but on the notify path it's the live heading — verified
|
|
3405
|
+
/// on-device: values sweep 0–359 as the wearer turns.)
|
|
3406
|
+
private func handleNavigationResponse(_ payload: Data) {
|
|
3407
|
+
var reader = ProtobufReader(payload)
|
|
3408
|
+
let fields = reader.parseFields()
|
|
3409
|
+
guard let cmd = fields[1] as? Int32 else { return }
|
|
3410
|
+
|
|
3411
|
+
switch cmd {
|
|
3412
|
+
case NavigationCmd.osNotifyCompassChanged.rawValue:
|
|
3413
|
+
guard let compassData = fields[10] as? Data else { return }
|
|
3414
|
+
var cReader = ProtobufReader(compassData)
|
|
3415
|
+
let cFields = cReader.parseFields()
|
|
3416
|
+
guard let heading = cFields[1] as? Int32 else { return }
|
|
3417
|
+
// Heading in degrees, 0…359.
|
|
3418
|
+
Bridge.log("G2: compass heading=\(heading)°")
|
|
3419
|
+
Bridge.sendTypedMessage(
|
|
3420
|
+
"CompassHeadingEvent",
|
|
3421
|
+
body: [
|
|
3422
|
+
"heading": Int(heading),
|
|
3423
|
+
"timestamp": Int64(Date().timeIntervalSince1970 * 1000),
|
|
3424
|
+
])
|
|
3425
|
+
|
|
3426
|
+
case NavigationCmd.osNotifyCompassCalibrateStart.rawValue:
|
|
3427
|
+
Bridge.log("G2: compass calibration started — wearer should look around")
|
|
3428
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", body: ["status": "start"])
|
|
3429
|
+
|
|
3430
|
+
case NavigationCmd.osNotifyCompassCalibrateComplete.rawValue:
|
|
3431
|
+
Bridge.log("G2: compass calibration complete")
|
|
3432
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", body: ["status": "complete"])
|
|
3433
|
+
|
|
3434
|
+
default:
|
|
3435
|
+
break
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3108
3439
|
private func handleEvenHubResponse(_ payload: Data) {
|
|
3109
3440
|
// Parse evenhub_main_msg_ctx: field 1 = Cmd (varint), field 13 = DevEvent (submessage)
|
|
3110
3441
|
var reader = ProtobufReader(payload)
|
|
@@ -3159,6 +3490,16 @@ class G2: NSObject, SGCManager {
|
|
|
3159
3490
|
}
|
|
3160
3491
|
}
|
|
3161
3492
|
} else {
|
|
3493
|
+
|
|
3494
|
+
// response codes:
|
|
3495
|
+
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
3496
|
+
if lastEvenHubResponseTimestamp != nil
|
|
3497
|
+
&& timestamp - lastEvenHubResponseTimestamp! < 100
|
|
3498
|
+
{
|
|
3499
|
+
return
|
|
3500
|
+
}
|
|
3501
|
+
lastEvenHubResponseTimestamp = timestamp
|
|
3502
|
+
|
|
3162
3503
|
// Log unhandled EvenHub commands (helps debug menu selection and stock dashboard interactions)
|
|
3163
3504
|
// Bridge.log(
|
|
3164
3505
|
// "G2: EvenHub response cmd=\(cmdValue), \(payload.count) bytes, fields=\(Array(fields.keys).sorted())"
|
|
@@ -3177,10 +3518,7 @@ class G2: NSObject, SGCManager {
|
|
|
3177
3518
|
Bridge.log(
|
|
3178
3519
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
3179
3520
|
)
|
|
3180
|
-
startupPageCreated = false
|
|
3181
3521
|
pageCreated = false
|
|
3182
|
-
pageHasTextContainer = false
|
|
3183
|
-
currentTextContent = ""
|
|
3184
3522
|
}
|
|
3185
3523
|
}
|
|
3186
3524
|
if let errorCode = resFields[8] as? Int32 {
|
|
@@ -3197,10 +3535,7 @@ class G2: NSObject, SGCManager {
|
|
|
3197
3535
|
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state
|
|
3198
3536
|
if cmdValue == 9 || cmdValue == 10 {
|
|
3199
3537
|
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3200
|
-
startupPageCreated = false
|
|
3201
3538
|
pageCreated = false
|
|
3202
|
-
pageHasTextContainer = false
|
|
3203
|
-
currentTextContent = ""
|
|
3204
3539
|
}
|
|
3205
3540
|
}
|
|
3206
3541
|
}
|
|
@@ -3229,6 +3564,37 @@ class G2: NSObject, SGCManager {
|
|
|
3229
3564
|
}
|
|
3230
3565
|
}
|
|
3231
3566
|
|
|
3567
|
+
/// Parse an IMU_Report_Data sub-message: fields 1/2/3 = x/y/z as 32-bit floats
|
|
3568
|
+
/// (wire type 5). `ProtobufReader.parseFields()` skips wire-type-5 fields, so this
|
|
3569
|
+
/// walks the bytes manually.
|
|
3570
|
+
private func parseImuReportData(_ data: Data) -> (x: Float, y: Float, z: Float)? {
|
|
3571
|
+
var x: Float?
|
|
3572
|
+
var y: Float?
|
|
3573
|
+
var z: Float?
|
|
3574
|
+
var i = data.startIndex
|
|
3575
|
+
while i < data.endIndex {
|
|
3576
|
+
let tag = data[i]
|
|
3577
|
+
i = data.index(after: i)
|
|
3578
|
+
let fieldNum = Int(tag >> 3)
|
|
3579
|
+
let wireType = Int(tag & 0x07)
|
|
3580
|
+
guard wireType == 5, data.distance(from: i, to: data.endIndex) >= 4 else { break }
|
|
3581
|
+
var bits: UInt32 = 0
|
|
3582
|
+
for b in 0..<4 {
|
|
3583
|
+
bits |= UInt32(data[data.index(i, offsetBy: b)]) << (8 * b) // little-endian
|
|
3584
|
+
}
|
|
3585
|
+
i = data.index(i, offsetBy: 4)
|
|
3586
|
+
let value = Float(bitPattern: bits)
|
|
3587
|
+
switch fieldNum {
|
|
3588
|
+
case 1: x = value
|
|
3589
|
+
case 2: y = value
|
|
3590
|
+
case 3: z = value
|
|
3591
|
+
default: break
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
guard let x = x, let y = y, let z = z else { return nil }
|
|
3595
|
+
return (x, y, z)
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3232
3598
|
private func handleTouchEvent(_ devEventData: Data) {
|
|
3233
3599
|
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
3234
3600
|
var reader = ProtobufReader(devEventData)
|
|
@@ -3247,6 +3613,19 @@ class G2: NSObject, SGCManager {
|
|
|
3247
3613
|
if let sysData = fields[3] as? Data {
|
|
3248
3614
|
var sysReader = ProtobufReader(sysData)
|
|
3249
3615
|
let sysFields = sysReader.parseFields()
|
|
3616
|
+
|
|
3617
|
+
// IMU data report: eventType == IMU_DATA_REPORT (8), imuData in field 3
|
|
3618
|
+
// (IMU_Report_Data { x, y, z } as 32-bit floats). Handle and return before
|
|
3619
|
+
// the gesture-mapping path.
|
|
3620
|
+
if (sysFields[1] as? Int32) == OsEventType.imuDataReport.rawValue,
|
|
3621
|
+
let imuData = sysFields[3] as? Data,
|
|
3622
|
+
let imu = parseImuReportData(imuData)
|
|
3623
|
+
{
|
|
3624
|
+
Bridge.log("G2: IMU data report: \(imu.x), \(imu.y), \(imu.z)")
|
|
3625
|
+
Bridge.sendAccelEvent(x: imu.x, y: imu.y, z: imu.z, timestamp: timestamp)
|
|
3626
|
+
return
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3250
3629
|
var eventType: OsEventType? = nil
|
|
3251
3630
|
var eventSource: Int32? = nil
|
|
3252
3631
|
if let normalType = sysFields[1] as? Int32 {
|
|
@@ -3286,7 +3665,8 @@ class G2: NSObject, SGCManager {
|
|
|
3286
3665
|
// trigger dashboard:
|
|
3287
3666
|
let isHeadUp = DeviceStore.shared.get("glasses", "headUp") as? Bool ?? false
|
|
3288
3667
|
|
|
3289
|
-
let useNativeDashboard =
|
|
3668
|
+
let useNativeDashboard =
|
|
3669
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
3290
3670
|
if useNativeDashboard {
|
|
3291
3671
|
showDashboard()
|
|
3292
3672
|
} else {
|
|
@@ -3305,7 +3685,6 @@ class G2: NSObject, SGCManager {
|
|
|
3305
3685
|
|
|
3306
3686
|
// toggle head up:
|
|
3307
3687
|
// DeviceStore.shared.apply("glasses", "headUp", true)
|
|
3308
|
-
// runDashboardSequence()
|
|
3309
3688
|
}
|
|
3310
3689
|
|
|
3311
3690
|
// if eventType == .foregroundEnter {
|
|
@@ -3319,14 +3698,8 @@ class G2: NSObject, SGCManager {
|
|
|
3319
3698
|
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
3320
3699
|
// Reset page state and re-create the page to reclaim EvenHub focus
|
|
3321
3700
|
if eventType == .systemExit || eventType == .abnormalExit {
|
|
3322
|
-
let savedText = currentTextContent
|
|
3323
|
-
let savedBitmap = currentBitmapBase64
|
|
3324
3701
|
// Bridge.log("G2: System exit detected")
|
|
3325
|
-
startupPageCreated = false
|
|
3326
3702
|
pageCreated = false
|
|
3327
|
-
pageHasTextContainer = false
|
|
3328
|
-
currentTextContent = ""
|
|
3329
|
-
currentBitmapBase64 = ""
|
|
3330
3703
|
// Firmware kills the mic on system exit; re-arm it if it should be on
|
|
3331
3704
|
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
3332
3705
|
DeviceManager.shared.updateMicState()
|
|
@@ -3348,7 +3721,7 @@ class G2: NSObject, SGCManager {
|
|
|
3348
3721
|
var textReader = ProtobufReader(textData)
|
|
3349
3722
|
let textFields = textReader.parseFields()
|
|
3350
3723
|
if let eventTypeRaw = textFields[3] as? Int32,
|
|
3351
|
-
|
|
3724
|
+
let eventType = OsEventType(rawValue: eventTypeRaw)
|
|
3352
3725
|
{
|
|
3353
3726
|
guard let gestureName = mapEventTypeToGesture(eventType) else {
|
|
3354
3727
|
Bridge.log("G2: no gesture mapping for \(eventType) \(textFields)")
|
|
@@ -3389,7 +3762,8 @@ class G2: NSObject, SGCManager {
|
|
|
3389
3762
|
case .foregroundEnter: return "foreground_enter"
|
|
3390
3763
|
case .foregroundExit: return "foreground_exit"
|
|
3391
3764
|
case .systemExit: return "system_exit"
|
|
3392
|
-
case .
|
|
3765
|
+
case .imuDataReport: return nil
|
|
3766
|
+
case .abnormalExit: return nil // don't report abnormal exits as gestures
|
|
3393
3767
|
}
|
|
3394
3768
|
}
|
|
3395
3769
|
|
|
@@ -3412,15 +3786,15 @@ class G2: NSObject, SGCManager {
|
|
|
3412
3786
|
|
|
3413
3787
|
// if the data is just a heartbeat, ignore it:
|
|
3414
3788
|
if let cmdValue = fields[1] as? Int32,
|
|
3415
|
-
|
|
3789
|
+
cmdValue == DevCfgCommandId.baseConnHeartBeat.rawValue
|
|
3416
3790
|
{
|
|
3417
3791
|
return
|
|
3418
3792
|
}
|
|
3419
3793
|
// Bridge.log("G2: DevSettings response cmdValue=\(cmdValue)")
|
|
3420
3794
|
|
|
3421
|
-
Bridge.log(
|
|
3422
|
-
|
|
3423
|
-
)
|
|
3795
|
+
// Bridge.log(
|
|
3796
|
+
// "G2: DevSettings response: \(data.prefix(32).map { String(format: "%02X", $0) }.joined(separator: ":"))"
|
|
3797
|
+
// )
|
|
3424
3798
|
|
|
3425
3799
|
// RING_CONNECT_INFO response (cmd 6)
|
|
3426
3800
|
if cmdValue == DevCfgCommandId.ringConnectInfo.rawValue {
|
|
@@ -3436,7 +3810,7 @@ class G2: NSObject, SGCManager {
|
|
|
3436
3810
|
// Bridge.log("G2: Ring connection status: connStat=\(connStat)")
|
|
3437
3811
|
|
|
3438
3812
|
// Bridge.log("G2: RingConnectInfo: \(fields)")
|
|
3439
|
-
if let ringData = fields[5] as? Data {
|
|
3813
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
3440
3814
|
var ringReader = ProtobufReader(ringData)
|
|
3441
3815
|
let ringFields = ringReader.parseFields()
|
|
3442
3816
|
|
|
@@ -3463,13 +3837,13 @@ class G2: NSObject, SGCManager {
|
|
|
3463
3837
|
// DeviceStore.shared.apply("glasses", "controllerSearching", true)
|
|
3464
3838
|
// }
|
|
3465
3839
|
|
|
3466
|
-
if let ringData = fields[5] as? Data {
|
|
3840
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
3467
3841
|
var ringReader = ProtobufReader(ringData)
|
|
3468
3842
|
let ringFields = ringReader.parseFields()
|
|
3469
|
-
let connStatus = ringFields[4] as? Int32 ?? -1
|
|
3470
|
-
Bridge.log(
|
|
3471
|
-
|
|
3472
|
-
)
|
|
3843
|
+
let connStatus = ringFields[4] as? Int32 ?? -1 // field 4 = connStatus
|
|
3844
|
+
// Bridge.log(
|
|
3845
|
+
// "G2: Ring connection status: connStatus?=\(connStatus))"
|
|
3846
|
+
// )
|
|
3473
3847
|
|
|
3474
3848
|
if connStatus == 22 {
|
|
3475
3849
|
Bridge.log("G2: Ring disconnected")
|
|
@@ -3570,13 +3944,13 @@ class G2: NSObject, SGCManager {
|
|
|
3570
3944
|
|
|
3571
3945
|
// Software versions
|
|
3572
3946
|
if let leftVer = fields[5] as? Data,
|
|
3573
|
-
|
|
3947
|
+
let leftVersion = String(data: leftVer, encoding: .utf8)
|
|
3574
3948
|
{
|
|
3575
3949
|
// Bridge.log("G2: Left firmware: \(leftVersion)")
|
|
3576
3950
|
DeviceStore.shared.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
3577
3951
|
}
|
|
3578
3952
|
if let rightVer = fields[6] as? Data,
|
|
3579
|
-
|
|
3953
|
+
let rightVersion = String(data: rightVer, encoding: .utf8)
|
|
3580
3954
|
{
|
|
3581
3955
|
// Bridge.log("G2: Right firmware: \(rightVersion)")
|
|
3582
3956
|
DeviceStore.shared.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
@@ -3595,7 +3969,7 @@ class G2: NSObject, SGCManager {
|
|
|
3595
3969
|
|
|
3596
3970
|
private func handleDashboardResponse(_ payload: Data) {
|
|
3597
3971
|
Bridge.log(
|
|
3598
|
-
"G2: dashboard response: \(payload.
|
|
3972
|
+
"G2: dashboard response: \(payload.map { String(format: "%02X", $0) }.joined())"
|
|
3599
3973
|
)
|
|
3600
3974
|
var reader = ProtobufReader(payload)
|
|
3601
3975
|
let fields = reader.parseFields()
|
|
@@ -3614,13 +3988,13 @@ class G2: NSObject, SGCManager {
|
|
|
3614
3988
|
// AppRespondToDashboard: field1=packageId, field2=flag (0=success)
|
|
3615
3989
|
if cmd == 3 {
|
|
3616
3990
|
var appRespW = ProtobufWriter()
|
|
3617
|
-
appRespW.writeInt32Field(1, packageId)
|
|
3618
|
-
appRespW.writeInt32Field(2, 0)
|
|
3991
|
+
appRespW.writeInt32Field(1, packageId) // packageId
|
|
3992
|
+
appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
|
|
3619
3993
|
|
|
3620
3994
|
var pkgW = ProtobufWriter()
|
|
3621
|
-
pkgW.writeInt32Field(1, 4)
|
|
3995
|
+
pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
|
|
3622
3996
|
pkgW.writeInt32Field(2, magicRandom)
|
|
3623
|
-
pkgW.writeMessageField(5, appRespW.data)
|
|
3997
|
+
pkgW.writeMessageField(5, appRespW.data) // field5 = appRespond
|
|
3624
3998
|
sendDashboardCommand(pkgW.data)
|
|
3625
3999
|
}
|
|
3626
4000
|
}
|
|
@@ -3632,7 +4006,7 @@ class G2: NSObject, SGCManager {
|
|
|
3632
4006
|
)
|
|
3633
4007
|
}
|
|
3634
4008
|
|
|
3635
|
-
private func handleGestureCtrl(_ data: Data) {
|
|
4009
|
+
private func handleGestureCtrl(_ data: Data) async {
|
|
3636
4010
|
// gesture_ctrl (service 0x0D): foreground lifecycle signals from glasses
|
|
3637
4011
|
// (informational only — log if needed for debugging)
|
|
3638
4012
|
// log first few bytes of the response:
|
|
@@ -3640,12 +4014,11 @@ class G2: NSObject, SGCManager {
|
|
|
3640
4014
|
// "G2: gesture_ctrl response: \(data.map { String(format: "%02X", $0) }.joined())"
|
|
3641
4015
|
// )
|
|
3642
4016
|
// Bridge.log("G2: gesture_ctrl response:")
|
|
3643
|
-
|
|
3644
4017
|
|
|
3645
4018
|
// Dedup: L and R peripherals both deliver this event, so debounce or
|
|
3646
4019
|
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
3647
4020
|
if lastGestureCtrlTimestamp != nil && timestamp - lastGestureCtrlTimestamp! < 500 {
|
|
3648
|
-
Bridge.log("G2: gesture_ctrl dedup")
|
|
4021
|
+
// Bridge.log("G2: gesture_ctrl dedup")
|
|
3649
4022
|
return
|
|
3650
4023
|
}
|
|
3651
4024
|
lastGestureCtrlTimestamp = timestamp
|
|
@@ -3654,27 +4027,31 @@ class G2: NSObject, SGCManager {
|
|
|
3654
4027
|
// so we need to revive it:
|
|
3655
4028
|
if data == Data([0x08, 0x01, 0x1A, 0x00]) {
|
|
3656
4029
|
Bridge.log("G2: dashboard closed / shutdown - dashboardShowing=\(dashboardShowing)")
|
|
3657
|
-
let useNativeDashboard =
|
|
4030
|
+
let useNativeDashboard =
|
|
4031
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
3658
4032
|
if !useNativeDashboard {
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
4033
|
+
dashboardShowing = 0
|
|
4034
|
+
// rebuild state:
|
|
4035
|
+
Task {
|
|
4036
|
+
await rebuildState()
|
|
4037
|
+
// set the mic back on if it should be on
|
|
4038
|
+
let micEnabled =
|
|
4039
|
+
DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
4040
|
+
if micEnabled {
|
|
4041
|
+
restartMic()
|
|
4042
|
+
}
|
|
3665
4043
|
}
|
|
3666
|
-
|
|
3667
|
-
// sendTextWall(" ")
|
|
3668
|
-
// createPageWithText(" ")
|
|
4044
|
+
return
|
|
3669
4045
|
} else {
|
|
3670
4046
|
// if we aren't trying to show the dashboard
|
|
3671
4047
|
// then we need to turn the mic back on and display the mentra main page:
|
|
3672
4048
|
if dashboardShowing <= 1 {
|
|
3673
4049
|
dashboardShowing = 0
|
|
3674
|
-
//
|
|
3675
|
-
|
|
4050
|
+
// rebuild state:
|
|
4051
|
+
await rebuildState()
|
|
3676
4052
|
// set the mic back on if it should be on
|
|
3677
|
-
let micEnabled =
|
|
4053
|
+
let micEnabled =
|
|
4054
|
+
DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
3678
4055
|
if micEnabled {
|
|
3679
4056
|
restartMic()
|
|
3680
4057
|
}
|
|
@@ -3707,7 +4084,7 @@ class G2: NSObject, SGCManager {
|
|
|
3707
4084
|
|
|
3708
4085
|
private var lastAudioFrame: Data?
|
|
3709
4086
|
|
|
3710
|
-
private func handleAudioData(_ data: Data) {
|
|
4087
|
+
private func handleAudioData(_ data: Data) async {
|
|
3711
4088
|
// G2 audio arrives on AUDIO_NOTIFY characteristic
|
|
3712
4089
|
// Format: ~200+ byte chunks, use first 200 bytes, split into 40-byte LC3 frames
|
|
3713
4090
|
// Each frame: LC3, 16kHz, mono, 10ms, 40 bytes
|
|
@@ -3737,7 +4114,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
3737
4114
|
// where the SN string starts.
|
|
3738
4115
|
|
|
3739
4116
|
// Skip "ER" prefix (2 bytes), read 14 bytes of SN
|
|
3740
|
-
let snData = data[2
|
|
4117
|
+
let snData = data[2..<16]
|
|
3741
4118
|
return String(data: snData, encoding: .ascii)?
|
|
3742
4119
|
.replacingOccurrences(
|
|
3743
4120
|
of: "[\\x00-\\x1F\\x7F]", with: "", options: .regularExpression
|
|
@@ -3749,7 +4126,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
3749
4126
|
/// Returns "AA:BB:CC:DD:EE:FF" (big-endian, colon-separated).
|
|
3750
4127
|
func extractMac(from data: Data) -> String? {
|
|
3751
4128
|
guard data.count >= 22 else { return nil }
|
|
3752
|
-
let macLE = data[16
|
|
4129
|
+
let macLE = data[16..<22]
|
|
3753
4130
|
return macLE.reversed().map { String(format: "%02X", $0) }.joined(separator: ":")
|
|
3754
4131
|
}
|
|
3755
4132
|
|
|
@@ -3773,13 +4150,13 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3773
4150
|
) {
|
|
3774
4151
|
guard
|
|
3775
4152
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey]
|
|
3776
|
-
|
|
4153
|
+
as? String
|
|
3777
4154
|
else { return }
|
|
3778
4155
|
|
|
3779
4156
|
// G2 glasses have "Even" prefix and "G2" in name, with _L_ or _R_ for side
|
|
3780
4157
|
guard name.contains("G2") else { return }
|
|
3781
4158
|
guard let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
3782
|
-
|
|
4159
|
+
mfgData.count >= 16
|
|
3783
4160
|
else { return }
|
|
3784
4161
|
|
|
3785
4162
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -3791,7 +4168,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3791
4168
|
}
|
|
3792
4169
|
// sn = "S200LACA040040"
|
|
3793
4170
|
let mfgHex = mfgData.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
3794
|
-
Bridge.log(
|
|
4171
|
+
Bridge.log(
|
|
4172
|
+
"G2: Discovered: \(name) (SN: \(serialNumber)) mfgData[\(mfgData.count)]: \(mfgHex)"
|
|
4173
|
+
)
|
|
3795
4174
|
self.deviceNameToSerialNumber[name] = serialNumber
|
|
3796
4175
|
|
|
3797
4176
|
// Save MAC per side; ring's advStart needs the left lens MAC.
|
|
@@ -3854,7 +4233,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3854
4233
|
self.setRightGlassUUID(peripheral.identifier, forSN: sn)
|
|
3855
4234
|
}
|
|
3856
4235
|
} else {
|
|
3857
|
-
Bridge.log(
|
|
4236
|
+
Bridge.log(
|
|
4237
|
+
"G2: didConnect — no SN for \(peripheral.name ?? "unknown"), skipping UUID save"
|
|
4238
|
+
)
|
|
3858
4239
|
}
|
|
3859
4240
|
|
|
3860
4241
|
// Discover services - scan for all since we need to find the EvenHub characteristics
|
|
@@ -3888,7 +4269,6 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3888
4269
|
|
|
3889
4270
|
self.startupPageCreated = false
|
|
3890
4271
|
self.pageCreated = false
|
|
3891
|
-
self.pageHasTextContainer = false
|
|
3892
4272
|
self.dashboardShowing = 0
|
|
3893
4273
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
3894
4274
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
@@ -3904,7 +4284,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3904
4284
|
guard let self else { return false }
|
|
3905
4285
|
|
|
3906
4286
|
// Check if already connected
|
|
3907
|
-
if await MainActor.run(body: {
|
|
4287
|
+
if await MainActor.run(body: {
|
|
4288
|
+
DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
4289
|
+
}) {
|
|
3908
4290
|
Bridge.log("G2: Already connected, stopping reconnection")
|
|
3909
4291
|
return true
|
|
3910
4292
|
}
|
|
@@ -3996,7 +4378,7 @@ extension G2: CBPeripheralDelegate {
|
|
|
3996
4378
|
if self.leftInitialized && self.rightInitialized && !self.authStarted {
|
|
3997
4379
|
self.authStarted = true
|
|
3998
4380
|
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
3999
|
-
self.runAuthSequence()
|
|
4381
|
+
Task { await self.runAuthSequence() }
|
|
4000
4382
|
}
|
|
4001
4383
|
}
|
|
4002
4384
|
}
|
|
@@ -4007,15 +4389,15 @@ extension G2: CBPeripheralDelegate {
|
|
|
4007
4389
|
) {
|
|
4008
4390
|
guard let data = characteristic.value, error == nil else { return }
|
|
4009
4391
|
|
|
4010
|
-
|
|
4392
|
+
Task { @MainActor [weak self] in
|
|
4011
4393
|
guard let self = self else { return }
|
|
4012
4394
|
|
|
4013
4395
|
if characteristic.uuid == G2BLE.AUDIO_NOTIFY {
|
|
4014
4396
|
// Audio data - forward to mic system
|
|
4015
|
-
self.handleAudioData(data)
|
|
4397
|
+
await self.handleAudioData(data)
|
|
4016
4398
|
} else if characteristic.uuid == G2BLE.CHAR_NOTIFY {
|
|
4017
4399
|
// Protocol data
|
|
4018
|
-
self.handleNotifyData(data, from: peripheral)
|
|
4400
|
+
await self.handleNotifyData(data, from: peripheral)
|
|
4019
4401
|
}
|
|
4020
4402
|
}
|
|
4021
4403
|
}
|