@mentra/bluetooth-sdk 0.1.9 → 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 +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +195 -34
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +103 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +210 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +30 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +125 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1293 -758
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +197 -149
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +64 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +253 -44
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +37 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +43 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +25 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +177 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +68 -19
- package/ios/Source/DeviceManager.swift +167 -32
- package/ios/Source/DeviceStore.swift +24 -8
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +6 -0
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +226 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +5 -5
- package/ios/Source/sgcs/G2.swift +1494 -870
- package/ios/Source/sgcs/Mach1.swift +4 -4
- package/ios/Source/sgcs/MentraLive.swift +814 -321
- package/ios/Source/sgcs/MentraNex.swift +604 -244
- package/ios/Source/sgcs/SGCManager.swift +68 -5
- package/ios/Source/sgcs/Simulated.swift +9 -5
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +344 -60
- package/src/_private/BluetoothSdkModule.ts +132 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +61 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -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,31 +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
|
|
57
|
-
case
|
|
58
|
-
case
|
|
59
|
-
case
|
|
60
|
-
case
|
|
61
|
-
case
|
|
62
|
-
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
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/// EvenHub command IDs from EvenHub.proto
|
|
66
68
|
private enum EvenHubCmd: Int32 {
|
|
67
|
-
case createStartupPage = 0
|
|
68
|
-
case updateImageRawData = 3
|
|
69
|
-
case updateTextData = 5
|
|
70
|
-
case rebuildPage = 7
|
|
71
|
-
case shutdownPage = 9
|
|
72
|
-
case heartbeat = 12
|
|
73
|
-
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
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
/// EvenHub response command IDs (from glasses → phone)
|
|
77
93
|
private enum EvenHubResponseCmd: Int32 {
|
|
78
|
-
case osNotifyEventToApp = 2
|
|
94
|
+
case osNotifyEventToApp = 2 // OS_NOITY_EVENT_TO_APP_PACKET - touch/gesture events
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
/// OsEventTypeList from EvenHub.proto
|
|
@@ -88,15 +104,16 @@ private enum OsEventType: Int32 {
|
|
|
88
104
|
case foregroundExit = 5
|
|
89
105
|
case abnormalExit = 6
|
|
90
106
|
case systemExit = 7
|
|
107
|
+
case imuDataReport = 8 // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
/// g2_settingCommandId from g2_setting.proto
|
|
94
111
|
private enum G2SettingCommandId: Int32 {
|
|
95
112
|
case none = 0
|
|
96
|
-
case deviceReceiveInfo = 1
|
|
97
|
-
case deviceReceiveRequest = 2
|
|
98
|
-
case deviceSendToApp = 3
|
|
99
|
-
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
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
/// DevCfgCommandId from dev_config_protocol.proto
|
|
@@ -140,7 +157,7 @@ private struct ProtobufWriter {
|
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
mutating func writeInt32Field(_ fieldNumber: Int, _ value: Int32) {
|
|
143
|
-
let tag = UInt64(fieldNumber << 3) | 0
|
|
160
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
144
161
|
writeVarint(tag)
|
|
145
162
|
// protobuf int32 uses varint encoding; negative values use 10 bytes
|
|
146
163
|
if value >= 0 {
|
|
@@ -150,8 +167,14 @@ private struct ProtobufWriter {
|
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
|
|
170
|
+
mutating func writeInt64Field(_ fieldNumber: Int, _ value: Int64) {
|
|
171
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
172
|
+
writeVarint(tag)
|
|
173
|
+
writeVarint(UInt64(bitPattern: value))
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
mutating func writeStringField(_ fieldNumber: Int, _ value: String) {
|
|
154
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
177
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
155
178
|
writeVarint(tag)
|
|
156
179
|
let utf8 = Array(value.utf8)
|
|
157
180
|
writeVarint(UInt64(utf8.count))
|
|
@@ -159,7 +182,7 @@ private struct ProtobufWriter {
|
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
mutating func writeBytesField(_ fieldNumber: Int, _ value: Data) {
|
|
162
|
-
let tag = UInt64(fieldNumber << 3) | 2
|
|
185
|
+
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
163
186
|
writeVarint(tag)
|
|
164
187
|
writeVarint(UInt64(value.count))
|
|
165
188
|
data.append(value)
|
|
@@ -221,7 +244,7 @@ private struct ProtobufReader {
|
|
|
221
244
|
guard let len = readVarint() else { return nil }
|
|
222
245
|
let length = Int(len)
|
|
223
246
|
guard offset + length <= data.count else { return nil }
|
|
224
|
-
let result = data[(data.startIndex + offset)
|
|
247
|
+
let result = data[(data.startIndex + offset)..<(data.startIndex + offset + length)]
|
|
225
248
|
offset += length
|
|
226
249
|
return Data(result)
|
|
227
250
|
}
|
|
@@ -234,10 +257,10 @@ private struct ProtobufReader {
|
|
|
234
257
|
/// Skip a field value based on wire type
|
|
235
258
|
mutating func skipField(wireType: Int) {
|
|
236
259
|
switch wireType {
|
|
237
|
-
case 0: _ = readVarint()
|
|
238
|
-
case 1: offset += 8
|
|
239
|
-
case 2: _ = readBytes()
|
|
240
|
-
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
|
|
241
264
|
default: break
|
|
242
265
|
}
|
|
243
266
|
}
|
|
@@ -249,9 +272,9 @@ private struct ProtobufReader {
|
|
|
249
272
|
while hasMore {
|
|
250
273
|
guard let (fieldNum, wireType) = readTag() else { break }
|
|
251
274
|
switch wireType {
|
|
252
|
-
case 0:
|
|
275
|
+
case 0: // varint
|
|
253
276
|
if let v = readVarint() { fields[fieldNum] = Int32(truncatingIfNeeded: v) }
|
|
254
|
-
case 2:
|
|
277
|
+
case 2: // length-delimited (submessage or bytes or string)
|
|
255
278
|
if let d = readBytes() { fields[fieldNum] = d }
|
|
256
279
|
default:
|
|
257
280
|
skipField(wireType: wireType)
|
|
@@ -273,21 +296,21 @@ private enum EvenHubProto {
|
|
|
273
296
|
content: String? = nil
|
|
274
297
|
) -> Data {
|
|
275
298
|
var w = ProtobufWriter()
|
|
276
|
-
w.writeInt32Field(1, x)
|
|
277
|
-
w.writeInt32Field(2, y)
|
|
278
|
-
w.writeInt32Field(3, width)
|
|
279
|
-
w.writeInt32Field(4, height)
|
|
280
|
-
w.writeInt32Field(5, borderWidth)
|
|
281
|
-
w.writeInt32Field(6, borderColor)
|
|
282
|
-
w.writeInt32Field(7, borderRadius)
|
|
283
|
-
w.writeInt32Field(8, paddingLength)
|
|
284
|
-
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
|
|
285
308
|
if let name = containerName {
|
|
286
|
-
w.writeStringField(10, name)
|
|
309
|
+
w.writeStringField(10, name) // ContainerName
|
|
287
310
|
}
|
|
288
|
-
w.writeInt32Field(11, isEventCapture ? 1 : 0)
|
|
311
|
+
w.writeInt32Field(11, isEventCapture ? 1 : 0) // IsEventCapture
|
|
289
312
|
if let content = content {
|
|
290
|
-
w.writeStringField(12, content)
|
|
313
|
+
w.writeStringField(12, content) // Content
|
|
291
314
|
}
|
|
292
315
|
return w.data
|
|
293
316
|
}
|
|
@@ -298,13 +321,13 @@ private enum EvenHubProto {
|
|
|
298
321
|
containerID: Int32, containerName: String? = nil
|
|
299
322
|
) -> Data {
|
|
300
323
|
var w = ProtobufWriter()
|
|
301
|
-
w.writeInt32Field(1, x)
|
|
302
|
-
w.writeInt32Field(2, y)
|
|
303
|
-
w.writeInt32Field(3, width)
|
|
304
|
-
w.writeInt32Field(4, height)
|
|
305
|
-
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
|
|
306
329
|
if let name = containerName {
|
|
307
|
-
w.writeStringField(6, name)
|
|
330
|
+
w.writeStringField(6, name) // ContainerName
|
|
308
331
|
}
|
|
309
332
|
return w.data
|
|
310
333
|
}
|
|
@@ -316,16 +339,16 @@ private enum EvenHubProto {
|
|
|
316
339
|
mapFragmentIndex: Int32, mapFragmentPacketSize: Int32, mapRawData: Data
|
|
317
340
|
) -> Data {
|
|
318
341
|
var w = ProtobufWriter()
|
|
319
|
-
w.writeInt32Field(1, containerID)
|
|
342
|
+
w.writeInt32Field(1, containerID) // ContainerID
|
|
320
343
|
if let name = containerName {
|
|
321
|
-
w.writeStringField(2, name)
|
|
322
|
-
}
|
|
323
|
-
w.writeInt32Field(3, mapSessionId)
|
|
324
|
-
w.writeInt32Field(4, mapTotalSize)
|
|
325
|
-
w.writeInt32Field(5, compressMode)
|
|
326
|
-
w.writeInt32Field(6, mapFragmentIndex)
|
|
327
|
-
w.writeInt32Field(7, mapFragmentPacketSize)
|
|
328
|
-
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
|
|
329
352
|
return w.data
|
|
330
353
|
}
|
|
331
354
|
|
|
@@ -336,13 +359,13 @@ private enum EvenHubProto {
|
|
|
336
359
|
imageContainers: [Data] = []
|
|
337
360
|
) -> Data {
|
|
338
361
|
var w = ProtobufWriter()
|
|
339
|
-
w.writeInt32Field(1, containerTotalNum)
|
|
362
|
+
w.writeInt32Field(1, containerTotalNum) // ContainerTotalNum
|
|
340
363
|
// field 2 = repeated ListContainerProperty ListObject (not used here)
|
|
341
364
|
for tc in textContainers {
|
|
342
|
-
w.writeMessageField(3, tc)
|
|
365
|
+
w.writeMessageField(3, tc) // field 3 = repeated TextObject
|
|
343
366
|
}
|
|
344
367
|
for ic in imageContainers {
|
|
345
|
-
w.writeMessageField(4, ic)
|
|
368
|
+
w.writeMessageField(4, ic) // field 4 = repeated ImageObject
|
|
346
369
|
}
|
|
347
370
|
return w.data
|
|
348
371
|
}
|
|
@@ -353,17 +376,17 @@ private enum EvenHubProto {
|
|
|
353
376
|
contentLength: Int32, content: String
|
|
354
377
|
) -> Data {
|
|
355
378
|
var w = ProtobufWriter()
|
|
356
|
-
w.writeInt32Field(1, containerID)
|
|
357
|
-
w.writeInt32Field(3, contentOffset)
|
|
358
|
-
w.writeInt32Field(4, contentLength)
|
|
359
|
-
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
|
|
360
383
|
return w.data
|
|
361
384
|
}
|
|
362
385
|
|
|
363
386
|
/// Build a ShutDownContaniner message (sic - typo in proto)
|
|
364
387
|
static func shutdownContainer(exitMode: Int32 = 0) -> Data {
|
|
365
388
|
var w = ProtobufWriter()
|
|
366
|
-
w.writeInt32Field(1, exitMode)
|
|
389
|
+
w.writeInt32Field(1, exitMode) // exitMode
|
|
367
390
|
return w.data
|
|
368
391
|
}
|
|
369
392
|
|
|
@@ -371,7 +394,7 @@ private enum EvenHubProto {
|
|
|
371
394
|
static func heartbeatPacket(cnt: Int32 = 0) -> Data {
|
|
372
395
|
var w = ProtobufWriter()
|
|
373
396
|
if cnt != 0 {
|
|
374
|
-
w.writeInt32Field(1, cnt)
|
|
397
|
+
w.writeInt32Field(1, cnt) // Cnt
|
|
375
398
|
}
|
|
376
399
|
return w.data
|
|
377
400
|
}
|
|
@@ -379,7 +402,7 @@ private enum EvenHubProto {
|
|
|
379
402
|
/// Build an AudioCtrCmd message
|
|
380
403
|
static func audioCtrCmd(enable: Bool) -> Data {
|
|
381
404
|
var w = ProtobufWriter()
|
|
382
|
-
w.writeInt32Field(1, enable ? 1 : 0)
|
|
405
|
+
w.writeInt32Field(1, enable ? 1 : 0) // AudoFuncEn
|
|
383
406
|
return w.data
|
|
384
407
|
}
|
|
385
408
|
|
|
@@ -390,11 +413,11 @@ private enum EvenHubProto {
|
|
|
390
413
|
appId: Int32? = nil
|
|
391
414
|
) -> Data {
|
|
392
415
|
var w = ProtobufWriter()
|
|
393
|
-
w.writeInt32Field(1, cmd.rawValue)
|
|
394
|
-
w.writeInt32Field(2, magicRandom)
|
|
395
|
-
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
|
|
396
419
|
if let appId = appId {
|
|
397
|
-
w.writeInt32Field(5, appId)
|
|
420
|
+
w.writeInt32Field(5, appId) // Associate page with a menu item appId
|
|
398
421
|
}
|
|
399
422
|
return w.data
|
|
400
423
|
}
|
|
@@ -479,6 +502,48 @@ private enum EvenHubProto {
|
|
|
479
502
|
cmd: .audioControl, subFieldNumber: 18, subMessage: audioMsg, magicRandom: magicRandom
|
|
480
503
|
)
|
|
481
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
|
+
}
|
|
482
547
|
}
|
|
483
548
|
|
|
484
549
|
// MARK: - DevSettings Auth Protobuf Builders
|
|
@@ -491,17 +556,17 @@ private enum DevSettingsProto {
|
|
|
491
556
|
// field 2 = magicRandom (int32)
|
|
492
557
|
// field 3 = authMgr (AuthMgr message)
|
|
493
558
|
var w = ProtobufWriter()
|
|
494
|
-
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue)
|
|
495
|
-
w.writeInt32Field(2, magicRandom)
|
|
559
|
+
w.writeInt32Field(1, DevCfgCommandId.authentication.rawValue) // commandId
|
|
560
|
+
w.writeInt32Field(2, magicRandom) // magicRandom
|
|
496
561
|
|
|
497
562
|
// AuthMgr sub-message:
|
|
498
563
|
// field 1 = secAuth (bool)
|
|
499
564
|
// field 2 = phoneType (enum eDevice: PHONE_IOS=3, PHONE_ANDROID=4)
|
|
500
565
|
var authW = ProtobufWriter()
|
|
501
|
-
authW.writeBoolField(1, true)
|
|
502
|
-
authW.writeInt32Field(2, 3)
|
|
566
|
+
authW.writeBoolField(1, true) // secAuth
|
|
567
|
+
authW.writeInt32Field(2, 3) // phoneType = PHONE_IOS (eDevice.PHONE_IOS=3)
|
|
503
568
|
|
|
504
|
-
w.writeMessageField(3, authW.data)
|
|
569
|
+
w.writeMessageField(3, authW.data) // authMgr
|
|
505
570
|
return w.data
|
|
506
571
|
}
|
|
507
572
|
|
|
@@ -513,24 +578,53 @@ private enum DevSettingsProto {
|
|
|
513
578
|
|
|
514
579
|
// PipeRoleChange: field 1 = asCmdRole (enum GlassesLR.RIGHT=1)
|
|
515
580
|
var roleW = ProtobufWriter()
|
|
516
|
-
roleW.writeInt32Field(1, 1)
|
|
517
|
-
w.writeMessageField(4, roleW.data)
|
|
581
|
+
roleW.writeInt32Field(1, 1) // RIGHT
|
|
582
|
+
w.writeMessageField(4, roleW.data) // roleChange (field 4 in DevCfgDataPackage)
|
|
518
583
|
return w.data
|
|
519
584
|
}
|
|
520
585
|
|
|
521
|
-
/// DevCfgDataPackage with TIME_SYNC command
|
|
586
|
+
/// DevCfgDataPackage with TIME_SYNC command.
|
|
587
|
+
/// TimeSync submessage: f1 = (Unix seconds + TZ offset seconds) as Int32, no TZ field.
|
|
588
|
+
/// Firmware appears to ignore the TZ field, so we pre-shift the timestamp itself
|
|
589
|
+
/// to make UTC interpretation read as local. Empirically confirmed via probe variants in dbg1().
|
|
522
590
|
static func timeSync(magicRandom: Int32) -> Data {
|
|
523
591
|
var w = ProtobufWriter()
|
|
524
592
|
w.writeInt32Field(1, DevCfgCommandId.timeSync.rawValue)
|
|
525
593
|
w.writeInt32Field(2, magicRandom)
|
|
526
594
|
|
|
527
|
-
// TimeSync: field 1 = timestamp (int32), field 2 = timezone (int32)
|
|
528
595
|
var tsW = ProtobufWriter()
|
|
529
|
-
let
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
w.
|
|
596
|
+
let nowSec = Int64(Date().timeIntervalSince1970)
|
|
597
|
+
let tzSec = Int64(TimeZone.current.secondsFromGMT())
|
|
598
|
+
tsW.writeInt32Field(1, Int32(truncatingIfNeeded: nowSec + tzSec))
|
|
599
|
+
w.writeMessageField(128, tsW.data) // timeSync (field 128 in DevCfgDataPackage)
|
|
600
|
+
return w.data
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/// Parameterized TIME_SYNC for probing the right wire format from dbg1().
|
|
604
|
+
/// - tsField: protobuf field # for the timestamp varint (typically 1)
|
|
605
|
+
/// - tsValue: raw timestamp varint value
|
|
606
|
+
/// - tsBits64: encode timestamp as Int64 (true) or Int32 (false)
|
|
607
|
+
/// - tzField: protobuf field # for TZ (nil to omit entirely)
|
|
608
|
+
/// - tzValue: TZ value to write if tzField != nil
|
|
609
|
+
static func timeSyncVariant(
|
|
610
|
+
magicRandom: Int32,
|
|
611
|
+
tsField: Int, tsValue: Int64, tsBits64: Bool,
|
|
612
|
+
tzField: Int?, tzValue: Int32
|
|
613
|
+
) -> Data {
|
|
614
|
+
var w = ProtobufWriter()
|
|
615
|
+
w.writeInt32Field(1, DevCfgCommandId.timeSync.rawValue)
|
|
616
|
+
w.writeInt32Field(2, magicRandom)
|
|
617
|
+
|
|
618
|
+
var tsW = ProtobufWriter()
|
|
619
|
+
if tsBits64 {
|
|
620
|
+
tsW.writeInt64Field(tsField, tsValue)
|
|
621
|
+
} else {
|
|
622
|
+
tsW.writeInt32Field(tsField, Int32(truncatingIfNeeded: tsValue))
|
|
623
|
+
}
|
|
624
|
+
if let tzField = tzField {
|
|
625
|
+
tsW.writeInt32Field(tzField, tzValue)
|
|
626
|
+
}
|
|
627
|
+
w.writeMessageField(128, tsW.data)
|
|
534
628
|
return w.data
|
|
535
629
|
}
|
|
536
630
|
|
|
@@ -542,8 +636,8 @@ private enum DevSettingsProto {
|
|
|
542
636
|
|
|
543
637
|
// BaseConnHeartBeat: empty message
|
|
544
638
|
var hbW = ProtobufWriter()
|
|
545
|
-
_ = hbW
|
|
546
|
-
w.writeMessageField(13, hbW.data)
|
|
639
|
+
_ = hbW // empty
|
|
640
|
+
w.writeMessageField(13, hbW.data) // baseHeartBeat (field 13)
|
|
547
641
|
return w.data
|
|
548
642
|
}
|
|
549
643
|
|
|
@@ -554,18 +648,18 @@ private enum DevSettingsProto {
|
|
|
554
648
|
magicRandom: Int32, connect: Bool, ringMac: Data, ringName: String = ""
|
|
555
649
|
) -> Data {
|
|
556
650
|
var w = ProtobufWriter()
|
|
557
|
-
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue)
|
|
651
|
+
w.writeInt32Field(1, DevCfgCommandId.ringConnectInfo.rawValue) // commandId = RING_CONNECT_INFO (6)
|
|
558
652
|
w.writeInt32Field(2, magicRandom)
|
|
559
653
|
|
|
560
654
|
// RingInfo sub-message (field 5 in DevCfgDataPackage)
|
|
561
655
|
var ringW = ProtobufWriter()
|
|
562
|
-
ringW.writeBoolField(1, connect)
|
|
563
|
-
ringW.writeBytesField(2, ringMac)
|
|
656
|
+
ringW.writeBoolField(1, connect) // connectRing
|
|
657
|
+
ringW.writeBytesField(2, ringMac) // ringMac (6 bytes)
|
|
564
658
|
if !ringName.isEmpty {
|
|
565
|
-
ringW.writeBytesField(3, Data(ringName.utf8))
|
|
659
|
+
ringW.writeBytesField(3, Data(ringName.utf8)) // ringName
|
|
566
660
|
}
|
|
567
661
|
|
|
568
|
-
w.writeMessageField(5, ringW.data)
|
|
662
|
+
w.writeMessageField(5, ringW.data) // ringInfo (field 5)
|
|
569
663
|
return w.data
|
|
570
664
|
}
|
|
571
665
|
}
|
|
@@ -577,18 +671,18 @@ private enum G2SettingProto {
|
|
|
577
671
|
static func setBrightness(magicRandom: Int32, level: Int32, autoAdjust: Bool) -> Data {
|
|
578
672
|
// DeviceReceive_Brightness
|
|
579
673
|
var brightnessW = ProtobufWriter()
|
|
580
|
-
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0)
|
|
581
|
-
brightnessW.writeInt32Field(2, level)
|
|
674
|
+
brightnessW.writeInt32Field(1, autoAdjust ? 1 : 0) // autoAdjust
|
|
675
|
+
brightnessW.writeInt32Field(2, level) // brightnessLevel
|
|
582
676
|
|
|
583
677
|
// DeviceReceiveInfoFromAPP
|
|
584
678
|
var infoW = ProtobufWriter()
|
|
585
|
-
infoW.writeMessageField(1, brightnessW.data)
|
|
679
|
+
infoW.writeMessageField(1, brightnessW.data) // deviceReceiveBrightness (field 1)
|
|
586
680
|
|
|
587
681
|
// G2SettingPackage
|
|
588
682
|
var w = ProtobufWriter()
|
|
589
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
683
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue) // commandId
|
|
590
684
|
w.writeInt32Field(2, magicRandom)
|
|
591
|
-
w.writeMessageField(3, infoW.data)
|
|
685
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
592
686
|
return w.data
|
|
593
687
|
}
|
|
594
688
|
|
|
@@ -597,13 +691,13 @@ private enum G2SettingProto {
|
|
|
597
691
|
// DeviceReceiveRequestFromAPP - empty message triggers glasses to respond with all fields
|
|
598
692
|
var reqW = ProtobufWriter()
|
|
599
693
|
// Request brightness info type
|
|
600
|
-
reqW.writeInt32Field(1, 1)
|
|
694
|
+
reqW.writeInt32Field(1, 1) // settingInfoType = APP_REQUIRE_BASIC_SETTING
|
|
601
695
|
|
|
602
696
|
// G2SettingPackage
|
|
603
697
|
var w = ProtobufWriter()
|
|
604
|
-
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue)
|
|
698
|
+
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveRequest.rawValue) // commandId
|
|
605
699
|
w.writeInt32Field(2, magicRandom)
|
|
606
|
-
w.writeMessageField(4, reqW.data)
|
|
700
|
+
w.writeMessageField(4, reqW.data) // deviceReceiveRequestFromApp (field 4)
|
|
607
701
|
return w.data
|
|
608
702
|
}
|
|
609
703
|
|
|
@@ -611,17 +705,17 @@ private enum G2SettingProto {
|
|
|
611
705
|
static func setHeadUpSwitch(magicRandom: Int32, enabled: Bool) -> Data {
|
|
612
706
|
// DeviceReceive_Head_UP_Setting
|
|
613
707
|
var headUpW = ProtobufWriter()
|
|
614
|
-
headUpW.writeInt32Field(1, enabled ? 1 : 0)
|
|
708
|
+
headUpW.writeInt32Field(1, enabled ? 1 : 0) // headUpSwitch
|
|
615
709
|
|
|
616
710
|
// DeviceReceiveInfoFromAPP
|
|
617
711
|
var infoW = ProtobufWriter()
|
|
618
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
712
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
619
713
|
|
|
620
714
|
// G2SettingPackage
|
|
621
715
|
var w = ProtobufWriter()
|
|
622
716
|
w.writeInt32Field(1, G2SettingCommandId.deviceReceiveInfo.rawValue)
|
|
623
717
|
w.writeInt32Field(2, magicRandom)
|
|
624
|
-
w.writeMessageField(3, infoW.data)
|
|
718
|
+
w.writeMessageField(3, infoW.data) // deviceReceiveInfoFromApp (field 3)
|
|
625
719
|
return w.data
|
|
626
720
|
}
|
|
627
721
|
|
|
@@ -629,11 +723,11 @@ private enum G2SettingProto {
|
|
|
629
723
|
static func setHeadUpAngle(magicRandom: Int32, angle: Int32) -> Data {
|
|
630
724
|
// DeviceReceive_Head_UP_Setting
|
|
631
725
|
var headUpW = ProtobufWriter()
|
|
632
|
-
headUpW.writeInt32Field(2, angle)
|
|
726
|
+
headUpW.writeInt32Field(2, angle) // headUpAngle (field 2)
|
|
633
727
|
|
|
634
728
|
// DeviceReceiveInfoFromAPP
|
|
635
729
|
var infoW = ProtobufWriter()
|
|
636
|
-
infoW.writeMessageField(4, headUpW.data)
|
|
730
|
+
infoW.writeMessageField(4, headUpW.data) // deviceReceiveHeadUpSetting (field 4)
|
|
637
731
|
|
|
638
732
|
// G2SettingPackage
|
|
639
733
|
var w = ProtobufWriter()
|
|
@@ -647,11 +741,11 @@ private enum G2SettingProto {
|
|
|
647
741
|
static func setScreenHeight(magicRandom: Int32, level: Int32) -> Data {
|
|
648
742
|
// DeviceReceive_Y_Coordinate
|
|
649
743
|
var yW = ProtobufWriter()
|
|
650
|
-
yW.writeInt32Field(1, level)
|
|
744
|
+
yW.writeInt32Field(1, level) // yCoordinateLevel
|
|
651
745
|
|
|
652
746
|
// DeviceReceiveInfoFromAPP
|
|
653
747
|
var infoW = ProtobufWriter()
|
|
654
|
-
infoW.writeMessageField(2, yW.data)
|
|
748
|
+
infoW.writeMessageField(2, yW.data) // deviceReceiveYCoordinate (field 2)
|
|
655
749
|
|
|
656
750
|
// G2SettingPackage
|
|
657
751
|
var w = ProtobufWriter()
|
|
@@ -665,11 +759,11 @@ private enum G2SettingProto {
|
|
|
665
759
|
static func setScreenDepth(magicRandom: Int32, level: Int32) -> Data {
|
|
666
760
|
// DeviceReceive_X_Coordinate
|
|
667
761
|
var xW = ProtobufWriter()
|
|
668
|
-
xW.writeInt32Field(1, level)
|
|
762
|
+
xW.writeInt32Field(1, level) // xCoordinateLevel
|
|
669
763
|
|
|
670
764
|
// DeviceReceiveInfoFromAPP
|
|
671
765
|
var infoW = ProtobufWriter()
|
|
672
|
-
infoW.writeMessageField(3, xW.data)
|
|
766
|
+
infoW.writeMessageField(3, xW.data) // deviceReceiveXCoordinate (field 3)
|
|
673
767
|
|
|
674
768
|
// G2SettingPackage
|
|
675
769
|
var w = ProtobufWriter()
|
|
@@ -687,13 +781,13 @@ private enum OnboardingProto {
|
|
|
687
781
|
static func skipOnboarding(magicRandom: Int32) -> Data {
|
|
688
782
|
// OnboardingConfig: processId = FINISH (4)
|
|
689
783
|
var configW = ProtobufWriter()
|
|
690
|
-
configW.writeInt32Field(1, 4)
|
|
784
|
+
configW.writeInt32Field(1, 4) // processId = FINISH
|
|
691
785
|
|
|
692
786
|
// OnboardingDataPackage
|
|
693
787
|
var w = ProtobufWriter()
|
|
694
|
-
w.writeInt32Field(1, 1)
|
|
788
|
+
w.writeInt32Field(1, 1) // commandId = CONFIG
|
|
695
789
|
w.writeInt32Field(2, magicRandom)
|
|
696
|
-
w.writeMessageField(3, configW.data)
|
|
790
|
+
w.writeMessageField(3, configW.data) // config (field 3)
|
|
697
791
|
return w.data
|
|
698
792
|
}
|
|
699
793
|
}
|
|
@@ -701,19 +795,129 @@ private enum OnboardingProto {
|
|
|
701
795
|
// MARK: - EvenAI Protobuf Builders (even_ai.proto, service ID 7)
|
|
702
796
|
|
|
703
797
|
private enum EvenAIProto {
|
|
704
|
-
/// EvenAIDataPackage with CONFIG command to toggle Hey Even wakeword
|
|
705
|
-
/// 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})
|
|
706
806
|
static func setHeyEven(magicRandom: Int32, enabled: Bool) -> Data {
|
|
707
807
|
// EvenAIConfig
|
|
708
808
|
var configW = ProtobufWriter()
|
|
709
|
-
|
|
710
|
-
|
|
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)
|
|
711
813
|
|
|
712
814
|
// EvenAIDataPackage
|
|
713
815
|
var w = ProtobufWriter()
|
|
714
|
-
w.writeInt32Field(1, 10)
|
|
816
|
+
w.writeInt32Field(1, 10) // commandId = CONFIG
|
|
817
|
+
w.writeInt32Field(2, magicRandom)
|
|
818
|
+
w.writeMessageField(13, configW.data) // config (field 13)
|
|
819
|
+
return w.data
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/// EvenAIDataPackage with ASK command — what the phone sends after cloud
|
|
823
|
+
/// ASR resolves the user's audio into text. Mirrors Flutter `sendAsr`:
|
|
824
|
+
/// `EvenAIAskInfo { text, streamEnable=0 }`. Used to inject an ASR result
|
|
825
|
+
/// into the glasses' AI session so the following SKILL packet has context.
|
|
826
|
+
static func aiAsk(magicRandom: Int32, text: String, streamEnable: Int32 = 0) -> Data {
|
|
827
|
+
var askW = ProtobufWriter()
|
|
828
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
829
|
+
askW.writeBytesField(4, Data(text.utf8)) // text
|
|
830
|
+
|
|
831
|
+
var w = ProtobufWriter()
|
|
832
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
833
|
+
w.writeInt32Field(2, magicRandom)
|
|
834
|
+
w.writeMessageField(5, askW.data) // askInfo (field 5)
|
|
835
|
+
return w.data
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/// EvenAIDataPackage with CTRL command — used to put glasses into / out of
|
|
839
|
+
/// an AI session. Mirrors Flutter `sendWakeupResp`, which sends
|
|
840
|
+
/// `EvenAIControl { status = EVEN_AI_ENTER }` after the glasses send WAKE_UP.
|
|
841
|
+
/// status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
842
|
+
static func aiCtrl(magicRandom: Int32, status: Int32) -> Data {
|
|
843
|
+
var ctrlW = ProtobufWriter()
|
|
844
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
845
|
+
|
|
846
|
+
var w = ProtobufWriter()
|
|
847
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
848
|
+
w.writeInt32Field(2, magicRandom)
|
|
849
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
850
|
+
return w.data
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/// EvenAIDataPackage with SKILL command — triggers a built-in glasses UI
|
|
854
|
+
/// the same way the "Hey Even, show X" voice command does.
|
|
855
|
+
/// skillId values (per even_ai.proto):
|
|
856
|
+
/// 0 SKILL_NONE, 1 BRIGHTNESS, 2 TRANSLATE_CTRL, 3 NOTIFICATION,
|
|
857
|
+
/// 4 TELEPROMPT, 5 NAVIGATE, 6 CONVERSATE, 7 QUICKLIST, 8 AUTO_BRIGHTNESS
|
|
858
|
+
static func triggerSkill(
|
|
859
|
+
magicRandom: Int32, skillId: Int32, skillParam: Int32 = 0,
|
|
860
|
+
text: String = "", streamEnable: Int32 = 1, fTextEnd: Int32 = 1
|
|
861
|
+
) -> Data {
|
|
862
|
+
// EvenAISkillInfo
|
|
863
|
+
var skillW = ProtobufWriter()
|
|
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"
|
|
869
|
+
|
|
870
|
+
// EvenAIDataPackage
|
|
871
|
+
var w = ProtobufWriter()
|
|
872
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
873
|
+
w.writeInt32Field(2, magicRandom)
|
|
874
|
+
w.writeMessageField(8, skillW.data) // skillInfo (field 8)
|
|
875
|
+
return w.data
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// MARK: - Notification Protobuf Builders (notification.proto, service ID 4)
|
|
880
|
+
|
|
881
|
+
private enum NotificationProto {
|
|
882
|
+
/// NotificationDataPackage with commandId=NOTIFICATION_IOS (2), carrying
|
|
883
|
+
/// `NotificationIOS { appID, displayName }`. We saw the glasses emit this
|
|
884
|
+
/// inbound after "Hey Even, show notifications" with appID="com.burbn.instagram";
|
|
885
|
+
/// trying the same shape outbound to see if the glasses display it.
|
|
886
|
+
/// (Returned errorCode=8 NOT_SUPPORT in testing — Service 4 doesn't accept this outbound.)
|
|
887
|
+
static func iosNotification(magicRandom: Int32, appID: String, displayName: String) -> Data {
|
|
888
|
+
var iosW = ProtobufWriter()
|
|
889
|
+
iosW.writeBytesField(1, Data(appID.utf8)) // appID
|
|
890
|
+
iosW.writeBytesField(2, Data(displayName.utf8)) // displayName
|
|
891
|
+
|
|
892
|
+
var w = ProtobufWriter()
|
|
893
|
+
w.writeInt32Field(1, 2) // commandId = NOTIFICATION_IOS
|
|
894
|
+
w.writeInt32Field(2, magicRandom)
|
|
895
|
+
w.writeMessageField(4, iosW.data) // IOS (field 4)
|
|
896
|
+
return w.data
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/// NotificationDataPackage with commandId=NOTIFICATION_CTRL (1), carrying
|
|
900
|
+
/// `NotificationControl { notifEnable, autoDispEnable, dispTime, avoidDisturbEnable }`.
|
|
901
|
+
/// Per Flutter `ProtoNotificationExt.settingNotification` this is how the
|
|
902
|
+
/// official app configures notification behavior on the glasses. Worth
|
|
903
|
+
/// testing whether toggling notifEnable also opens the notification panel.
|
|
904
|
+
static func notificationCtrl(
|
|
905
|
+
magicRandom: Int32,
|
|
906
|
+
notifEnable: Int32 = 1,
|
|
907
|
+
autoDispEnable: Int32 = 1,
|
|
908
|
+
dispTime: Int32 = 5,
|
|
909
|
+
avoidDisturbEnable: Int32 = 0
|
|
910
|
+
) -> Data {
|
|
911
|
+
var ctrlW = ProtobufWriter()
|
|
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
|
|
916
|
+
|
|
917
|
+
var w = ProtobufWriter()
|
|
918
|
+
w.writeInt32Field(1, 1) // commandId = NOTIFICATION_CTRL
|
|
715
919
|
w.writeInt32Field(2, magicRandom)
|
|
716
|
-
w.writeMessageField(
|
|
920
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
717
921
|
return w.data
|
|
718
922
|
}
|
|
719
923
|
}
|
|
@@ -731,7 +935,7 @@ private enum MenuProto {
|
|
|
731
935
|
/// G2 firmware requires minimum 5, maximum 10 menu items
|
|
732
936
|
static let MIN_MENU_SIZE = 5
|
|
733
937
|
static let MAX_MENU_SIZE = 10
|
|
734
|
-
static let MAX_NAME_LENGTH = 15
|
|
938
|
+
static let MAX_NAME_LENGTH = 15 // 17 char limit minus 2 for running indicator prefix
|
|
735
939
|
/// Placeholder appIds for padding slots (in valid Even range, unique per slot)
|
|
736
940
|
static let PLACEHOLDER_APP_IDS: [Int32] = [10535, 10536, 10537, 10538, 10539]
|
|
737
941
|
|
|
@@ -758,7 +962,7 @@ private enum MenuProto {
|
|
|
758
962
|
|
|
759
963
|
// Wire items carry either a built-in (itemType=0, no name) or third-party (itemType=1, with name)
|
|
760
964
|
struct WireItem {
|
|
761
|
-
let displayName: String?
|
|
965
|
+
let displayName: String? // nil for built-ins
|
|
762
966
|
let appId: Int32
|
|
763
967
|
let isBuiltIn: Bool
|
|
764
968
|
}
|
|
@@ -775,8 +979,8 @@ private enum MenuProto {
|
|
|
775
979
|
|
|
776
980
|
let truncated =
|
|
777
981
|
item.name.count > MAX_NAME_LENGTH
|
|
778
|
-
|
|
779
|
-
|
|
982
|
+
? String(item.name.prefix(MAX_NAME_LENGTH))
|
|
983
|
+
: item.name
|
|
780
984
|
let prefix = item.running ? "● " : ""
|
|
781
985
|
wireItems.append(
|
|
782
986
|
WireItem(displayName: prefix + truncated, appId: appId, isBuiltIn: false)
|
|
@@ -785,7 +989,7 @@ private enum MenuProto {
|
|
|
785
989
|
|
|
786
990
|
// Pad to MIN_MENU_SIZE with placeholder third-party items
|
|
787
991
|
while wireItems.count < MIN_MENU_SIZE {
|
|
788
|
-
let idx = wireItems.count - 1
|
|
992
|
+
let idx = wireItems.count - 1 // -1 because built-in occupies slot 0
|
|
789
993
|
wireItems.append(
|
|
790
994
|
WireItem(
|
|
791
995
|
displayName: " ---",
|
|
@@ -797,31 +1001,167 @@ private enum MenuProto {
|
|
|
797
1001
|
|
|
798
1002
|
// MenuInfoSend
|
|
799
1003
|
var menuW = ProtobufWriter()
|
|
800
|
-
menuW.writeInt32Field(1, Int32(wireItems.count))
|
|
1004
|
+
menuW.writeInt32Field(1, Int32(wireItems.count)) // itemTotalNum
|
|
801
1005
|
|
|
802
1006
|
for item in wireItems {
|
|
803
1007
|
var itemW = ProtobufWriter()
|
|
804
1008
|
if item.isBuiltIn {
|
|
805
|
-
itemW.writeInt32Field(1, 0)
|
|
806
|
-
itemW.writeInt32Field(4, item.appId)
|
|
1009
|
+
itemW.writeInt32Field(1, 0) // itemType = 0 (built-in)
|
|
1010
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId = SID
|
|
807
1011
|
} else {
|
|
808
|
-
itemW.writeInt32Field(1, 1)
|
|
809
|
-
itemW.writeInt32Field(2, 1)
|
|
810
|
-
itemW.writeStringField(3, item.displayName ?? "")
|
|
811
|
-
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
|
|
812
1016
|
}
|
|
813
|
-
menuW.writeMessageField(2, itemW.data)
|
|
1017
|
+
menuW.writeMessageField(2, itemW.data) // repeated item (field 2)
|
|
814
1018
|
}
|
|
815
1019
|
|
|
816
1020
|
// meun_main_msg_ctx
|
|
817
1021
|
var w = ProtobufWriter()
|
|
818
|
-
w.writeInt32Field(1, 0)
|
|
819
|
-
w.writeInt32Field(2, magicRandom)
|
|
820
|
-
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)
|
|
821
1025
|
return (w.data, appIdMap)
|
|
822
1026
|
}
|
|
823
1027
|
}
|
|
824
1028
|
|
|
1029
|
+
// MARK: - Dashboard Protobuf Builders (dashboard.proto, service ID 1)
|
|
1030
|
+
|
|
1031
|
+
/// Builders for the dashboard widget service (service 0x01).
|
|
1032
|
+
/// Field numbers come from the extracted dashboard.proto v2.1.0_beta_v3.
|
|
1033
|
+
private enum DashboardProto {
|
|
1034
|
+
/// eDashboardCommandId values from dashboard.proto
|
|
1035
|
+
enum CommandId: Int32 {
|
|
1036
|
+
case dashboardRespond = 1
|
|
1037
|
+
case dashboardReceive = 2 // phone → glasses widget/config push
|
|
1038
|
+
case appRespond = 3
|
|
1039
|
+
case appReceive = 4
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/// Build a Schedule submessage (the calendar event payload).
|
|
1043
|
+
/// f1 = scheduleId (int32, required)
|
|
1044
|
+
/// f2 = title (string, optional)
|
|
1045
|
+
/// f3 = location (string, optional)
|
|
1046
|
+
/// f4 = time (string, optional — display text e.g. "10:00 AM")
|
|
1047
|
+
/// f5 = endTimestamp (int32, presumed Unix seconds — pre-shift by TZ
|
|
1048
|
+
/// to match the time-sync hack so glasses display local time)
|
|
1049
|
+
static func schedule(
|
|
1050
|
+
scheduleId: Int32,
|
|
1051
|
+
title: String?,
|
|
1052
|
+
location: String?,
|
|
1053
|
+
time: String?,
|
|
1054
|
+
endTimestamp: Int32
|
|
1055
|
+
) -> Data {
|
|
1056
|
+
var w = ProtobufWriter()
|
|
1057
|
+
w.writeInt32Field(1, scheduleId)
|
|
1058
|
+
if let title = title { w.writeStringField(2, title) }
|
|
1059
|
+
if let location = location { w.writeStringField(3, location) }
|
|
1060
|
+
if let time = time { w.writeStringField(4, time) }
|
|
1061
|
+
w.writeInt32Field(5, endTimestamp)
|
|
1062
|
+
return w.data
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/// Build an rScheduleWidget wrapping a single Schedule.
|
|
1066
|
+
/// f1 = scheduleTotal, f2 = scheduleNum (0-based), f3 = Schedule, f4 = scheduleAuthority
|
|
1067
|
+
static func rScheduleWidget(
|
|
1068
|
+
scheduleTotal: Int32,
|
|
1069
|
+
scheduleNum: Int32,
|
|
1070
|
+
schedule: Data,
|
|
1071
|
+
scheduleAuthority: Int32
|
|
1072
|
+
) -> Data {
|
|
1073
|
+
var w = ProtobufWriter()
|
|
1074
|
+
w.writeInt32Field(1, scheduleTotal)
|
|
1075
|
+
w.writeInt32Field(2, scheduleNum)
|
|
1076
|
+
w.writeMessageField(3, schedule)
|
|
1077
|
+
w.writeInt32Field(4, scheduleAuthority)
|
|
1078
|
+
return w.data
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/// Build the full calendar-push DashboardDataPackage:
|
|
1082
|
+
/// DashboardDataPackage {
|
|
1083
|
+
/// commandId = Dashboard_Receive (2)
|
|
1084
|
+
/// magicRandom
|
|
1085
|
+
/// dashboardReceive = DashboardReceiveFromApp {
|
|
1086
|
+
/// packageId = 1
|
|
1087
|
+
/// bashboardConfig = DashboardContent {
|
|
1088
|
+
/// widgetComponents = rWidgetComponent {
|
|
1089
|
+
/// schedule = rScheduleWidget { ... }
|
|
1090
|
+
/// }
|
|
1091
|
+
/// }
|
|
1092
|
+
/// }
|
|
1093
|
+
/// }
|
|
1094
|
+
static func calendarPush(
|
|
1095
|
+
magicRandom: Int32,
|
|
1096
|
+
packageId: Int32,
|
|
1097
|
+
scheduleId: Int32,
|
|
1098
|
+
title: String?,
|
|
1099
|
+
location: String?,
|
|
1100
|
+
time: String?,
|
|
1101
|
+
endTimestamp: Int32,
|
|
1102
|
+
scheduleAuthority: Int32,
|
|
1103
|
+
scheduleTotal: Int32 = 1,
|
|
1104
|
+
scheduleNum: Int32 = 0
|
|
1105
|
+
) -> Data {
|
|
1106
|
+
let sched = schedule(
|
|
1107
|
+
scheduleId: scheduleId, title: title, location: location,
|
|
1108
|
+
time: time, endTimestamp: endTimestamp
|
|
1109
|
+
)
|
|
1110
|
+
let rSched = rScheduleWidget(
|
|
1111
|
+
scheduleTotal: scheduleTotal, scheduleNum: scheduleNum,
|
|
1112
|
+
schedule: sched, scheduleAuthority: scheduleAuthority
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
// rWidgetComponent { f3 = rScheduleWidget }
|
|
1116
|
+
var rWidget = ProtobufWriter()
|
|
1117
|
+
rWidget.writeMessageField(3, rSched)
|
|
1118
|
+
|
|
1119
|
+
// DashboardContent { f2 = rWidgetComponent }
|
|
1120
|
+
var content = ProtobufWriter()
|
|
1121
|
+
content.writeMessageField(2, rWidget.data)
|
|
1122
|
+
|
|
1123
|
+
// DashboardReceiveFromApp { f1 = packageId, f3 = DashboardContent }
|
|
1124
|
+
var receive = ProtobufWriter()
|
|
1125
|
+
receive.writeInt32Field(1, packageId)
|
|
1126
|
+
receive.writeMessageField(3, content.data)
|
|
1127
|
+
|
|
1128
|
+
// DashboardDataPackage { f1 = commandId, f2 = magicRandom, f4 = dashboardReceive }
|
|
1129
|
+
var pkg = ProtobufWriter()
|
|
1130
|
+
pkg.writeInt32Field(1, CommandId.dashboardReceive.rawValue)
|
|
1131
|
+
pkg.writeInt32Field(2, magicRandom)
|
|
1132
|
+
pkg.writeMessageField(4, receive.data)
|
|
1133
|
+
return pkg.data
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
static func calendarClear(
|
|
1137
|
+
magicRandom: Int32,
|
|
1138
|
+
packageId: Int32,
|
|
1139
|
+
scheduleAuthority: Int32
|
|
1140
|
+
) -> Data {
|
|
1141
|
+
// rScheduleWidget with scheduleTotal=0 clears the widget without sending a stale Schedule.
|
|
1142
|
+
var rSched = ProtobufWriter()
|
|
1143
|
+
rSched.writeInt32Field(1, 0)
|
|
1144
|
+
rSched.writeInt32Field(2, 0)
|
|
1145
|
+
rSched.writeInt32Field(4, scheduleAuthority)
|
|
1146
|
+
|
|
1147
|
+
var rWidget = ProtobufWriter()
|
|
1148
|
+
rWidget.writeMessageField(3, rSched.data)
|
|
1149
|
+
|
|
1150
|
+
var content = ProtobufWriter()
|
|
1151
|
+
content.writeMessageField(2, rWidget.data)
|
|
1152
|
+
|
|
1153
|
+
var receive = ProtobufWriter()
|
|
1154
|
+
receive.writeInt32Field(1, packageId)
|
|
1155
|
+
receive.writeMessageField(3, content.data)
|
|
1156
|
+
|
|
1157
|
+
var pkg = ProtobufWriter()
|
|
1158
|
+
pkg.writeInt32Field(1, CommandId.dashboardReceive.rawValue)
|
|
1159
|
+
pkg.writeInt32Field(2, magicRandom)
|
|
1160
|
+
pkg.writeMessageField(4, receive.data)
|
|
1161
|
+
return pkg.data
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
825
1165
|
// MARK: - EvenBLE Transport Layer
|
|
826
1166
|
|
|
827
1167
|
/// Builds and splits payloads into BLE packets with the EvenHub transport framing
|
|
@@ -839,7 +1179,7 @@ private struct EvenBLETransport {
|
|
|
839
1179
|
var offset = 0
|
|
840
1180
|
while offset < payload.count {
|
|
841
1181
|
let end = min(offset + maxPayload, payload.count)
|
|
842
|
-
chunks.append(payload[offset
|
|
1182
|
+
chunks.append(payload[offset..<end])
|
|
843
1183
|
offset = end
|
|
844
1184
|
}
|
|
845
1185
|
if chunks.isEmpty {
|
|
@@ -867,20 +1207,20 @@ private struct EvenBLETransport {
|
|
|
867
1207
|
let payloadLen = UInt8(chunk.count + (isLast ? 2 : 0))
|
|
868
1208
|
|
|
869
1209
|
var packet = Data()
|
|
870
|
-
packet.append(G2BLE.HEADER_BYTE)
|
|
871
|
-
packet.append((G2BLE.DEST_GLASSES << 4) | G2BLE.SOURCE_PHONE)
|
|
872
|
-
packet.append(syncId)
|
|
873
|
-
packet.append(payloadLen)
|
|
874
|
-
packet.append(totalPackets)
|
|
875
|
-
packet.append(serialNum)
|
|
876
|
-
packet.append(serviceId)
|
|
877
|
-
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
|
|
878
1218
|
|
|
879
1219
|
packet.append(chunk)
|
|
880
1220
|
|
|
881
1221
|
if isLast {
|
|
882
|
-
packet.append(UInt8(crc & 0xFF))
|
|
883
|
-
packet.append(UInt8((crc >> 8) & 0xFF))
|
|
1222
|
+
packet.append(UInt8(crc & 0xFF)) // CRC low
|
|
1223
|
+
packet.append(UInt8((crc >> 8) & 0xFF)) // CRC high
|
|
884
1224
|
}
|
|
885
1225
|
|
|
886
1226
|
packets.append(packet)
|
|
@@ -920,9 +1260,10 @@ private class G2SendManager {
|
|
|
920
1260
|
// MARK: - G2 Receive Manager (multi-part reassembly)
|
|
921
1261
|
|
|
922
1262
|
private class G2ReceiveManager {
|
|
923
|
-
private var partials: [String: (Data, UInt8)] = [:]
|
|
1263
|
+
private var partials: [String: (Data, UInt8)] = [:] // key -> (accumulated payload, lastSerialNum)
|
|
924
1264
|
|
|
925
|
-
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)?
|
|
1265
|
+
func handlePacket(_ rawData: Data, sourceKey: String = "") -> (serviceId: UInt8, payload: Data)?
|
|
1266
|
+
{
|
|
926
1267
|
guard rawData.count >= 8 else { return nil }
|
|
927
1268
|
guard rawData[0] == G2BLE.HEADER_BYTE else { return nil }
|
|
928
1269
|
|
|
@@ -941,7 +1282,7 @@ private class G2ReceiveManager {
|
|
|
941
1282
|
let isLast = (serialNum == totalPackets)
|
|
942
1283
|
let hasCrc = isLast
|
|
943
1284
|
let payloadEnd = 8 + payloadLen - (hasCrc ? 2 : 0)
|
|
944
|
-
let payload = rawData[8
|
|
1285
|
+
let payload = rawData[8..<payloadEnd]
|
|
945
1286
|
|
|
946
1287
|
let syncId = rawData[2]
|
|
947
1288
|
// Key partials by source peripheral too — left and right glasses have independent syncId counters
|
|
@@ -988,7 +1329,7 @@ actor G2ReconnectionManager {
|
|
|
988
1329
|
private var task: Task<Void, Never>?
|
|
989
1330
|
private let intervalSeconds: TimeInterval
|
|
990
1331
|
private var attempts = 0
|
|
991
|
-
private let maxAttempts: Int
|
|
1332
|
+
private let maxAttempts: Int // -1 for unlimited
|
|
992
1333
|
|
|
993
1334
|
init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
|
|
994
1335
|
self.intervalSeconds = intervalSeconds
|
|
@@ -1074,12 +1415,18 @@ class G2: NSObject, SGCManager {
|
|
|
1074
1415
|
/// Maps serial number -> peripheral UUID string. Persisted across forget() so previously
|
|
1075
1416
|
/// paired devices can reconnect quickly without a fresh scan.
|
|
1076
1417
|
private var leftGlassUUIDMap: [String: String] {
|
|
1077
|
-
get {
|
|
1418
|
+
get {
|
|
1419
|
+
UserDefaults.standard.dictionary(forKey: "g2_leftGlassUUIDMap") as? [String: String]
|
|
1420
|
+
?? [:]
|
|
1421
|
+
}
|
|
1078
1422
|
set { UserDefaults.standard.set(newValue, forKey: "g2_leftGlassUUIDMap") }
|
|
1079
1423
|
}
|
|
1080
1424
|
|
|
1081
1425
|
private var rightGlassUUIDMap: [String: String] {
|
|
1082
|
-
get {
|
|
1426
|
+
get {
|
|
1427
|
+
UserDefaults.standard.dictionary(forKey: "g2_rightGlassUUIDMap") as? [String: String]
|
|
1428
|
+
?? [:]
|
|
1429
|
+
}
|
|
1083
1430
|
set { UserDefaults.standard.set(newValue, forKey: "g2_rightGlassUUIDMap") }
|
|
1084
1431
|
}
|
|
1085
1432
|
|
|
@@ -1110,9 +1457,8 @@ class G2: NSObject, SGCManager {
|
|
|
1110
1457
|
private let sendManager = G2SendManager()
|
|
1111
1458
|
private let receiveManager = G2ReceiveManager()
|
|
1112
1459
|
private var foregroundObserver: NSObjectProtocol?
|
|
1113
|
-
private var startupPageCreated: Bool = false
|
|
1460
|
+
private var startupPageCreated: Bool = false // createStartUpPageContainer can only be called once
|
|
1114
1461
|
private var pageCreated: Bool = false
|
|
1115
|
-
private var pageHasTextContainer: Bool = false // tracks if current page has a text container
|
|
1116
1462
|
private var currentTextContent: String = ""
|
|
1117
1463
|
private var currentBitmapBase64: String = ""
|
|
1118
1464
|
private var textContainerID: Int32 = 1
|
|
@@ -1135,7 +1481,67 @@ class G2: NSObject, SGCManager {
|
|
|
1135
1481
|
/// Set to the first menu item's appId so glasses know our page belongs to the menu
|
|
1136
1482
|
private var activeMenuAppId: Int32?
|
|
1137
1483
|
private var lastClickTimestamp: Int64?
|
|
1484
|
+
private var lastEvenHubResponseTimestamp: Int64?
|
|
1138
1485
|
private var lastMenuSelectTimestamp: Int64?
|
|
1486
|
+
private var lastGestureCtrlTimestamp: Int64?
|
|
1487
|
+
|
|
1488
|
+
/// A tracked image container on the current page. Keyed by its rect for reuse.
|
|
1489
|
+
private struct ImgContainer: Equatable {
|
|
1490
|
+
let id: Int32
|
|
1491
|
+
let x: Int32
|
|
1492
|
+
let y: Int32
|
|
1493
|
+
let width: Int32
|
|
1494
|
+
let height: Int32
|
|
1495
|
+
var name: String {
|
|
1496
|
+
"img-\(id)"
|
|
1497
|
+
}
|
|
1498
|
+
var bmpData: Data
|
|
1499
|
+
|
|
1500
|
+
func matches(x: Int32, y: Int32, width: Int32, height: Int32) -> Bool {
|
|
1501
|
+
self.x == x && self.y == y && self.width == width && self.height == height
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
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
|
+
|
|
1530
|
+
/// Live list of image containers on the page, ordered oldest→newest (for LRU eviction).
|
|
1531
|
+
/// The page may hold at most 4 image containers (IDs from the pool below).
|
|
1532
|
+
private var imageContainers: [ImgContainer] = []
|
|
1533
|
+
private var textContainers: [TextContainer] = []
|
|
1534
|
+
/// Fixed pool of container IDs the page protocol expects.
|
|
1535
|
+
private let imageContainerIDPool: [Int32] = [10, 11, 12, 13]
|
|
1536
|
+
private let textContainerIDPool: [Int32] = [1, 2, 3, 4, 5, 6]
|
|
1537
|
+
/// Default container seeded into every fresh page: 100x100 in the top-left.
|
|
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
|
+
)
|
|
1139
1545
|
|
|
1140
1546
|
@Published var aiListening: Bool = false
|
|
1141
1547
|
|
|
@@ -1159,6 +1565,7 @@ class G2: NSObject, SGCManager {
|
|
|
1159
1565
|
// MARK: - BLE Sending
|
|
1160
1566
|
|
|
1161
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])")
|
|
1162
1569
|
for packet in packets {
|
|
1163
1570
|
if right, let char = rightWriteChar, let peripheral = rightPeripheral {
|
|
1164
1571
|
peripheral.writeValue(packet, for: char, type: .withoutResponse)
|
|
@@ -1186,6 +1593,15 @@ class G2: NSObject, SGCManager {
|
|
|
1186
1593
|
sendToGlasses(packets, left: left, right: right)
|
|
1187
1594
|
}
|
|
1188
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
|
+
|
|
1189
1605
|
private func sendG2SettingCommand(_ payload: Data) {
|
|
1190
1606
|
let packets = sendManager.buildPackets(
|
|
1191
1607
|
serviceId: ServiceID.g2Setting.rawValue,
|
|
@@ -1213,6 +1629,15 @@ class G2: NSObject, SGCManager {
|
|
|
1213
1629
|
sendToGlasses(packets)
|
|
1214
1630
|
}
|
|
1215
1631
|
|
|
1632
|
+
private func sendNotificationCommand(_ payload: Data) {
|
|
1633
|
+
let packets = sendManager.buildPackets(
|
|
1634
|
+
serviceId: ServiceID.notification.rawValue,
|
|
1635
|
+
payload: payload,
|
|
1636
|
+
reserveFlag: true
|
|
1637
|
+
)
|
|
1638
|
+
sendToGlasses(packets)
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1216
1641
|
private func sendMenuCommand(_ payload: Data) {
|
|
1217
1642
|
let packets = sendManager.buildPackets(
|
|
1218
1643
|
serviceId: ServiceID.menu.rawValue,
|
|
@@ -1246,7 +1671,7 @@ class G2: NSObject, SGCManager {
|
|
|
1246
1671
|
payload: payload,
|
|
1247
1672
|
reserveFlag: true
|
|
1248
1673
|
)
|
|
1249
|
-
sendToGlasses(packets)
|
|
1674
|
+
sendToGlasses(packets, left: true, right: true)
|
|
1250
1675
|
}
|
|
1251
1676
|
|
|
1252
1677
|
// MARK: - Authentication Sequence
|
|
@@ -1264,7 +1689,7 @@ class G2: NSObject, SGCManager {
|
|
|
1264
1689
|
sendDevSettingsCommand(authR, left: false, right: true)
|
|
1265
1690
|
}
|
|
1266
1691
|
|
|
1267
|
-
private func runAuthSequence() {
|
|
1692
|
+
private func runAuthSequence() async {
|
|
1268
1693
|
Bridge.log("G2: Running auth sequence")
|
|
1269
1694
|
|
|
1270
1695
|
// Auth to left side
|
|
@@ -1274,245 +1699,150 @@ class G2: NSObject, SGCManager {
|
|
|
1274
1699
|
}
|
|
1275
1700
|
|
|
1276
1701
|
// Small delay then auth right + pipe role change + time sync
|
|
1277
|
-
|
|
1278
|
-
guard let self = self else { return }
|
|
1702
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1279
1703
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1704
|
+
let authR = DevSettingsProto.authCmd(magicRandom: self.sendManager.nextMagicRandom())
|
|
1705
|
+
self.sendDevSettingsCommand(authR, left: false, right: true)
|
|
1282
1706
|
|
|
1283
|
-
|
|
1284
|
-
guard let self = self else { return }
|
|
1285
|
-
|
|
1286
|
-
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1287
|
-
magicRandom: self.sendManager.nextMagicRandom()
|
|
1288
|
-
)
|
|
1289
|
-
self.sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1707
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1290
1708
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
magicRandom: self.sendManager.nextMagicRandom()
|
|
1296
|
-
)
|
|
1297
|
-
self.sendDevSettingsCommand(timeSync)
|
|
1298
|
-
|
|
1299
|
-
// Skip onboarding on connect
|
|
1300
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
1301
|
-
guard let self = self else { return }
|
|
1302
|
-
let onboarding = OnboardingProto.skipOnboarding(
|
|
1303
|
-
magicRandom: self.sendManager.nextMagicRandom()
|
|
1304
|
-
)
|
|
1305
|
-
self.sendOnboardingCommand(onboarding)
|
|
1306
|
-
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1307
|
-
|
|
1308
|
-
// Disable "Hey Even" wakeword on connect
|
|
1309
|
-
let heyEvenOff = EvenAIProto.setHeyEven(
|
|
1310
|
-
magicRandom: self.sendManager.nextMagicRandom(),
|
|
1311
|
-
enabled: false
|
|
1312
|
-
)
|
|
1313
|
-
self.sendEvenAICommand(heyEvenOff)
|
|
1314
|
-
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1315
|
-
|
|
1316
|
-
// Replicate Even app's full init sequence for menu selection support:
|
|
1317
|
-
|
|
1318
|
-
// 0. Universe settings (g2_setting cmd=1 field3 with field9=universe settings)
|
|
1319
|
-
// Even app's bytes: 4a 0a 08 00 10 00 18 01 20 00 28 01
|
|
1320
|
-
// = field 9 (universe), {1:0, 2:0, 3:1, 4:0, 5:1}
|
|
1321
|
-
var univW = ProtobufWriter()
|
|
1322
|
-
univW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1323
|
-
univW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1324
|
-
univW.writeMessageField(
|
|
1325
|
-
3,
|
|
1326
|
-
Data([
|
|
1327
|
-
0x4A, 0x0A, // field 9, length 10
|
|
1328
|
-
0x08, 0x00, // unitFormat=0
|
|
1329
|
-
0x10, 0x00, // distanceUnit=0
|
|
1330
|
-
0x18, 0x01, // timeFormat=1
|
|
1331
|
-
0x20, 0x00, // dateFormat=0
|
|
1332
|
-
0x28, 0x01, // temperatureUnit=1
|
|
1333
|
-
])
|
|
1334
|
-
)
|
|
1335
|
-
self.sendG2SettingCommand(univW.data)
|
|
1336
|
-
|
|
1337
|
-
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1338
|
-
var gestureInitW = ProtobufWriter()
|
|
1339
|
-
gestureInitW.writeInt32Field(1, 0)
|
|
1340
|
-
gestureInitW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1341
|
-
self.sendGestureCtrlCommand(gestureInitW.data)
|
|
1342
|
-
|
|
1343
|
-
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1344
|
-
var uiSettW = ProtobufWriter()
|
|
1345
|
-
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1346
|
-
uiSettW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1347
|
-
uiSettW.writeMessageField(4, Data([0x08, 0x01, 0x10, 0x00])) // {1:1, 2:0}
|
|
1348
|
-
self.sendToGlasses(
|
|
1349
|
-
self.sendManager.buildPackets(
|
|
1350
|
-
serviceId: 0x0C, payload: uiSettW.data, reserveFlag: true
|
|
1351
|
-
)
|
|
1352
|
-
)
|
|
1353
|
-
|
|
1354
|
-
// 3. teleprompter (0x10) — config (cmd=1, field3={1:4})
|
|
1355
|
-
var teleW = ProtobufWriter()
|
|
1356
|
-
teleW.writeInt32Field(1, 1)
|
|
1357
|
-
teleW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1358
|
-
teleW.writeMessageField(3, Data([0x08, 0x04])) // {1:4}
|
|
1359
|
-
self.sendToGlasses(
|
|
1360
|
-
self.sendManager.buildPackets(
|
|
1361
|
-
serviceId: 0x10, payload: teleW.data, reserveFlag: true
|
|
1362
|
-
)
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
// 4. EvenHub CTRL on service 0x81 (cmd=1, empty field3)
|
|
1366
|
-
var ehCtrlW = ProtobufWriter()
|
|
1367
|
-
ehCtrlW.writeInt32Field(1, 1)
|
|
1368
|
-
ehCtrlW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1369
|
-
ehCtrlW.writeMessageField(3, Data())
|
|
1370
|
-
self.sendEvenHubCtrlCommand(ehCtrlW.data)
|
|
1371
|
-
|
|
1372
|
-
// 5. calendar (0x04) — config
|
|
1373
|
-
var calW = ProtobufWriter()
|
|
1374
|
-
calW.writeInt32Field(1, 1)
|
|
1375
|
-
calW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1376
|
-
calW.writeMessageField(
|
|
1377
|
-
3, Data([0x08, 0x01, 0x10, 0x01, 0x18, 0x05, 0x28, 0x01])
|
|
1378
|
-
)
|
|
1379
|
-
self.sendToGlasses(
|
|
1380
|
-
self.sendManager.buildPackets(
|
|
1381
|
-
serviceId: 0x04, payload: calW.data, reserveFlag: true
|
|
1382
|
-
)
|
|
1383
|
-
)
|
|
1384
|
-
|
|
1385
|
-
// 6. Dashboard init (0x01) — display settings
|
|
1386
|
-
var dashDisplayW = ProtobufWriter()
|
|
1387
|
-
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1388
|
-
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1389
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1390
|
-
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1391
|
-
dashDisplayW.writeMessageField(5, Data([1, 3, 2, 2])) // widgetDisplayOrder
|
|
1392
|
-
dashDisplayW.writeInt32Field(6, 1) // halfDayFormat
|
|
1393
|
-
dashDisplayW.writeInt32Field(7, 1) // temperatureUnit
|
|
1394
|
-
|
|
1395
|
-
var dashRecvW = ProtobufWriter()
|
|
1396
|
-
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1397
|
-
|
|
1398
|
-
var dashPkgW = ProtobufWriter()
|
|
1399
|
-
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1400
|
-
dashPkgW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1401
|
-
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1402
|
-
self.sendDashboardCommand(dashPkgW.data)
|
|
1403
|
-
|
|
1404
|
-
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5, field7={1:1})
|
|
1405
|
-
var dashNewsReqW = ProtobufWriter()
|
|
1406
|
-
dashNewsReqW.writeInt32Field(1, 5) // REQUEST_NEWS_INFO
|
|
1407
|
-
dashNewsReqW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1408
|
-
dashNewsReqW.writeMessageField(7, Data([0x08, 0x01])) // {1:1}
|
|
1409
|
-
self.sendDashboardCommand(dashNewsReqW.data)
|
|
1410
|
-
|
|
1411
|
-
// 8. Gesture control list via g2_setting
|
|
1412
|
-
var gestListW = ProtobufWriter()
|
|
1413
|
-
gestListW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1414
|
-
gestListW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1415
|
-
// field 3 with field 10 (gestureControlList): 3 items, all app_unable
|
|
1416
|
-
let gestureCtrlPayload = Data([
|
|
1417
|
-
0x52, 0x18, // field 10, length 24
|
|
1418
|
-
0x0A, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, // item 1
|
|
1419
|
-
0x0A, 0x06, 0x08, 0x00, 0x10, 0x01, 0x18, 0x00, // item 2
|
|
1420
|
-
0x0A, 0x06, 0x08, 0x00, 0x10, 0x02, 0x18, 0x00, // item 3
|
|
1421
|
-
])
|
|
1422
|
-
gestListW.writeMessageField(3, gestureCtrlPayload)
|
|
1423
|
-
self.sendG2SettingCommand(gestListW.data)
|
|
1424
|
-
|
|
1425
|
-
// 9. Dashboard APP_REQUEST_NEWS_INFO (cmd=7, field9={1:1})
|
|
1426
|
-
var dashAppNewsW = ProtobufWriter()
|
|
1427
|
-
dashAppNewsW.writeInt32Field(1, 7) // APP_REQUEST_NEWS_INFO
|
|
1428
|
-
dashAppNewsW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1429
|
-
dashAppNewsW.writeMessageField(9, Data([0x08, 0x01])) // {1:1}
|
|
1430
|
-
self.sendDashboardCommand(dashAppNewsW.data)
|
|
1431
|
-
|
|
1432
|
-
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Start heartbeats after auth
|
|
1436
|
-
self.startHeartbeats()
|
|
1709
|
+
let roleChange = DevSettingsProto.pipeRoleChange(
|
|
1710
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
1711
|
+
)
|
|
1712
|
+
self.sendDevSettingsCommand(roleChange, left: false, right: true)
|
|
1437
1713
|
|
|
1438
|
-
|
|
1439
|
-
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
1714
|
+
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
1440
1715
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
{
|
|
1446
|
-
DeviceStore.shared.apply("bluetooth", "device_name", serialNumber)
|
|
1447
|
-
Bridge.log("G2: Set device_name to \(serialNumber)")
|
|
1448
|
-
}
|
|
1716
|
+
let timeSync = DevSettingsProto.timeSync(
|
|
1717
|
+
magicRandom: self.sendManager.nextMagicRandom()
|
|
1718
|
+
)
|
|
1719
|
+
self.sendDevSettingsCommand(timeSync)
|
|
1449
1720
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
+
)
|
|
1456
1745
|
|
|
1457
|
-
|
|
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
|
|
1758
|
+
|
|
1759
|
+
var dashRecvW = ProtobufWriter()
|
|
1760
|
+
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1761
|
+
|
|
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)
|
|
1767
|
+
|
|
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
|
+
}
|
|
1458
1820
|
|
|
1459
|
-
|
|
1460
|
-
|
|
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)
|
|
1461
1827
|
|
|
1462
|
-
|
|
1463
|
-
self.requestDeviceInfo()
|
|
1828
|
+
self.setFullyConnected()
|
|
1464
1829
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1830
|
+
// connnect a controller if we have one:
|
|
1831
|
+
self.connectController()
|
|
1471
1832
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1833
|
+
// Query version + battery info from glasses
|
|
1834
|
+
self.requestDeviceInfo()
|
|
1474
1835
|
|
|
1475
|
-
// send
|
|
1476
|
-
|
|
1477
|
-
sendEvenHubCommand(msg)
|
|
1478
|
-
pageCreated = false
|
|
1479
|
-
currentTextContent = ""
|
|
1836
|
+
// send dashboard menu if we have stored items
|
|
1837
|
+
self.sendMenuApps()
|
|
1480
1838
|
|
|
1481
|
-
//
|
|
1482
|
-
|
|
1483
|
-
// let authL = DevSettingsProto.authCmd(magicRandom: sendManager.nextMagicRandom())
|
|
1484
|
-
// sendDevSettingsCommand(authL, left: true, right: false)
|
|
1485
|
-
// }
|
|
1839
|
+
// order the calendar (Schedule) widget first on the dashboard
|
|
1840
|
+
self.setCalendarWidgetFirst()
|
|
1486
1841
|
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
var gestureInitW = ProtobufWriter()
|
|
1492
|
-
gestureInitW.writeInt32Field(1, 0)
|
|
1493
|
-
gestureInitW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1494
|
-
self.sendGestureCtrlCommand(gestureInitW.data)
|
|
1495
|
-
|
|
1496
|
-
// 6. Dashboard init (0x01) — display settings
|
|
1497
|
-
var dashDisplayW = ProtobufWriter()
|
|
1498
|
-
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1499
|
-
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1500
|
-
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1501
|
-
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1502
|
-
dashDisplayW.writeMessageField(5, Data([1, 3, 2, 2])) // widgetDisplayOrder
|
|
1503
|
-
dashDisplayW.writeInt32Field(6, 1) // halfDayFormat
|
|
1504
|
-
dashDisplayW.writeInt32Field(7, 1) // temperatureUnit
|
|
1505
|
-
|
|
1506
|
-
var dashRecvW = ProtobufWriter()
|
|
1507
|
-
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
1508
|
-
|
|
1509
|
-
var dashPkgW = ProtobufWriter()
|
|
1510
|
-
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1511
|
-
dashPkgW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1512
|
-
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
1513
|
-
self.sendDashboardCommand(dashPkgW.data)
|
|
1514
|
-
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1515
|
-
}
|
|
1842
|
+
// send calendar events
|
|
1843
|
+
let calendarEvents =
|
|
1844
|
+
DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
1845
|
+
self.sendCalendarEvents(calendarEvents)
|
|
1516
1846
|
}
|
|
1517
1847
|
|
|
1518
1848
|
// MARK: - Heartbeats
|
|
@@ -1521,7 +1851,7 @@ class G2: NSObject, SGCManager {
|
|
|
1521
1851
|
heartbeatTask?.cancel()
|
|
1522
1852
|
heartbeatTask = Task { [weak self] in
|
|
1523
1853
|
while !Task.isCancelled {
|
|
1524
|
-
try? await Task.sleep(nanoseconds:
|
|
1854
|
+
try? await Task.sleep(nanoseconds: 10_000_000_000)
|
|
1525
1855
|
guard !Task.isCancelled else { break }
|
|
1526
1856
|
await MainActor.run {
|
|
1527
1857
|
self?.sendEvenHubHeartbeat()
|
|
@@ -1596,55 +1926,112 @@ class G2: NSObject, SGCManager {
|
|
|
1596
1926
|
|
|
1597
1927
|
// MARK: - SGCManager: Display Control
|
|
1598
1928
|
|
|
1599
|
-
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 {
|
|
1600
1948
|
// Bridge.log("G2: sendTextWall(\(text.prefix(50))...)")
|
|
1601
1949
|
|
|
1602
1950
|
// ignore events while the ER dashboard is open:
|
|
1603
|
-
let useNativeDashboard =
|
|
1951
|
+
let useNativeDashboard =
|
|
1952
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
1604
1953
|
if useNativeDashboard && dashboardShowing > 0 {
|
|
1605
1954
|
return
|
|
1606
1955
|
}
|
|
1607
1956
|
|
|
1608
|
-
|
|
1609
|
-
|
|
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)
|
|
1610
1990
|
return
|
|
1611
1991
|
}
|
|
1612
1992
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
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()
|
|
1621
2000
|
}
|
|
1622
2001
|
|
|
1623
|
-
func sendDoubleTextWall(_ top: String, _ bottom: String) {
|
|
2002
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
2003
|
+
Bridge.log("G2: sendDoubleTextWall() - top: \(top), bottom: \(bottom)")
|
|
1624
2004
|
// G2 doesn't have native double text wall, combine them
|
|
1625
|
-
let combined = top + "\n" + bottom
|
|
1626
|
-
sendTextWall(combined)
|
|
2005
|
+
let combined = top + "\n\n" + bottom
|
|
2006
|
+
await sendTextWall(combined)
|
|
1627
2007
|
}
|
|
1628
2008
|
|
|
1629
2009
|
func clearDisplay() {
|
|
1630
2010
|
Bridge.log("G2: clearDisplay()")
|
|
1631
2011
|
// Don't shutdown the EvenHub page — that kills audio streaming too.
|
|
1632
2012
|
// Instead, just clear the text content by sending a space.
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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()
|
|
1641
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()
|
|
1642
2031
|
}
|
|
1643
2032
|
|
|
1644
2033
|
/// Send BMP data to an image container via fragmented updateImageRawData
|
|
1645
|
-
private func sendImageData(containerID: Int32, containerName: String, bmpData: Data) async
|
|
1646
|
-
-> Bool
|
|
1647
|
-
{
|
|
2034
|
+
private func sendImageData(containerID: Int32, containerName: String, bmpData: Data) async {
|
|
1648
2035
|
let fragmentSize = 4096
|
|
1649
2036
|
imageSessionCounter += 1
|
|
1650
2037
|
let sessionId = imageSessionCounter
|
|
@@ -1652,9 +2039,13 @@ class G2: NSObject, SGCManager {
|
|
|
1652
2039
|
var fragmentIndex: Int32 = 0
|
|
1653
2040
|
var offset = 0
|
|
1654
2041
|
|
|
2042
|
+
Bridge.log(
|
|
2043
|
+
"G2: sendImageData(\(containerName)) - \(fragmentIndex) fragments, \(bmpData.count) bytes"
|
|
2044
|
+
)
|
|
2045
|
+
|
|
1655
2046
|
while offset < bmpData.count {
|
|
1656
2047
|
let end = min(offset + fragmentSize, bmpData.count)
|
|
1657
|
-
let fragment = bmpData[offset
|
|
2048
|
+
let fragment = bmpData[offset..<end]
|
|
1658
2049
|
|
|
1659
2050
|
let msg = EvenHubProto.updateImageRawDataMessage(
|
|
1660
2051
|
containerID: containerID,
|
|
@@ -1670,149 +2061,152 @@ class G2: NSObject, SGCManager {
|
|
|
1670
2061
|
|
|
1671
2062
|
fragmentIndex += 1
|
|
1672
2063
|
offset = end
|
|
1673
|
-
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
2064
|
+
try? await Task.sleep(nanoseconds: 200_000_000) // 200ms between fragments
|
|
1674
2065
|
}
|
|
1675
|
-
|
|
1676
|
-
Bridge.log(
|
|
1677
|
-
"G2: sendImageData(\(containerName)) - \(fragmentIndex) fragments, \(bmpData.count) bytes"
|
|
1678
|
-
)
|
|
1679
|
-
return true
|
|
1680
2066
|
}
|
|
1681
2067
|
|
|
1682
|
-
|
|
1683
|
-
|
|
2068
|
+
/// Display a bitmap inside a positioned image container.
|
|
2069
|
+
///
|
|
2070
|
+
/// The page keeps a live list of up to 4 image containers keyed by exact rect:
|
|
2071
|
+
/// - If a container with the requested rect already exists, the image is just resent to it
|
|
2072
|
+
/// (no page rebuild).
|
|
2073
|
+
/// - Otherwise a new container is added (evicting the oldest when the list would exceed 4) and
|
|
2074
|
+
/// the page is rebuilt before the image is sent.
|
|
2075
|
+
///
|
|
2076
|
+
/// Omitted params default to a 100x100 container in the top-left corner.
|
|
2077
|
+
func displayBitmap(
|
|
2078
|
+
base64ImageData: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil,
|
|
2079
|
+
height: Int32? = nil
|
|
2080
|
+
) async -> Bool {
|
|
2081
|
+
let rx = x ?? G2.defaultImgContainer.x
|
|
2082
|
+
let ry = y ?? G2.defaultImgContainer.y
|
|
2083
|
+
let rw = width ?? G2.defaultImgContainer.width
|
|
2084
|
+
let rh = height ?? G2.defaultImgContainer.height
|
|
1684
2085
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
// --- Single-tile approach: scale source to fit 200x100, send as one image container ---
|
|
1690
|
-
guard let bmpData = convertToG2Bmp(rawData, containerWidth: 200, containerHeight: 100)
|
|
1691
|
-
else {
|
|
1692
|
-
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
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 {
|
|
1693
2090
|
return false
|
|
1694
2091
|
}
|
|
1695
2092
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
let containerY: Int32 = y
|
|
1701
|
-
let containerID: Int32 = id
|
|
1702
|
-
let containerName = "img-\(id)"
|
|
2093
|
+
guard let rawData = Data(base64Encoded: base64ImageData) else {
|
|
2094
|
+
Bridge.log("G2: failed to decode base64")
|
|
2095
|
+
return false
|
|
2096
|
+
}
|
|
1703
2097
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2098
|
+
guard
|
|
2099
|
+
let bmpData = convertToG2Bmp(rawData, containerWidth: Int(rw), containerHeight: Int(rh))
|
|
2100
|
+
else {
|
|
2101
|
+
Bridge.log("G2: failed to convert image to BMP")
|
|
2102
|
+
return false
|
|
2103
|
+
}
|
|
1709
2104
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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)"
|
|
1716
2114
|
)
|
|
1717
|
-
|
|
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
|
|
1718
2123
|
} else {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
appId: activeMenuAppId
|
|
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"
|
|
1723
2127
|
)
|
|
2128
|
+
await rebuildPage()
|
|
1724
2129
|
}
|
|
1725
|
-
|
|
1726
|
-
pageCreated = true
|
|
1727
|
-
pageHasTextContainer = false
|
|
1728
|
-
currentTextContent = ""
|
|
1729
|
-
Bridge.log("G2: displayBitmap() - page sent, waiting 1s before sending fragments...")
|
|
1730
|
-
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s - give glasses time to process page
|
|
1731
|
-
|
|
1732
|
-
// Send the BMP data
|
|
1733
|
-
let success = await sendImageData(
|
|
1734
|
-
containerID: containerID, containerName: containerName, bmpData: bmpData
|
|
1735
|
-
)
|
|
1736
|
-
if !success {
|
|
1737
|
-
Bridge.log("G2: displayBitmap() - failed sending image data")
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
Bridge.log("G2: displayBitmap() - single tile sent, \(bmpData.count) bytes")
|
|
1741
|
-
return success
|
|
2130
|
+
return true
|
|
1742
2131
|
}
|
|
1743
2132
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2133
|
+
/// Add a new image container for `rect`, evicting the oldest when the list is full (max 4).
|
|
2134
|
+
/// Returns the newly tracked container (with an assigned ID from the pool).
|
|
2135
|
+
private func addImageContainer(x: Int32, y: Int32, width: Int32, height: Int32, bmpData: Data)
|
|
2136
|
+
-> ImgContainer
|
|
2137
|
+
{
|
|
2138
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2139
|
+
if imageContainers.count >= imageContainerIDPool.count {
|
|
2140
|
+
let evicted = imageContainers.removeFirst()
|
|
2141
|
+
Bridge.log("G2: evicting oldest image container \(evicted.id)")
|
|
1748
2142
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2143
|
+
// Pick the lowest free ID from the pool.
|
|
2144
|
+
let usedIDs = Set(imageContainers.map { $0.id })
|
|
2145
|
+
let id = imageContainerIDPool.first { !usedIDs.contains($0) } ?? imageContainerIDPool[0]
|
|
2146
|
+
let container = ImgContainer(
|
|
2147
|
+
id: id, x: x, y: y, width: width, height: height, bmpData: bmpData)
|
|
2148
|
+
imageContainers.append(container)
|
|
2149
|
+
return container
|
|
2150
|
+
}
|
|
2151
|
+
|
|
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)")
|
|
1753
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
|
+
}
|
|
1754
2179
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
)
|
|
1760
|
-
let container2 = EvenHubProto.imageContainerProperty(
|
|
1761
|
-
x: 200, y: 0, width: 200, height: 100,
|
|
1762
|
-
containerID: 11, containerName: "img-11"
|
|
1763
|
-
)
|
|
1764
|
-
let container3 = EvenHubProto.imageContainerProperty(
|
|
1765
|
-
x: 0, y: 100, width: 200, height: 100,
|
|
1766
|
-
containerID: 12, containerName: "img-12"
|
|
1767
|
-
)
|
|
1768
|
-
let container4 = EvenHubProto.imageContainerProperty(
|
|
1769
|
-
x: 200, y: 100, width: 200, height: 100,
|
|
1770
|
-
containerID: 13, containerName: "img-13"
|
|
1771
|
-
)
|
|
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()
|
|
1772
2185
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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"
|
|
1779
2192
|
)
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
msg = EvenHubProto.rebuildPageMessage(
|
|
1783
|
-
imageContainers: [
|
|
1784
|
-
container1, container2, container3, container4,
|
|
1785
|
-
], magicRandom: sendManager.nextMagicRandom(), appId: activeMenuAppId
|
|
2193
|
+
await sendImageData(
|
|
2194
|
+
containerID: container.id, containerName: container.name, bmpData: container.bmpData
|
|
1786
2195
|
)
|
|
2196
|
+
try? await Task.sleep(nanoseconds: 300_000_000) // 300ms between containers
|
|
1787
2197
|
}
|
|
1788
|
-
sendEvenHubCommand(msg)
|
|
1789
|
-
pageCreated = true
|
|
1790
|
-
pageHasTextContainer = false
|
|
1791
|
-
currentTextContent = ""
|
|
1792
2198
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
//
|
|
1796
|
-
let
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
)
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
)
|
|
1805
|
-
let success4 = await sendImageData(
|
|
1806
|
-
containerID: 13, containerName: "img-13", bmpData: tiles[3]
|
|
1807
|
-
)
|
|
1808
|
-
|
|
1809
|
-
return success1 && success2 && success3 && success4
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
func displayBitmap(base64ImageData: String) async -> Bool {
|
|
1813
|
-
currentBitmapBase64 = base64ImageData
|
|
1814
|
-
currentTextContent = ""
|
|
1815
|
-
return await displayBitmapQuad(base64ImageData: base64ImageData)
|
|
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
|
+
// }
|
|
1816
2210
|
}
|
|
1817
2211
|
|
|
1818
2212
|
/// Upscale BMP pixel data by 2x (200x100 → 400x200) using nearest-neighbor
|
|
@@ -1825,7 +2219,7 @@ class G2: NSObject, SGCManager {
|
|
|
1825
2219
|
return nil
|
|
1826
2220
|
}
|
|
1827
2221
|
|
|
1828
|
-
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3
|
|
2222
|
+
let srcPaddedRowSize = ((srcWidth + 1) / 2 + 3) & ~3 // 4-bit rows padded to 4 bytes
|
|
1829
2223
|
let pixelDataOffset = headerSize
|
|
1830
2224
|
|
|
1831
2225
|
let dstWidth = srcWidth * 2
|
|
@@ -1858,7 +2252,7 @@ class G2: NSObject, SGCManager {
|
|
|
1858
2252
|
dst.appendLittleEndian(UInt32(0))
|
|
1859
2253
|
|
|
1860
2254
|
// --- Color Table (same 16-entry grayscale) ---
|
|
1861
|
-
for i in 0
|
|
2255
|
+
for i in 0..<16 {
|
|
1862
2256
|
let val = UInt8(i * 17)
|
|
1863
2257
|
dst.append(contentsOf: [val, val, val, 0])
|
|
1864
2258
|
}
|
|
@@ -1866,12 +2260,12 @@ class G2: NSObject, SGCManager {
|
|
|
1866
2260
|
// --- Pixel Data (nearest-neighbor 2x upscale) ---
|
|
1867
2261
|
// BMP is bottom-up, so row 0 = bottom of image
|
|
1868
2262
|
// Each dst row maps to srcRow = dstRow / 2
|
|
1869
|
-
for dstRow in 0
|
|
2263
|
+
for dstRow in 0..<dstHeight {
|
|
1870
2264
|
let srcRow = dstRow / 2
|
|
1871
2265
|
let srcRowOffset = pixelDataOffset + srcRow * srcPaddedRowSize
|
|
1872
2266
|
var rowBuf = [UInt8](repeating: 0, count: dstPaddedRowSize)
|
|
1873
2267
|
|
|
1874
|
-
for dstCol in 0
|
|
2268
|
+
for dstCol in 0..<dstWidth {
|
|
1875
2269
|
let srcCol = dstCol / 2
|
|
1876
2270
|
|
|
1877
2271
|
// Read 4-bit nibble from source
|
|
@@ -1897,73 +2291,6 @@ class G2: NSObject, SGCManager {
|
|
|
1897
2291
|
return dst
|
|
1898
2292
|
}
|
|
1899
2293
|
|
|
1900
|
-
func displayBitmapOriginal(base64ImageData: String) async -> Bool {
|
|
1901
|
-
guard let rawData = Data(base64Encoded: base64ImageData) else {
|
|
1902
|
-
Bridge.log("G2: displayBitmap() - failed to decode base64")
|
|
1903
|
-
return false
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
Bridge.log("G2: displayBitmap() - decoded \(rawData.count) bytes from base64")
|
|
1907
|
-
|
|
1908
|
-
Bridge.log(
|
|
1909
|
-
"G2: displayBitmap() - state: startupPageCreated=\(startupPageCreated), pageCreated=\(pageCreated)"
|
|
1910
|
-
)
|
|
1911
|
-
|
|
1912
|
-
// --- Single-tile approach: scale source to fit 200x100, send as one image container ---
|
|
1913
|
-
guard let bmpData = convertToG2Bmp(rawData, containerWidth: 200, containerHeight: 100)
|
|
1914
|
-
else {
|
|
1915
|
-
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
1916
|
-
return false
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// Center the 200x100 container on the 576x288 canvas
|
|
1920
|
-
let containerW: Int32 = 200
|
|
1921
|
-
let containerH: Int32 = 100
|
|
1922
|
-
let containerX: Int32 = (576 - containerW) / 2
|
|
1923
|
-
let containerY: Int32 = (288 - containerH) / 2
|
|
1924
|
-
let containerID: Int32 = 10
|
|
1925
|
-
let containerName = "img-single"
|
|
1926
|
-
|
|
1927
|
-
let imageContainer = EvenHubProto.imageContainerProperty(
|
|
1928
|
-
x: containerX, y: containerY,
|
|
1929
|
-
width: containerW, height: containerH,
|
|
1930
|
-
containerID: containerID, containerName: containerName
|
|
1931
|
-
)
|
|
1932
|
-
|
|
1933
|
-
let msg: Data
|
|
1934
|
-
if !startupPageCreated {
|
|
1935
|
-
Bridge.log("G2: displayBitmap() - creating startup page with image container")
|
|
1936
|
-
msg = EvenHubProto.createPageMessage(
|
|
1937
|
-
imageContainers: [imageContainer], magicRandom: sendManager.nextMagicRandom(),
|
|
1938
|
-
appId: activeMenuAppId
|
|
1939
|
-
)
|
|
1940
|
-
startupPageCreated = true
|
|
1941
|
-
} else {
|
|
1942
|
-
Bridge.log("G2: displayBitmap() - rebuilding page with image container")
|
|
1943
|
-
msg = EvenHubProto.rebuildPageMessage(
|
|
1944
|
-
imageContainers: [imageContainer], magicRandom: sendManager.nextMagicRandom(),
|
|
1945
|
-
appId: activeMenuAppId
|
|
1946
|
-
)
|
|
1947
|
-
}
|
|
1948
|
-
sendEvenHubCommand(msg)
|
|
1949
|
-
pageCreated = true
|
|
1950
|
-
pageHasTextContainer = false
|
|
1951
|
-
currentTextContent = ""
|
|
1952
|
-
Bridge.log("G2: displayBitmap() - page sent, waiting 1s before sending fragments...")
|
|
1953
|
-
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s - give glasses time to process page
|
|
1954
|
-
|
|
1955
|
-
// Send the BMP data
|
|
1956
|
-
let success = await sendImageData(
|
|
1957
|
-
containerID: containerID, containerName: containerName, bmpData: bmpData
|
|
1958
|
-
)
|
|
1959
|
-
if !success {
|
|
1960
|
-
Bridge.log("G2: displayBitmap() - failed sending image data")
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
Bridge.log("G2: displayBitmap() - single tile sent, \(bmpData.count) bytes")
|
|
1964
|
-
return success
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
2294
|
// MARK: - Bitmap Conversion
|
|
1968
2295
|
|
|
1969
2296
|
/// Scale source image to fit within containerWidth x containerHeight (maintaining aspect ratio),
|
|
@@ -1987,9 +2314,9 @@ class G2: NSObject, SGCManager {
|
|
|
1987
2314
|
let offsetX = (containerWidth - scaledW) / 2
|
|
1988
2315
|
let offsetY = (containerHeight - scaledH) / 2
|
|
1989
2316
|
|
|
1990
|
-
Bridge.log(
|
|
1991
|
-
|
|
1992
|
-
)
|
|
2317
|
+
// Bridge.log(
|
|
2318
|
+
// "G2: convertToG2Bmp - input \(srcWidth)x\(srcHeight) → scaled \(scaledW)x\(scaledH) in \(containerWidth)x\(containerHeight)"
|
|
2319
|
+
// )
|
|
1993
2320
|
|
|
1994
2321
|
// Render to 8-bit grayscale at the CONTAINER size (not scaled size)
|
|
1995
2322
|
guard
|
|
@@ -2013,7 +2340,7 @@ class G2: NSObject, SGCManager {
|
|
|
2013
2340
|
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
2014
2341
|
|
|
2015
2342
|
guard let renderedImage = ctx.makeImage(),
|
|
2016
|
-
|
|
2343
|
+
let pixels = renderedImage.dataProvider?.data as Data?
|
|
2017
2344
|
else {
|
|
2018
2345
|
Bridge.log("G2: convertToG2Bmp - failed to get pixel data")
|
|
2019
2346
|
return nil
|
|
@@ -2031,99 +2358,12 @@ class G2: NSObject, SGCManager {
|
|
|
2031
2358
|
return bmp
|
|
2032
2359
|
}
|
|
2033
2360
|
|
|
2034
|
-
// MARK: - Bitmap Conversion (4-tile approach for G2 - kept for future use)
|
|
2035
|
-
|
|
2036
|
-
private static let tileWidth = 200
|
|
2037
|
-
private static let tileHeight = 100
|
|
2038
|
-
// Total image area: 400x200 (2x2 grid of 200x100 tiles)
|
|
2039
|
-
|
|
2040
|
-
/// Render any image to 400x200 grayscale, then slice into 4 tiles (200x100 each).
|
|
2041
|
-
/// Returns 4 BMP Data objects: [top-left, top-right, bottom-left, bottom-right].
|
|
2042
|
-
private func renderAndSliceTo4Tiles(_ data: Data) -> [Data]? {
|
|
2043
|
-
guard let image = UIImage(data: data), let cgImage = image.cgImage else {
|
|
2044
|
-
Bridge.log("G2: renderAndSliceTo4Tiles - could not decode image")
|
|
2045
|
-
return nil
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
let srcWidth = cgImage.width
|
|
2049
|
-
let srcHeight = cgImage.height
|
|
2050
|
-
let totalW = G2.tileWidth * 2 // 400
|
|
2051
|
-
let totalH = G2.tileHeight * 2 // 200
|
|
2052
|
-
|
|
2053
|
-
// Scale source to fit within 400x200 (maintain aspect ratio)
|
|
2054
|
-
let scale = min(Double(totalW) / Double(srcWidth), Double(totalH) / Double(srcHeight))
|
|
2055
|
-
let scaledW = Int(Double(srcWidth) * scale)
|
|
2056
|
-
let scaledH = Int(Double(srcHeight) * scale)
|
|
2057
|
-
let offsetX = (totalW - scaledW) / 2
|
|
2058
|
-
let offsetY = (totalH - scaledH) / 2
|
|
2059
|
-
|
|
2060
|
-
Bridge.log(
|
|
2061
|
-
"G2: renderAndSliceTo4Tiles - input \(srcWidth)x\(srcHeight) → \(scaledW)x\(scaledH) in \(totalW)x\(totalH)"
|
|
2062
|
-
)
|
|
2063
|
-
|
|
2064
|
-
// Render to 400x200 8-bit grayscale
|
|
2065
|
-
guard
|
|
2066
|
-
let ctx = CGContext(
|
|
2067
|
-
data: nil,
|
|
2068
|
-
width: totalW,
|
|
2069
|
-
height: totalH,
|
|
2070
|
-
bitsPerComponent: 8,
|
|
2071
|
-
bytesPerRow: totalW,
|
|
2072
|
-
space: CGColorSpaceCreateDeviceGray(),
|
|
2073
|
-
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
2074
|
-
)
|
|
2075
|
-
else {
|
|
2076
|
-
Bridge.log("G2: renderAndSliceTo4Tiles - failed to create CGContext")
|
|
2077
|
-
return nil
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
ctx.setFillColor(gray: 0, alpha: 1)
|
|
2081
|
-
ctx.fill(CGRect(x: 0, y: 0, width: totalW, height: totalH))
|
|
2082
|
-
ctx.interpolationQuality = .high
|
|
2083
|
-
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
2084
|
-
|
|
2085
|
-
guard let renderedImage = ctx.makeImage(),
|
|
2086
|
-
let fullPixels = renderedImage.dataProvider?.data as Data?
|
|
2087
|
-
else {
|
|
2088
|
-
Bridge.log("G2: renderAndSliceTo4Tiles - failed to get pixel data")
|
|
2089
|
-
return nil
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// Slice into 4 tiles and build BMP for each
|
|
2093
|
-
// CGContext origin is bottom-left, but pixel data is top-left row-first
|
|
2094
|
-
let tw = G2.tileWidth // 200
|
|
2095
|
-
let th = G2.tileHeight // 100
|
|
2096
|
-
let tileOrigins = [
|
|
2097
|
-
(0, 0), // top-left
|
|
2098
|
-
(tw, 0), // top-right
|
|
2099
|
-
(0, th), // bottom-left
|
|
2100
|
-
(tw, th), // bottom-right
|
|
2101
|
-
]
|
|
2102
|
-
|
|
2103
|
-
var tiles: [Data] = []
|
|
2104
|
-
for (ox, oy) in tileOrigins {
|
|
2105
|
-
// Extract tile pixels from the full 400x200 buffer
|
|
2106
|
-
var tilePixels = Data(capacity: tw * th)
|
|
2107
|
-
for row in 0 ..< th {
|
|
2108
|
-
let srcRowStart = (oy + row) * totalW + ox
|
|
2109
|
-
tilePixels.append(fullPixels[srcRowStart ..< (srcRowStart + tw)])
|
|
2110
|
-
}
|
|
2111
|
-
guard let bmp = build4BitBmp(grayscalePixels: tilePixels, width: tw, height: th) else {
|
|
2112
|
-
Bridge.log("G2: renderAndSliceTo4Tiles - failed to build BMP for tile")
|
|
2113
|
-
return nil
|
|
2114
|
-
}
|
|
2115
|
-
tiles.append(bmp)
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
return tiles
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
2361
|
/// Build a 4-bit indexed BMP file from 8-bit grayscale pixel data.
|
|
2122
2362
|
/// BMP rows are stored bottom-up. Each row is padded to a 4-byte boundary.
|
|
2123
2363
|
private func build4BitBmp(grayscalePixels: Data, width: Int, height: Int) -> Data? {
|
|
2124
2364
|
// 4-bit: 2 pixels per byte, rows padded to 4-byte boundary
|
|
2125
|
-
let bytesPerRow4bit = (width + 1) / 2
|
|
2126
|
-
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
|
|
2127
2367
|
let pixelDataSize = paddedRowSize * height
|
|
2128
2368
|
|
|
2129
2369
|
// BMP file header (14 bytes) + DIB header (40 bytes) + color table (16 * 4 = 64 bytes)
|
|
@@ -2133,46 +2373,46 @@ class G2: NSObject, SGCManager {
|
|
|
2133
2373
|
var bmp = Data(capacity: fileSize)
|
|
2134
2374
|
|
|
2135
2375
|
// --- BMP File Header (14 bytes) ---
|
|
2136
|
-
bmp.append(contentsOf: [0x42, 0x4D])
|
|
2137
|
-
bmp.appendLittleEndian(UInt32(fileSize))
|
|
2138
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
2139
|
-
bmp.appendLittleEndian(UInt16(0))
|
|
2140
|
-
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
|
|
2141
2381
|
|
|
2142
2382
|
// --- DIB Header (BITMAPINFOHEADER, 40 bytes) ---
|
|
2143
|
-
bmp.appendLittleEndian(UInt32(40))
|
|
2144
|
-
bmp.appendLittleEndian(Int32(width))
|
|
2145
|
-
bmp.appendLittleEndian(Int32(height))
|
|
2146
|
-
bmp.appendLittleEndian(UInt16(1))
|
|
2147
|
-
bmp.appendLittleEndian(UInt16(4))
|
|
2148
|
-
bmp.appendLittleEndian(UInt32(0))
|
|
2149
|
-
bmp.appendLittleEndian(UInt32(pixelDataSize))
|
|
2150
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
2151
|
-
bmp.appendLittleEndian(Int32(2835))
|
|
2152
|
-
bmp.appendLittleEndian(UInt32(16))
|
|
2153
|
-
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)
|
|
2154
2394
|
|
|
2155
2395
|
// --- Color Table (16 entries, 4 bytes each: B, G, R, 0) ---
|
|
2156
|
-
for i in 0
|
|
2157
|
-
let val = UInt8(i * 17)
|
|
2158
|
-
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
|
|
2159
2399
|
}
|
|
2160
2400
|
|
|
2161
2401
|
// --- Pixel Data (bottom-up rows, 4-bit packed) ---
|
|
2162
2402
|
let rowBytes = [UInt8](repeating: 0, count: paddedRowSize)
|
|
2163
|
-
for row in 0
|
|
2403
|
+
for row in 0..<height {
|
|
2164
2404
|
// BMP is bottom-up: row 0 in BMP = last row of image
|
|
2165
2405
|
let srcRow = height - 1 - row
|
|
2166
2406
|
let srcOffset = srcRow * width
|
|
2167
2407
|
var rowBuf = rowBytes
|
|
2168
2408
|
|
|
2169
|
-
for col in 0
|
|
2409
|
+
for col in 0..<width {
|
|
2170
2410
|
let pixelIndex = srcOffset + col
|
|
2171
2411
|
guard pixelIndex < grayscalePixels.count else { continue }
|
|
2172
2412
|
|
|
2173
2413
|
// Map 8-bit grayscale (0-255) to 4-bit index (0-15)
|
|
2174
2414
|
let gray8 = grayscalePixels[pixelIndex]
|
|
2175
|
-
let index4 = gray8 >> 4
|
|
2415
|
+
let index4 = gray8 >> 4 // divide by 16
|
|
2176
2416
|
|
|
2177
2417
|
let bytePos = col / 2
|
|
2178
2418
|
if col % 2 == 0 {
|
|
@@ -2186,9 +2426,9 @@ class G2: NSObject, SGCManager {
|
|
|
2186
2426
|
bmp.append(contentsOf: rowBuf)
|
|
2187
2427
|
}
|
|
2188
2428
|
|
|
2189
|
-
Bridge.log(
|
|
2190
|
-
|
|
2191
|
-
)
|
|
2429
|
+
// Bridge.log(
|
|
2430
|
+
// "G2: build4BitBmp - \(bmp.count) bytes (header=\(headerSize), pixels=\(pixelDataSize), rows=\(paddedRowSize)x\(height))"
|
|
2431
|
+
// )
|
|
2192
2432
|
return bmp
|
|
2193
2433
|
}
|
|
2194
2434
|
|
|
@@ -2196,13 +2436,11 @@ class G2: NSObject, SGCManager {
|
|
|
2196
2436
|
/// the foreground by tearing down whatever EvenHub page we currently own.
|
|
2197
2437
|
/// The glasses fall back to the dashboard automatically when no page is up.
|
|
2198
2438
|
func showDashboard() {
|
|
2199
|
-
Bridge.log("G2: showDashboard")
|
|
2439
|
+
Bridge.log("G2: showDashboard()")
|
|
2200
2440
|
dashboardShowing += 2
|
|
2201
2441
|
let msg = EvenHubProto.shutdownMessage()
|
|
2202
2442
|
sendEvenHubCommand(msg)
|
|
2203
2443
|
pageCreated = false
|
|
2204
|
-
pageHasTextContainer = false
|
|
2205
|
-
currentTextContent = ""
|
|
2206
2444
|
currentBitmapBase64 = ""
|
|
2207
2445
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
2208
2446
|
guard let self = self else { return }
|
|
@@ -2212,6 +2450,114 @@ class G2: NSObject, SGCManager {
|
|
|
2212
2450
|
}
|
|
2213
2451
|
}
|
|
2214
2452
|
|
|
2453
|
+
private func dashboardHalfDayFormat() -> Int32 {
|
|
2454
|
+
let twelveHour = DeviceStore.shared.get("bluetooth", "twelve_hour_time") as? Bool ?? true
|
|
2455
|
+
return twelveHour ? 1 : 0
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
private func dashboardTemperatureUnit() -> Int32 {
|
|
2459
|
+
let metric = DeviceStore.shared.get("bluetooth", "metric_system") as? Bool ?? false
|
|
2460
|
+
return metric ? 1 : 2
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
func sendDashboardDisplaySettings() {
|
|
2464
|
+
var dashDisplayW = ProtobufWriter()
|
|
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
|
|
2469
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
2470
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
2471
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
2472
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
2473
|
+
|
|
2474
|
+
var dashRecvW = ProtobufWriter()
|
|
2475
|
+
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
2476
|
+
|
|
2477
|
+
var dashPkgW = ProtobufWriter()
|
|
2478
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
2479
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2480
|
+
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
2481
|
+
sendDashboardCommand(dashPkgW.data)
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/// Push a calendar event to the dashboard's Schedule widget (service 0x01).
|
|
2485
|
+
///
|
|
2486
|
+
/// - title: event title displayed on the widget
|
|
2487
|
+
/// - location: optional location string
|
|
2488
|
+
/// - time: pre-formatted display string (e.g. "10:00 AM" or "10:00 – 10:30").
|
|
2489
|
+
/// The widget shows this verbatim — format it however you want it to read.
|
|
2490
|
+
/// - endDate: when the event ends. Encoded the same way as the time-sync hack:
|
|
2491
|
+
/// Unix seconds with the local TZ offset folded in, so the glasses (which
|
|
2492
|
+
/// appear to treat timestamps as already-local) read it correctly.
|
|
2493
|
+
/// - scheduleId: stable per-event id. Reuse the same id when updating an event.
|
|
2494
|
+
func sendCalendarEvent(
|
|
2495
|
+
title: String,
|
|
2496
|
+
location: String? = nil,
|
|
2497
|
+
time: String? = nil,
|
|
2498
|
+
endDate: Date,
|
|
2499
|
+
scheduleId: Int32 = 1,
|
|
2500
|
+
scheduleTotal: Int32 = 1,
|
|
2501
|
+
scheduleNum: Int32 = 0
|
|
2502
|
+
) {
|
|
2503
|
+
Bridge.log("G2: sendCalendarEvent(\(title), endDate=\(endDate))")
|
|
2504
|
+
let tzSec = Int64(TimeZone.current.secondsFromGMT())
|
|
2505
|
+
let endTs = Int32(truncatingIfNeeded: Int64(endDate.timeIntervalSince1970) + tzSec)
|
|
2506
|
+
|
|
2507
|
+
let payload = DashboardProto.calendarPush(
|
|
2508
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2509
|
+
packageId: 1,
|
|
2510
|
+
scheduleId: scheduleId,
|
|
2511
|
+
title: title,
|
|
2512
|
+
location: location,
|
|
2513
|
+
time: time,
|
|
2514
|
+
endTimestamp: endTs,
|
|
2515
|
+
scheduleAuthority: 1,
|
|
2516
|
+
scheduleTotal: scheduleTotal,
|
|
2517
|
+
scheduleNum: scheduleNum
|
|
2518
|
+
)
|
|
2519
|
+
sendDashboardCommand(payload)
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/// Bridge entry for `calendar_events` store updates. Each dict is expected
|
|
2523
|
+
/// to match the TS `CalendarEvent` shape: { title, location?, time, endDate }
|
|
2524
|
+
/// where endDate is unix seconds.
|
|
2525
|
+
///
|
|
2526
|
+
/// Sends one BLE push per event, with `scheduleTotal` set to the batch size
|
|
2527
|
+
/// and `scheduleNum` set to this event's 0-based slot. The widget pages
|
|
2528
|
+
/// through them on the glasses — without paging info the firmware overwrites
|
|
2529
|
+
/// slot 0 on each push and only the last event survives.
|
|
2530
|
+
func sendCalendarEvents(_ events: [[String: Any]]) {
|
|
2531
|
+
Bridge.log("G2: sendCalendarEvents — \(events.count) events")
|
|
2532
|
+
if events.isEmpty {
|
|
2533
|
+
let payload = DashboardProto.calendarClear(
|
|
2534
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2535
|
+
packageId: 1,
|
|
2536
|
+
scheduleAuthority: 1
|
|
2537
|
+
)
|
|
2538
|
+
sendDashboardCommand(payload)
|
|
2539
|
+
return
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
let total = Int32(events.count)
|
|
2543
|
+
for (i, ev) in events.enumerated() {
|
|
2544
|
+
guard let title = ev["title"] as? String,
|
|
2545
|
+
let time = ev["time"] as? String,
|
|
2546
|
+
let endTs = ev["endDate"] as? Double
|
|
2547
|
+
else { continue }
|
|
2548
|
+
let location = ev["location"] as? String
|
|
2549
|
+
sendCalendarEvent(
|
|
2550
|
+
title: title,
|
|
2551
|
+
location: location,
|
|
2552
|
+
time: time,
|
|
2553
|
+
endDate: Date(timeIntervalSince1970: endTs),
|
|
2554
|
+
scheduleId: Int32(i + 1),
|
|
2555
|
+
scheduleTotal: total,
|
|
2556
|
+
scheduleNum: Int32(i)
|
|
2557
|
+
)
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2215
2561
|
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
2216
2562
|
Bridge.log("G2: setDashboardPosition(height=\(height), depth=\(depth))")
|
|
2217
2563
|
setDashboardHeightOnly(height)
|
|
@@ -2250,47 +2596,65 @@ class G2: NSObject, SGCManager {
|
|
|
2250
2596
|
|
|
2251
2597
|
// MARK: - Private Display Helpers
|
|
2252
2598
|
|
|
2253
|
-
private func
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Build the page's image containers from the live tracked list.
|
|
2631
|
+
let imageContainerProps: [Data] = imageContainers.map { c in
|
|
2632
|
+
EvenHubProto.imageContainerProperty(
|
|
2633
|
+
x: c.x, y: c.y, width: c.width, height: c.height,
|
|
2634
|
+
containerID: c.id, containerName: c.name
|
|
2635
|
+
)
|
|
2636
|
+
}
|
|
2261
2637
|
|
|
2262
2638
|
let msg: Data
|
|
2263
|
-
if !
|
|
2264
|
-
Bridge.log("G2:
|
|
2639
|
+
if !pageCreated {
|
|
2640
|
+
Bridge.log("G2: createPageWithContainers() - using createPageMessage (first time)")
|
|
2265
2641
|
msg = EvenHubProto.createPageMessage(
|
|
2266
|
-
textContainers:
|
|
2642
|
+
textContainers: textContainerProps,
|
|
2643
|
+
imageContainers: imageContainerProps,
|
|
2644
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2267
2645
|
appId: activeMenuAppId
|
|
2268
2646
|
)
|
|
2269
|
-
startupPageCreated = true
|
|
2270
2647
|
} else {
|
|
2271
|
-
Bridge.log("G2:
|
|
2648
|
+
Bridge.log("G2: createPageWithContainers() - using rebuildPageMessage")
|
|
2272
2649
|
msg = EvenHubProto.rebuildPageMessage(
|
|
2273
|
-
textContainers:
|
|
2650
|
+
textContainers: textContainerProps,
|
|
2651
|
+
imageContainers: imageContainerProps,
|
|
2652
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2274
2653
|
appId: activeMenuAppId
|
|
2275
2654
|
)
|
|
2276
2655
|
}
|
|
2277
2656
|
sendEvenHubCommand(msg)
|
|
2278
2657
|
pageCreated = true
|
|
2279
|
-
pageHasTextContainer = true
|
|
2280
|
-
currentTextContent = text
|
|
2281
|
-
currentBitmapBase64 = ""
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
private func updateText(_ text: String) {
|
|
2285
|
-
let msg = EvenHubProto.updateTextMessage(
|
|
2286
|
-
containerID: textContainerID,
|
|
2287
|
-
contentOffset: 0,
|
|
2288
|
-
contentLength: Int32(text.utf8.count),
|
|
2289
|
-
content: text
|
|
2290
|
-
)
|
|
2291
|
-
queueEvenHubCommand(msg)
|
|
2292
|
-
currentTextContent = text
|
|
2293
|
-
currentBitmapBase64 = ""
|
|
2294
2658
|
}
|
|
2295
2659
|
|
|
2296
2660
|
private func queueEvenHubCommand(_ payload: Data) {
|
|
@@ -2319,6 +2683,13 @@ class G2: NSObject, SGCManager {
|
|
|
2319
2683
|
sendEvenHubCommand(toSend)
|
|
2320
2684
|
}
|
|
2321
2685
|
|
|
2686
|
+
private func restartMicIfAlreadyEnabled() {
|
|
2687
|
+
let currentEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
2688
|
+
if currentEnabled {
|
|
2689
|
+
restartMic()
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2322
2693
|
func restartMic() {
|
|
2323
2694
|
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2324
2695
|
DeviceStore.shared.apply("glasses", "micEnabled", true)
|
|
@@ -2326,13 +2697,14 @@ class G2: NSObject, SGCManager {
|
|
|
2326
2697
|
sendEvenHubCommand(msg)
|
|
2327
2698
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
2328
2699
|
guard let self = self else { return }
|
|
2329
|
-
let useNativeDashboard =
|
|
2330
|
-
|
|
2700
|
+
let useNativeDashboard =
|
|
2701
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
2702
|
+
// Bridge.log("G2: setMicEnabled - useNativeDashboard=\(useNativeDashboard), dashboardShowing=\(dashboardShowing)")
|
|
2331
2703
|
if useNativeDashboard && dashboardShowing > 0 {
|
|
2332
2704
|
return
|
|
2333
2705
|
}
|
|
2334
|
-
if !pageCreated
|
|
2335
|
-
DeviceManager.shared.sendCurrentState()
|
|
2706
|
+
if !pageCreated {
|
|
2707
|
+
DeviceManager.shared.sendCurrentState() // should re-create the page if needed
|
|
2336
2708
|
}
|
|
2337
2709
|
let msg = EvenHubProto.audioControlMessage(enable: true)
|
|
2338
2710
|
self.sendEvenHubCommand(msg)
|
|
@@ -2419,7 +2791,7 @@ class G2: NSObject, SGCManager {
|
|
|
2419
2791
|
rightAuthenticated = false
|
|
2420
2792
|
startupPageCreated = false
|
|
2421
2793
|
pageCreated = false
|
|
2422
|
-
|
|
2794
|
+
dashboardShowing = 0
|
|
2423
2795
|
heartbeatCounter = 0
|
|
2424
2796
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
2425
2797
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
@@ -2517,53 +2889,137 @@ class G2: NSObject, SGCManager {
|
|
|
2517
2889
|
Bridge.log("G2: Sent RING_DISCONNECT_INFO for MAC \(mac)")
|
|
2518
2890
|
}
|
|
2519
2891
|
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2892
|
+
/// Fire an EvenAI skill — the same path "Hey Even, show X" uses. Triggers a
|
|
2893
|
+
/// built-in glasses UI (notification list, navigation, teleprompter, etc).
|
|
2894
|
+
/// See `EvenAIProto.triggerSkill` for the skillId table.
|
|
2895
|
+
func triggerSkill(
|
|
2896
|
+
_ skillId: Int32, skillParam: Int32 = 0,
|
|
2897
|
+
text: String = "", streamEnable: Int32 = 1, fTextEnd: Int32 = 1
|
|
2898
|
+
) {
|
|
2899
|
+
Bridge.log(
|
|
2900
|
+
"G2: triggerSkill(\(skillId), skillParam=\(skillParam), text=\"\(text)\", streamEnable=\(streamEnable), fTextEnd=\(fTextEnd))"
|
|
2901
|
+
)
|
|
2902
|
+
let payload = EvenAIProto.triggerSkill(
|
|
2903
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2904
|
+
skillId: skillId,
|
|
2905
|
+
skillParam: skillParam,
|
|
2906
|
+
text: text,
|
|
2907
|
+
streamEnable: streamEnable,
|
|
2908
|
+
fTextEnd: fTextEnd
|
|
2909
|
+
)
|
|
2910
|
+
sendEvenAICommand(payload)
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/// Open the on-glasses notification panel — same effect as the user saying
|
|
2914
|
+
/// "Hey Even, show notifications". Replicates the official-app voice flow:
|
|
2915
|
+
/// 1. CTRL{status=ENTER} — puts glasses in AI session
|
|
2916
|
+
/// 2. ASK{text=" "} — minimal ASR transcript to seed session context
|
|
2917
|
+
/// 3. SKILL{skillId=NOTIFICATION, skillParam=show, ...} — dispatches the intent
|
|
2918
|
+
/// The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2919
|
+
/// supply the session context that lets the glasses act on the SKILL.
|
|
2920
|
+
func showNotificationsPanel() async {
|
|
2921
|
+
Bridge.log("G2: showNotificationsPanel()")
|
|
2922
|
+
|
|
2923
|
+
let enterPayload = EvenAIProto.aiCtrl(
|
|
2924
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2925
|
+
status: 2 // EVEN_AI_ENTER
|
|
2926
|
+
)
|
|
2927
|
+
sendEvenAICommand(enterPayload)
|
|
2928
|
+
|
|
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)
|
|
2535
2936
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
+
)
|
|
2539
2943
|
}
|
|
2540
2944
|
|
|
2541
|
-
func
|
|
2542
|
-
|
|
2945
|
+
func dbg1() {
|
|
2946
|
+
setCalendarWidgetFirst()
|
|
2947
|
+
// toggleHeyEven()
|
|
2948
|
+
}
|
|
2543
2949
|
|
|
2544
|
-
|
|
2950
|
+
private var compassRunning = false
|
|
2545
2951
|
|
|
2546
|
-
|
|
2547
|
-
//
|
|
2548
|
-
//
|
|
2549
|
-
//
|
|
2550
|
-
//
|
|
2551
|
-
//
|
|
2552
|
-
//
|
|
2952
|
+
func dbg2() {
|
|
2953
|
+
// compassRunning.toggle()
|
|
2954
|
+
// Bridge.log("G2: dbg2() — \(compassRunning ? "start" : "stop") compass")
|
|
2955
|
+
// if compassRunning {
|
|
2956
|
+
// startCompass()
|
|
2957
|
+
// } else {
|
|
2958
|
+
// stopCompass()
|
|
2959
|
+
// }
|
|
2960
|
+
}
|
|
2553
2961
|
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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
|
+
}
|
|
2559
2975
|
|
|
2560
|
-
|
|
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
|
+
}
|
|
2987
|
+
|
|
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
|
+
}
|
|
2561
3007
|
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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
|
+
}
|
|
3016
|
+
|
|
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
|
+
}
|
|
2567
3023
|
}
|
|
2568
3024
|
|
|
2569
3025
|
// MARK: - SGCManager: Device Control
|
|
@@ -2592,10 +3048,41 @@ class G2: NSObject, SGCManager {
|
|
|
2592
3048
|
requestDeviceInfo()
|
|
2593
3049
|
}
|
|
2594
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
|
+
|
|
2595
3082
|
func setDashboardMenu(_ items: [[String: Any]]) {
|
|
2596
3083
|
let menuItems = items.compactMap { dict -> MenuProto.MenuItem? in
|
|
2597
3084
|
guard let name = dict["name"] as? String,
|
|
2598
|
-
|
|
3085
|
+
let packageName = dict["packageName"] as? String
|
|
2599
3086
|
else { return nil }
|
|
2600
3087
|
let running = dict["running"] as? Bool ?? false
|
|
2601
3088
|
return MenuProto.MenuItem(packageName: packageName, name: name, running: running)
|
|
@@ -2630,6 +3117,14 @@ class G2: NSObject, SGCManager {
|
|
|
2630
3117
|
// TODO: Implement via dev_settings
|
|
2631
3118
|
}
|
|
2632
3119
|
|
|
3120
|
+
/// Push the current time to the glasses. Useful after DST transitions,
|
|
3121
|
+
/// time-zone travel, or a long sleep where the glasses' clock has drifted.
|
|
3122
|
+
func syncTime() {
|
|
3123
|
+
Bridge.log("G2: syncTime()")
|
|
3124
|
+
let msg = DevSettingsProto.timeSync(magicRandom: sendManager.nextMagicRandom())
|
|
3125
|
+
sendDevSettingsCommand(msg, left: true, right: true)
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2633
3128
|
func sendRgbLedControl(
|
|
2634
3129
|
requestId _: String, packageName _: String?, action _: String, color _: String?,
|
|
2635
3130
|
onDurationMs _: Int, offDurationMs _: Int, count _: Int
|
|
@@ -2647,7 +3142,8 @@ class G2: NSObject, SGCManager {
|
|
|
2647
3142
|
|
|
2648
3143
|
func requestPhoto(
|
|
2649
3144
|
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
2650
|
-
compress _: String?, flash _: Bool,
|
|
3145
|
+
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?,
|
|
3146
|
+
iso _: Int?
|
|
2651
3147
|
) {}
|
|
2652
3148
|
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
2653
3149
|
func startStream(_: [String: Any]) {}
|
|
@@ -2748,7 +3244,7 @@ class G2: NSObject, SGCManager {
|
|
|
2748
3244
|
centralManager!.scanForPeripherals(
|
|
2749
3245
|
withServices: nil,
|
|
2750
3246
|
options: [
|
|
2751
|
-
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
3247
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
|
2752
3248
|
]
|
|
2753
3249
|
)
|
|
2754
3250
|
return true
|
|
@@ -2766,7 +3262,7 @@ class G2: NSObject, SGCManager {
|
|
|
2766
3262
|
}
|
|
2767
3263
|
|
|
2768
3264
|
guard let leftUUID = leftGlassUUID(forSN: DEVICE_SEARCH_ID),
|
|
2769
|
-
|
|
3265
|
+
let rightUUID = rightGlassUUID(forSN: DEVICE_SEARCH_ID)
|
|
2770
3266
|
else { return false }
|
|
2771
3267
|
|
|
2772
3268
|
let knownLeft = centralManager?.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
@@ -2807,8 +3303,8 @@ class G2: NSObject, SGCManager {
|
|
|
2807
3303
|
// a different service than our primary one, and retrieveConnectedPeripherals only
|
|
2808
3304
|
// returns peripherals whose services match.
|
|
2809
3305
|
let serviceUUIDs: [CBUUID] = [
|
|
2810
|
-
G2BLE.SERVICE_UUID,
|
|
2811
|
-
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
3306
|
+
G2BLE.SERVICE_UUID, // EvenHub: 00002760-...-0000
|
|
3307
|
+
CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"), // Nordic UART
|
|
2812
3308
|
]
|
|
2813
3309
|
var devices: [CBPeripheral] = []
|
|
2814
3310
|
for svc in serviceUUIDs {
|
|
@@ -2836,8 +3332,8 @@ class G2: NSObject, SGCManager {
|
|
|
2836
3332
|
// Extract XX (the numeric ID between G2_ and _L_/_R_)
|
|
2837
3333
|
let pattern = "G2_(\\d+)_"
|
|
2838
3334
|
guard let regex = try? NSRegularExpression(pattern: pattern),
|
|
2839
|
-
|
|
2840
|
-
|
|
3335
|
+
let match = regex.firstMatch(in: name, range: NSRange(name.startIndex..., in: name)),
|
|
3336
|
+
let range = Range(match.range(at: 1), in: name)
|
|
2841
3337
|
else {
|
|
2842
3338
|
return nil
|
|
2843
3339
|
}
|
|
@@ -2846,7 +3342,7 @@ class G2: NSObject, SGCManager {
|
|
|
2846
3342
|
|
|
2847
3343
|
// MARK: - Incoming Data Handling
|
|
2848
3344
|
|
|
2849
|
-
private func handleNotifyData(_ data: Data, from peripheral: CBPeripheral) {
|
|
3345
|
+
private func handleNotifyData(_ data: Data, from peripheral: CBPeripheral) async {
|
|
2850
3346
|
// Distinguish left vs right peripheral so multi-packet reassembly doesn't collide
|
|
2851
3347
|
let sourceKey = peripheral === leftPeripheral ? "L" : "R"
|
|
2852
3348
|
guard let result = receiveManager.handlePacket(data, sourceKey: sourceKey) else { return }
|
|
@@ -2867,16 +3363,79 @@ class G2: NSObject, SGCManager {
|
|
|
2867
3363
|
case ServiceID.dashboard.rawValue:
|
|
2868
3364
|
handleDashboardResponse(result.payload)
|
|
2869
3365
|
case ServiceID.gestureCtrl.rawValue:
|
|
2870
|
-
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)
|
|
2871
3371
|
case ServiceID.evenHubCtrl.rawValue:
|
|
2872
3372
|
handleEvenHubCtrlResponse(result.payload)
|
|
2873
3373
|
default:
|
|
2874
3374
|
Bridge.log(
|
|
2875
|
-
"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())"
|
|
2876
3376
|
)
|
|
2877
3377
|
}
|
|
2878
3378
|
}
|
|
2879
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())"
|
|
3396
|
+
)
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
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
|
+
|
|
2880
3439
|
private func handleEvenHubResponse(_ payload: Data) {
|
|
2881
3440
|
// Parse evenhub_main_msg_ctx: field 1 = Cmd (varint), field 13 = DevEvent (submessage)
|
|
2882
3441
|
var reader = ProtobufReader(payload)
|
|
@@ -2931,6 +3490,16 @@ class G2: NSObject, SGCManager {
|
|
|
2931
3490
|
}
|
|
2932
3491
|
}
|
|
2933
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
|
+
|
|
2934
3503
|
// Log unhandled EvenHub commands (helps debug menu selection and stock dashboard interactions)
|
|
2935
3504
|
// Bridge.log(
|
|
2936
3505
|
// "G2: EvenHub response cmd=\(cmdValue), \(payload.count) bytes, fields=\(Array(fields.keys).sorted())"
|
|
@@ -2949,15 +3518,16 @@ class G2: NSObject, SGCManager {
|
|
|
2949
3518
|
Bridge.log(
|
|
2950
3519
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
2951
3520
|
)
|
|
2952
|
-
startupPageCreated = false
|
|
2953
3521
|
pageCreated = false
|
|
2954
|
-
pageHasTextContainer = false
|
|
2955
|
-
currentTextContent = ""
|
|
2956
3522
|
}
|
|
2957
3523
|
}
|
|
2958
3524
|
if let errorCode = resFields[8] as? Int32 {
|
|
2959
3525
|
// ImgResCmd has ErrorCode in field 8
|
|
2960
|
-
|
|
3526
|
+
if errorCode == 4 {
|
|
3527
|
+
Bridge.log("G2: img_success")
|
|
3528
|
+
} else {
|
|
3529
|
+
Bridge.log("G2: EvenHub ImgRes errorCode=\(errorCode)")
|
|
3530
|
+
}
|
|
2961
3531
|
}
|
|
2962
3532
|
}
|
|
2963
3533
|
}
|
|
@@ -2965,10 +3535,7 @@ class G2: NSObject, SGCManager {
|
|
|
2965
3535
|
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state
|
|
2966
3536
|
if cmdValue == 9 || cmdValue == 10 {
|
|
2967
3537
|
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
2968
|
-
startupPageCreated = false
|
|
2969
3538
|
pageCreated = false
|
|
2970
|
-
pageHasTextContainer = false
|
|
2971
|
-
currentTextContent = ""
|
|
2972
3539
|
}
|
|
2973
3540
|
}
|
|
2974
3541
|
}
|
|
@@ -2997,6 +3564,37 @@ class G2: NSObject, SGCManager {
|
|
|
2997
3564
|
}
|
|
2998
3565
|
}
|
|
2999
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
|
+
|
|
3000
3598
|
private func handleTouchEvent(_ devEventData: Data) {
|
|
3001
3599
|
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
3002
3600
|
var reader = ProtobufReader(devEventData)
|
|
@@ -3015,6 +3613,19 @@ class G2: NSObject, SGCManager {
|
|
|
3015
3613
|
if let sysData = fields[3] as? Data {
|
|
3016
3614
|
var sysReader = ProtobufReader(sysData)
|
|
3017
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
|
+
|
|
3018
3629
|
var eventType: OsEventType? = nil
|
|
3019
3630
|
var eventSource: Int32? = nil
|
|
3020
3631
|
if let normalType = sysFields[1] as? Int32 {
|
|
@@ -3054,7 +3665,8 @@ class G2: NSObject, SGCManager {
|
|
|
3054
3665
|
// trigger dashboard:
|
|
3055
3666
|
let isHeadUp = DeviceStore.shared.get("glasses", "headUp") as? Bool ?? false
|
|
3056
3667
|
|
|
3057
|
-
let useNativeDashboard =
|
|
3668
|
+
let useNativeDashboard =
|
|
3669
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
3058
3670
|
if useNativeDashboard {
|
|
3059
3671
|
showDashboard()
|
|
3060
3672
|
} else {
|
|
@@ -3073,7 +3685,6 @@ class G2: NSObject, SGCManager {
|
|
|
3073
3685
|
|
|
3074
3686
|
// toggle head up:
|
|
3075
3687
|
// DeviceStore.shared.apply("glasses", "headUp", true)
|
|
3076
|
-
// runDashboardSequence()
|
|
3077
3688
|
}
|
|
3078
3689
|
|
|
3079
3690
|
// if eventType == .foregroundEnter {
|
|
@@ -3087,14 +3698,8 @@ class G2: NSObject, SGCManager {
|
|
|
3087
3698
|
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
3088
3699
|
// Reset page state and re-create the page to reclaim EvenHub focus
|
|
3089
3700
|
if eventType == .systemExit || eventType == .abnormalExit {
|
|
3090
|
-
let savedText = currentTextContent
|
|
3091
|
-
let savedBitmap = currentBitmapBase64
|
|
3092
3701
|
// Bridge.log("G2: System exit detected")
|
|
3093
|
-
startupPageCreated = false
|
|
3094
3702
|
pageCreated = false
|
|
3095
|
-
pageHasTextContainer = false
|
|
3096
|
-
currentTextContent = ""
|
|
3097
|
-
currentBitmapBase64 = ""
|
|
3098
3703
|
// Firmware kills the mic on system exit; re-arm it if it should be on
|
|
3099
3704
|
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
3100
3705
|
DeviceManager.shared.updateMicState()
|
|
@@ -3116,7 +3721,7 @@ class G2: NSObject, SGCManager {
|
|
|
3116
3721
|
var textReader = ProtobufReader(textData)
|
|
3117
3722
|
let textFields = textReader.parseFields()
|
|
3118
3723
|
if let eventTypeRaw = textFields[3] as? Int32,
|
|
3119
|
-
|
|
3724
|
+
let eventType = OsEventType(rawValue: eventTypeRaw)
|
|
3120
3725
|
{
|
|
3121
3726
|
guard let gestureName = mapEventTypeToGesture(eventType) else {
|
|
3122
3727
|
Bridge.log("G2: no gesture mapping for \(eventType) \(textFields)")
|
|
@@ -3157,7 +3762,8 @@ class G2: NSObject, SGCManager {
|
|
|
3157
3762
|
case .foregroundEnter: return "foreground_enter"
|
|
3158
3763
|
case .foregroundExit: return "foreground_exit"
|
|
3159
3764
|
case .systemExit: return "system_exit"
|
|
3160
|
-
case .
|
|
3765
|
+
case .imuDataReport: return nil
|
|
3766
|
+
case .abnormalExit: return nil // don't report abnormal exits as gestures
|
|
3161
3767
|
}
|
|
3162
3768
|
}
|
|
3163
3769
|
|
|
@@ -3180,15 +3786,15 @@ class G2: NSObject, SGCManager {
|
|
|
3180
3786
|
|
|
3181
3787
|
// if the data is just a heartbeat, ignore it:
|
|
3182
3788
|
if let cmdValue = fields[1] as? Int32,
|
|
3183
|
-
|
|
3789
|
+
cmdValue == DevCfgCommandId.baseConnHeartBeat.rawValue
|
|
3184
3790
|
{
|
|
3185
3791
|
return
|
|
3186
3792
|
}
|
|
3187
3793
|
// Bridge.log("G2: DevSettings response cmdValue=\(cmdValue)")
|
|
3188
3794
|
|
|
3189
|
-
Bridge.log(
|
|
3190
|
-
|
|
3191
|
-
)
|
|
3795
|
+
// Bridge.log(
|
|
3796
|
+
// "G2: DevSettings response: \(data.prefix(32).map { String(format: "%02X", $0) }.joined(separator: ":"))"
|
|
3797
|
+
// )
|
|
3192
3798
|
|
|
3193
3799
|
// RING_CONNECT_INFO response (cmd 6)
|
|
3194
3800
|
if cmdValue == DevCfgCommandId.ringConnectInfo.rawValue {
|
|
@@ -3204,7 +3810,7 @@ class G2: NSObject, SGCManager {
|
|
|
3204
3810
|
// Bridge.log("G2: Ring connection status: connStat=\(connStat)")
|
|
3205
3811
|
|
|
3206
3812
|
// Bridge.log("G2: RingConnectInfo: \(fields)")
|
|
3207
|
-
if let ringData = fields[5] as? Data {
|
|
3813
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
3208
3814
|
var ringReader = ProtobufReader(ringData)
|
|
3209
3815
|
let ringFields = ringReader.parseFields()
|
|
3210
3816
|
|
|
@@ -3231,13 +3837,13 @@ class G2: NSObject, SGCManager {
|
|
|
3231
3837
|
// DeviceStore.shared.apply("glasses", "controllerSearching", true)
|
|
3232
3838
|
// }
|
|
3233
3839
|
|
|
3234
|
-
if let ringData = fields[5] as? Data {
|
|
3840
|
+
if let ringData = fields[5] as? Data { // field 5 = ringInfo
|
|
3235
3841
|
var ringReader = ProtobufReader(ringData)
|
|
3236
3842
|
let ringFields = ringReader.parseFields()
|
|
3237
|
-
let connStatus = ringFields[4] as? Int32 ?? -1
|
|
3238
|
-
Bridge.log(
|
|
3239
|
-
|
|
3240
|
-
)
|
|
3843
|
+
let connStatus = ringFields[4] as? Int32 ?? -1 // field 4 = connStatus
|
|
3844
|
+
// Bridge.log(
|
|
3845
|
+
// "G2: Ring connection status: connStatus?=\(connStatus))"
|
|
3846
|
+
// )
|
|
3241
3847
|
|
|
3242
3848
|
if connStatus == 22 {
|
|
3243
3849
|
Bridge.log("G2: Ring disconnected")
|
|
@@ -3338,13 +3944,13 @@ class G2: NSObject, SGCManager {
|
|
|
3338
3944
|
|
|
3339
3945
|
// Software versions
|
|
3340
3946
|
if let leftVer = fields[5] as? Data,
|
|
3341
|
-
|
|
3947
|
+
let leftVersion = String(data: leftVer, encoding: .utf8)
|
|
3342
3948
|
{
|
|
3343
3949
|
// Bridge.log("G2: Left firmware: \(leftVersion)")
|
|
3344
3950
|
DeviceStore.shared.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
3345
3951
|
}
|
|
3346
3952
|
if let rightVer = fields[6] as? Data,
|
|
3347
|
-
|
|
3953
|
+
let rightVersion = String(data: rightVer, encoding: .utf8)
|
|
3348
3954
|
{
|
|
3349
3955
|
// Bridge.log("G2: Right firmware: \(rightVersion)")
|
|
3350
3956
|
DeviceStore.shared.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
@@ -3363,7 +3969,7 @@ class G2: NSObject, SGCManager {
|
|
|
3363
3969
|
|
|
3364
3970
|
private func handleDashboardResponse(_ payload: Data) {
|
|
3365
3971
|
Bridge.log(
|
|
3366
|
-
"G2: dashboard response: \(payload.
|
|
3972
|
+
"G2: dashboard response: \(payload.map { String(format: "%02X", $0) }.joined())"
|
|
3367
3973
|
)
|
|
3368
3974
|
var reader = ProtobufReader(payload)
|
|
3369
3975
|
let fields = reader.parseFields()
|
|
@@ -3382,13 +3988,13 @@ class G2: NSObject, SGCManager {
|
|
|
3382
3988
|
// AppRespondToDashboard: field1=packageId, field2=flag (0=success)
|
|
3383
3989
|
if cmd == 3 {
|
|
3384
3990
|
var appRespW = ProtobufWriter()
|
|
3385
|
-
appRespW.writeInt32Field(1, packageId)
|
|
3386
|
-
appRespW.writeInt32Field(2, 0)
|
|
3991
|
+
appRespW.writeInt32Field(1, packageId) // packageId
|
|
3992
|
+
appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
|
|
3387
3993
|
|
|
3388
3994
|
var pkgW = ProtobufWriter()
|
|
3389
|
-
pkgW.writeInt32Field(1, 4)
|
|
3995
|
+
pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
|
|
3390
3996
|
pkgW.writeInt32Field(2, magicRandom)
|
|
3391
|
-
pkgW.writeMessageField(5, appRespW.data)
|
|
3997
|
+
pkgW.writeMessageField(5, appRespW.data) // field5 = appRespond
|
|
3392
3998
|
sendDashboardCommand(pkgW.data)
|
|
3393
3999
|
}
|
|
3394
4000
|
}
|
|
@@ -3400,7 +4006,7 @@ class G2: NSObject, SGCManager {
|
|
|
3400
4006
|
)
|
|
3401
4007
|
}
|
|
3402
4008
|
|
|
3403
|
-
private func handleGestureCtrl(_ data: Data) {
|
|
4009
|
+
private func handleGestureCtrl(_ data: Data) async {
|
|
3404
4010
|
// gesture_ctrl (service 0x0D): foreground lifecycle signals from glasses
|
|
3405
4011
|
// (informational only — log if needed for debugging)
|
|
3406
4012
|
// log first few bytes of the response:
|
|
@@ -3409,31 +4015,43 @@ class G2: NSObject, SGCManager {
|
|
|
3409
4015
|
// )
|
|
3410
4016
|
// Bridge.log("G2: gesture_ctrl response:")
|
|
3411
4017
|
|
|
4018
|
+
// Dedup: L and R peripherals both deliver this event, so debounce or
|
|
4019
|
+
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
4020
|
+
if lastGestureCtrlTimestamp != nil && timestamp - lastGestureCtrlTimestamp! < 500 {
|
|
4021
|
+
// Bridge.log("G2: gesture_ctrl dedup")
|
|
4022
|
+
return
|
|
4023
|
+
}
|
|
4024
|
+
lastGestureCtrlTimestamp = timestamp
|
|
4025
|
+
|
|
3412
4026
|
// if we got 08011A00 that means we closed the dashboard, which means the mic is probably dead,
|
|
3413
4027
|
// so we need to revive it:
|
|
3414
4028
|
if data == Data([0x08, 0x01, 0x1A, 0x00]) {
|
|
3415
4029
|
Bridge.log("G2: dashboard closed / shutdown - dashboardShowing=\(dashboardShowing)")
|
|
3416
|
-
let useNativeDashboard =
|
|
4030
|
+
let useNativeDashboard =
|
|
4031
|
+
DeviceStore.shared.get("bluetooth", "use_native_dashboard") as? Bool ?? false
|
|
3417
4032
|
if !useNativeDashboard {
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
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
|
+
}
|
|
3424
4043
|
}
|
|
3425
|
-
|
|
3426
|
-
// sendTextWall(" ")
|
|
3427
|
-
// createPageWithText(" ")
|
|
4044
|
+
return
|
|
3428
4045
|
} else {
|
|
3429
4046
|
// if we aren't trying to show the dashboard
|
|
3430
4047
|
// then we need to turn the mic back on and display the mentra main page:
|
|
3431
4048
|
if dashboardShowing <= 1 {
|
|
3432
4049
|
dashboardShowing = 0
|
|
3433
|
-
//
|
|
3434
|
-
|
|
4050
|
+
// rebuild state:
|
|
4051
|
+
await rebuildState()
|
|
3435
4052
|
// set the mic back on if it should be on
|
|
3436
|
-
let micEnabled =
|
|
4053
|
+
let micEnabled =
|
|
4054
|
+
DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
3437
4055
|
if micEnabled {
|
|
3438
4056
|
restartMic()
|
|
3439
4057
|
}
|
|
@@ -3466,7 +4084,7 @@ class G2: NSObject, SGCManager {
|
|
|
3466
4084
|
|
|
3467
4085
|
private var lastAudioFrame: Data?
|
|
3468
4086
|
|
|
3469
|
-
private func handleAudioData(_ data: Data) {
|
|
4087
|
+
private func handleAudioData(_ data: Data) async {
|
|
3470
4088
|
// G2 audio arrives on AUDIO_NOTIFY characteristic
|
|
3471
4089
|
// Format: ~200+ byte chunks, use first 200 bytes, split into 40-byte LC3 frames
|
|
3472
4090
|
// Each frame: LC3, 16kHz, mono, 10ms, 40 bytes
|
|
@@ -3496,7 +4114,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
3496
4114
|
// where the SN string starts.
|
|
3497
4115
|
|
|
3498
4116
|
// Skip "ER" prefix (2 bytes), read 14 bytes of SN
|
|
3499
|
-
let snData = data[2
|
|
4117
|
+
let snData = data[2..<16]
|
|
3500
4118
|
return String(data: snData, encoding: .ascii)?
|
|
3501
4119
|
.replacingOccurrences(
|
|
3502
4120
|
of: "[\\x00-\\x1F\\x7F]", with: "", options: .regularExpression
|
|
@@ -3508,7 +4126,7 @@ func extractSN(from data: Data) -> String? {
|
|
|
3508
4126
|
/// Returns "AA:BB:CC:DD:EE:FF" (big-endian, colon-separated).
|
|
3509
4127
|
func extractMac(from data: Data) -> String? {
|
|
3510
4128
|
guard data.count >= 22 else { return nil }
|
|
3511
|
-
let macLE = data[16
|
|
4129
|
+
let macLE = data[16..<22]
|
|
3512
4130
|
return macLE.reversed().map { String(format: "%02X", $0) }.joined(separator: ":")
|
|
3513
4131
|
}
|
|
3514
4132
|
|
|
@@ -3532,13 +4150,13 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3532
4150
|
) {
|
|
3533
4151
|
guard
|
|
3534
4152
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey]
|
|
3535
|
-
|
|
4153
|
+
as? String
|
|
3536
4154
|
else { return }
|
|
3537
4155
|
|
|
3538
4156
|
// G2 glasses have "Even" prefix and "G2" in name, with _L_ or _R_ for side
|
|
3539
4157
|
guard name.contains("G2") else { return }
|
|
3540
4158
|
guard let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
3541
|
-
|
|
4159
|
+
mfgData.count >= 16
|
|
3542
4160
|
else { return }
|
|
3543
4161
|
|
|
3544
4162
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -3550,7 +4168,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3550
4168
|
}
|
|
3551
4169
|
// sn = "S200LACA040040"
|
|
3552
4170
|
let mfgHex = mfgData.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
3553
|
-
Bridge.log(
|
|
4171
|
+
Bridge.log(
|
|
4172
|
+
"G2: Discovered: \(name) (SN: \(serialNumber)) mfgData[\(mfgData.count)]: \(mfgHex)"
|
|
4173
|
+
)
|
|
3554
4174
|
self.deviceNameToSerialNumber[name] = serialNumber
|
|
3555
4175
|
|
|
3556
4176
|
// Save MAC per side; ring's advStart needs the left lens MAC.
|
|
@@ -3613,7 +4233,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3613
4233
|
self.setRightGlassUUID(peripheral.identifier, forSN: sn)
|
|
3614
4234
|
}
|
|
3615
4235
|
} else {
|
|
3616
|
-
Bridge.log(
|
|
4236
|
+
Bridge.log(
|
|
4237
|
+
"G2: didConnect — no SN for \(peripheral.name ?? "unknown"), skipping UUID save"
|
|
4238
|
+
)
|
|
3617
4239
|
}
|
|
3618
4240
|
|
|
3619
4241
|
// Discover services - scan for all since we need to find the EvenHub characteristics
|
|
@@ -3647,7 +4269,7 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3647
4269
|
|
|
3648
4270
|
self.startupPageCreated = false
|
|
3649
4271
|
self.pageCreated = false
|
|
3650
|
-
self.
|
|
4272
|
+
self.dashboardShowing = 0
|
|
3651
4273
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
3652
4274
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
3653
4275
|
|
|
@@ -3662,7 +4284,9 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3662
4284
|
guard let self else { return false }
|
|
3663
4285
|
|
|
3664
4286
|
// Check if already connected
|
|
3665
|
-
if await MainActor.run(body: {
|
|
4287
|
+
if await MainActor.run(body: {
|
|
4288
|
+
DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
4289
|
+
}) {
|
|
3666
4290
|
Bridge.log("G2: Already connected, stopping reconnection")
|
|
3667
4291
|
return true
|
|
3668
4292
|
}
|
|
@@ -3754,7 +4378,7 @@ extension G2: CBPeripheralDelegate {
|
|
|
3754
4378
|
if self.leftInitialized && self.rightInitialized && !self.authStarted {
|
|
3755
4379
|
self.authStarted = true
|
|
3756
4380
|
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
3757
|
-
self.runAuthSequence()
|
|
4381
|
+
Task { await self.runAuthSequence() }
|
|
3758
4382
|
}
|
|
3759
4383
|
}
|
|
3760
4384
|
}
|
|
@@ -3765,15 +4389,15 @@ extension G2: CBPeripheralDelegate {
|
|
|
3765
4389
|
) {
|
|
3766
4390
|
guard let data = characteristic.value, error == nil else { return }
|
|
3767
4391
|
|
|
3768
|
-
|
|
4392
|
+
Task { @MainActor [weak self] in
|
|
3769
4393
|
guard let self = self else { return }
|
|
3770
4394
|
|
|
3771
4395
|
if characteristic.uuid == G2BLE.AUDIO_NOTIFY {
|
|
3772
4396
|
// Audio data - forward to mic system
|
|
3773
|
-
self.handleAudioData(data)
|
|
4397
|
+
await self.handleAudioData(data)
|
|
3774
4398
|
} else if characteristic.uuid == G2BLE.CHAR_NOTIFY {
|
|
3775
4399
|
// Protocol data
|
|
3776
|
-
self.handleNotifyData(data, from: peripheral)
|
|
4400
|
+
await self.handleNotifyData(data, from: peripheral)
|
|
3777
4401
|
}
|
|
3778
4402
|
}
|
|
3779
4403
|
}
|