@mentra/bluetooth-sdk 0.1.9 → 0.1.10
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 +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -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 +123 -10
- 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 +569 -220
- 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 +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -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 +241 -43
- 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 +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -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 +23 -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 +174 -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 +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- 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 +225 -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 +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- 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 +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -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
|
@@ -53,6 +53,7 @@ private enum G2BLE {
|
|
|
53
53
|
private enum ServiceID: UInt8 {
|
|
54
54
|
case dashboard = 1 // 0x01 - UI_BACKGROUND_DASHBOARD_APP_ID
|
|
55
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
|
|
56
57
|
case evenAI = 7 // 0x07 - UI_FOREGROUND_EVEN_AI_ID
|
|
57
58
|
case g2Setting = 9 // 0x09 - UI_SETTING_APP_ID
|
|
58
59
|
case gestureCtrl = 13 // 0x0D - gesture_ctrl lifecycle signals
|
|
@@ -150,6 +151,12 @@ private struct ProtobufWriter {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
mutating func writeInt64Field(_ fieldNumber: Int, _ value: Int64) {
|
|
155
|
+
let tag = UInt64(fieldNumber << 3) | 0 // wire type 0 = varint
|
|
156
|
+
writeVarint(tag)
|
|
157
|
+
writeVarint(UInt64(bitPattern: value))
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
mutating func writeStringField(_ fieldNumber: Int, _ value: String) {
|
|
154
161
|
let tag = UInt64(fieldNumber << 3) | 2 // wire type 2 = length-delimited
|
|
155
162
|
writeVarint(tag)
|
|
@@ -518,22 +525,51 @@ private enum DevSettingsProto {
|
|
|
518
525
|
return w.data
|
|
519
526
|
}
|
|
520
527
|
|
|
521
|
-
/// DevCfgDataPackage with TIME_SYNC command
|
|
528
|
+
/// DevCfgDataPackage with TIME_SYNC command.
|
|
529
|
+
/// TimeSync submessage: f1 = (Unix seconds + TZ offset seconds) as Int32, no TZ field.
|
|
530
|
+
/// Firmware appears to ignore the TZ field, so we pre-shift the timestamp itself
|
|
531
|
+
/// to make UTC interpretation read as local. Empirically confirmed via probe variants in dbg1().
|
|
522
532
|
static func timeSync(magicRandom: Int32) -> Data {
|
|
523
533
|
var w = ProtobufWriter()
|
|
524
534
|
w.writeInt32Field(1, DevCfgCommandId.timeSync.rawValue)
|
|
525
535
|
w.writeInt32Field(2, magicRandom)
|
|
526
536
|
|
|
527
|
-
// TimeSync: field 1 = timestamp (int32), field 2 = timezone (int32)
|
|
528
537
|
var tsW = ProtobufWriter()
|
|
529
|
-
let
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
tsW.writeInt32Field(2, tz)
|
|
538
|
+
let nowSec = Int64(Date().timeIntervalSince1970)
|
|
539
|
+
let tzSec = Int64(TimeZone.current.secondsFromGMT())
|
|
540
|
+
tsW.writeInt32Field(1, Int32(truncatingIfNeeded: nowSec + tzSec))
|
|
533
541
|
w.writeMessageField(128, tsW.data) // timeSync (field 128 in DevCfgDataPackage)
|
|
534
542
|
return w.data
|
|
535
543
|
}
|
|
536
544
|
|
|
545
|
+
/// Parameterized TIME_SYNC for probing the right wire format from dbg1().
|
|
546
|
+
/// - tsField: protobuf field # for the timestamp varint (typically 1)
|
|
547
|
+
/// - tsValue: raw timestamp varint value
|
|
548
|
+
/// - tsBits64: encode timestamp as Int64 (true) or Int32 (false)
|
|
549
|
+
/// - tzField: protobuf field # for TZ (nil to omit entirely)
|
|
550
|
+
/// - tzValue: TZ value to write if tzField != nil
|
|
551
|
+
static func timeSyncVariant(
|
|
552
|
+
magicRandom: Int32,
|
|
553
|
+
tsField: Int, tsValue: Int64, tsBits64: Bool,
|
|
554
|
+
tzField: Int?, tzValue: Int32
|
|
555
|
+
) -> Data {
|
|
556
|
+
var w = ProtobufWriter()
|
|
557
|
+
w.writeInt32Field(1, DevCfgCommandId.timeSync.rawValue)
|
|
558
|
+
w.writeInt32Field(2, magicRandom)
|
|
559
|
+
|
|
560
|
+
var tsW = ProtobufWriter()
|
|
561
|
+
if tsBits64 {
|
|
562
|
+
tsW.writeInt64Field(tsField, tsValue)
|
|
563
|
+
} else {
|
|
564
|
+
tsW.writeInt32Field(tsField, Int32(truncatingIfNeeded: tsValue))
|
|
565
|
+
}
|
|
566
|
+
if let tzField = tzField {
|
|
567
|
+
tsW.writeInt32Field(tzField, tzValue)
|
|
568
|
+
}
|
|
569
|
+
w.writeMessageField(128, tsW.data)
|
|
570
|
+
return w.data
|
|
571
|
+
}
|
|
572
|
+
|
|
537
573
|
/// DevCfgDataPackage with BASE_CONNECT_HEART_BEAT command
|
|
538
574
|
static func baseHeartbeat(magicRandom: Int32) -> Data {
|
|
539
575
|
var w = ProtobufWriter()
|
|
@@ -716,6 +752,108 @@ private enum EvenAIProto {
|
|
|
716
752
|
w.writeMessageField(13, configW.data) // config (field 13)
|
|
717
753
|
return w.data
|
|
718
754
|
}
|
|
755
|
+
|
|
756
|
+
/// EvenAIDataPackage with ASK command — what the phone sends after cloud
|
|
757
|
+
/// ASR resolves the user's audio into text. Mirrors Flutter `sendAsr`:
|
|
758
|
+
/// `EvenAIAskInfo { text, streamEnable=0 }`. Used to inject an ASR result
|
|
759
|
+
/// into the glasses' AI session so the following SKILL packet has context.
|
|
760
|
+
static func aiAsk(magicRandom: Int32, text: String, streamEnable: Int32 = 0) -> Data {
|
|
761
|
+
var askW = ProtobufWriter()
|
|
762
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
763
|
+
askW.writeBytesField(4, Data(text.utf8)) // text
|
|
764
|
+
|
|
765
|
+
var w = ProtobufWriter()
|
|
766
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
767
|
+
w.writeInt32Field(2, magicRandom)
|
|
768
|
+
w.writeMessageField(5, askW.data) // askInfo (field 5)
|
|
769
|
+
return w.data
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/// EvenAIDataPackage with CTRL command — used to put glasses into / out of
|
|
773
|
+
/// an AI session. Mirrors Flutter `sendWakeupResp`, which sends
|
|
774
|
+
/// `EvenAIControl { status = EVEN_AI_ENTER }` after the glasses send WAKE_UP.
|
|
775
|
+
/// status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
776
|
+
static func aiCtrl(magicRandom: Int32, status: Int32) -> Data {
|
|
777
|
+
var ctrlW = ProtobufWriter()
|
|
778
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
779
|
+
|
|
780
|
+
var w = ProtobufWriter()
|
|
781
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
782
|
+
w.writeInt32Field(2, magicRandom)
|
|
783
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
784
|
+
return w.data
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/// EvenAIDataPackage with SKILL command — triggers a built-in glasses UI
|
|
788
|
+
/// the same way the "Hey Even, show X" voice command does.
|
|
789
|
+
/// skillId values (per even_ai.proto):
|
|
790
|
+
/// 0 SKILL_NONE, 1 BRIGHTNESS, 2 TRANSLATE_CTRL, 3 NOTIFICATION,
|
|
791
|
+
/// 4 TELEPROMPT, 5 NAVIGATE, 6 CONVERSATE, 7 QUICKLIST, 8 AUTO_BRIGHTNESS
|
|
792
|
+
static func triggerSkill(
|
|
793
|
+
magicRandom: Int32, skillId: Int32, skillParam: Int32 = 0,
|
|
794
|
+
text: String = "", streamEnable: Int32 = 1, fTextEnd: Int32 = 1
|
|
795
|
+
) -> Data {
|
|
796
|
+
// EvenAISkillInfo
|
|
797
|
+
var skillW = ProtobufWriter()
|
|
798
|
+
skillW.writeInt32Field(1, streamEnable) // streamEnable
|
|
799
|
+
skillW.writeInt32Field(2, skillId) // skillId
|
|
800
|
+
skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
|
|
801
|
+
skillW.writeBytesField(4, Data(text.utf8)) // text (utterance / payload)
|
|
802
|
+
skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
|
|
803
|
+
|
|
804
|
+
// EvenAIDataPackage
|
|
805
|
+
var w = ProtobufWriter()
|
|
806
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
807
|
+
w.writeInt32Field(2, magicRandom)
|
|
808
|
+
w.writeMessageField(8, skillW.data) // skillInfo (field 8)
|
|
809
|
+
return w.data
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// MARK: - Notification Protobuf Builders (notification.proto, service ID 4)
|
|
814
|
+
|
|
815
|
+
private enum NotificationProto {
|
|
816
|
+
/// NotificationDataPackage with commandId=NOTIFICATION_IOS (2), carrying
|
|
817
|
+
/// `NotificationIOS { appID, displayName }`. We saw the glasses emit this
|
|
818
|
+
/// inbound after "Hey Even, show notifications" with appID="com.burbn.instagram";
|
|
819
|
+
/// trying the same shape outbound to see if the glasses display it.
|
|
820
|
+
/// (Returned errorCode=8 NOT_SUPPORT in testing — Service 4 doesn't accept this outbound.)
|
|
821
|
+
static func iosNotification(magicRandom: Int32, appID: String, displayName: String) -> Data {
|
|
822
|
+
var iosW = ProtobufWriter()
|
|
823
|
+
iosW.writeBytesField(1, Data(appID.utf8)) // appID
|
|
824
|
+
iosW.writeBytesField(2, Data(displayName.utf8)) // displayName
|
|
825
|
+
|
|
826
|
+
var w = ProtobufWriter()
|
|
827
|
+
w.writeInt32Field(1, 2) // commandId = NOTIFICATION_IOS
|
|
828
|
+
w.writeInt32Field(2, magicRandom)
|
|
829
|
+
w.writeMessageField(4, iosW.data) // IOS (field 4)
|
|
830
|
+
return w.data
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/// NotificationDataPackage with commandId=NOTIFICATION_CTRL (1), carrying
|
|
834
|
+
/// `NotificationControl { notifEnable, autoDispEnable, dispTime, avoidDisturbEnable }`.
|
|
835
|
+
/// Per Flutter `ProtoNotificationExt.settingNotification` this is how the
|
|
836
|
+
/// official app configures notification behavior on the glasses. Worth
|
|
837
|
+
/// testing whether toggling notifEnable also opens the notification panel.
|
|
838
|
+
static func notificationCtrl(
|
|
839
|
+
magicRandom: Int32,
|
|
840
|
+
notifEnable: Int32 = 1,
|
|
841
|
+
autoDispEnable: Int32 = 1,
|
|
842
|
+
dispTime: Int32 = 5,
|
|
843
|
+
avoidDisturbEnable: Int32 = 0
|
|
844
|
+
) -> Data {
|
|
845
|
+
var ctrlW = ProtobufWriter()
|
|
846
|
+
ctrlW.writeInt32Field(1, notifEnable) // notifEnable
|
|
847
|
+
ctrlW.writeInt32Field(2, autoDispEnable) // autoDispEnable
|
|
848
|
+
ctrlW.writeInt32Field(3, dispTime) // dispTime (seconds)
|
|
849
|
+
ctrlW.writeInt32Field(5, avoidDisturbEnable) // avoidDisturbEnable
|
|
850
|
+
|
|
851
|
+
var w = ProtobufWriter()
|
|
852
|
+
w.writeInt32Field(1, 1) // commandId = NOTIFICATION_CTRL
|
|
853
|
+
w.writeInt32Field(2, magicRandom)
|
|
854
|
+
w.writeMessageField(3, ctrlW.data) // ctrl (field 3)
|
|
855
|
+
return w.data
|
|
856
|
+
}
|
|
719
857
|
}
|
|
720
858
|
|
|
721
859
|
// MARK: - Menu Protobuf Builders (menu.proto, service ID 3)
|
|
@@ -822,6 +960,142 @@ private enum MenuProto {
|
|
|
822
960
|
}
|
|
823
961
|
}
|
|
824
962
|
|
|
963
|
+
// MARK: - Dashboard Protobuf Builders (dashboard.proto, service ID 1)
|
|
964
|
+
|
|
965
|
+
/// Builders for the dashboard widget service (service 0x01).
|
|
966
|
+
/// Field numbers come from the extracted dashboard.proto v2.1.0_beta_v3.
|
|
967
|
+
private enum DashboardProto {
|
|
968
|
+
/// eDashboardCommandId values from dashboard.proto
|
|
969
|
+
enum CommandId: Int32 {
|
|
970
|
+
case dashboardRespond = 1
|
|
971
|
+
case dashboardReceive = 2 // phone → glasses widget/config push
|
|
972
|
+
case appRespond = 3
|
|
973
|
+
case appReceive = 4
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/// Build a Schedule submessage (the calendar event payload).
|
|
977
|
+
/// f1 = scheduleId (int32, required)
|
|
978
|
+
/// f2 = title (string, optional)
|
|
979
|
+
/// f3 = location (string, optional)
|
|
980
|
+
/// f4 = time (string, optional — display text e.g. "10:00 AM")
|
|
981
|
+
/// f5 = endTimestamp (int32, presumed Unix seconds — pre-shift by TZ
|
|
982
|
+
/// to match the time-sync hack so glasses display local time)
|
|
983
|
+
static func schedule(
|
|
984
|
+
scheduleId: Int32,
|
|
985
|
+
title: String?,
|
|
986
|
+
location: String?,
|
|
987
|
+
time: String?,
|
|
988
|
+
endTimestamp: Int32
|
|
989
|
+
) -> Data {
|
|
990
|
+
var w = ProtobufWriter()
|
|
991
|
+
w.writeInt32Field(1, scheduleId)
|
|
992
|
+
if let title = title { w.writeStringField(2, title) }
|
|
993
|
+
if let location = location { w.writeStringField(3, location) }
|
|
994
|
+
if let time = time { w.writeStringField(4, time) }
|
|
995
|
+
w.writeInt32Field(5, endTimestamp)
|
|
996
|
+
return w.data
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/// Build an rScheduleWidget wrapping a single Schedule.
|
|
1000
|
+
/// f1 = scheduleTotal, f2 = scheduleNum (0-based), f3 = Schedule, f4 = scheduleAuthority
|
|
1001
|
+
static func rScheduleWidget(
|
|
1002
|
+
scheduleTotal: Int32,
|
|
1003
|
+
scheduleNum: Int32,
|
|
1004
|
+
schedule: Data,
|
|
1005
|
+
scheduleAuthority: Int32
|
|
1006
|
+
) -> Data {
|
|
1007
|
+
var w = ProtobufWriter()
|
|
1008
|
+
w.writeInt32Field(1, scheduleTotal)
|
|
1009
|
+
w.writeInt32Field(2, scheduleNum)
|
|
1010
|
+
w.writeMessageField(3, schedule)
|
|
1011
|
+
w.writeInt32Field(4, scheduleAuthority)
|
|
1012
|
+
return w.data
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/// Build the full calendar-push DashboardDataPackage:
|
|
1016
|
+
/// DashboardDataPackage {
|
|
1017
|
+
/// commandId = Dashboard_Receive (2)
|
|
1018
|
+
/// magicRandom
|
|
1019
|
+
/// dashboardReceive = DashboardReceiveFromApp {
|
|
1020
|
+
/// packageId = 1
|
|
1021
|
+
/// bashboardConfig = DashboardContent {
|
|
1022
|
+
/// widgetComponents = rWidgetComponent {
|
|
1023
|
+
/// schedule = rScheduleWidget { ... }
|
|
1024
|
+
/// }
|
|
1025
|
+
/// }
|
|
1026
|
+
/// }
|
|
1027
|
+
/// }
|
|
1028
|
+
static func calendarPush(
|
|
1029
|
+
magicRandom: Int32,
|
|
1030
|
+
packageId: Int32,
|
|
1031
|
+
scheduleId: Int32,
|
|
1032
|
+
title: String?,
|
|
1033
|
+
location: String?,
|
|
1034
|
+
time: String?,
|
|
1035
|
+
endTimestamp: Int32,
|
|
1036
|
+
scheduleAuthority: Int32,
|
|
1037
|
+
scheduleTotal: Int32 = 1,
|
|
1038
|
+
scheduleNum: Int32 = 0
|
|
1039
|
+
) -> Data {
|
|
1040
|
+
let sched = schedule(
|
|
1041
|
+
scheduleId: scheduleId, title: title, location: location,
|
|
1042
|
+
time: time, endTimestamp: endTimestamp
|
|
1043
|
+
)
|
|
1044
|
+
let rSched = rScheduleWidget(
|
|
1045
|
+
scheduleTotal: scheduleTotal, scheduleNum: scheduleNum,
|
|
1046
|
+
schedule: sched, scheduleAuthority: scheduleAuthority
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
// rWidgetComponent { f3 = rScheduleWidget }
|
|
1050
|
+
var rWidget = ProtobufWriter()
|
|
1051
|
+
rWidget.writeMessageField(3, rSched)
|
|
1052
|
+
|
|
1053
|
+
// DashboardContent { f2 = rWidgetComponent }
|
|
1054
|
+
var content = ProtobufWriter()
|
|
1055
|
+
content.writeMessageField(2, rWidget.data)
|
|
1056
|
+
|
|
1057
|
+
// DashboardReceiveFromApp { f1 = packageId, f3 = DashboardContent }
|
|
1058
|
+
var receive = ProtobufWriter()
|
|
1059
|
+
receive.writeInt32Field(1, packageId)
|
|
1060
|
+
receive.writeMessageField(3, content.data)
|
|
1061
|
+
|
|
1062
|
+
// DashboardDataPackage { f1 = commandId, f2 = magicRandom, f4 = dashboardReceive }
|
|
1063
|
+
var pkg = ProtobufWriter()
|
|
1064
|
+
pkg.writeInt32Field(1, CommandId.dashboardReceive.rawValue)
|
|
1065
|
+
pkg.writeInt32Field(2, magicRandom)
|
|
1066
|
+
pkg.writeMessageField(4, receive.data)
|
|
1067
|
+
return pkg.data
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
static func calendarClear(
|
|
1071
|
+
magicRandom: Int32,
|
|
1072
|
+
packageId: Int32,
|
|
1073
|
+
scheduleAuthority: Int32
|
|
1074
|
+
) -> Data {
|
|
1075
|
+
// rScheduleWidget with scheduleTotal=0 clears the widget without sending a stale Schedule.
|
|
1076
|
+
var rSched = ProtobufWriter()
|
|
1077
|
+
rSched.writeInt32Field(1, 0)
|
|
1078
|
+
rSched.writeInt32Field(2, 0)
|
|
1079
|
+
rSched.writeInt32Field(4, scheduleAuthority)
|
|
1080
|
+
|
|
1081
|
+
var rWidget = ProtobufWriter()
|
|
1082
|
+
rWidget.writeMessageField(3, rSched.data)
|
|
1083
|
+
|
|
1084
|
+
var content = ProtobufWriter()
|
|
1085
|
+
content.writeMessageField(2, rWidget.data)
|
|
1086
|
+
|
|
1087
|
+
var receive = ProtobufWriter()
|
|
1088
|
+
receive.writeInt32Field(1, packageId)
|
|
1089
|
+
receive.writeMessageField(3, content.data)
|
|
1090
|
+
|
|
1091
|
+
var pkg = ProtobufWriter()
|
|
1092
|
+
pkg.writeInt32Field(1, CommandId.dashboardReceive.rawValue)
|
|
1093
|
+
pkg.writeInt32Field(2, magicRandom)
|
|
1094
|
+
pkg.writeMessageField(4, receive.data)
|
|
1095
|
+
return pkg.data
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
825
1099
|
// MARK: - EvenBLE Transport Layer
|
|
826
1100
|
|
|
827
1101
|
/// Builds and splits payloads into BLE packets with the EvenHub transport framing
|
|
@@ -1136,6 +1410,28 @@ class G2: NSObject, SGCManager {
|
|
|
1136
1410
|
private var activeMenuAppId: Int32?
|
|
1137
1411
|
private var lastClickTimestamp: Int64?
|
|
1138
1412
|
private var lastMenuSelectTimestamp: Int64?
|
|
1413
|
+
private var lastGestureCtrlTimestamp: Int64?
|
|
1414
|
+
|
|
1415
|
+
/// A tracked image container on the current page. Keyed by its rect for reuse.
|
|
1416
|
+
private struct ImgContainer: Equatable {
|
|
1417
|
+
let id: Int32
|
|
1418
|
+
let x: Int32
|
|
1419
|
+
let y: Int32
|
|
1420
|
+
let width: Int32
|
|
1421
|
+
let height: Int32
|
|
1422
|
+
var name: String { "img-\(id)" }
|
|
1423
|
+
func matches(x: Int32, y: Int32, width: Int32, height: Int32) -> Bool {
|
|
1424
|
+
self.x == x && self.y == y && self.width == width && self.height == height
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/// Live list of image containers on the page, ordered oldest→newest (for LRU eviction).
|
|
1429
|
+
/// The page may hold at most 4 image containers (IDs from the pool below).
|
|
1430
|
+
private var imageContainers: [ImgContainer] = []
|
|
1431
|
+
/// Fixed pool of container IDs the page protocol expects.
|
|
1432
|
+
private let imageContainerIDPool: [Int32] = [10, 11, 12, 13]
|
|
1433
|
+
/// Default container seeded into every fresh page: 100x100 in the top-left.
|
|
1434
|
+
private static let defaultImgContainer = (x: Int32(288), y: Int32(144), width: Int32(200), height: Int32(100))
|
|
1139
1435
|
|
|
1140
1436
|
@Published var aiListening: Bool = false
|
|
1141
1437
|
|
|
@@ -1213,6 +1509,15 @@ class G2: NSObject, SGCManager {
|
|
|
1213
1509
|
sendToGlasses(packets)
|
|
1214
1510
|
}
|
|
1215
1511
|
|
|
1512
|
+
private func sendNotificationCommand(_ payload: Data) {
|
|
1513
|
+
let packets = sendManager.buildPackets(
|
|
1514
|
+
serviceId: ServiceID.notification.rawValue,
|
|
1515
|
+
payload: payload,
|
|
1516
|
+
reserveFlag: true
|
|
1517
|
+
)
|
|
1518
|
+
sendToGlasses(packets)
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1216
1521
|
private func sendMenuCommand(_ payload: Data) {
|
|
1217
1522
|
let packets = sendManager.buildPackets(
|
|
1218
1523
|
serviceId: ServiceID.menu.rawValue,
|
|
@@ -1246,7 +1551,7 @@ class G2: NSObject, SGCManager {
|
|
|
1246
1551
|
payload: payload,
|
|
1247
1552
|
reserveFlag: true
|
|
1248
1553
|
)
|
|
1249
|
-
sendToGlasses(packets)
|
|
1554
|
+
sendToGlasses(packets, left: true, right: true)
|
|
1250
1555
|
}
|
|
1251
1556
|
|
|
1252
1557
|
// MARK: - Authentication Sequence
|
|
@@ -1327,9 +1632,9 @@ class G2: NSObject, SGCManager {
|
|
|
1327
1632
|
0x4A, 0x0A, // field 9, length 10
|
|
1328
1633
|
0x08, 0x00, // unitFormat=0
|
|
1329
1634
|
0x10, 0x00, // distanceUnit=0
|
|
1330
|
-
0x18,
|
|
1635
|
+
0x18, UInt8(self.dashboardHalfDayFormat()), // timeFormat / halfDayFormat
|
|
1331
1636
|
0x20, 0x00, // dateFormat=0
|
|
1332
|
-
0x28,
|
|
1637
|
+
0x28, UInt8(self.dashboardTemperatureUnit()), // temperatureUnit
|
|
1333
1638
|
])
|
|
1334
1639
|
)
|
|
1335
1640
|
self.sendG2SettingCommand(univW.data)
|
|
@@ -1352,45 +1657,48 @@ class G2: NSObject, SGCManager {
|
|
|
1352
1657
|
)
|
|
1353
1658
|
|
|
1354
1659
|
// 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
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
1378
|
-
)
|
|
1379
|
-
self.sendToGlasses(
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
)
|
|
1660
|
+
// var teleW = ProtobufWriter()
|
|
1661
|
+
// teleW.writeInt32Field(1, 1)
|
|
1662
|
+
// teleW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1663
|
+
// teleW.writeMessageField(3, Data([0x08, 0x04])) // {1:4}
|
|
1664
|
+
// self.sendToGlasses(
|
|
1665
|
+
// self.sendManager.buildPackets(
|
|
1666
|
+
// serviceId: 0x10, payload: teleW.data, reserveFlag: true
|
|
1667
|
+
// )
|
|
1668
|
+
// )
|
|
1669
|
+
|
|
1670
|
+
// // 4. EvenHub CTRL on service 0x81 (cmd=1, empty field3)
|
|
1671
|
+
// var ehCtrlW = ProtobufWriter()
|
|
1672
|
+
// ehCtrlW.writeInt32Field(1, 1)
|
|
1673
|
+
// ehCtrlW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1674
|
+
// ehCtrlW.writeMessageField(3, Data())
|
|
1675
|
+
// self.sendEvenHubCtrlCommand(ehCtrlW.data)
|
|
1676
|
+
|
|
1677
|
+
// // 5. calendar (0x04) — config
|
|
1678
|
+
// var calW = ProtobufWriter()
|
|
1679
|
+
// calW.writeInt32Field(1, 1)
|
|
1680
|
+
// calW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1681
|
+
// calW.writeMessageField(
|
|
1682
|
+
// 3, Data([0x08, 0x01, 0x10, 0x01, 0x18, 0x05, 0x28, 0x01])
|
|
1683
|
+
// )
|
|
1684
|
+
// self.sendToGlasses(
|
|
1685
|
+
// self.sendManager.buildPackets(
|
|
1686
|
+
// serviceId: 0x04, payload: calW.data, reserveFlag: true
|
|
1687
|
+
// )
|
|
1688
|
+
// )
|
|
1384
1689
|
|
|
1385
1690
|
// 6. Dashboard init (0x01) — display settings
|
|
1691
|
+
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1692
|
+
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1386
1693
|
var dashDisplayW = ProtobufWriter()
|
|
1387
1694
|
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1388
1695
|
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1389
1696
|
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
1390
1697
|
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1391
|
-
|
|
1392
|
-
dashDisplayW.
|
|
1393
|
-
dashDisplayW.writeInt32Field(
|
|
1698
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1699
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5])) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1700
|
+
dashDisplayW.writeInt32Field(6, self.dashboardHalfDayFormat()) // halfDayFormat
|
|
1701
|
+
dashDisplayW.writeInt32Field(7, self.dashboardTemperatureUnit()) // temperatureUnit
|
|
1394
1702
|
|
|
1395
1703
|
var dashRecvW = ProtobufWriter()
|
|
1396
1704
|
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
@@ -1402,32 +1710,32 @@ class G2: NSObject, SGCManager {
|
|
|
1402
1710
|
self.sendDashboardCommand(dashPkgW.data)
|
|
1403
1711
|
|
|
1404
1712
|
// 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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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)
|
|
1713
|
+
// var dashNewsReqW = ProtobufWriter()
|
|
1714
|
+
// dashNewsReqW.writeInt32Field(1, 5) // REQUEST_NEWS_INFO
|
|
1715
|
+
// dashNewsReqW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1716
|
+
// dashNewsReqW.writeMessageField(7, Data([0x08, 0x01])) // {1:1}
|
|
1717
|
+
// self.sendDashboardCommand(dashNewsReqW.data)
|
|
1718
|
+
|
|
1719
|
+
// // 8. Gesture control list via g2_setting
|
|
1720
|
+
// var gestListW = ProtobufWriter()
|
|
1721
|
+
// gestListW.writeInt32Field(1, 1) // DeviceReceiveInfo
|
|
1722
|
+
// gestListW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1723
|
+
// // field 3 with field 10 (gestureControlList): 3 items, all app_unable
|
|
1724
|
+
// let gestureCtrlPayload = Data([
|
|
1725
|
+
// 0x52, 0x18, // field 10, length 24
|
|
1726
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x00, 0x18, 0x00, // item 1
|
|
1727
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x01, 0x18, 0x00, // item 2
|
|
1728
|
+
// 0x0A, 0x06, 0x08, 0x00, 0x10, 0x02, 0x18, 0x00, // item 3
|
|
1729
|
+
// ])
|
|
1730
|
+
// gestListW.writeMessageField(3, gestureCtrlPayload)
|
|
1731
|
+
// self.sendG2SettingCommand(gestListW.data)
|
|
1732
|
+
|
|
1733
|
+
// // 9. Dashboard APP_REQUEST_NEWS_INFO (cmd=7, field9={1:1})
|
|
1734
|
+
// var dashAppNewsW = ProtobufWriter()
|
|
1735
|
+
// dashAppNewsW.writeInt32Field(1, 7) // APP_REQUEST_NEWS_INFO
|
|
1736
|
+
// dashAppNewsW.writeInt32Field(2, self.sendManager.nextMagicRandom())
|
|
1737
|
+
// dashAppNewsW.writeMessageField(9, Data([0x08, 0x01])) // {1:1}
|
|
1738
|
+
// self.sendDashboardCommand(dashAppNewsW.data)
|
|
1431
1739
|
|
|
1432
1740
|
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1433
1741
|
}
|
|
@@ -1464,57 +1772,15 @@ class G2: NSObject, SGCManager {
|
|
|
1464
1772
|
|
|
1465
1773
|
// send dashboard menu if we have stored items
|
|
1466
1774
|
self.sendMenuApps()
|
|
1775
|
+
|
|
1776
|
+
// send calendar events
|
|
1777
|
+
let calendarEvents = DeviceStore.shared.get("bluetooth", "calendar_events") as? [[String: Any]] ?? []
|
|
1778
|
+
self.sendCalendarEvents(calendarEvents)
|
|
1467
1779
|
}
|
|
1468
1780
|
}
|
|
1469
1781
|
}
|
|
1470
1782
|
}
|
|
1471
1783
|
|
|
1472
|
-
private func runDashboardSequence() {
|
|
1473
|
-
Bridge.log("G2: Running dashboard sequence")
|
|
1474
|
-
|
|
1475
|
-
// send the shutdown command to the glasses:
|
|
1476
|
-
let msg = EvenHubProto.shutdownMessage()
|
|
1477
|
-
sendEvenHubCommand(msg)
|
|
1478
|
-
pageCreated = false
|
|
1479
|
-
currentTextContent = ""
|
|
1480
|
-
|
|
1481
|
-
// // Auth to left side
|
|
1482
|
-
// if leftPeripheral != nil && leftWriteChar != nil {
|
|
1483
|
-
// let authL = DevSettingsProto.authCmd(magicRandom: sendManager.nextMagicRandom())
|
|
1484
|
-
// sendDevSettingsCommand(authL, left: true, right: false)
|
|
1485
|
-
// }
|
|
1486
|
-
|
|
1487
|
-
// // Small delay then auth right + pipe role change + time sync
|
|
1488
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
|
1489
|
-
guard let self = self else { return }
|
|
1490
|
-
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
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
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
1784
|
// MARK: - Heartbeats
|
|
1519
1785
|
|
|
1520
1786
|
private func startHeartbeats() {
|
|
@@ -1679,140 +1945,90 @@ class G2: NSObject, SGCManager {
|
|
|
1679
1945
|
return true
|
|
1680
1946
|
}
|
|
1681
1947
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1948
|
+
/// Display a bitmap inside a positioned image container.
|
|
1949
|
+
///
|
|
1950
|
+
/// The page keeps a live list of up to 4 image containers keyed by exact rect:
|
|
1951
|
+
/// - If a container with the requested rect already exists, the image is just resent to it
|
|
1952
|
+
/// (no page rebuild).
|
|
1953
|
+
/// - Otherwise a new container is added (evicting the oldest when the list would exceed 4) and
|
|
1954
|
+
/// the page is rebuilt before the image is sent.
|
|
1955
|
+
///
|
|
1956
|
+
/// Omitted params default to a 100x100 container in the top-left corner.
|
|
1957
|
+
func displayBitmap(base64ImageData: String, x: Int32? = nil, y: Int32? = nil, width: Int32? = nil, height: Int32? = nil) async -> Bool {
|
|
1958
|
+
currentBitmapBase64 = base64ImageData
|
|
1959
|
+
currentTextContent = ""
|
|
1684
1960
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1961
|
+
let rx = x ?? G2.defaultImgContainer.x
|
|
1962
|
+
let ry = y ?? G2.defaultImgContainer.y
|
|
1963
|
+
let rw = width ?? G2.defaultImgContainer.width
|
|
1964
|
+
let rh = height ?? G2.defaultImgContainer.height
|
|
1688
1965
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
else {
|
|
1692
|
-
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
1966
|
+
guard let rawData = Data(base64Encoded: base64ImageData) else {
|
|
1967
|
+
Bridge.log("G2: displayBitmap() - failed to decode base64")
|
|
1693
1968
|
return false
|
|
1694
1969
|
}
|
|
1695
1970
|
|
|
1696
|
-
//
|
|
1697
|
-
let containerW: Int32 = 200
|
|
1698
|
-
let containerH: Int32 = 100
|
|
1699
|
-
let containerX: Int32 = x
|
|
1700
|
-
let containerY: Int32 = y
|
|
1701
|
-
let containerID: Int32 = id
|
|
1702
|
-
let containerName = "img-\(id)"
|
|
1703
|
-
|
|
1704
|
-
let imageContainer = EvenHubProto.imageContainerProperty(
|
|
1705
|
-
x: containerX, y: containerY,
|
|
1706
|
-
width: containerW, height: containerH,
|
|
1707
|
-
containerID: containerID, containerName: containerName
|
|
1708
|
-
)
|
|
1709
|
-
|
|
1710
|
-
let msg: Data
|
|
1971
|
+
// Create the startup page lazily, seeded with the default top-left container.
|
|
1711
1972
|
if !startupPageCreated {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1973
|
+
imageContainers = [
|
|
1974
|
+
ImgContainer(
|
|
1975
|
+
id: imageContainerIDPool[0],
|
|
1976
|
+
x: G2.defaultImgContainer.x, y: G2.defaultImgContainer.y,
|
|
1977
|
+
width: G2.defaultImgContainer.width, height: G2.defaultImgContainer.height
|
|
1978
|
+
)
|
|
1979
|
+
]
|
|
1980
|
+
createPageWithText("")
|
|
1981
|
+
Bridge.log("G2: displayBitmap() - startup page created, waiting 1s before sending image data...")
|
|
1982
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
1986
|
+
let container: ImgContainer
|
|
1987
|
+
if let existing = imageContainers.first(where: { $0.matches(x: rx, y: ry, width: rw, height: rh) }) {
|
|
1988
|
+
container = existing
|
|
1989
|
+
Bridge.log("G2: displayBitmap() - reusing container \(existing.id) for rect \(rx),\(ry) \(rw)x\(rh)")
|
|
1718
1990
|
} else {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1991
|
+
container = addImageContainer(x: rx, y: ry, width: rw, height: rh)
|
|
1992
|
+
Bridge.log("G2: displayBitmap() - added container \(container.id) for rect \(rx),\(ry) \(rw)x\(rh), rebuilding page")
|
|
1993
|
+
rebuildPage()
|
|
1994
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000) // settle before sending image data
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
guard let bmpData = convertToG2Bmp(rawData, containerWidth: Int(container.width), containerHeight: Int(container.height))
|
|
1998
|
+
else {
|
|
1999
|
+
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
2000
|
+
return false
|
|
1724
2001
|
}
|
|
1725
|
-
sendEvenHubCommand(msg)
|
|
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
2002
|
|
|
1732
|
-
// Send the BMP data
|
|
1733
2003
|
let success = await sendImageData(
|
|
1734
|
-
containerID:
|
|
2004
|
+
containerID: container.id, containerName: container.name, bmpData: bmpData
|
|
1735
2005
|
)
|
|
1736
2006
|
if !success {
|
|
1737
2007
|
Bridge.log("G2: displayBitmap() - failed sending image data")
|
|
1738
2008
|
}
|
|
1739
|
-
|
|
1740
|
-
Bridge.log("G2: displayBitmap() - single tile sent, \(bmpData.count) bytes")
|
|
2009
|
+
Bridge.log("G2: displayBitmap() - image sent to container \(container.id), \(bmpData.count) bytes")
|
|
1741
2010
|
return success
|
|
1742
2011
|
}
|
|
1743
2012
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
Bridge.log("G2: displayBitmapQuad() - failed to slice image into tiles")
|
|
1752
|
-
return false
|
|
2013
|
+
/// Add a new image container for `rect`, evicting the oldest when the list is full (max 4).
|
|
2014
|
+
/// Returns the newly tracked container (with an assigned ID from the pool).
|
|
2015
|
+
private func addImageContainer(x: Int32, y: Int32, width: Int32, height: Int32) -> ImgContainer {
|
|
2016
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2017
|
+
if imageContainers.count >= imageContainerIDPool.count {
|
|
2018
|
+
let evicted = imageContainers.removeFirst()
|
|
2019
|
+
Bridge.log("G2: evicting oldest image container \(evicted.id)")
|
|
1753
2020
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
let
|
|
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
|
-
)
|
|
1772
|
-
|
|
1773
|
-
let msg: Data
|
|
1774
|
-
if !startupPageCreated {
|
|
1775
|
-
msg = EvenHubProto.createPageMessage(
|
|
1776
|
-
imageContainers: [
|
|
1777
|
-
container1, container2, container3, container4,
|
|
1778
|
-
], magicRandom: sendManager.nextMagicRandom(), appId: activeMenuAppId
|
|
1779
|
-
)
|
|
1780
|
-
startupPageCreated = true
|
|
1781
|
-
} else {
|
|
1782
|
-
msg = EvenHubProto.rebuildPageMessage(
|
|
1783
|
-
imageContainers: [
|
|
1784
|
-
container1, container2, container3, container4,
|
|
1785
|
-
], magicRandom: sendManager.nextMagicRandom(), appId: activeMenuAppId
|
|
1786
|
-
)
|
|
1787
|
-
}
|
|
1788
|
-
sendEvenHubCommand(msg)
|
|
1789
|
-
pageCreated = true
|
|
1790
|
-
pageHasTextContainer = false
|
|
1791
|
-
currentTextContent = ""
|
|
1792
|
-
|
|
1793
|
-
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
1794
|
-
|
|
1795
|
-
// Send each tile's unique BMP data to its container
|
|
1796
|
-
let success1 = await sendImageData(
|
|
1797
|
-
containerID: 10, containerName: "img-10", bmpData: tiles[0]
|
|
1798
|
-
)
|
|
1799
|
-
let success2 = await sendImageData(
|
|
1800
|
-
containerID: 11, containerName: "img-11", bmpData: tiles[1]
|
|
1801
|
-
)
|
|
1802
|
-
let success3 = await sendImageData(
|
|
1803
|
-
containerID: 12, containerName: "img-12", bmpData: tiles[2]
|
|
1804
|
-
)
|
|
1805
|
-
let success4 = await sendImageData(
|
|
1806
|
-
containerID: 13, containerName: "img-13", bmpData: tiles[3]
|
|
1807
|
-
)
|
|
1808
|
-
|
|
1809
|
-
return success1 && success2 && success3 && success4
|
|
2021
|
+
// Pick the lowest free ID from the pool.
|
|
2022
|
+
let usedIDs = Set(imageContainers.map { $0.id })
|
|
2023
|
+
let id = imageContainerIDPool.first { !usedIDs.contains($0) } ?? imageContainerIDPool[0]
|
|
2024
|
+
let container = ImgContainer(id: id, x: x, y: y, width: width, height: height)
|
|
2025
|
+
imageContainers.append(container)
|
|
2026
|
+
return container
|
|
1810
2027
|
}
|
|
1811
2028
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
currentTextContent
|
|
1815
|
-
return await displayBitmapQuad(base64ImageData: base64ImageData)
|
|
2029
|
+
/// Rebuild the page from the current text + image container list.
|
|
2030
|
+
private func rebuildPage() {
|
|
2031
|
+
createPageWithText(currentTextContent)
|
|
1816
2032
|
}
|
|
1817
2033
|
|
|
1818
2034
|
/// Upscale BMP pixel data by 2x (200x100 → 400x200) using nearest-neighbor
|
|
@@ -1897,73 +2113,6 @@ class G2: NSObject, SGCManager {
|
|
|
1897
2113
|
return dst
|
|
1898
2114
|
}
|
|
1899
2115
|
|
|
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
2116
|
// MARK: - Bitmap Conversion
|
|
1968
2117
|
|
|
1969
2118
|
/// Scale source image to fit within containerWidth x containerHeight (maintaining aspect ratio),
|
|
@@ -2031,93 +2180,6 @@ class G2: NSObject, SGCManager {
|
|
|
2031
2180
|
return bmp
|
|
2032
2181
|
}
|
|
2033
2182
|
|
|
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
2183
|
/// Build a 4-bit indexed BMP file from 8-bit grayscale pixel data.
|
|
2122
2184
|
/// BMP rows are stored bottom-up. Each row is padded to a 4-byte boundary.
|
|
2123
2185
|
private func build4BitBmp(grayscalePixels: Data, width: Int, height: Int) -> Data? {
|
|
@@ -2196,7 +2258,7 @@ class G2: NSObject, SGCManager {
|
|
|
2196
2258
|
/// the foreground by tearing down whatever EvenHub page we currently own.
|
|
2197
2259
|
/// The glasses fall back to the dashboard automatically when no page is up.
|
|
2198
2260
|
func showDashboard() {
|
|
2199
|
-
Bridge.log("G2: showDashboard")
|
|
2261
|
+
Bridge.log("G2: showDashboard()")
|
|
2200
2262
|
dashboardShowing += 2
|
|
2201
2263
|
let msg = EvenHubProto.shutdownMessage()
|
|
2202
2264
|
sendEvenHubCommand(msg)
|
|
@@ -2212,6 +2274,114 @@ class G2: NSObject, SGCManager {
|
|
|
2212
2274
|
}
|
|
2213
2275
|
}
|
|
2214
2276
|
|
|
2277
|
+
private func dashboardHalfDayFormat() -> Int32 {
|
|
2278
|
+
let twelveHour = DeviceStore.shared.get("bluetooth", "twelve_hour_time") as? Bool ?? true
|
|
2279
|
+
return twelveHour ? 1 : 0
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
private func dashboardTemperatureUnit() -> Int32 {
|
|
2283
|
+
let metric = DeviceStore.shared.get("bluetooth", "metric_system") as? Bool ?? false
|
|
2284
|
+
return metric ? 1 : 2
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
func sendDashboardDisplaySettings() {
|
|
2288
|
+
var dashDisplayW = ProtobufWriter()
|
|
2289
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
2290
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
2291
|
+
dashDisplayW.writeMessageField(3, Data([1, 2, 3])) // statusDisplayOrder
|
|
2292
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
2293
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
2294
|
+
dashDisplayW.writeMessageField(5, Data([3, 1, 2, 4, 5]))
|
|
2295
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
2296
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
2297
|
+
|
|
2298
|
+
var dashRecvW = ProtobufWriter()
|
|
2299
|
+
dashRecvW.writeMessageField(2, dashDisplayW.data)
|
|
2300
|
+
|
|
2301
|
+
var dashPkgW = ProtobufWriter()
|
|
2302
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
2303
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2304
|
+
dashPkgW.writeMessageField(4, dashRecvW.data)
|
|
2305
|
+
sendDashboardCommand(dashPkgW.data)
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/// Push a calendar event to the dashboard's Schedule widget (service 0x01).
|
|
2309
|
+
///
|
|
2310
|
+
/// - title: event title displayed on the widget
|
|
2311
|
+
/// - location: optional location string
|
|
2312
|
+
/// - time: pre-formatted display string (e.g. "10:00 AM" or "10:00 – 10:30").
|
|
2313
|
+
/// The widget shows this verbatim — format it however you want it to read.
|
|
2314
|
+
/// - endDate: when the event ends. Encoded the same way as the time-sync hack:
|
|
2315
|
+
/// Unix seconds with the local TZ offset folded in, so the glasses (which
|
|
2316
|
+
/// appear to treat timestamps as already-local) read it correctly.
|
|
2317
|
+
/// - scheduleId: stable per-event id. Reuse the same id when updating an event.
|
|
2318
|
+
func sendCalendarEvent(
|
|
2319
|
+
title: String,
|
|
2320
|
+
location: String? = nil,
|
|
2321
|
+
time: String? = nil,
|
|
2322
|
+
endDate: Date,
|
|
2323
|
+
scheduleId: Int32 = 1,
|
|
2324
|
+
scheduleTotal: Int32 = 1,
|
|
2325
|
+
scheduleNum: Int32 = 0
|
|
2326
|
+
) {
|
|
2327
|
+
Bridge.log("G2: sendCalendarEvent(\(title), endDate=\(endDate))")
|
|
2328
|
+
let tzSec = Int64(TimeZone.current.secondsFromGMT())
|
|
2329
|
+
let endTs = Int32(truncatingIfNeeded: Int64(endDate.timeIntervalSince1970) + tzSec)
|
|
2330
|
+
|
|
2331
|
+
let payload = DashboardProto.calendarPush(
|
|
2332
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2333
|
+
packageId: 1,
|
|
2334
|
+
scheduleId: scheduleId,
|
|
2335
|
+
title: title,
|
|
2336
|
+
location: location,
|
|
2337
|
+
time: time,
|
|
2338
|
+
endTimestamp: endTs,
|
|
2339
|
+
scheduleAuthority: 1,
|
|
2340
|
+
scheduleTotal: scheduleTotal,
|
|
2341
|
+
scheduleNum: scheduleNum
|
|
2342
|
+
)
|
|
2343
|
+
sendDashboardCommand(payload)
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/// Bridge entry for `calendar_events` store updates. Each dict is expected
|
|
2347
|
+
/// to match the TS `CalendarEvent` shape: { title, location?, time, endDate }
|
|
2348
|
+
/// where endDate is unix seconds.
|
|
2349
|
+
///
|
|
2350
|
+
/// Sends one BLE push per event, with `scheduleTotal` set to the batch size
|
|
2351
|
+
/// and `scheduleNum` set to this event's 0-based slot. The widget pages
|
|
2352
|
+
/// through them on the glasses — without paging info the firmware overwrites
|
|
2353
|
+
/// slot 0 on each push and only the last event survives.
|
|
2354
|
+
func sendCalendarEvents(_ events: [[String: Any]]) {
|
|
2355
|
+
Bridge.log("G2: sendCalendarEvents — \(events.count) events")
|
|
2356
|
+
if events.isEmpty {
|
|
2357
|
+
let payload = DashboardProto.calendarClear(
|
|
2358
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2359
|
+
packageId: 1,
|
|
2360
|
+
scheduleAuthority: 1
|
|
2361
|
+
)
|
|
2362
|
+
sendDashboardCommand(payload)
|
|
2363
|
+
return
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
let total = Int32(events.count)
|
|
2367
|
+
for (i, ev) in events.enumerated() {
|
|
2368
|
+
guard let title = ev["title"] as? String,
|
|
2369
|
+
let time = ev["time"] as? String,
|
|
2370
|
+
let endTs = ev["endDate"] as? Double
|
|
2371
|
+
else { continue }
|
|
2372
|
+
let location = ev["location"] as? String
|
|
2373
|
+
sendCalendarEvent(
|
|
2374
|
+
title: title,
|
|
2375
|
+
location: location,
|
|
2376
|
+
time: time,
|
|
2377
|
+
endDate: Date(timeIntervalSince1970: endTs),
|
|
2378
|
+
scheduleId: Int32(i + 1),
|
|
2379
|
+
scheduleTotal: total,
|
|
2380
|
+
scheduleNum: Int32(i)
|
|
2381
|
+
)
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2215
2385
|
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
2216
2386
|
Bridge.log("G2: setDashboardPosition(height=\(height), depth=\(depth))")
|
|
2217
2387
|
setDashboardHeightOnly(height)
|
|
@@ -2259,18 +2429,30 @@ class G2: NSObject, SGCManager {
|
|
|
2259
2429
|
content: text
|
|
2260
2430
|
)
|
|
2261
2431
|
|
|
2432
|
+
// Build the page's image containers from the live tracked list.
|
|
2433
|
+
let imageContainerProps: [Data] = imageContainers.map { c in
|
|
2434
|
+
EvenHubProto.imageContainerProperty(
|
|
2435
|
+
x: c.x, y: c.y, width: c.width, height: c.height,
|
|
2436
|
+
containerID: c.id, containerName: c.name
|
|
2437
|
+
)
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2262
2440
|
let msg: Data
|
|
2263
2441
|
if !startupPageCreated {
|
|
2264
2442
|
Bridge.log("G2: createPageWithText - using createPageMessage (first time)")
|
|
2265
2443
|
msg = EvenHubProto.createPageMessage(
|
|
2266
|
-
textContainers: [tc],
|
|
2444
|
+
textContainers: [tc],
|
|
2445
|
+
imageContainers: imageContainerProps,
|
|
2446
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2267
2447
|
appId: activeMenuAppId
|
|
2268
2448
|
)
|
|
2269
2449
|
startupPageCreated = true
|
|
2270
2450
|
} else {
|
|
2271
2451
|
Bridge.log("G2: createPageWithText - using rebuildPageMessage")
|
|
2272
2452
|
msg = EvenHubProto.rebuildPageMessage(
|
|
2273
|
-
textContainers: [tc],
|
|
2453
|
+
textContainers: [tc],
|
|
2454
|
+
imageContainers: imageContainerProps,
|
|
2455
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2274
2456
|
appId: activeMenuAppId
|
|
2275
2457
|
)
|
|
2276
2458
|
}
|
|
@@ -2420,6 +2602,7 @@ class G2: NSObject, SGCManager {
|
|
|
2420
2602
|
startupPageCreated = false
|
|
2421
2603
|
pageCreated = false
|
|
2422
2604
|
pageHasTextContainer = false
|
|
2605
|
+
dashboardShowing = 0
|
|
2423
2606
|
heartbeatCounter = 0
|
|
2424
2607
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
2425
2608
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
@@ -2517,25 +2700,62 @@ class G2: NSObject, SGCManager {
|
|
|
2517
2700
|
Bridge.log("G2: Sent RING_DISCONNECT_INFO for MAC \(mac)")
|
|
2518
2701
|
}
|
|
2519
2702
|
|
|
2520
|
-
|
|
2521
|
-
|
|
2703
|
+
/// Fire an EvenAI skill — the same path "Hey Even, show X" uses. Triggers a
|
|
2704
|
+
/// built-in glasses UI (notification list, navigation, teleprompter, etc).
|
|
2705
|
+
/// See `EvenAIProto.triggerSkill` for the skillId table.
|
|
2706
|
+
func triggerSkill(
|
|
2707
|
+
_ skillId: Int32, skillParam: Int32 = 0,
|
|
2708
|
+
text: String = "", streamEnable: Int32 = 1, fTextEnd: Int32 = 1
|
|
2709
|
+
) {
|
|
2710
|
+
Bridge.log("G2: triggerSkill(\(skillId), skillParam=\(skillParam), text=\"\(text)\", streamEnable=\(streamEnable), fTextEnd=\(fTextEnd))")
|
|
2711
|
+
let payload = EvenAIProto.triggerSkill(
|
|
2712
|
+
magicRandom: sendManager.nextMagicRandom(),
|
|
2713
|
+
skillId: skillId,
|
|
2714
|
+
skillParam: skillParam,
|
|
2715
|
+
text: text,
|
|
2716
|
+
streamEnable: streamEnable,
|
|
2717
|
+
fTextEnd: fTextEnd
|
|
2718
|
+
)
|
|
2719
|
+
sendEvenAICommand(payload)
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/// Open the on-glasses notification panel — same effect as the user saying
|
|
2723
|
+
/// "Hey Even, show notifications". Replicates the official-app voice flow:
|
|
2724
|
+
/// 1. CTRL{status=ENTER} — puts glasses in AI session
|
|
2725
|
+
/// 2. ASK{text=" "} — minimal ASR transcript to seed session context
|
|
2726
|
+
/// 3. SKILL{skillId=NOTIFICATION, skillParam=show, ...} — dispatches the intent
|
|
2727
|
+
/// The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2728
|
+
/// supply the session context that lets the glasses act on the SKILL.
|
|
2729
|
+
func showNotificationsPanel() {
|
|
2730
|
+
Bridge.log("G2: showNotificationsPanel()")
|
|
2731
|
+
Task { @MainActor [weak self] in
|
|
2732
|
+
guard let self = self else { return }
|
|
2522
2733
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2734
|
+
let enterPayload = EvenAIProto.aiCtrl(
|
|
2735
|
+
magicRandom: self.sendManager.nextMagicRandom(),
|
|
2736
|
+
status: 2 // EVEN_AI_ENTER
|
|
2737
|
+
)
|
|
2738
|
+
self.sendEvenAICommand(enterPayload)
|
|
2739
|
+
|
|
2740
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
2741
|
+
let askPayload = EvenAIProto.aiAsk(
|
|
2742
|
+
magicRandom: self.sendManager.nextMagicRandom(),
|
|
2743
|
+
text: " ",
|
|
2744
|
+
streamEnable: 0
|
|
2745
|
+
)
|
|
2746
|
+
self.sendEvenAICommand(askPayload)
|
|
2747
|
+
|
|
2748
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
2749
|
+
self.triggerSkill(
|
|
2750
|
+
3, skillParam: 1, // NOTIFICATION, show
|
|
2751
|
+
text: " ",
|
|
2752
|
+
streamEnable: 1, fTextEnd: 1
|
|
2753
|
+
)
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2535
2756
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
showDashboard()
|
|
2757
|
+
func dbg1() {
|
|
2758
|
+
showNotificationsPanel()
|
|
2539
2759
|
}
|
|
2540
2760
|
|
|
2541
2761
|
func dbg2() {
|
|
@@ -2630,6 +2850,14 @@ class G2: NSObject, SGCManager {
|
|
|
2630
2850
|
// TODO: Implement via dev_settings
|
|
2631
2851
|
}
|
|
2632
2852
|
|
|
2853
|
+
/// Push the current time to the glasses. Useful after DST transitions,
|
|
2854
|
+
/// time-zone travel, or a long sleep where the glasses' clock has drifted.
|
|
2855
|
+
func syncTime() {
|
|
2856
|
+
Bridge.log("G2: syncTime()")
|
|
2857
|
+
let msg = DevSettingsProto.timeSync(magicRandom: sendManager.nextMagicRandom())
|
|
2858
|
+
sendDevSettingsCommand(msg, left: true, right: true)
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2633
2861
|
func sendRgbLedControl(
|
|
2634
2862
|
requestId _: String, packageName _: String?, action _: String, color _: String?,
|
|
2635
2863
|
onDurationMs _: Int, offDurationMs _: Int, count _: Int
|
|
@@ -2647,7 +2875,7 @@ class G2: NSObject, SGCManager {
|
|
|
2647
2875
|
|
|
2648
2876
|
func requestPhoto(
|
|
2649
2877
|
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
2650
|
-
compress _: String?, flash _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
2878
|
+
compress _: String?, flash _: Bool, save _: Bool, sound _: Bool, exposureTimeNs _: Double?, iso _: Int?
|
|
2651
2879
|
) {}
|
|
2652
2880
|
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
2653
2881
|
func startStream(_: [String: Any]) {}
|
|
@@ -2957,7 +3185,11 @@ class G2: NSObject, SGCManager {
|
|
|
2957
3185
|
}
|
|
2958
3186
|
if let errorCode = resFields[8] as? Int32 {
|
|
2959
3187
|
// ImgResCmd has ErrorCode in field 8
|
|
2960
|
-
|
|
3188
|
+
if errorCode == 4 {
|
|
3189
|
+
Bridge.log("G2: img_success")
|
|
3190
|
+
} else {
|
|
3191
|
+
Bridge.log("G2: EvenHub ImgRes errorCode=\(errorCode)")
|
|
3192
|
+
}
|
|
2961
3193
|
}
|
|
2962
3194
|
}
|
|
2963
3195
|
}
|
|
@@ -3408,6 +3640,15 @@ class G2: NSObject, SGCManager {
|
|
|
3408
3640
|
// "G2: gesture_ctrl response: \(data.map { String(format: "%02X", $0) }.joined())"
|
|
3409
3641
|
// )
|
|
3410
3642
|
// Bridge.log("G2: gesture_ctrl response:")
|
|
3643
|
+
|
|
3644
|
+
|
|
3645
|
+
// Dedup: L and R peripherals both deliver this event, so debounce or
|
|
3646
|
+
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
3647
|
+
if lastGestureCtrlTimestamp != nil && timestamp - lastGestureCtrlTimestamp! < 500 {
|
|
3648
|
+
Bridge.log("G2: gesture_ctrl dedup")
|
|
3649
|
+
return
|
|
3650
|
+
}
|
|
3651
|
+
lastGestureCtrlTimestamp = timestamp
|
|
3411
3652
|
|
|
3412
3653
|
// if we got 08011A00 that means we closed the dashboard, which means the mic is probably dead,
|
|
3413
3654
|
// so we need to revive it:
|
|
@@ -3648,6 +3889,7 @@ extension G2: CBCentralManagerDelegate {
|
|
|
3648
3889
|
self.startupPageCreated = false
|
|
3649
3890
|
self.pageCreated = false
|
|
3650
3891
|
self.pageHasTextContainer = false
|
|
3892
|
+
self.dashboardShowing = 0
|
|
3651
3893
|
DeviceStore.shared.apply("glasses", "connected", false)
|
|
3652
3894
|
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
3653
3895
|
|