@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
|
@@ -27,6 +27,13 @@ import java.io.ByteArrayOutputStream
|
|
|
27
27
|
import java.util.TimeZone
|
|
28
28
|
import java.util.UUID
|
|
29
29
|
import java.util.regex.Pattern
|
|
30
|
+
import kotlinx.coroutines.CoroutineScope
|
|
31
|
+
import kotlinx.coroutines.Dispatchers
|
|
32
|
+
import kotlinx.coroutines.SupervisorJob
|
|
33
|
+
import kotlinx.coroutines.delay
|
|
34
|
+
import kotlinx.coroutines.launch
|
|
35
|
+
import kotlinx.coroutines.sync.Mutex
|
|
36
|
+
import kotlinx.coroutines.sync.withLock
|
|
30
37
|
|
|
31
38
|
// ---------- G2 Protocol Constants ----------
|
|
32
39
|
|
|
@@ -49,6 +56,7 @@ private enum class ServiceID(val value: Byte) {
|
|
|
49
56
|
DASHBOARD(0x01), // UI_BACKGROUND_DASHBOARD_APP_ID
|
|
50
57
|
MENU(0x03), // UI_FOREGROUND_MEUN_ID (typo is intentional — matches Even's proto)
|
|
51
58
|
EVEN_AI(0x07), // UI_FOREGROUND_EVEN_AI_ID
|
|
59
|
+
NAVIGATION(0x08), // UI_BACKGROUND_NAVIGATION_ID (compass/heading lives here)
|
|
52
60
|
G2_SETTING(0x09), // UI_SETTING_APP_ID
|
|
53
61
|
GESTURE_CTRL(0x0D), // gesture_ctrl lifecycle signals
|
|
54
62
|
ONBOARDING(0x10), // UI_ONBOARDING_APP_ID
|
|
@@ -69,7 +77,21 @@ private enum class EvenHubCmd(val value: Int) {
|
|
|
69
77
|
REBUILD_PAGE(7),
|
|
70
78
|
SHUTDOWN_PAGE(9),
|
|
71
79
|
HEARTBEAT(12),
|
|
72
|
-
AUDIO_CONTROL(15)
|
|
80
|
+
AUDIO_CONTROL(15),
|
|
81
|
+
IMU_CONTROL(19) // APP_REQUEST_IMU_CTR_PACKET (confirmed via on-device brute-force)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Navigation_Cmd_list from navigation.proto (service 0x08)
|
|
85
|
+
private enum class NavigationCmd(val value: Int) {
|
|
86
|
+
APP_SEND_HEARTBEAT(0), // APP_SEND_HEARTBEAT_CMD
|
|
87
|
+
APP_REQUEST_START_UP(5), // APP_REQUEST_START_UP — begin navigation/compass session
|
|
88
|
+
APP_SEND_BASIC_INFO(7), // APP_SEND_BASIC_INFO
|
|
89
|
+
APP_REQUEST_EXIT(12), // APP_REQUEST_EXIT
|
|
90
|
+
OS_NOTIFY_EXIT(13), // OS_NOTIFY_EXIT
|
|
91
|
+
OS_NOTIFY_REVIEW_CHANGED(14), // OS_NOTIFY_REVIEW_CHANGED
|
|
92
|
+
OS_NOTIFY_COMPASS_CHANGED(15), // OS_NOTIFY_COMPASS_CHANGED — heading update
|
|
93
|
+
OS_NOTIFY_COMPASS_CALIBRATE_START(16), // OS_NOTIFY_COMPASS_CALIBRATE_STRAT (sic)
|
|
94
|
+
OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE(17) // OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
// EvenHub response command IDs (glasses → phone)
|
|
@@ -86,7 +108,8 @@ private enum class OsEventType(val value: Int) {
|
|
|
86
108
|
FOREGROUND_ENTER(4),
|
|
87
109
|
FOREGROUND_EXIT(5),
|
|
88
110
|
ABNORMAL_EXIT(6),
|
|
89
|
-
SYSTEM_EXIT(7)
|
|
111
|
+
SYSTEM_EXIT(7),
|
|
112
|
+
IMU_DATA_REPORT(8); // IMU_DATA_REPORT — Sys_ItemEvent carries imuData
|
|
90
113
|
|
|
91
114
|
companion object {
|
|
92
115
|
fun fromInt(v: Int): OsEventType? = entries.find { it.value == v }
|
|
@@ -465,6 +488,52 @@ private object EvenHubProto {
|
|
|
465
488
|
val msg = audioCtrCmd(enable)
|
|
466
489
|
return evenHubMessage(EvenHubCmd.AUDIO_CONTROL, 18, msg, magicRandom = magicRandom)
|
|
467
490
|
}
|
|
491
|
+
|
|
492
|
+
// ---------- IMU control ----------
|
|
493
|
+
//
|
|
494
|
+
// Wire format recovered by on-device brute-force (sample magnitude ≈ 1.0 g confirms
|
|
495
|
+
// the decode). Shapes from even_hub_sdk@0.0.10; numeric proto tags confirmed live:
|
|
496
|
+
// EvenHub_Cmd_List IMU command = 19
|
|
497
|
+
// evenhub_main_msg_ctx ImuCtrlCmd slot = field 20
|
|
498
|
+
// ImuCtrlCmd { field 1 = IMU_ReportEn (bool), field 2 = reportFrq (pacing 100…1000) }
|
|
499
|
+
// Report path: cmd=2 (osNotifyEventToApp) → SendDeviceEvent.field13 →
|
|
500
|
+
// Sys_ItemEvent { field 1 = eventType = 8 (IMU_DATA_REPORT),
|
|
501
|
+
// field 3 = imuData = IMU_Report_Data }
|
|
502
|
+
// IMU_Report_Data { field 1 = x, 2 = y, 3 = z } — each a 32-bit float (NOT double),
|
|
503
|
+
// gravity-normalized (|v| ≈ 1 at rest).
|
|
504
|
+
const val IMU_CTRL_SUB_FIELD = 20
|
|
505
|
+
|
|
506
|
+
/** ImuReportPace pacing codes (protocol values, NOT literal Hz). Step 100, 100…1000. */
|
|
507
|
+
const val IMU_PACE_P100 = 100
|
|
508
|
+
const val IMU_PACE_P500 = 500
|
|
509
|
+
const val IMU_PACE_P1000 = 1000
|
|
510
|
+
|
|
511
|
+
/** Build an ImuCtrlCmd sub-message. */
|
|
512
|
+
fun imuCtrlCmd(enable: Boolean, reportFrq: Int): ByteArray {
|
|
513
|
+
val w = ProtobufWriter()
|
|
514
|
+
w.writeInt32Field(1, if (enable) 1 else 0) // IMU_ReportEn
|
|
515
|
+
if (enable) {
|
|
516
|
+
w.writeInt32Field(2, reportFrq) // reportFrq (pacing code 100…1000)
|
|
517
|
+
}
|
|
518
|
+
return w.toByteArray()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Build a full evenhub_main_msg_ctx that enables/disables IMU reporting. `reportFrq` is an
|
|
523
|
+
* ImuReportPace pacing code; ignored when disabling.
|
|
524
|
+
*/
|
|
525
|
+
fun imuControlMessage(
|
|
526
|
+
enable: Boolean,
|
|
527
|
+
reportFrq: Int = IMU_PACE_P100,
|
|
528
|
+
magicRandom: Int = 0
|
|
529
|
+
): ByteArray {
|
|
530
|
+
val imuMsg = imuCtrlCmd(enable, reportFrq)
|
|
531
|
+
val w = ProtobufWriter()
|
|
532
|
+
w.writeInt32Field(1, EvenHubCmd.IMU_CONTROL.value) // Cmd
|
|
533
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
534
|
+
w.writeMessageField(IMU_CTRL_SUB_FIELD, imuMsg) // ImuCtrlCmd slot (field 20)
|
|
535
|
+
return w.toByteArray()
|
|
536
|
+
}
|
|
468
537
|
}
|
|
469
538
|
|
|
470
539
|
// ---------- DevSettings Auth Protobuf Builders ----------
|
|
@@ -496,16 +565,20 @@ private object DevSettingsProto {
|
|
|
496
565
|
return w.toByteArray()
|
|
497
566
|
}
|
|
498
567
|
|
|
568
|
+
/// DevCfgDataPackage with TIME_SYNC command.
|
|
569
|
+
/// TimeSync submessage: f1 = (Unix seconds + TZ offset seconds) as Int32, no TZ field.
|
|
570
|
+
/// Firmware appears to ignore the TZ field, so we pre-shift the timestamp itself
|
|
571
|
+
/// to make UTC interpretation read as local. Empirically confirmed via probe variants in dbg1().
|
|
499
572
|
fun timeSync(magicRandom: Int): ByteArray {
|
|
500
573
|
val w = ProtobufWriter()
|
|
501
574
|
w.writeInt32Field(1, DevCfgCommandId.TIME_SYNC.value)
|
|
502
575
|
w.writeInt32Field(2, magicRandom)
|
|
503
576
|
|
|
504
577
|
val tsW = ProtobufWriter()
|
|
505
|
-
val
|
|
506
|
-
|
|
507
|
-
val
|
|
508
|
-
tsW.writeInt32Field(
|
|
578
|
+
val nowMs = System.currentTimeMillis()
|
|
579
|
+
val nowSec = nowMs / 1000
|
|
580
|
+
val tzSec = (TimeZone.getDefault().getOffset(nowMs) / 1000).toLong()
|
|
581
|
+
tsW.writeInt32Field(1, (nowSec + tzSec).toInt())
|
|
509
582
|
w.writeMessageField(128, tsW.toByteArray())
|
|
510
583
|
return w.toByteArray()
|
|
511
584
|
}
|
|
@@ -663,11 +736,23 @@ private object OnboardingProto {
|
|
|
663
736
|
// ---------- EvenAI Protobuf Builders (even_ai.proto, service ID 7) ----------
|
|
664
737
|
|
|
665
738
|
private object EvenAIProto {
|
|
739
|
+
/**
|
|
740
|
+
* EvenAIDataPackage with CONFIG command to toggle Hey Even wakeword.
|
|
741
|
+
* voiceSwitch: 0 = OFF, 1 = ON.
|
|
742
|
+
*
|
|
743
|
+
* Wire format confirmed by sniffing the official app toggling the setting:
|
|
744
|
+
* EvenAIConfig (field 13) = { f1=voiceSwitch, f2=32 }
|
|
745
|
+
* The app OMITS f1 when disabling (proto3 zero) and sends f2=32 (0x20), NOT 80.
|
|
746
|
+
* Observed echoes: ON → 6A04 08 01 10 20 ({f1:1, f2:32})
|
|
747
|
+
* OFF → 6A02 10 20 ({f2:32})
|
|
748
|
+
*/
|
|
666
749
|
fun setHeyEven(magicRandom: Int, enabled: Boolean): ByteArray {
|
|
667
750
|
// EvenAIConfig
|
|
668
751
|
val configW = ProtobufWriter()
|
|
669
|
-
|
|
670
|
-
|
|
752
|
+
if (enabled) {
|
|
753
|
+
configW.writeInt32Field(1, 1) // voiceSwitch (omitted when off, matching the app)
|
|
754
|
+
}
|
|
755
|
+
configW.writeInt32Field(2, 32) // streamSpeed (always sent, app uses 32)
|
|
671
756
|
|
|
672
757
|
// EvenAIDataPackage
|
|
673
758
|
val w = ProtobufWriter()
|
|
@@ -676,6 +761,69 @@ private object EvenAIProto {
|
|
|
676
761
|
w.writeMessageField(13, configW.toByteArray()) // config (field 13)
|
|
677
762
|
return w.toByteArray()
|
|
678
763
|
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* EvenAIDataPackage with CTRL — used to put glasses into / out of an AI session.
|
|
767
|
+
* Mirrors Flutter `sendWakeupResp` which sends EvenAIControl{status=ENTER}.
|
|
768
|
+
* status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
769
|
+
*/
|
|
770
|
+
fun aiCtrl(magicRandom: Int, status: Int): ByteArray {
|
|
771
|
+
val ctrlW = ProtobufWriter()
|
|
772
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
773
|
+
|
|
774
|
+
val w = ProtobufWriter()
|
|
775
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
776
|
+
w.writeInt32Field(2, magicRandom)
|
|
777
|
+
w.writeMessageField(3, ctrlW.toByteArray()) // ctrl (field 3)
|
|
778
|
+
return w.toByteArray()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* EvenAIDataPackage with ASK — what the phone sends after cloud ASR resolves the
|
|
783
|
+
* user's audio into text. Mirrors Flutter `sendAsr`: EvenAIAskInfo{text, streamEnable=0}.
|
|
784
|
+
*/
|
|
785
|
+
fun aiAsk(magicRandom: Int, text: String, streamEnable: Int = 0): ByteArray {
|
|
786
|
+
val askW = ProtobufWriter()
|
|
787
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
788
|
+
askW.writeBytesField(4, text.toByteArray(Charsets.UTF_8)) // text
|
|
789
|
+
|
|
790
|
+
val w = ProtobufWriter()
|
|
791
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
792
|
+
w.writeInt32Field(2, magicRandom)
|
|
793
|
+
w.writeMessageField(5, askW.toByteArray()) // askInfo (field 5)
|
|
794
|
+
return w.toByteArray()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* EvenAIDataPackage with SKILL — triggers a built-in glasses UI the same way
|
|
799
|
+
* "Hey Even, show X" voice command does.
|
|
800
|
+
* skillId values (per even_ai.proto):
|
|
801
|
+
* 0 SKILL_NONE, 1 BRIGHTNESS, 2 TRANSLATE_CTRL, 3 NOTIFICATION,
|
|
802
|
+
* 4 TELEPROMPT, 5 NAVIGATE, 6 CONVERSATE, 7 QUICKLIST, 8 AUTO_BRIGHTNESS
|
|
803
|
+
*/
|
|
804
|
+
fun triggerSkill(
|
|
805
|
+
magicRandom: Int,
|
|
806
|
+
skillId: Int,
|
|
807
|
+
skillParam: Int = 0,
|
|
808
|
+
text: String = "",
|
|
809
|
+
streamEnable: Int = 1,
|
|
810
|
+
fTextEnd: Int = 1
|
|
811
|
+
): ByteArray {
|
|
812
|
+
// EvenAISkillInfo
|
|
813
|
+
val skillW = ProtobufWriter()
|
|
814
|
+
skillW.writeInt32Field(1, streamEnable) // streamEnable
|
|
815
|
+
skillW.writeInt32Field(2, skillId) // skillId
|
|
816
|
+
skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
|
|
817
|
+
skillW.writeBytesField(4, text.toByteArray(Charsets.UTF_8)) // text
|
|
818
|
+
skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
|
|
819
|
+
|
|
820
|
+
// EvenAIDataPackage
|
|
821
|
+
val w = ProtobufWriter()
|
|
822
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
823
|
+
w.writeInt32Field(2, magicRandom)
|
|
824
|
+
w.writeMessageField(8, skillW.toByteArray()) // skillInfo (field 8)
|
|
825
|
+
return w.toByteArray()
|
|
826
|
+
}
|
|
679
827
|
}
|
|
680
828
|
|
|
681
829
|
// ---------- Menu Protobuf Builders (menu.proto, service ID 3) ----------
|
|
@@ -757,6 +905,124 @@ private object MenuProto {
|
|
|
757
905
|
}
|
|
758
906
|
}
|
|
759
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Builders for the dashboard calendar widget (service 0x01, command Dashboard_Receive = 2).
|
|
910
|
+
* Field numbers come from the extracted dashboard.proto v2.1.0_beta_v3 and mirror the iOS
|
|
911
|
+
* DashboardProto.calendarPush builder so both platforms produce the same wire payload.
|
|
912
|
+
*/
|
|
913
|
+
private object CalendarProto {
|
|
914
|
+
// Dashboard_Receive — phone → glasses widget/config push.
|
|
915
|
+
const val DASHBOARD_RECEIVE = 2
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Build a Schedule submessage (the calendar event payload).
|
|
919
|
+
* f1 = scheduleId (int32, required)
|
|
920
|
+
* f2 = title (string, optional)
|
|
921
|
+
* f3 = location (string, optional)
|
|
922
|
+
* f4 = time (string, optional — display text e.g. "10:00 AM")
|
|
923
|
+
* f5 = endTimestamp (int32, Unix seconds — pre-shifted by TZ so the glasses,
|
|
924
|
+
* which treat timestamps as already-local, display local time)
|
|
925
|
+
*/
|
|
926
|
+
private fun schedule(
|
|
927
|
+
scheduleId: Int,
|
|
928
|
+
title: String?,
|
|
929
|
+
location: String?,
|
|
930
|
+
time: String?,
|
|
931
|
+
endTimestamp: Int
|
|
932
|
+
): ByteArray {
|
|
933
|
+
val w = ProtobufWriter()
|
|
934
|
+
w.writeInt32Field(1, scheduleId)
|
|
935
|
+
title?.let { w.writeStringField(2, it) }
|
|
936
|
+
location?.let { w.writeStringField(3, it) }
|
|
937
|
+
time?.let { w.writeStringField(4, it) }
|
|
938
|
+
w.writeInt32Field(5, endTimestamp)
|
|
939
|
+
return w.toByteArray()
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Build the full calendar-push DashboardDataPackage:
|
|
944
|
+
* DashboardDataPackage {
|
|
945
|
+
* commandId = Dashboard_Receive (2)
|
|
946
|
+
* magicRandom
|
|
947
|
+
* dashboardReceive = DashboardReceiveFromApp {
|
|
948
|
+
* packageId = 1
|
|
949
|
+
* bashboardConfig = DashboardContent {
|
|
950
|
+
* widgetComponents = rWidgetComponent {
|
|
951
|
+
* schedule = rScheduleWidget {
|
|
952
|
+
* scheduleTotal, scheduleNum (0-based), Schedule, scheduleAuthority
|
|
953
|
+
* }
|
|
954
|
+
* }
|
|
955
|
+
* }
|
|
956
|
+
* }
|
|
957
|
+
* }
|
|
958
|
+
*/
|
|
959
|
+
fun calendarPush(
|
|
960
|
+
magicRandom: Int,
|
|
961
|
+
packageId: Int,
|
|
962
|
+
scheduleId: Int,
|
|
963
|
+
title: String?,
|
|
964
|
+
location: String?,
|
|
965
|
+
time: String?,
|
|
966
|
+
endTimestamp: Int,
|
|
967
|
+
scheduleAuthority: Int,
|
|
968
|
+
scheduleTotal: Int,
|
|
969
|
+
scheduleNum: Int
|
|
970
|
+
): ByteArray {
|
|
971
|
+
val sched = schedule(scheduleId, title, location, time, endTimestamp)
|
|
972
|
+
|
|
973
|
+
// rScheduleWidget { f1 = scheduleTotal, f2 = scheduleNum, f3 = Schedule, f4 = scheduleAuthority }
|
|
974
|
+
val rSchedW = ProtobufWriter()
|
|
975
|
+
rSchedW.writeInt32Field(1, scheduleTotal)
|
|
976
|
+
rSchedW.writeInt32Field(2, scheduleNum)
|
|
977
|
+
rSchedW.writeMessageField(3, sched)
|
|
978
|
+
rSchedW.writeInt32Field(4, scheduleAuthority)
|
|
979
|
+
|
|
980
|
+
// rWidgetComponent { f3 = rScheduleWidget }
|
|
981
|
+
val rWidgetW = ProtobufWriter()
|
|
982
|
+
rWidgetW.writeMessageField(3, rSchedW.toByteArray())
|
|
983
|
+
|
|
984
|
+
// DashboardContent { f2 = rWidgetComponent }
|
|
985
|
+
val contentW = ProtobufWriter()
|
|
986
|
+
contentW.writeMessageField(2, rWidgetW.toByteArray())
|
|
987
|
+
|
|
988
|
+
// DashboardReceiveFromApp { f1 = packageId, f3 = DashboardContent }
|
|
989
|
+
val receiveW = ProtobufWriter()
|
|
990
|
+
receiveW.writeInt32Field(1, packageId)
|
|
991
|
+
receiveW.writeMessageField(3, contentW.toByteArray())
|
|
992
|
+
|
|
993
|
+
// DashboardDataPackage { f1 = commandId, f2 = magicRandom, f4 = dashboardReceive }
|
|
994
|
+
val pkgW = ProtobufWriter()
|
|
995
|
+
pkgW.writeInt32Field(1, DASHBOARD_RECEIVE)
|
|
996
|
+
pkgW.writeInt32Field(2, magicRandom)
|
|
997
|
+
pkgW.writeMessageField(4, receiveW.toByteArray())
|
|
998
|
+
return pkgW.toByteArray()
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
fun calendarClear(magicRandom: Int, packageId: Int, scheduleAuthority: Int): ByteArray {
|
|
1002
|
+
// rScheduleWidget with scheduleTotal=0 clears the widget without sending a stale Schedule.
|
|
1003
|
+
val rSchedW = ProtobufWriter()
|
|
1004
|
+
rSchedW.writeInt32Field(1, 0)
|
|
1005
|
+
rSchedW.writeInt32Field(2, 0)
|
|
1006
|
+
rSchedW.writeInt32Field(4, scheduleAuthority)
|
|
1007
|
+
|
|
1008
|
+
val rWidgetW = ProtobufWriter()
|
|
1009
|
+
rWidgetW.writeMessageField(3, rSchedW.toByteArray())
|
|
1010
|
+
|
|
1011
|
+
val contentW = ProtobufWriter()
|
|
1012
|
+
contentW.writeMessageField(2, rWidgetW.toByteArray())
|
|
1013
|
+
|
|
1014
|
+
val receiveW = ProtobufWriter()
|
|
1015
|
+
receiveW.writeInt32Field(1, packageId)
|
|
1016
|
+
receiveW.writeMessageField(3, contentW.toByteArray())
|
|
1017
|
+
|
|
1018
|
+
val pkgW = ProtobufWriter()
|
|
1019
|
+
pkgW.writeInt32Field(1, DASHBOARD_RECEIVE)
|
|
1020
|
+
pkgW.writeInt32Field(2, magicRandom)
|
|
1021
|
+
pkgW.writeMessageField(4, receiveW.toByteArray())
|
|
1022
|
+
return pkgW.toByteArray()
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
760
1026
|
// ---------- EvenBLE Transport Layer ----------
|
|
761
1027
|
|
|
762
1028
|
private object EvenBLETransport {
|
|
@@ -1041,7 +1307,6 @@ class G2 : SGCManager() {
|
|
|
1041
1307
|
private val EVEN_HUB_QUEUE_TICK_MS = 100L
|
|
1042
1308
|
private var startupPageCreated: Boolean = false
|
|
1043
1309
|
private var pageCreated: Boolean = false
|
|
1044
|
-
private var pageHasTextContainer: Boolean = false
|
|
1045
1310
|
private var currentTextContent: String = ""
|
|
1046
1311
|
private var textContainerID: Int = 1
|
|
1047
1312
|
private var imageSessionCounter: Int = 0
|
|
@@ -1050,13 +1315,103 @@ class G2 : SGCManager() {
|
|
|
1050
1315
|
private var leftAuthenticated: Boolean = false
|
|
1051
1316
|
private var rightAuthenticated: Boolean = false
|
|
1052
1317
|
private var currentBitmapBase64: String = ""
|
|
1318
|
+
private var dashboardShowing = 0
|
|
1053
1319
|
|
|
1054
1320
|
// Dashboard menu state
|
|
1055
1321
|
private var menuAppIdToPackageName: MutableMap<Int, String> = mutableMapOf()
|
|
1056
1322
|
private var dashboardMenuItems: MutableList<MenuProto.MenuItem> = mutableListOf()
|
|
1057
1323
|
private var activeMenuAppId: Int? = null
|
|
1058
1324
|
private var lastClickTimestamp: Long? = null
|
|
1325
|
+
private var lastEvenHubResponseTimestamp: Long? = null
|
|
1059
1326
|
private var lastMenuSelectTimestamp: Long? = null
|
|
1327
|
+
private var lastGestureCtrlTimestamp: Long? = null
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Serializes all display mutations (text/bitmap/clear/rebuild). Each public display entry point
|
|
1331
|
+
* launches on [displayScope] and runs its body under [displayMutex] so page shutdown/create and
|
|
1332
|
+
* fragment sends can never interleave with another update on the glasses.
|
|
1333
|
+
*/
|
|
1334
|
+
private val displayScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
1335
|
+
private val displayMutex = Mutex()
|
|
1336
|
+
|
|
1337
|
+
/** A tracked image container on the current page. Keyed by its rect for reuse. */
|
|
1338
|
+
private data class ImgContainer(
|
|
1339
|
+
val id: Int,
|
|
1340
|
+
val x: Int,
|
|
1341
|
+
val y: Int,
|
|
1342
|
+
val width: Int,
|
|
1343
|
+
val height: Int,
|
|
1344
|
+
// The converted BMP cached so the container can be re-sent on a page rebuild.
|
|
1345
|
+
// NOTE: this ByteArray makes the auto-generated equals/hashCode unreliable, but the
|
|
1346
|
+
// class is only ever compared via matches()/id, so that is never relied upon.
|
|
1347
|
+
var bmpData: ByteArray
|
|
1348
|
+
) {
|
|
1349
|
+
val name: String
|
|
1350
|
+
get() = "img-$id"
|
|
1351
|
+
|
|
1352
|
+
fun matches(x: Int, y: Int, width: Int, height: Int): Boolean =
|
|
1353
|
+
this.x == x && this.y == y && this.width == width && this.height == height
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** A tracked text container on the current page. Keyed by its rect for reuse. */
|
|
1357
|
+
private data class TextContainer(
|
|
1358
|
+
val id: Int,
|
|
1359
|
+
val x: Int,
|
|
1360
|
+
val y: Int,
|
|
1361
|
+
val width: Int,
|
|
1362
|
+
val height: Int,
|
|
1363
|
+
var content: String,
|
|
1364
|
+
val borderWidth: Int,
|
|
1365
|
+
val borderColor: Int,
|
|
1366
|
+
val borderRadius: Int,
|
|
1367
|
+
val paddingLength: Int
|
|
1368
|
+
) {
|
|
1369
|
+
val name: String
|
|
1370
|
+
get() = "text-$id"
|
|
1371
|
+
|
|
1372
|
+
fun matches(
|
|
1373
|
+
x: Int,
|
|
1374
|
+
y: Int,
|
|
1375
|
+
width: Int,
|
|
1376
|
+
height: Int,
|
|
1377
|
+
borderWidth: Int,
|
|
1378
|
+
borderColor: Int,
|
|
1379
|
+
borderRadius: Int,
|
|
1380
|
+
paddingLength: Int
|
|
1381
|
+
): Boolean =
|
|
1382
|
+
this.x == x &&
|
|
1383
|
+
this.y == y &&
|
|
1384
|
+
this.width == width &&
|
|
1385
|
+
this.height == height &&
|
|
1386
|
+
this.borderWidth == borderWidth &&
|
|
1387
|
+
this.borderColor == borderColor &&
|
|
1388
|
+
this.borderRadius == borderRadius &&
|
|
1389
|
+
this.paddingLength == paddingLength
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Live list of image containers on the page, ordered oldest→newest (for LRU eviction). The page
|
|
1394
|
+
* may hold at most 4 image containers (IDs from the pool below).
|
|
1395
|
+
*/
|
|
1396
|
+
private val imageContainers: MutableList<ImgContainer> = mutableListOf()
|
|
1397
|
+
private val textContainers: MutableList<TextContainer> = mutableListOf()
|
|
1398
|
+
/** Fixed pool of container IDs the page protocol expects. */
|
|
1399
|
+
private val imageContainerIDPool: List<Int> = listOf(10, 11, 12, 13)
|
|
1400
|
+
private val textContainerIDPool: List<Int> = listOf(1, 2, 3, 4, 5, 6)
|
|
1401
|
+
/** Default container seeded into every fresh page: 200x100 centered at 188,44. */
|
|
1402
|
+
private val defaultImgX = 188
|
|
1403
|
+
private val defaultImgY = 44
|
|
1404
|
+
private val defaultImgWidth = 200
|
|
1405
|
+
private val defaultImgHeight = 100
|
|
1406
|
+
/** Default text container: full-screen 576x288 with a 4px padding. */
|
|
1407
|
+
private val defaultTextX = 0
|
|
1408
|
+
private val defaultTextY = 0
|
|
1409
|
+
private val defaultTextWidth = 576
|
|
1410
|
+
private val defaultTextHeight = 288
|
|
1411
|
+
private val defaultTextBorderWidth = 0
|
|
1412
|
+
private val defaultTextBorderColor = 0
|
|
1413
|
+
private val defaultTextBorderRadius = 0
|
|
1414
|
+
private val defaultTextPaddingLength = 4
|
|
1060
1415
|
|
|
1061
1416
|
// Battery state
|
|
1062
1417
|
private var _batteryLevel: Int = -1
|
|
@@ -1114,6 +1469,7 @@ class G2 : SGCManager() {
|
|
|
1114
1469
|
left: Boolean = false,
|
|
1115
1470
|
right: Boolean = true
|
|
1116
1471
|
) {
|
|
1472
|
+
// Bridge.log("G2: sendToGlasses() - sending ${packets.size} packets first byte: ${packets[0][0]}")
|
|
1117
1473
|
if (packets.isEmpty()) return
|
|
1118
1474
|
// Single-packet sends (the common case for text/settings) go straight through.
|
|
1119
1475
|
if (packets.size == 1) {
|
|
@@ -1137,7 +1493,7 @@ class G2 : SGCManager() {
|
|
|
1137
1493
|
payload = payload,
|
|
1138
1494
|
reserveFlag = true
|
|
1139
1495
|
)
|
|
1140
|
-
sendToGlasses(packets)
|
|
1496
|
+
sendToGlasses(packets, left = true, right = true)
|
|
1141
1497
|
}
|
|
1142
1498
|
|
|
1143
1499
|
private fun sendDevSettingsCommand(
|
|
@@ -1153,6 +1509,16 @@ class G2 : SGCManager() {
|
|
|
1153
1509
|
sendToGlasses(packets, left = left, right = right)
|
|
1154
1510
|
}
|
|
1155
1511
|
|
|
1512
|
+
private fun sendNavigationCommand(payload: ByteArray) {
|
|
1513
|
+
val packets =
|
|
1514
|
+
sendManager.buildPackets(
|
|
1515
|
+
serviceId = ServiceID.NAVIGATION.value,
|
|
1516
|
+
payload = payload,
|
|
1517
|
+
reserveFlag = true
|
|
1518
|
+
)
|
|
1519
|
+
sendToGlasses(packets)
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1156
1522
|
private fun sendG2SettingCommand(payload: ByteArray) {
|
|
1157
1523
|
val packets =
|
|
1158
1524
|
sendManager.buildPackets(
|
|
@@ -1225,7 +1591,7 @@ class G2 : SGCManager() {
|
|
|
1225
1591
|
|
|
1226
1592
|
// ---------- Authentication Sequence ----------
|
|
1227
1593
|
|
|
1228
|
-
private fun runAuthSequence() {
|
|
1594
|
+
private suspend fun runAuthSequence() {
|
|
1229
1595
|
Bridge.log("G2: Running auth sequence")
|
|
1230
1596
|
|
|
1231
1597
|
// Auth to left side
|
|
@@ -1235,457 +1601,160 @@ class G2 : SGCManager() {
|
|
|
1235
1601
|
}
|
|
1236
1602
|
|
|
1237
1603
|
// Small delay then auth right + pipe role change + time sync
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
val heyEvenOff =
|
|
1274
|
-
EvenAIProto.setHeyEven(
|
|
1275
|
-
sendManager
|
|
1276
|
-
.nextMagicRandom(),
|
|
1277
|
-
false
|
|
1278
|
-
)
|
|
1279
|
-
sendEvenAICommand(heyEvenOff)
|
|
1280
|
-
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1281
|
-
|
|
1282
|
-
// Replicate Even app's full init sequence
|
|
1283
|
-
// for menu selection support:
|
|
1284
|
-
|
|
1285
|
-
// 0. Universe settings (g2_setting cmd=1
|
|
1286
|
-
// field3 with field9=universe settings)
|
|
1287
|
-
val univW = ProtobufWriter()
|
|
1288
|
-
univW.writeInt32Field(
|
|
1289
|
-
1,
|
|
1290
|
-
1
|
|
1291
|
-
) // DeviceReceiveInfo
|
|
1292
|
-
univW.writeInt32Field(
|
|
1293
|
-
2,
|
|
1294
|
-
sendManager.nextMagicRandom()
|
|
1295
|
-
)
|
|
1296
|
-
univW.writeMessageField(
|
|
1297
|
-
3,
|
|
1298
|
-
byteArrayOf(
|
|
1299
|
-
0x4A,
|
|
1300
|
-
0x0A, // field 9, length 10
|
|
1301
|
-
0x08,
|
|
1302
|
-
0x00, // unitFormat=0
|
|
1303
|
-
0x10,
|
|
1304
|
-
0x00, // distanceUnit=0
|
|
1305
|
-
0x18,
|
|
1306
|
-
0x01, // timeFormat=1
|
|
1307
|
-
0x20,
|
|
1308
|
-
0x00, // dateFormat=0
|
|
1309
|
-
0x28,
|
|
1310
|
-
0x01 // temperatureUnit=1
|
|
1311
|
-
)
|
|
1312
|
-
)
|
|
1313
|
-
sendG2SettingCommand(univW.toByteArray())
|
|
1314
|
-
|
|
1315
|
-
// 1. gesture_ctrl init (field1=0,
|
|
1316
|
-
// field2=magicRandom)
|
|
1317
|
-
val gestureInitW = ProtobufWriter()
|
|
1318
|
-
gestureInitW.writeInt32Field(1, 0)
|
|
1319
|
-
gestureInitW.writeInt32Field(
|
|
1320
|
-
2,
|
|
1321
|
-
sendManager.nextMagicRandom()
|
|
1322
|
-
)
|
|
1323
|
-
sendGestureCtrlCommand(
|
|
1324
|
-
gestureInitW.toByteArray()
|
|
1325
|
-
)
|
|
1326
|
-
|
|
1327
|
-
// 2. ui_setting_app (0x0C) — query
|
|
1328
|
-
val uiSettW = ProtobufWriter()
|
|
1329
|
-
uiSettW.writeInt32Field(
|
|
1330
|
-
1,
|
|
1331
|
-
2
|
|
1332
|
-
) // cmd = DeviceReceiveRequest
|
|
1333
|
-
uiSettW.writeInt32Field(
|
|
1334
|
-
2,
|
|
1335
|
-
sendManager.nextMagicRandom()
|
|
1336
|
-
)
|
|
1337
|
-
uiSettW.writeMessageField(
|
|
1338
|
-
4,
|
|
1339
|
-
byteArrayOf(0x08, 0x01, 0x10, 0x00)
|
|
1340
|
-
) // {1:1, 2:0}
|
|
1341
|
-
sendToGlasses(
|
|
1342
|
-
sendManager.buildPackets(
|
|
1343
|
-
serviceId = 0x0C,
|
|
1344
|
-
payload =
|
|
1345
|
-
uiSettW.toByteArray(),
|
|
1346
|
-
reserveFlag = true
|
|
1347
|
-
)
|
|
1348
|
-
)
|
|
1349
|
-
|
|
1350
|
-
// 3. teleprompter (0x10) — config (cmd=1,
|
|
1351
|
-
// field3={1:4})
|
|
1352
|
-
val teleW = ProtobufWriter()
|
|
1353
|
-
teleW.writeInt32Field(1, 1)
|
|
1354
|
-
teleW.writeInt32Field(
|
|
1355
|
-
2,
|
|
1356
|
-
sendManager.nextMagicRandom()
|
|
1357
|
-
)
|
|
1358
|
-
teleW.writeMessageField(
|
|
1359
|
-
3,
|
|
1360
|
-
byteArrayOf(0x08, 0x04)
|
|
1361
|
-
) // {1:4}
|
|
1362
|
-
sendToGlasses(
|
|
1363
|
-
sendManager.buildPackets(
|
|
1364
|
-
serviceId = 0x10,
|
|
1365
|
-
payload =
|
|
1366
|
-
teleW.toByteArray(),
|
|
1367
|
-
reserveFlag = true
|
|
1368
|
-
)
|
|
1369
|
-
)
|
|
1370
|
-
|
|
1371
|
-
// 4. EvenHub CTRL on service 0x81 (cmd=1,
|
|
1372
|
-
// empty field3)
|
|
1373
|
-
val ehCtrlW = ProtobufWriter()
|
|
1374
|
-
ehCtrlW.writeInt32Field(1, 1)
|
|
1375
|
-
ehCtrlW.writeInt32Field(
|
|
1376
|
-
2,
|
|
1377
|
-
sendManager.nextMagicRandom()
|
|
1378
|
-
)
|
|
1379
|
-
ehCtrlW.writeMessageField(3, ByteArray(0))
|
|
1380
|
-
sendEvenHubCtrlCommand(
|
|
1381
|
-
ehCtrlW.toByteArray()
|
|
1382
|
-
)
|
|
1383
|
-
|
|
1384
|
-
// 5. calendar (0x04) — config
|
|
1385
|
-
val calW = ProtobufWriter()
|
|
1386
|
-
calW.writeInt32Field(1, 1)
|
|
1387
|
-
calW.writeInt32Field(
|
|
1388
|
-
2,
|
|
1389
|
-
sendManager.nextMagicRandom()
|
|
1390
|
-
)
|
|
1391
|
-
calW.writeMessageField(
|
|
1392
|
-
3,
|
|
1393
|
-
byteArrayOf(
|
|
1394
|
-
0x08,
|
|
1395
|
-
0x01,
|
|
1396
|
-
0x10,
|
|
1397
|
-
0x01,
|
|
1398
|
-
0x18,
|
|
1399
|
-
0x05,
|
|
1400
|
-
0x28,
|
|
1401
|
-
0x01
|
|
1402
|
-
)
|
|
1403
|
-
)
|
|
1404
|
-
sendToGlasses(
|
|
1405
|
-
sendManager.buildPackets(
|
|
1406
|
-
serviceId = 0x04,
|
|
1407
|
-
payload =
|
|
1408
|
-
calW.toByteArray(),
|
|
1409
|
-
reserveFlag = true
|
|
1410
|
-
)
|
|
1411
|
-
)
|
|
1412
|
-
|
|
1413
|
-
// 6. Dashboard init (0x01) — display
|
|
1414
|
-
// settings
|
|
1415
|
-
val dashDisplayW = ProtobufWriter()
|
|
1416
|
-
dashDisplayW.writeInt32Field(
|
|
1417
|
-
1,
|
|
1418
|
-
4
|
|
1419
|
-
) // displayMode
|
|
1420
|
-
dashDisplayW.writeInt32Field(
|
|
1421
|
-
2,
|
|
1422
|
-
3
|
|
1423
|
-
) // statusDisplayCount
|
|
1424
|
-
dashDisplayW.writeMessageField(
|
|
1425
|
-
3,
|
|
1426
|
-
byteArrayOf(1, 2, 3)
|
|
1427
|
-
) // statusDisplayOrder
|
|
1428
|
-
dashDisplayW.writeInt32Field(
|
|
1429
|
-
4,
|
|
1430
|
-
4
|
|
1431
|
-
) // widgetDisplayCount
|
|
1432
|
-
dashDisplayW.writeMessageField(
|
|
1433
|
-
5,
|
|
1434
|
-
byteArrayOf(1, 3, 2, 2)
|
|
1435
|
-
) // widgetDisplayOrder
|
|
1436
|
-
dashDisplayW.writeInt32Field(
|
|
1437
|
-
6,
|
|
1438
|
-
1
|
|
1439
|
-
) // halfDayFormat
|
|
1440
|
-
dashDisplayW.writeInt32Field(
|
|
1441
|
-
7,
|
|
1442
|
-
1
|
|
1443
|
-
) // temperatureUnit
|
|
1444
|
-
|
|
1445
|
-
val dashRecvW = ProtobufWriter()
|
|
1446
|
-
dashRecvW.writeMessageField(
|
|
1447
|
-
2,
|
|
1448
|
-
dashDisplayW.toByteArray()
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
val dashPkgW = ProtobufWriter()
|
|
1452
|
-
dashPkgW.writeInt32Field(
|
|
1453
|
-
1,
|
|
1454
|
-
2
|
|
1455
|
-
) // Dashboard_Receive
|
|
1456
|
-
dashPkgW.writeInt32Field(
|
|
1457
|
-
2,
|
|
1458
|
-
sendManager.nextMagicRandom()
|
|
1459
|
-
)
|
|
1460
|
-
dashPkgW.writeMessageField(
|
|
1461
|
-
4,
|
|
1462
|
-
dashRecvW.toByteArray()
|
|
1463
|
-
)
|
|
1464
|
-
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1465
|
-
|
|
1466
|
-
// 7. Dashboard REQUEST_NEWS_INFO (cmd=5,
|
|
1467
|
-
// field7={1:1})
|
|
1468
|
-
val dashNewsReqW = ProtobufWriter()
|
|
1469
|
-
dashNewsReqW.writeInt32Field(
|
|
1470
|
-
1,
|
|
1471
|
-
5
|
|
1472
|
-
) // REQUEST_NEWS_INFO
|
|
1473
|
-
dashNewsReqW.writeInt32Field(
|
|
1474
|
-
2,
|
|
1475
|
-
sendManager.nextMagicRandom()
|
|
1476
|
-
)
|
|
1477
|
-
dashNewsReqW.writeMessageField(
|
|
1478
|
-
7,
|
|
1479
|
-
byteArrayOf(0x08, 0x01)
|
|
1480
|
-
) // {1:1}
|
|
1481
|
-
sendDashboardCommand(
|
|
1482
|
-
dashNewsReqW.toByteArray()
|
|
1483
|
-
)
|
|
1484
|
-
|
|
1485
|
-
// 8. Gesture control list via g2_setting
|
|
1486
|
-
val gestListW = ProtobufWriter()
|
|
1487
|
-
gestListW.writeInt32Field(
|
|
1488
|
-
1,
|
|
1489
|
-
1
|
|
1490
|
-
) // DeviceReceiveInfo
|
|
1491
|
-
gestListW.writeInt32Field(
|
|
1492
|
-
2,
|
|
1493
|
-
sendManager.nextMagicRandom()
|
|
1494
|
-
)
|
|
1495
|
-
// field 3 with field 10
|
|
1496
|
-
// (gestureControlList): 3 items, all
|
|
1497
|
-
// app_unable
|
|
1498
|
-
val gestureCtrlPayload =
|
|
1499
|
-
byteArrayOf(
|
|
1500
|
-
0x52,
|
|
1501
|
-
0x18, // field 10, length 24
|
|
1502
|
-
0x0A,
|
|
1503
|
-
0x06,
|
|
1504
|
-
0x08,
|
|
1505
|
-
0x00,
|
|
1506
|
-
0x10,
|
|
1507
|
-
0x00,
|
|
1508
|
-
0x18,
|
|
1509
|
-
0x00, // item 1
|
|
1510
|
-
0x0A,
|
|
1511
|
-
0x06,
|
|
1512
|
-
0x08,
|
|
1513
|
-
0x00,
|
|
1514
|
-
0x10,
|
|
1515
|
-
0x01,
|
|
1516
|
-
0x18,
|
|
1517
|
-
0x00, // item 2
|
|
1518
|
-
0x0A,
|
|
1519
|
-
0x06,
|
|
1520
|
-
0x08,
|
|
1521
|
-
0x00,
|
|
1522
|
-
0x10,
|
|
1523
|
-
0x02,
|
|
1524
|
-
0x18,
|
|
1525
|
-
0x00 // item 3
|
|
1526
|
-
)
|
|
1527
|
-
gestListW.writeMessageField(
|
|
1528
|
-
3,
|
|
1529
|
-
gestureCtrlPayload
|
|
1530
|
-
)
|
|
1531
|
-
sendG2SettingCommand(
|
|
1532
|
-
gestListW.toByteArray()
|
|
1533
|
-
)
|
|
1534
|
-
|
|
1535
|
-
// 9. Dashboard APP_REQUEST_NEWS_INFO
|
|
1536
|
-
// (cmd=7, field9={1:1})
|
|
1537
|
-
val dashAppNewsW = ProtobufWriter()
|
|
1538
|
-
dashAppNewsW.writeInt32Field(
|
|
1539
|
-
1,
|
|
1540
|
-
7
|
|
1541
|
-
) // APP_REQUEST_NEWS_INFO
|
|
1542
|
-
dashAppNewsW.writeInt32Field(
|
|
1543
|
-
2,
|
|
1544
|
-
sendManager.nextMagicRandom()
|
|
1545
|
-
)
|
|
1546
|
-
dashAppNewsW.writeMessageField(
|
|
1547
|
-
9,
|
|
1548
|
-
byteArrayOf(0x08, 0x01)
|
|
1549
|
-
) // {1:1}
|
|
1550
|
-
sendDashboardCommand(
|
|
1551
|
-
dashAppNewsW.toByteArray()
|
|
1552
|
-
)
|
|
1553
|
-
|
|
1554
|
-
Bridge.log(
|
|
1555
|
-
"G2: Sent full Even-compatible init sequence"
|
|
1556
|
-
)
|
|
1557
|
-
},
|
|
1558
|
-
200
|
|
1559
|
-
)
|
|
1560
|
-
|
|
1561
|
-
// Start heartbeats after auth
|
|
1562
|
-
startHeartbeats()
|
|
1563
|
-
|
|
1564
|
-
// Mark as ready and request device info
|
|
1565
|
-
mainHandler.postDelayed(
|
|
1566
|
-
{
|
|
1567
|
-
reconnectionManager.stop()
|
|
1568
|
-
Bridge.log(
|
|
1569
|
-
"G2: Auth sequence complete, glasses ready"
|
|
1570
|
-
)
|
|
1571
|
-
|
|
1572
|
-
// Set device_name so DeviceManager can save
|
|
1573
|
-
// it for reconnection
|
|
1574
|
-
val peripheralName =
|
|
1575
|
-
rightGatt?.device?.name
|
|
1576
|
-
?: leftGatt?.device?.name
|
|
1577
|
-
val serialNumber =
|
|
1578
|
-
peripheralName?.let {
|
|
1579
|
-
deviceNameToSerialNumber[it]
|
|
1580
|
-
}
|
|
1581
|
-
if (serialNumber != null) {
|
|
1582
|
-
DeviceStore.apply(
|
|
1583
|
-
"bluetooth",
|
|
1584
|
-
"device_name",
|
|
1585
|
-
serialNumber
|
|
1586
|
-
)
|
|
1587
|
-
Bridge.log(
|
|
1588
|
-
"G2: Set device_name to $serialNumber"
|
|
1589
|
-
)
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
// Set bluetooth name and device model for
|
|
1593
|
-
// Device Info page
|
|
1594
|
-
val btName =
|
|
1595
|
-
rightGatt?.device?.name
|
|
1596
|
-
?: leftGatt?.device?.name
|
|
1597
|
-
?: ""
|
|
1598
|
-
DeviceStore.apply(
|
|
1599
|
-
"glasses",
|
|
1600
|
-
"bluetoothName",
|
|
1601
|
-
btName
|
|
1602
|
-
)
|
|
1603
|
-
DeviceStore.apply(
|
|
1604
|
-
"glasses",
|
|
1605
|
-
"deviceModel",
|
|
1606
|
-
DeviceTypes.G2
|
|
1607
|
-
)
|
|
1608
|
-
|
|
1609
|
-
setFullyConnected()
|
|
1610
|
-
|
|
1611
|
-
// Connect a controller if we have one
|
|
1612
|
-
connectController()
|
|
1613
|
-
|
|
1614
|
-
// Query version + battery info from glasses
|
|
1615
|
-
requestDeviceInfo()
|
|
1616
|
-
|
|
1617
|
-
sendMenuApps()
|
|
1618
|
-
},
|
|
1619
|
-
500
|
|
1620
|
-
)
|
|
1621
|
-
},
|
|
1622
|
-
200
|
|
1623
|
-
)
|
|
1624
|
-
},
|
|
1625
|
-
200
|
|
1626
|
-
)
|
|
1627
|
-
},
|
|
1628
|
-
200
|
|
1604
|
+
delay(200)
|
|
1605
|
+
val authR = DevSettingsProto.authCmd(sendManager.nextMagicRandom())
|
|
1606
|
+
sendDevSettingsCommand(authR, left = false, right = true)
|
|
1607
|
+
|
|
1608
|
+
delay(200)
|
|
1609
|
+
val roleChange = DevSettingsProto.pipeRoleChange(sendManager.nextMagicRandom())
|
|
1610
|
+
sendDevSettingsCommand(roleChange, left = false, right = true)
|
|
1611
|
+
|
|
1612
|
+
delay(200)
|
|
1613
|
+
val timeSync = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
|
|
1614
|
+
sendDevSettingsCommand(timeSync)
|
|
1615
|
+
|
|
1616
|
+
// Skip onboarding on connect
|
|
1617
|
+
delay(200)
|
|
1618
|
+
val onboarding = OnboardingProto.skipOnboarding(sendManager.nextMagicRandom())
|
|
1619
|
+
sendOnboardingCommand(onboarding)
|
|
1620
|
+
Bridge.log("G2: Sent onboarding skip (FINISH)")
|
|
1621
|
+
|
|
1622
|
+
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1623
|
+
val gestureInitW = ProtobufWriter()
|
|
1624
|
+
gestureInitW.writeInt32Field(1, 0)
|
|
1625
|
+
gestureInitW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1626
|
+
sendGestureCtrlCommand(gestureInitW.toByteArray())
|
|
1627
|
+
|
|
1628
|
+
// 2. ui_setting_app (0x0C) — query (cmd=2, field4={settingInfoType=1, autoBrightnessLevel=0})
|
|
1629
|
+
val uiSettW = ProtobufWriter()
|
|
1630
|
+
uiSettW.writeInt32Field(1, 2) // cmd = DeviceReceiveRequest
|
|
1631
|
+
uiSettW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1632
|
+
uiSettW.writeMessageField(4, byteArrayOf(0x08, 0x01, 0x10, 0x00)) // {1:1, 2:0}
|
|
1633
|
+
sendToGlasses(
|
|
1634
|
+
sendManager.buildPackets(
|
|
1635
|
+
serviceId = 0x0C,
|
|
1636
|
+
payload = uiSettW.toByteArray(),
|
|
1637
|
+
reserveFlag = true
|
|
1638
|
+
)
|
|
1629
1639
|
)
|
|
1640
|
+
|
|
1641
|
+
// 6. Dashboard init (0x01) — display settings
|
|
1642
|
+
// halfDayFormat: 1 = 12h, 0 = 24h
|
|
1643
|
+
// temperatureUnit: 1 = Celsius (metric), 2 = Fahrenheit (imperial)
|
|
1644
|
+
val dashDisplayW = ProtobufWriter()
|
|
1645
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1646
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1647
|
+
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1648
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1649
|
+
// WidgetType: 1=News, 2=Stock, 3=Schedule, 4=Quicklist, 5=Health
|
|
1650
|
+
dashDisplayW.writeMessageField(
|
|
1651
|
+
5,
|
|
1652
|
+
byteArrayOf(3, 1, 2, 4, 5)
|
|
1653
|
+
) // widgetDisplayOrder: Schedule, News, Stock, Quicklist
|
|
1654
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1655
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1656
|
+
|
|
1657
|
+
val dashRecvW = ProtobufWriter()
|
|
1658
|
+
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
1659
|
+
|
|
1660
|
+
val dashPkgW = ProtobufWriter()
|
|
1661
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1662
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1663
|
+
dashPkgW.writeMessageField(4, dashRecvW.toByteArray())
|
|
1664
|
+
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1665
|
+
|
|
1666
|
+
// Disable "Hey Even" wakeword on connect
|
|
1667
|
+
val heyEvenOff = EvenAIProto.setHeyEven(sendManager.nextMagicRandom(), false)
|
|
1668
|
+
sendEvenAICommand(heyEvenOff)
|
|
1669
|
+
Bridge.log("G2: Disabled Hey Even wakeword")
|
|
1670
|
+
|
|
1671
|
+
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1672
|
+
|
|
1673
|
+
// Start heartbeats after auth
|
|
1674
|
+
startHeartbeats()
|
|
1675
|
+
|
|
1676
|
+
reconnectionManager.stop()
|
|
1677
|
+
Bridge.log("G2: Auth sequence complete, glasses ready")
|
|
1678
|
+
|
|
1679
|
+
// Set device_name so DeviceManager can save it for reconnection
|
|
1680
|
+
val peripheralName = rightGatt?.device?.name ?: leftGatt?.device?.name
|
|
1681
|
+
val serialNumber = peripheralName?.let { deviceNameToSerialNumber[it] }
|
|
1682
|
+
if (serialNumber != null) {
|
|
1683
|
+
DeviceStore.apply("bluetooth", "device_name", serialNumber)
|
|
1684
|
+
Bridge.log("G2: Set device_name to $serialNumber")
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Set bluetooth name and device model for Device Info page
|
|
1688
|
+
val btName = rightGatt?.device?.name ?: leftGatt?.device?.name ?: ""
|
|
1689
|
+
DeviceStore.apply("glasses", "bluetoothName", btName)
|
|
1690
|
+
DeviceStore.apply("glasses", "deviceModel", DeviceTypes.G2)
|
|
1691
|
+
|
|
1692
|
+
setFullyConnected()
|
|
1693
|
+
|
|
1694
|
+
// Connect a controller if we have one
|
|
1695
|
+
connectController()
|
|
1696
|
+
|
|
1697
|
+
// Query version + battery info from glasses
|
|
1698
|
+
requestDeviceInfo()
|
|
1699
|
+
|
|
1700
|
+
sendMenuApps()
|
|
1701
|
+
sendStoredCalendarEvents()
|
|
1702
|
+
|
|
1703
|
+
// Re-apply the IMU preference: the store only pushes imu_enabled to the glasses when
|
|
1704
|
+
// the value changes, so after a reconnect an already-on IMU would otherwise stay off
|
|
1705
|
+
// (accel_event stops) until the user toggles the setting again.
|
|
1706
|
+
val imuEnabled = DeviceStore.get("bluetooth", "imu_enabled") as? Boolean ?: false
|
|
1707
|
+
if (imuEnabled) {
|
|
1708
|
+
Bridge.log("G2: re-applying imu_enabled=true after connect")
|
|
1709
|
+
setImuEnabled(true)
|
|
1710
|
+
}
|
|
1630
1711
|
}
|
|
1631
1712
|
|
|
1632
|
-
private fun
|
|
1633
|
-
|
|
1713
|
+
private fun dashboardHalfDayFormat(): Int {
|
|
1714
|
+
val twelveHour = DeviceStore.get("bluetooth", "twelve_hour_time") as? Boolean ?: true
|
|
1715
|
+
return if (twelveHour) 1 else 0
|
|
1716
|
+
}
|
|
1634
1717
|
|
|
1635
|
-
|
|
1636
|
-
val
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
val dashRecvW = ProtobufWriter()
|
|
1660
|
-
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
1661
|
-
|
|
1662
|
-
val dashPkgW = ProtobufWriter()
|
|
1663
|
-
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1664
|
-
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1665
|
-
dashPkgW.writeMessageField(4, dashRecvW.toByteArray())
|
|
1666
|
-
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1667
|
-
|
|
1668
|
-
Bridge.log("G2: Sent full Even-compatible init sequence")
|
|
1669
|
-
},
|
|
1670
|
-
1000
|
|
1671
|
-
)
|
|
1718
|
+
private fun dashboardTemperatureUnit(): Int {
|
|
1719
|
+
val metric = DeviceStore.get("bluetooth", "metric_system") as? Boolean ?: false
|
|
1720
|
+
return if (metric) 1 else 2
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
override fun sendDashboardDisplaySettings() {
|
|
1724
|
+
val dashDisplayW = ProtobufWriter()
|
|
1725
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1726
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1727
|
+
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1728
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1729
|
+
dashDisplayW.writeMessageField(5, byteArrayOf(1, 3, 2, 2)) // widgetDisplayOrder
|
|
1730
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1731
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1732
|
+
|
|
1733
|
+
val dashRecvW = ProtobufWriter()
|
|
1734
|
+
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
1735
|
+
|
|
1736
|
+
val dashPkgW = ProtobufWriter()
|
|
1737
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1738
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1739
|
+
dashPkgW.writeMessageField(4, dashRecvW.toByteArray())
|
|
1740
|
+
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1672
1741
|
}
|
|
1673
1742
|
|
|
1674
1743
|
// ---------- Heartbeats ----------
|
|
1675
1744
|
|
|
1676
1745
|
private fun startHeartbeats() {
|
|
1677
|
-
// EvenHub heartbeat every
|
|
1746
|
+
// EvenHub heartbeat every 10 seconds
|
|
1678
1747
|
stopHeartbeats()
|
|
1679
1748
|
|
|
1680
1749
|
val hbRunnable =
|
|
1681
1750
|
object : Runnable {
|
|
1682
1751
|
override fun run() {
|
|
1683
1752
|
sendEvenHubHeartbeat()
|
|
1684
|
-
mainHandler.postDelayed(this,
|
|
1753
|
+
mainHandler.postDelayed(this, 10000)
|
|
1685
1754
|
}
|
|
1686
1755
|
}
|
|
1687
1756
|
heartbeatRunnable = hbRunnable
|
|
1688
|
-
mainHandler.postDelayed(hbRunnable,
|
|
1757
|
+
mainHandler.postDelayed(hbRunnable, 10000)
|
|
1689
1758
|
|
|
1690
1759
|
// DevSettings heartbeat every 5 seconds
|
|
1691
1760
|
val dsRunnable =
|
|
@@ -1760,157 +1829,335 @@ class G2 : SGCManager() {
|
|
|
1760
1829
|
}
|
|
1761
1830
|
}
|
|
1762
1831
|
|
|
1832
|
+
private fun sendStoredCalendarEvents() {
|
|
1833
|
+
val calendarEvents =
|
|
1834
|
+
DeviceStore.get("bluetooth", "calendar_events") as? List<Map<String, Any>>
|
|
1835
|
+
?: emptyList()
|
|
1836
|
+
sendCalendarEvents(calendarEvents)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1763
1839
|
// ---------- SGCManager: Display Control ----------
|
|
1764
1840
|
|
|
1765
1841
|
override fun sendTextWall(text: String) {
|
|
1766
|
-
|
|
1842
|
+
displayScope.launch {
|
|
1843
|
+
displayMutex.withLock {
|
|
1844
|
+
sendText(
|
|
1845
|
+
text,
|
|
1846
|
+
x = defaultTextX,
|
|
1847
|
+
y = defaultTextY,
|
|
1848
|
+
width = defaultTextWidth,
|
|
1849
|
+
height = defaultTextHeight,
|
|
1850
|
+
borderWidth = defaultTextBorderWidth,
|
|
1851
|
+
borderColor = defaultTextBorderColor,
|
|
1852
|
+
borderRadius = defaultTextBorderRadius,
|
|
1853
|
+
paddingLength = defaultTextPaddingLength
|
|
1854
|
+
)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1767
1858
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1859
|
+
private suspend fun sendText(
|
|
1860
|
+
text: String,
|
|
1861
|
+
x: Int? = null,
|
|
1862
|
+
y: Int? = null,
|
|
1863
|
+
width: Int? = null,
|
|
1864
|
+
height: Int? = null,
|
|
1865
|
+
borderWidth: Int? = null,
|
|
1866
|
+
borderColor: Int? = null,
|
|
1867
|
+
borderRadius: Int? = null,
|
|
1868
|
+
paddingLength: Int? = null
|
|
1869
|
+
) {
|
|
1870
|
+
// Bridge.log("G2: sendTextWall(${text.take(50)}...)")
|
|
1871
|
+
|
|
1872
|
+
// ignore events while the ER dashboard is open:
|
|
1873
|
+
val useNativeDashboard =
|
|
1874
|
+
DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
1875
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
1770
1876
|
return
|
|
1771
1877
|
}
|
|
1772
1878
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1879
|
+
val rx = x ?: defaultTextX
|
|
1880
|
+
val ry = y ?: defaultTextY
|
|
1881
|
+
val rw = width ?: defaultTextWidth
|
|
1882
|
+
val rh = height ?: defaultTextHeight
|
|
1883
|
+
val rBorderWidth = defaultTextBorderWidth
|
|
1884
|
+
val rBorderColor = defaultTextBorderColor
|
|
1885
|
+
val rBorderRadius = defaultTextBorderRadius
|
|
1886
|
+
val rPaddingLength = defaultTextPaddingLength
|
|
1887
|
+
val content = if (text.isEmpty()) " " else text
|
|
1888
|
+
|
|
1889
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
1890
|
+
val container: TextContainer
|
|
1891
|
+
val existingIndex =
|
|
1892
|
+
textContainers.indexOfFirst {
|
|
1893
|
+
it.matches(
|
|
1894
|
+
rx,
|
|
1895
|
+
ry,
|
|
1896
|
+
rw,
|
|
1897
|
+
rh,
|
|
1898
|
+
rBorderWidth,
|
|
1899
|
+
rBorderColor,
|
|
1900
|
+
rBorderRadius,
|
|
1901
|
+
rPaddingLength
|
|
1902
|
+
)
|
|
1903
|
+
}
|
|
1904
|
+
if (existingIndex >= 0) {
|
|
1905
|
+
textContainers[existingIndex].content = content
|
|
1906
|
+
container = textContainers[existingIndex]
|
|
1907
|
+
Bridge.log("G2: sendText() - reusing container ${container.id} for rect $rx,$ry ${rw}x$rh")
|
|
1908
|
+
if (!pageCreated) {
|
|
1909
|
+
rebuildPage()
|
|
1910
|
+
return
|
|
1911
|
+
}
|
|
1912
|
+
// update the text container:
|
|
1913
|
+
val msg =
|
|
1914
|
+
EvenHubProto.updateTextMessage(
|
|
1915
|
+
containerID = container.id,
|
|
1916
|
+
contentOffset = 0,
|
|
1917
|
+
contentLength = container.content.toByteArray(Charsets.UTF_8).size,
|
|
1918
|
+
content = container.content
|
|
1919
|
+
)
|
|
1920
|
+
sendEvenHubCommand(msg)
|
|
1921
|
+
return
|
|
1777
1922
|
}
|
|
1923
|
+
container =
|
|
1924
|
+
addTextContainer(
|
|
1925
|
+
rx,
|
|
1926
|
+
ry,
|
|
1927
|
+
rw,
|
|
1928
|
+
rh,
|
|
1929
|
+
content,
|
|
1930
|
+
rBorderWidth,
|
|
1931
|
+
rBorderColor,
|
|
1932
|
+
rBorderRadius,
|
|
1933
|
+
rPaddingLength
|
|
1934
|
+
)
|
|
1935
|
+
Bridge.log("G2: sendText() - added text container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
1936
|
+
rebuildPage()
|
|
1778
1937
|
}
|
|
1779
1938
|
|
|
1780
1939
|
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
1781
|
-
|
|
1940
|
+
Bridge.log("G2: sendDoubleTextWall() - top: $top, bottom: $bottom")
|
|
1941
|
+
// G2 doesn't have native double text wall, combine them
|
|
1942
|
+
val combined = "$top\n\n$bottom"
|
|
1782
1943
|
sendTextWall(combined)
|
|
1783
1944
|
}
|
|
1784
1945
|
|
|
1785
1946
|
override fun clearDisplay() {
|
|
1786
1947
|
Bridge.log("G2: clearDisplay()")
|
|
1787
|
-
|
|
1788
|
-
|
|
1948
|
+
displayScope.launch {
|
|
1949
|
+
displayMutex.withLock {
|
|
1950
|
+
// reset the content of all text containers to empty:
|
|
1951
|
+
for (i in textContainers.indices) {
|
|
1952
|
+
textContainers[i].content = " "
|
|
1953
|
+
}
|
|
1954
|
+
for (i in imageContainers.indices) {
|
|
1955
|
+
imageContainers[i].bmpData = ByteArray(0)
|
|
1956
|
+
}
|
|
1957
|
+
// shutdown the page and then recreate the containers without the content.
|
|
1958
|
+
// Reset pageCreated so the recreate below issues a fresh createPageMessage rather
|
|
1959
|
+
// than a rebuildPageMessage that no longer matches the torn-down page.
|
|
1960
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
1961
|
+
sendEvenHubCommand(msg)
|
|
1962
|
+
pageCreated = false
|
|
1963
|
+
createPageWithContainers()
|
|
1964
|
+
restartMicIfAlreadyEnabled()
|
|
1965
|
+
}
|
|
1789
1966
|
}
|
|
1790
1967
|
}
|
|
1791
1968
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1969
|
+
/**
|
|
1970
|
+
* Display a bitmap inside a positioned image container.
|
|
1971
|
+
*
|
|
1972
|
+
* The page keeps a live list of up to 4 image containers keyed by exact rect:
|
|
1973
|
+
* - If a container with the requested rect already exists, the image is just resent to it (no
|
|
1974
|
+
* page rebuild).
|
|
1975
|
+
* - Otherwise a new container is added (evicting the oldest when the list would exceed 4) and
|
|
1976
|
+
* the page is rebuilt before the image is sent.
|
|
1977
|
+
*
|
|
1978
|
+
* Omitted params default to a 100x100 container in the top-left corner.
|
|
1979
|
+
*/
|
|
1980
|
+
override fun displayBitmap(
|
|
1981
|
+
base64ImageData: String,
|
|
1982
|
+
x: Int?,
|
|
1983
|
+
y: Int?,
|
|
1984
|
+
width: Int?,
|
|
1985
|
+
height: Int?
|
|
1986
|
+
): Boolean {
|
|
1987
|
+
val rx = x ?: defaultImgX
|
|
1988
|
+
val ry = y ?: defaultImgY
|
|
1989
|
+
val rw = width ?: defaultImgWidth
|
|
1990
|
+
val rh = height ?: defaultImgHeight
|
|
1991
|
+
|
|
1992
|
+
// ignore events while the ER dashboard is open:
|
|
1993
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
1994
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
1995
|
+
return false
|
|
1996
|
+
}
|
|
1797
1997
|
|
|
1798
|
-
private fun displayBitmapQuad(base64ImageData: String): Boolean {
|
|
1799
1998
|
val rawData =
|
|
1800
1999
|
Base64.decode(base64ImageData, Base64.DEFAULT)
|
|
1801
2000
|
?: run {
|
|
1802
|
-
Bridge.log("G2:
|
|
2001
|
+
Bridge.log("G2: displayBitmap() - failed to decode base64")
|
|
1803
2002
|
return false
|
|
1804
2003
|
}
|
|
1805
2004
|
|
|
1806
|
-
val
|
|
1807
|
-
|
|
2005
|
+
val bmpData =
|
|
2006
|
+
convertToG2Bmp(rawData, containerWidth = rw, containerHeight = rh)
|
|
1808
2007
|
?: run {
|
|
1809
|
-
Bridge.log(
|
|
1810
|
-
"G2: displayBitmapQuad() - failed to slice image into tiles"
|
|
1811
|
-
)
|
|
2008
|
+
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
1812
2009
|
return false
|
|
1813
2010
|
}
|
|
1814
2011
|
|
|
1815
|
-
//
|
|
1816
|
-
val
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
width = 200,
|
|
1830
|
-
height = 100,
|
|
1831
|
-
containerID = 11,
|
|
1832
|
-
containerName = "img-11"
|
|
1833
|
-
)
|
|
1834
|
-
val container3 =
|
|
1835
|
-
EvenHubProto.imageContainerProperty(
|
|
1836
|
-
x = 0,
|
|
1837
|
-
y = 100,
|
|
1838
|
-
width = 200,
|
|
1839
|
-
height = 100,
|
|
1840
|
-
containerID = 12,
|
|
1841
|
-
containerName = "img-12"
|
|
1842
|
-
)
|
|
1843
|
-
val container4 =
|
|
1844
|
-
EvenHubProto.imageContainerProperty(
|
|
1845
|
-
x = 200,
|
|
1846
|
-
y = 100,
|
|
1847
|
-
width = 200,
|
|
1848
|
-
height = 100,
|
|
1849
|
-
containerID = 13,
|
|
1850
|
-
containerName = "img-13"
|
|
1851
|
-
)
|
|
1852
|
-
val containers = listOf(container1, container2, container3, container4)
|
|
1853
|
-
|
|
1854
|
-
val msg: ByteArray =
|
|
1855
|
-
if (!startupPageCreated) {
|
|
1856
|
-
startupPageCreated = true
|
|
1857
|
-
EvenHubProto.createPageMessage(
|
|
1858
|
-
imageContainers = containers,
|
|
1859
|
-
magicRandom = sendManager.nextMagicRandom(),
|
|
1860
|
-
appId = activeMenuAppId
|
|
1861
|
-
)
|
|
1862
|
-
} else {
|
|
1863
|
-
EvenHubProto.rebuildPageMessage(
|
|
1864
|
-
imageContainers = containers,
|
|
1865
|
-
magicRandom = sendManager.nextMagicRandom(),
|
|
1866
|
-
appId = activeMenuAppId
|
|
1867
|
-
)
|
|
2012
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
2013
|
+
val container: ImgContainer
|
|
2014
|
+
val existingIndex = imageContainers.indexOfFirst { it.matches(rx, ry, rw, rh) }
|
|
2015
|
+
if (existingIndex >= 0) {
|
|
2016
|
+
imageContainers[existingIndex].bmpData = bmpData
|
|
2017
|
+
container = imageContainers[existingIndex]
|
|
2018
|
+
Bridge.log("G2: displayBitmap() - reusing container ${container.id} for rect $rx,$ry ${rw}x$rh")
|
|
2019
|
+
displayScope.launch {
|
|
2020
|
+
displayMutex.withLock {
|
|
2021
|
+
if (!pageCreated) {
|
|
2022
|
+
rebuildPage()
|
|
2023
|
+
} else {
|
|
2024
|
+
sendImageData(container.id, container.name, container.bmpData)
|
|
2025
|
+
}
|
|
1868
2026
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
sendImageDataChained(
|
|
1881
|
-
tiles =
|
|
1882
|
-
listOf(
|
|
1883
|
-
Triple(10, "img-10", tiles[0]),
|
|
1884
|
-
Triple(11, "img-11", tiles[1]),
|
|
1885
|
-
Triple(12, "img-12", tiles[2]),
|
|
1886
|
-
Triple(13, "img-13", tiles[3])
|
|
1887
|
-
),
|
|
1888
|
-
index = 0
|
|
1889
|
-
)
|
|
1890
|
-
},
|
|
1891
|
-
1000
|
|
1892
|
-
)
|
|
2027
|
+
}
|
|
2028
|
+
return true
|
|
2029
|
+
} else {
|
|
2030
|
+
container = addImageContainer(rx, ry, rw, rh, bmpData)
|
|
2031
|
+
Bridge.log("G2: displayBitmap() - added container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
2032
|
+
displayScope.launch {
|
|
2033
|
+
displayMutex.withLock {
|
|
2034
|
+
rebuildPage()
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1893
2038
|
|
|
1894
2039
|
return true
|
|
1895
2040
|
}
|
|
1896
2041
|
|
|
1897
|
-
/**
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2042
|
+
/**
|
|
2043
|
+
* Add a new image container for the rect, evicting the oldest when the list is full (max 4).
|
|
2044
|
+
* Returns the newly tracked container (with an assigned ID from the pool).
|
|
2045
|
+
*/
|
|
2046
|
+
private fun addImageContainer(
|
|
2047
|
+
x: Int,
|
|
2048
|
+
y: Int,
|
|
2049
|
+
width: Int,
|
|
2050
|
+
height: Int,
|
|
2051
|
+
bmpData: ByteArray
|
|
2052
|
+
): ImgContainer {
|
|
2053
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2054
|
+
if (imageContainers.size >= imageContainerIDPool.size) {
|
|
2055
|
+
val evicted = imageContainers.removeAt(0)
|
|
2056
|
+
Bridge.log("G2: evicting oldest image container ${evicted.id}")
|
|
2057
|
+
}
|
|
2058
|
+
// Pick the lowest free ID from the pool.
|
|
2059
|
+
val usedIDs = imageContainers.map { it.id }.toSet()
|
|
2060
|
+
val id = imageContainerIDPool.firstOrNull { it !in usedIDs } ?: imageContainerIDPool[0]
|
|
2061
|
+
val container =
|
|
2062
|
+
ImgContainer(id = id, x = x, y = y, width = width, height = height, bmpData = bmpData)
|
|
2063
|
+
imageContainers.add(container)
|
|
2064
|
+
return container
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
private fun addTextContainer(
|
|
2068
|
+
x: Int,
|
|
2069
|
+
y: Int,
|
|
2070
|
+
width: Int,
|
|
2071
|
+
height: Int,
|
|
2072
|
+
content: String,
|
|
2073
|
+
borderWidth: Int,
|
|
2074
|
+
borderColor: Int,
|
|
2075
|
+
borderRadius: Int,
|
|
2076
|
+
paddingLength: Int
|
|
2077
|
+
): TextContainer {
|
|
2078
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2079
|
+
if (textContainers.size >= textContainerIDPool.size) {
|
|
2080
|
+
val evicted = textContainers.removeAt(0)
|
|
2081
|
+
Bridge.log("G2: evicting oldest text container ${evicted.id}")
|
|
2082
|
+
}
|
|
2083
|
+
// Pick the lowest free ID from the pool.
|
|
2084
|
+
val usedIDs = textContainers.map { it.id }.toSet()
|
|
2085
|
+
val id = textContainerIDPool.firstOrNull { it !in usedIDs } ?: textContainerIDPool[0]
|
|
2086
|
+
val container =
|
|
2087
|
+
TextContainer(
|
|
2088
|
+
id = id,
|
|
2089
|
+
x = x,
|
|
2090
|
+
y = y,
|
|
2091
|
+
width = width,
|
|
2092
|
+
height = height,
|
|
2093
|
+
content = content,
|
|
2094
|
+
borderWidth = borderWidth,
|
|
2095
|
+
borderColor = borderColor,
|
|
2096
|
+
borderRadius = borderRadius,
|
|
2097
|
+
paddingLength = paddingLength
|
|
2098
|
+
)
|
|
2099
|
+
textContainers.add(container)
|
|
2100
|
+
return container
|
|
1907
2101
|
}
|
|
1908
2102
|
|
|
1909
|
-
|
|
2103
|
+
/**
|
|
2104
|
+
* Shutdown and rebuild everything, re-sending all data to the glasses.
|
|
2105
|
+
*
|
|
2106
|
+
* Suspends until the rebuild (shutdown → create → image/text re-send) has been issued, so
|
|
2107
|
+
* callers no longer race a detached coroutine. Always invoke from within [displayMutex].
|
|
2108
|
+
*/
|
|
2109
|
+
private suspend fun rebuildPage() {
|
|
2110
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2111
|
+
sendEvenHubCommand(msg)
|
|
2112
|
+
pageCreated = false
|
|
2113
|
+
rebuildState()
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* Re-creates the containers and sends all images and text again to the glasses.
|
|
2118
|
+
*
|
|
2119
|
+
* Runs inline (suspending) rather than launching a detached coroutine, so the page is fully
|
|
2120
|
+
* rebuilt before the caller continues. Always invoke from within [displayMutex].
|
|
2121
|
+
*/
|
|
2122
|
+
private suspend fun rebuildState() {
|
|
2123
|
+
Bridge.log("G2: rebuildState()")
|
|
2124
|
+
// recreate the containers:
|
|
2125
|
+
createPageWithContainers()
|
|
2126
|
+
|
|
2127
|
+
delay(300) // 300ms to settle
|
|
2128
|
+
|
|
2129
|
+
// go through each image container and send the data:
|
|
2130
|
+
for (container in imageContainers) {
|
|
2131
|
+
sendImageData(container.id, container.name, container.bmpData)
|
|
2132
|
+
delay(300) // 300ms between containers
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// go through each text container and re-send its content, otherwise text stays blank after a
|
|
2136
|
+
// disabled because text containers are initialized with their content:
|
|
2137
|
+
// for (container in textContainers) {
|
|
2138
|
+
// val textMsg =
|
|
2139
|
+
// EvenHubProto.updateTextMessage(
|
|
2140
|
+
// containerID = container.id,
|
|
2141
|
+
// contentOffset = 0,
|
|
2142
|
+
// contentLength = container.content.toByteArray(Charsets.UTF_8).size,
|
|
2143
|
+
// content = container.content
|
|
2144
|
+
// )
|
|
2145
|
+
// sendEvenHubCommand(textMsg)
|
|
2146
|
+
// delay(100)
|
|
2147
|
+
// }
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Send a bitmap to an image container as fragmented updateImageRawData packets.
|
|
2152
|
+
*
|
|
2153
|
+
* Suspends until every fragment has been sent, mirroring iOS `sendImageData` (which awaits
|
|
2154
|
+
* 200ms after every fragment, including the last). Callers therefore get the same serialized
|
|
2155
|
+
* timing window: the 300ms settle in [rebuildState] only runs once all fragments are out.
|
|
2156
|
+
*/
|
|
2157
|
+
private suspend fun sendImageData(
|
|
1910
2158
|
containerID: Int,
|
|
1911
2159
|
containerName: String,
|
|
1912
|
-
bmpData: ByteArray
|
|
1913
|
-
onComplete: (() -> Unit)? = null
|
|
2160
|
+
bmpData: ByteArray
|
|
1914
2161
|
) {
|
|
1915
2162
|
val fragmentSize = 4096
|
|
1916
2163
|
imageSessionCounter++
|
|
@@ -1919,15 +2166,9 @@ class G2 : SGCManager() {
|
|
|
1919
2166
|
var fragmentIndex = 0
|
|
1920
2167
|
var offset = 0
|
|
1921
2168
|
|
|
1922
|
-
|
|
1923
|
-
if (offset >= bmpData.size) {
|
|
1924
|
-
Bridge.log(
|
|
1925
|
-
"G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes"
|
|
1926
|
-
)
|
|
1927
|
-
onComplete?.invoke()
|
|
1928
|
-
return
|
|
1929
|
-
}
|
|
2169
|
+
Bridge.log("G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes")
|
|
1930
2170
|
|
|
2171
|
+
while (offset < bmpData.size) {
|
|
1931
2172
|
val end = minOf(offset + fragmentSize, bmpData.size)
|
|
1932
2173
|
val fragment = bmpData.copyOfRange(offset, end)
|
|
1933
2174
|
|
|
@@ -1947,110 +2188,25 @@ class G2 : SGCManager() {
|
|
|
1947
2188
|
|
|
1948
2189
|
fragmentIndex++
|
|
1949
2190
|
offset = end
|
|
1950
|
-
|
|
1951
|
-
// 200ms between fragments — and also before onComplete, so the next tile in
|
|
1952
|
-
// sendImageDataChained gets the same gap before its first fragment (matches iOS,
|
|
1953
|
-
// which awaits 200ms after every fragment including the last).
|
|
1954
|
-
if (offset < bmpData.size) {
|
|
1955
|
-
mainHandler.postDelayed({ sendNextFragment() }, 200)
|
|
1956
|
-
} else {
|
|
1957
|
-
Bridge.log(
|
|
1958
|
-
"G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes"
|
|
1959
|
-
)
|
|
1960
|
-
mainHandler.postDelayed({ onComplete?.invoke() }, 200)
|
|
1961
|
-
}
|
|
2191
|
+
delay(200) // 200ms between fragments (and after the last, matching iOS)
|
|
1962
2192
|
}
|
|
1963
|
-
|
|
1964
|
-
sendNextFragment()
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
/**
|
|
1968
|
-
* Render any image to 400x200 grayscale, then slice into 4 tiles (200x100 each).
|
|
1969
|
-
* Returns 4 BMP ByteArrays: [top-left, top-right, bottom-left, bottom-right].
|
|
1970
|
-
* Mirrors G2.swift renderAndSliceTo4Tiles.
|
|
1971
|
-
*/
|
|
1972
|
-
private fun renderAndSliceTo4Tiles(data: ByteArray): List<ByteArray>? {
|
|
1973
|
-
val srcBitmap =
|
|
1974
|
-
BitmapFactory.decodeByteArray(data, 0, data.size)
|
|
1975
|
-
?: run {
|
|
1976
|
-
Bridge.log("G2: renderAndSliceTo4Tiles - could not decode image")
|
|
1977
|
-
return null
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
val srcWidth = srcBitmap.width
|
|
1981
|
-
val srcHeight = srcBitmap.height
|
|
1982
|
-
val tileWidth = 200
|
|
1983
|
-
val tileHeight = 100
|
|
1984
|
-
val totalW = tileWidth * 2 // 400
|
|
1985
|
-
val totalH = tileHeight * 2 // 200
|
|
1986
|
-
|
|
1987
|
-
// Scale source to fit within 400x200 (maintain aspect ratio)
|
|
1988
|
-
val scale = minOf(totalW.toDouble() / srcWidth, totalH.toDouble() / srcHeight)
|
|
1989
|
-
val scaledW = maxOf(1, (srcWidth * scale).toInt())
|
|
1990
|
-
val scaledH = maxOf(1, (srcHeight * scale).toInt())
|
|
1991
|
-
val offsetX = (totalW - scaledW) / 2
|
|
1992
|
-
val offsetY = (totalH - scaledH) / 2
|
|
1993
|
-
|
|
1994
|
-
Bridge.log(
|
|
1995
|
-
"G2: renderAndSliceTo4Tiles - input ${srcWidth}x${srcHeight} → ${scaledW}x${scaledH} in ${totalW}x${totalH}"
|
|
1996
|
-
)
|
|
1997
|
-
|
|
1998
|
-
// Render to 400x200 with black background
|
|
1999
|
-
val destBitmap = Bitmap.createBitmap(totalW, totalH, Bitmap.Config.ARGB_8888)
|
|
2000
|
-
val canvas = Canvas(destBitmap)
|
|
2001
|
-
canvas.drawColor(Color.BLACK)
|
|
2002
|
-
val srcRect = Rect(0, 0, srcWidth, srcHeight)
|
|
2003
|
-
val dstRect = Rect(offsetX, offsetY, offsetX + scaledW, offsetY + scaledH)
|
|
2004
|
-
val paint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
2005
|
-
canvas.drawBitmap(srcBitmap, srcRect, dstRect, paint)
|
|
2006
|
-
srcBitmap.recycle()
|
|
2007
|
-
|
|
2008
|
-
// Pull a single 400x200 grayscale buffer once; slice each tile from it
|
|
2009
|
-
val fullPixels = ByteArray(totalW * totalH)
|
|
2010
|
-
val argbRow = IntArray(totalW)
|
|
2011
|
-
for (y in 0 until totalH) {
|
|
2012
|
-
destBitmap.getPixels(argbRow, 0, totalW, 0, y, totalW, 1)
|
|
2013
|
-
val rowOffset = y * totalW
|
|
2014
|
-
for (x in 0 until totalW) {
|
|
2015
|
-
val pixel = argbRow[x]
|
|
2016
|
-
val r = (pixel shr 16) and 0xFF
|
|
2017
|
-
val g = (pixel shr 8) and 0xFF
|
|
2018
|
-
val b = pixel and 0xFF
|
|
2019
|
-
fullPixels[rowOffset + x] = ((r * 299 + g * 587 + b * 114) / 1000).toByte()
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
destBitmap.recycle()
|
|
2023
|
-
|
|
2024
|
-
// Slice into 4 tiles: top-left, top-right, bottom-left, bottom-right (matches iOS order)
|
|
2025
|
-
val tileOrigins = listOf(0 to 0, tileWidth to 0, 0 to tileHeight, tileWidth to tileHeight)
|
|
2026
|
-
val tiles = mutableListOf<ByteArray>()
|
|
2027
|
-
for ((ox, oy) in tileOrigins) {
|
|
2028
|
-
val tilePixels = ByteArray(tileWidth * tileHeight)
|
|
2029
|
-
for (row in 0 until tileHeight) {
|
|
2030
|
-
val srcRowStart = (oy + row) * totalW + ox
|
|
2031
|
-
System.arraycopy(
|
|
2032
|
-
fullPixels,
|
|
2033
|
-
srcRowStart,
|
|
2034
|
-
tilePixels,
|
|
2035
|
-
row * tileWidth,
|
|
2036
|
-
tileWidth
|
|
2037
|
-
)
|
|
2038
|
-
}
|
|
2039
|
-
val bmp =
|
|
2040
|
-
build4BitBmp(tilePixels, tileWidth, tileHeight)
|
|
2041
|
-
?: run {
|
|
2042
|
-
Bridge.log(
|
|
2043
|
-
"G2: renderAndSliceTo4Tiles - failed to build BMP for tile"
|
|
2044
|
-
)
|
|
2045
|
-
return null
|
|
2046
|
-
}
|
|
2047
|
-
tiles.add(bmp)
|
|
2048
|
-
}
|
|
2049
|
-
return tiles
|
|
2050
2193
|
}
|
|
2051
2194
|
|
|
2195
|
+
/// Bring the Even Realities dashboard (the OS-level home/idle screen) to
|
|
2196
|
+
/// the foreground by tearing down whatever EvenHub page we currently own.
|
|
2197
|
+
/// The glasses fall back to the dashboard automatically when no page is up.
|
|
2052
2198
|
override fun showDashboard() {
|
|
2053
|
-
|
|
2199
|
+
Bridge.log("G2: showDashboard()")
|
|
2200
|
+
dashboardShowing += 2
|
|
2201
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2202
|
+
sendEvenHubCommand(msg)
|
|
2203
|
+
pageCreated = false
|
|
2204
|
+
currentBitmapBase64 = ""
|
|
2205
|
+
mainHandler.postDelayed({
|
|
2206
|
+
// activate the dashboard by setting depth to the current setting:
|
|
2207
|
+
val currentDepth = DeviceStore.get("bluetooth", "dashboard_depth") as? Int ?: 0
|
|
2208
|
+
setDashboardDepthOnly(currentDepth)
|
|
2209
|
+
}, 500)
|
|
2054
2210
|
}
|
|
2055
2211
|
|
|
2056
2212
|
override fun setDashboardPosition(height: Int, depth: Int) {
|
|
@@ -2091,6 +2247,93 @@ class G2 : SGCManager() {
|
|
|
2091
2247
|
sendMenuCommand(msg)
|
|
2092
2248
|
}
|
|
2093
2249
|
|
|
2250
|
+
/**
|
|
2251
|
+
* Bridge entry for `calendar_events` store updates. Each map is expected to match
|
|
2252
|
+
* the TS `CalendarEvent` shape: { title, location?, time, endDate } where `endDate`
|
|
2253
|
+
* is Unix seconds.
|
|
2254
|
+
*
|
|
2255
|
+
* Sends one BLE push per event, with `scheduleTotal` set to the batch size and
|
|
2256
|
+
* `scheduleNum` set to this event's 0-based slot. The widget pages through them on
|
|
2257
|
+
* the glasses — without paging info the firmware overwrites slot 0 on each push and
|
|
2258
|
+
* only the last event survives.
|
|
2259
|
+
*/
|
|
2260
|
+
override fun sendCalendarEvents(events: List<Map<String, Any>>) {
|
|
2261
|
+
Bridge.log("G2: sendCalendarEvents -- ${events.size} events")
|
|
2262
|
+
if (events.isEmpty()) {
|
|
2263
|
+
sendDashboardCommand(
|
|
2264
|
+
CalendarProto.calendarClear(
|
|
2265
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2266
|
+
packageId = 1,
|
|
2267
|
+
scheduleAuthority = 1
|
|
2268
|
+
)
|
|
2269
|
+
)
|
|
2270
|
+
return
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
val total = events.size
|
|
2274
|
+
// Fold the local TZ offset into the timestamp so the glasses (which treat
|
|
2275
|
+
// timestamps as already-local) display the correct time — same hack as time-sync.
|
|
2276
|
+
val tzSec = TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 1000
|
|
2277
|
+
events.forEachIndexed { i, ev ->
|
|
2278
|
+
val title = ev["title"] as? String ?: return@forEachIndexed
|
|
2279
|
+
val time = ev["time"] as? String ?: return@forEachIndexed
|
|
2280
|
+
val endTs = (ev["endDate"] as? Number)?.toLong() ?: return@forEachIndexed
|
|
2281
|
+
val location = ev["location"] as? String
|
|
2282
|
+
|
|
2283
|
+
val payload =
|
|
2284
|
+
CalendarProto.calendarPush(
|
|
2285
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2286
|
+
packageId = 1,
|
|
2287
|
+
scheduleId = i + 1,
|
|
2288
|
+
title = title,
|
|
2289
|
+
location = location,
|
|
2290
|
+
time = time,
|
|
2291
|
+
endTimestamp = (endTs + tzSec).toInt(),
|
|
2292
|
+
scheduleAuthority = 1,
|
|
2293
|
+
scheduleTotal = total,
|
|
2294
|
+
scheduleNum = i
|
|
2295
|
+
)
|
|
2296
|
+
sendDashboardCommand(payload)
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Open the on-glasses notification panel — same effect as the user saying
|
|
2302
|
+
* "Hey Even, show notifications". Replicates the official-app voice flow:
|
|
2303
|
+
* 1. CTRL{status=ENTER} — puts glasses in AI session
|
|
2304
|
+
* 2. ASK{text=" "} — minimal ASR transcript to seed session context
|
|
2305
|
+
* 3. SKILL{skillId=NOTIFICATION, skillParam=show, ...} — dispatches the intent
|
|
2306
|
+
* The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2307
|
+
* supply the session context that lets the glasses act on the SKILL.
|
|
2308
|
+
*/
|
|
2309
|
+
override suspend fun showNotificationsPanel() {
|
|
2310
|
+
Bridge.log("G2: showNotificationsPanel()")
|
|
2311
|
+
val enterPayload = EvenAIProto.aiCtrl(
|
|
2312
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2313
|
+
status = 2 // EVEN_AI_ENTER
|
|
2314
|
+
)
|
|
2315
|
+
sendEvenAICommand(enterPayload)
|
|
2316
|
+
|
|
2317
|
+
delay(400)
|
|
2318
|
+
val askPayload = EvenAIProto.aiAsk(
|
|
2319
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2320
|
+
text = " ",
|
|
2321
|
+
streamEnable = 0
|
|
2322
|
+
)
|
|
2323
|
+
sendEvenAICommand(askPayload)
|
|
2324
|
+
|
|
2325
|
+
delay(400)
|
|
2326
|
+
val skillPayload = EvenAIProto.triggerSkill(
|
|
2327
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2328
|
+
skillId = 3, // NOTIFICATION
|
|
2329
|
+
skillParam = 1, // show
|
|
2330
|
+
text = " ",
|
|
2331
|
+
streamEnable = 1,
|
|
2332
|
+
fTextEnd = 1
|
|
2333
|
+
)
|
|
2334
|
+
sendEvenAICommand(skillPayload)
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2094
2337
|
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
2095
2338
|
Bridge.log("G2: setBrightness($level, auto=$autoMode)")
|
|
2096
2339
|
val msg =
|
|
@@ -2104,60 +2347,81 @@ class G2 : SGCManager() {
|
|
|
2104
2347
|
|
|
2105
2348
|
// ---------- Private Display Helpers ----------
|
|
2106
2349
|
|
|
2107
|
-
private fun
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2350
|
+
private fun createPageWithContainers() {
|
|
2351
|
+
// build the page's text containers from the live tracked list.
|
|
2352
|
+
val textContainerProps: List<ByteArray> = ArrayList<ByteArray>(textContainers.size).apply {
|
|
2353
|
+
for (i in textContainers.indices) {
|
|
2354
|
+
val c = textContainers[i]
|
|
2355
|
+
add(
|
|
2356
|
+
EvenHubProto.textContainerProperty(
|
|
2357
|
+
x = c.x,
|
|
2358
|
+
y = c.y,
|
|
2359
|
+
width = c.width,
|
|
2360
|
+
height = c.height,
|
|
2361
|
+
borderWidth = c.borderWidth,
|
|
2362
|
+
borderColor = c.borderColor,
|
|
2363
|
+
borderRadius = c.borderRadius,
|
|
2364
|
+
paddingLength = c.paddingLength,
|
|
2365
|
+
containerID = c.id,
|
|
2366
|
+
containerName = c.name,
|
|
2367
|
+
isEventCapture = i == 0,// the first container is the event capture container
|
|
2368
|
+
content = c.content
|
|
2369
|
+
)
|
|
2122
2370
|
)
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
// iterate all image containers, remove any entries with duplicate ids or empty data,
|
|
2376
|
+
// and ensure the ids are still in the imageContainerIDPool:
|
|
2377
|
+
val seenIDs = mutableSetOf<Int>()
|
|
2378
|
+
imageContainers.retainAll { c ->
|
|
2379
|
+
if (c.bmpData.isEmpty()) {
|
|
2380
|
+
Bridge.log("G2: removing empty image container ${c.id}")
|
|
2381
|
+
return@retainAll false
|
|
2382
|
+
}
|
|
2383
|
+
if (!seenIDs.add(c.id)) {
|
|
2384
|
+
Bridge.log("G2: removing duplicate image container ${c.id}")
|
|
2385
|
+
return@retainAll false
|
|
2386
|
+
}
|
|
2387
|
+
imageContainerIDPool.contains(c.id)
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// Build the page's image containers from the live tracked list.
|
|
2391
|
+
val imageContainerProps: List<ByteArray> =
|
|
2392
|
+
imageContainers.map { c ->
|
|
2393
|
+
EvenHubProto.imageContainerProperty(
|
|
2394
|
+
x = c.x,
|
|
2395
|
+
y = c.y,
|
|
2396
|
+
width = c.width,
|
|
2397
|
+
height = c.height,
|
|
2398
|
+
containerID = c.id,
|
|
2399
|
+
containerName = c.name
|
|
2400
|
+
)
|
|
2401
|
+
}
|
|
2123
2402
|
|
|
2124
2403
|
val msg: ByteArray
|
|
2125
|
-
if (!
|
|
2126
|
-
Bridge.log("G2:
|
|
2404
|
+
if (!pageCreated) {
|
|
2405
|
+
Bridge.log("G2: using createPageMessage (first time)")
|
|
2127
2406
|
msg =
|
|
2128
2407
|
EvenHubProto.createPageMessage(
|
|
2129
|
-
textContainers =
|
|
2408
|
+
textContainers = textContainerProps,
|
|
2409
|
+
imageContainers = imageContainerProps,
|
|
2130
2410
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2131
2411
|
appId = activeMenuAppId
|
|
2132
2412
|
)
|
|
2133
|
-
startupPageCreated = true
|
|
2134
2413
|
} else {
|
|
2135
|
-
Bridge.log("G2:
|
|
2414
|
+
Bridge.log("G2: using rebuildPageMessage")
|
|
2136
2415
|
msg =
|
|
2137
2416
|
EvenHubProto.rebuildPageMessage(
|
|
2138
|
-
textContainers =
|
|
2417
|
+
textContainers = textContainerProps,
|
|
2418
|
+
imageContainers = imageContainerProps,
|
|
2139
2419
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2140
2420
|
appId = activeMenuAppId
|
|
2141
2421
|
)
|
|
2142
2422
|
}
|
|
2143
2423
|
sendEvenHubCommand(msg)
|
|
2144
2424
|
pageCreated = true
|
|
2145
|
-
pageHasTextContainer = true
|
|
2146
|
-
currentTextContent = text
|
|
2147
|
-
currentBitmapBase64 = ""
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
private fun updateText(text: String) {
|
|
2151
|
-
val msg =
|
|
2152
|
-
EvenHubProto.updateTextMessage(
|
|
2153
|
-
containerID = textContainerID,
|
|
2154
|
-
contentOffset = 0,
|
|
2155
|
-
contentLength = text.toByteArray(Charsets.UTF_8).size,
|
|
2156
|
-
content = text
|
|
2157
|
-
)
|
|
2158
|
-
queueEvenHubCommand(msg)
|
|
2159
|
-
currentTextContent = text
|
|
2160
|
-
currentBitmapBase64 = ""
|
|
2161
2425
|
}
|
|
2162
2426
|
|
|
2163
2427
|
@Synchronized
|
|
@@ -2307,9 +2571,9 @@ class G2 : SGCManager() {
|
|
|
2307
2571
|
bmp.write(rowBuf)
|
|
2308
2572
|
}
|
|
2309
2573
|
|
|
2310
|
-
Bridge.log(
|
|
2311
|
-
|
|
2312
|
-
)
|
|
2574
|
+
// Bridge.log(
|
|
2575
|
+
// "G2: build4BitBmp - ${bmp.size()} bytes (header=$headerSize, pixels=$pixelDataSize, rows=${paddedRowSize}x$height)"
|
|
2576
|
+
// )
|
|
2313
2577
|
return bmp.toByteArray()
|
|
2314
2578
|
}
|
|
2315
2579
|
|
|
@@ -2327,21 +2591,40 @@ class G2 : SGCManager() {
|
|
|
2327
2591
|
|
|
2328
2592
|
// ---------- SGCManager: Audio Control ----------
|
|
2329
2593
|
|
|
2330
|
-
|
|
2331
|
-
Bridge.log("G2: setMicEnabled($enabled)")
|
|
2594
|
+
private fun restartMicIfAlreadyEnabled() {
|
|
2332
2595
|
val currentEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
2596
|
+
if (currentEnabled) {
|
|
2597
|
+
restartMic()
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2333
2600
|
|
|
2601
|
+
fun restartMic() {
|
|
2334
2602
|
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2603
|
+
DeviceStore.apply("glasses", "micEnabled", true)
|
|
2604
|
+
val msg = EvenHubProto.audioControlMessage(false)
|
|
2605
|
+
sendEvenHubCommand(msg)
|
|
2606
|
+
mainHandler.postDelayed({
|
|
2607
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
2608
|
+
// Bridge.log("G2: setMicEnabled - useNativeDashboard=$useNativeDashboard, dashboardShowing=$dashboardShowing")
|
|
2609
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
2610
|
+
return@postDelayed
|
|
2611
|
+
}
|
|
2612
|
+
if (!pageCreated) {
|
|
2613
|
+
DeviceManager.getInstance().sendCurrentState() // should re-create the page if needed
|
|
2614
|
+
}
|
|
2615
|
+
val msg = EvenHubProto.audioControlMessage(true)
|
|
2338
2616
|
sendEvenHubCommand(msg)
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2617
|
+
}, 500)
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
override fun setMicEnabled(enabled: Boolean) {
|
|
2621
|
+
Bridge.log("G2: setMicEnabled($enabled)")
|
|
2622
|
+
val currentEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
2623
|
+
if (currentEnabled && enabled) {
|
|
2624
|
+
restartMic()
|
|
2343
2625
|
return
|
|
2344
2626
|
}
|
|
2627
|
+
|
|
2345
2628
|
DeviceStore.apply("glasses", "micEnabled", enabled)
|
|
2346
2629
|
val msg = EvenHubProto.audioControlMessage(enabled)
|
|
2347
2630
|
sendEvenHubCommand(msg)
|
|
@@ -2360,6 +2643,7 @@ class G2 : SGCManager() {
|
|
|
2360
2643
|
authToken: String?,
|
|
2361
2644
|
compress: String?,
|
|
2362
2645
|
flash: Boolean,
|
|
2646
|
+
save: Boolean,
|
|
2363
2647
|
sound: Boolean,
|
|
2364
2648
|
exposureTimeNs: Long?,
|
|
2365
2649
|
iso: Int?,
|
|
@@ -2463,7 +2747,9 @@ class G2 : SGCManager() {
|
|
|
2463
2747
|
rightAuthenticated = false
|
|
2464
2748
|
startupPageCreated = false
|
|
2465
2749
|
pageCreated = false
|
|
2466
|
-
|
|
2750
|
+
imageContainers.clear()
|
|
2751
|
+
textContainers.clear()
|
|
2752
|
+
dashboardShowing = 0
|
|
2467
2753
|
heartbeatCounter = 0
|
|
2468
2754
|
currentBitmapBase64 = ""
|
|
2469
2755
|
menuAppIdToPackageName.clear()
|
|
@@ -2506,10 +2792,78 @@ class G2 : SGCManager() {
|
|
|
2506
2792
|
}
|
|
2507
2793
|
|
|
2508
2794
|
override fun dbg1() {
|
|
2509
|
-
|
|
2795
|
+
// showNotificationsPanel()
|
|
2510
2796
|
}
|
|
2797
|
+
|
|
2798
|
+
private var compassRunning = false
|
|
2799
|
+
|
|
2511
2800
|
override fun dbg2() {
|
|
2512
|
-
|
|
2801
|
+
// compassRunning = !compassRunning
|
|
2802
|
+
// Bridge.log("G2: dbg2() — ${if (compassRunning) "start" else "stop"} compass")
|
|
2803
|
+
// if (compassRunning) {
|
|
2804
|
+
// startCompass()
|
|
2805
|
+
// } else {
|
|
2806
|
+
// stopCompass()
|
|
2807
|
+
// }
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* Start a navigation session so the glasses stream compass heading via
|
|
2812
|
+
* OS_NOTIFY_COMPASS_CHANGED — surfaced as `CompassHeadingEvent { heading: 0…359 }` in
|
|
2813
|
+
* handleNavigationResponse.
|
|
2814
|
+
*
|
|
2815
|
+
* If the magnetometer needs calibration, the glasses emit OS_NOTIFY_COMPASS_CALIBRATE_STRAT
|
|
2816
|
+
* (→ `CompassCalibrationEvent {status:"start"}`); the wearer should look around until
|
|
2817
|
+
* `…{status:"complete"}`.
|
|
2818
|
+
*/
|
|
2819
|
+
fun startCompass() {
|
|
2820
|
+
val w = ProtobufWriter()
|
|
2821
|
+
w.writeInt32Field(1, NavigationCmd.APP_REQUEST_START_UP.value) // cmd
|
|
2822
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom()) // magicRandom
|
|
2823
|
+
sendNavigationCommand(w.toByteArray())
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/** Stop the navigation/compass session (ends heading streaming). */
|
|
2827
|
+
fun stopCompass() {
|
|
2828
|
+
val w = ProtobufWriter()
|
|
2829
|
+
w.writeInt32Field(1, NavigationCmd.APP_REQUEST_EXIT.value)
|
|
2830
|
+
w.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
2831
|
+
sendNavigationCommand(w.toByteArray())
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
override suspend fun setImuEnabled(enabled: Boolean) {
|
|
2835
|
+
setImuEnabled(enabled, reportFrq = EvenHubProto.IMU_PACE_P100)
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
/**
|
|
2839
|
+
* Enable or disable IMU motion reporting on the glasses.
|
|
2840
|
+
*
|
|
2841
|
+
* When enabled, the glasses continuously push `IMU_Report_Data { x, y, z }` (32-bit floats,
|
|
2842
|
+
* gravity-normalized) via the EvenHub notify path; these surface in `handleTouchEvent` as a
|
|
2843
|
+
* Sys_ItemEvent with `eventType == IMU_DATA_REPORT (8)` and are emitted through
|
|
2844
|
+
* `Bridge.sendAccelEvent` (a single accelerometer reading; a richer combined IMU event
|
|
2845
|
+
* covering gyro + magnetometer is future work).
|
|
2846
|
+
*/
|
|
2847
|
+
suspend fun setImuEnabled(enabled: Boolean, reportFrq: Int) {
|
|
2848
|
+
Bridge.log("G2: setImuEnabled($enabled, frq=$reportFrq)")
|
|
2849
|
+
|
|
2850
|
+
displayMutex.withLock {
|
|
2851
|
+
// IMU requires an active EvenHub page (same prerequisite as the mic). Await the
|
|
2852
|
+
// rebuild so the control packet is sent only after the page actually exists — page
|
|
2853
|
+
// creation is async with variable delays, so a fixed wait could send too early and
|
|
2854
|
+
// reporting would never start.
|
|
2855
|
+
if (enabled && !pageCreated) {
|
|
2856
|
+
rebuildState()
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
val msg =
|
|
2860
|
+
EvenHubProto.imuControlMessage(
|
|
2861
|
+
enable = enabled,
|
|
2862
|
+
reportFrq = reportFrq,
|
|
2863
|
+
magicRandom = sendManager.nextMagicRandom()
|
|
2864
|
+
)
|
|
2865
|
+
sendEvenHubCommand(msg)
|
|
2866
|
+
}
|
|
2513
2867
|
}
|
|
2514
2868
|
|
|
2515
2869
|
fun reconnectController() {
|
|
@@ -2598,7 +2952,12 @@ class G2 : SGCManager() {
|
|
|
2598
2952
|
}
|
|
2599
2953
|
|
|
2600
2954
|
override fun sendShutdown() {
|
|
2601
|
-
clearDisplay()
|
|
2955
|
+
// Send the EvenHub shutdown synchronously before tearing down BLE. clearDisplay() is now
|
|
2956
|
+
// fire-and-forget (it serializes on displayScope), so calling it here would let disconnect()
|
|
2957
|
+
// close the GATT before the deferred shutdown packet was ever written.
|
|
2958
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2959
|
+
sendEvenHubCommand(msg)
|
|
2960
|
+
pageCreated = false
|
|
2602
2961
|
disconnect()
|
|
2603
2962
|
}
|
|
2604
2963
|
|
|
@@ -2606,6 +2965,14 @@ class G2 : SGCManager() {
|
|
|
2606
2965
|
// TODO: Implement via dev_settings
|
|
2607
2966
|
}
|
|
2608
2967
|
|
|
2968
|
+
/// Push the current time to the glasses. Useful after DST transitions,
|
|
2969
|
+
/// time-zone travel, or a long sleep where the glasses' clock has drifted.
|
|
2970
|
+
fun syncTime() {
|
|
2971
|
+
Bridge.log("G2: syncTime()")
|
|
2972
|
+
val msg = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
|
|
2973
|
+
sendDevSettingsCommand(msg, left = true, right = true)
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2609
2976
|
override fun sendRgbLedControl(
|
|
2610
2977
|
requestId: String,
|
|
2611
2978
|
packageName: String?,
|
|
@@ -2939,7 +3306,7 @@ class G2 : SGCManager() {
|
|
|
2939
3306
|
|
|
2940
3307
|
startupPageCreated = false
|
|
2941
3308
|
pageCreated = false
|
|
2942
|
-
|
|
3309
|
+
dashboardShowing = 0
|
|
2943
3310
|
DeviceStore.apply("glasses", "connected", false)
|
|
2944
3311
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2945
3312
|
|
|
@@ -3025,7 +3392,7 @@ class G2 : SGCManager() {
|
|
|
3025
3392
|
stopScan()
|
|
3026
3393
|
authStarted = true
|
|
3027
3394
|
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
3028
|
-
runAuthSequence()
|
|
3395
|
+
displayScope.launch { runAuthSequence() }
|
|
3029
3396
|
}
|
|
3030
3397
|
}
|
|
3031
3398
|
}
|
|
@@ -3105,6 +3472,8 @@ class G2 : SGCManager() {
|
|
|
3105
3472
|
ServiceID.MENU.value -> handleMenuResponse(payload)
|
|
3106
3473
|
ServiceID.DASHBOARD.value -> handleDashboardResponse(payload)
|
|
3107
3474
|
ServiceID.GESTURE_CTRL.value -> handleGestureCtrl(payload)
|
|
3475
|
+
ServiceID.NAVIGATION.value -> handleNavigationResponse(payload)
|
|
3476
|
+
ServiceID.EVEN_AI.value -> handleEvenAIResponse(payload)
|
|
3108
3477
|
ServiceID.EVEN_HUB_CTRL.value -> handleEvenHubCtrlResponse(payload)
|
|
3109
3478
|
else -> {
|
|
3110
3479
|
Bridge.log(
|
|
@@ -3116,6 +3485,69 @@ class G2 : SGCManager() {
|
|
|
3116
3485
|
}
|
|
3117
3486
|
}
|
|
3118
3487
|
|
|
3488
|
+
/**
|
|
3489
|
+
* EvenAI service (0x07). Logs the decoded EvenAIDataPackage so we can read the CONFIG
|
|
3490
|
+
* (Hey Even) echo: commandId=10 (CONFIG), config sub-message in field 13.
|
|
3491
|
+
*/
|
|
3492
|
+
private fun handleEvenAIResponse(payload: ByteArray) {
|
|
3493
|
+
val reader = ProtobufReader(payload)
|
|
3494
|
+
val fields = reader.parseFields()
|
|
3495
|
+
val cmd = fields[1] as? Int ?: -1
|
|
3496
|
+
val configData = fields[13] as? ByteArray
|
|
3497
|
+
if (cmd == 10 && configData != null) {
|
|
3498
|
+
val cReader = ProtobufReader(configData)
|
|
3499
|
+
val cFields = cReader.parseFields()
|
|
3500
|
+
val voiceSwitch = cFields[1] as? Int ?: 0 // omitted = 0 = OFF
|
|
3501
|
+
Bridge.log(
|
|
3502
|
+
"G2: EvenAI CONFIG echo — voiceSwitch=$voiceSwitch (${if (voiceSwitch == 1) "ON" else "OFF"}) config=$cFields"
|
|
3503
|
+
)
|
|
3504
|
+
} else {
|
|
3505
|
+
Bridge.log(
|
|
3506
|
+
"G2: EvenAI cmd=$cmd fields=${fields.keys.sorted()} raw=${payload.joinToString("") { String.format("%02X", it) }}"
|
|
3507
|
+
)
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
/**
|
|
3512
|
+
* Navigation service (0x08).
|
|
3513
|
+
*
|
|
3514
|
+
* OS_NOTIFY_COMPASS_CHANGED (15) carries the magnetometer heading in compass_info_msg
|
|
3515
|
+
* (field 10) → field 1, as whole degrees 0…359. (The proto names that field `compassIndex`,
|
|
3516
|
+
* but on the notify path it's the live heading — verified on-device: values sweep 0–359 as
|
|
3517
|
+
* the wearer turns.)
|
|
3518
|
+
*/
|
|
3519
|
+
private fun handleNavigationResponse(payload: ByteArray) {
|
|
3520
|
+
val reader = ProtobufReader(payload)
|
|
3521
|
+
val fields = reader.parseFields()
|
|
3522
|
+
val cmd = fields[1] as? Int ?: return
|
|
3523
|
+
|
|
3524
|
+
when (cmd) {
|
|
3525
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CHANGED.value -> {
|
|
3526
|
+
val compassData = fields[10] as? ByteArray ?: return
|
|
3527
|
+
val cReader = ProtobufReader(compassData)
|
|
3528
|
+
val cFields = cReader.parseFields()
|
|
3529
|
+
val heading = cFields[1] as? Int ?: return
|
|
3530
|
+
// Heading in degrees, 0…359.
|
|
3531
|
+
Bridge.log("G2: compass heading=$heading°")
|
|
3532
|
+
Bridge.sendTypedMessage(
|
|
3533
|
+
"CompassHeadingEvent",
|
|
3534
|
+
mapOf(
|
|
3535
|
+
"heading" to heading,
|
|
3536
|
+
"timestamp" to System.currentTimeMillis()
|
|
3537
|
+
)
|
|
3538
|
+
)
|
|
3539
|
+
}
|
|
3540
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CALIBRATE_START.value -> {
|
|
3541
|
+
Bridge.log("G2: compass calibration started — wearer should look around")
|
|
3542
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", mapOf("status" to "start"))
|
|
3543
|
+
}
|
|
3544
|
+
NavigationCmd.OS_NOTIFY_COMPASS_CALIBRATE_COMPLETE.value -> {
|
|
3545
|
+
Bridge.log("G2: compass calibration complete")
|
|
3546
|
+
Bridge.sendTypedMessage("CompassCalibrationEvent", mapOf("status" to "complete"))
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3119
3551
|
private fun handleEvenHubResponse(payload: ByteArray) {
|
|
3120
3552
|
val reader = ProtobufReader(payload)
|
|
3121
3553
|
val fields = reader.parseFields()
|
|
@@ -3166,7 +3598,22 @@ class G2 : SGCManager() {
|
|
|
3166
3598
|
Bridge.log("G2: Menu selection ignored — placeholder or unknown appId=$appId")
|
|
3167
3599
|
}
|
|
3168
3600
|
} else {
|
|
3169
|
-
//
|
|
3601
|
+
// Dedup only the non-critical logging path (img-success/error chatter), which L and R
|
|
3602
|
+
// both deliver. Page-state resets above are intentionally outside this window.
|
|
3603
|
+
val timestamp = System.currentTimeMillis()
|
|
3604
|
+
val lastResponse = lastEvenHubResponseTimestamp
|
|
3605
|
+
if (lastResponse != null && timestamp - lastResponse < 100) {
|
|
3606
|
+
return
|
|
3607
|
+
}
|
|
3608
|
+
lastEvenHubResponseTimestamp = timestamp
|
|
3609
|
+
|
|
3610
|
+
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state.
|
|
3611
|
+
if (cmdValue == 9 || cmdValue == 10) {
|
|
3612
|
+
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3613
|
+
pageCreated = false
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
// Scan response fields for a shutdown/error code regardless of the debounce window.
|
|
3170
3617
|
// field 4 = StartupResCmd, field 6 = ImgResCmd, field 8 = RebuildResCmd, field 10 =
|
|
3171
3618
|
// TextResCmd
|
|
3172
3619
|
for (resField in listOf(4, 6, 8, 10)) {
|
|
@@ -3180,26 +3627,24 @@ class G2 : SGCManager() {
|
|
|
3180
3627
|
Bridge.log(
|
|
3181
3628
|
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
3182
3629
|
)
|
|
3183
|
-
startupPageCreated = false
|
|
3184
3630
|
pageCreated = false
|
|
3185
|
-
pageHasTextContainer = false
|
|
3186
|
-
currentTextContent = ""
|
|
3187
3631
|
}
|
|
3188
3632
|
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
for (resField in listOf(4, 6, 8, 10)) {
|
|
3636
|
+
val resData = fields[resField] as? ByteArray ?: continue
|
|
3637
|
+
val resReader = ProtobufReader(resData)
|
|
3638
|
+
val resFields = resReader.parseFields()
|
|
3189
3639
|
(resFields[8] as? Int)?.let { errorCode ->
|
|
3190
3640
|
// ImgResCmd has ErrorCode in field 8
|
|
3191
|
-
|
|
3641
|
+
if (errorCode == 4) {
|
|
3642
|
+
Bridge.log("G2: img_success")
|
|
3643
|
+
} else {
|
|
3644
|
+
Bridge.log("G2: EvenHub ImgRes errorCode=$errorCode")
|
|
3645
|
+
}
|
|
3192
3646
|
}
|
|
3193
3647
|
}
|
|
3194
|
-
|
|
3195
|
-
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state
|
|
3196
|
-
if (cmdValue == 9 || cmdValue == 10) {
|
|
3197
|
-
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3198
|
-
startupPageCreated = false
|
|
3199
|
-
pageCreated = false
|
|
3200
|
-
pageHasTextContainer = false
|
|
3201
|
-
currentTextContent = ""
|
|
3202
|
-
}
|
|
3203
3648
|
}
|
|
3204
3649
|
}
|
|
3205
3650
|
|
|
@@ -3227,6 +3672,39 @@ class G2 : SGCManager() {
|
|
|
3227
3672
|
}
|
|
3228
3673
|
}
|
|
3229
3674
|
|
|
3675
|
+
/**
|
|
3676
|
+
* Parse an IMU_Report_Data sub-message: fields 1/2/3 = x/y/z as 32-bit floats (wire type 5).
|
|
3677
|
+
* `ProtobufReader.parseFields()` skips wire-type-5 fields, so this walks the bytes manually.
|
|
3678
|
+
*/
|
|
3679
|
+
private fun parseImuReportData(data: ByteArray): Triple<Float, Float, Float>? {
|
|
3680
|
+
var x: Float? = null
|
|
3681
|
+
var y: Float? = null
|
|
3682
|
+
var z: Float? = null
|
|
3683
|
+
var i = 0
|
|
3684
|
+
while (i < data.size) {
|
|
3685
|
+
val tag = data[i].toInt() and 0xFF
|
|
3686
|
+
i += 1
|
|
3687
|
+
val fieldNum = tag shr 3
|
|
3688
|
+
val wireType = tag and 0x07
|
|
3689
|
+
if (wireType != 5 || data.size - i < 4) break
|
|
3690
|
+
var bits = 0
|
|
3691
|
+
for (b in 0 until 4) {
|
|
3692
|
+
bits = bits or ((data[i + b].toInt() and 0xFF) shl (8 * b)) // little-endian
|
|
3693
|
+
}
|
|
3694
|
+
i += 4
|
|
3695
|
+
val value = Float.fromBits(bits)
|
|
3696
|
+
when (fieldNum) {
|
|
3697
|
+
1 -> x = value
|
|
3698
|
+
2 -> y = value
|
|
3699
|
+
3 -> z = value
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
val fx = x ?: return null
|
|
3703
|
+
val fy = y ?: return null
|
|
3704
|
+
val fz = z ?: return null
|
|
3705
|
+
return Triple(fx, fy, fz)
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3230
3708
|
private fun handleTouchEvent(devEventData: ByteArray) {
|
|
3231
3709
|
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
3232
3710
|
val reader = ProtobufReader(devEventData)
|
|
@@ -3242,6 +3720,19 @@ class G2 : SGCManager() {
|
|
|
3242
3720
|
val sysReader = ProtobufReader(sysData)
|
|
3243
3721
|
val sysFields = sysReader.parseFields()
|
|
3244
3722
|
|
|
3723
|
+
// IMU data report: eventType == IMU_DATA_REPORT (8), imuData in field 3
|
|
3724
|
+
// (IMU_Report_Data { x, y, z } as 32-bit floats). Handle and return before the
|
|
3725
|
+
// gesture-mapping path.
|
|
3726
|
+
val imuData = sysFields[3] as? ByteArray
|
|
3727
|
+
if ((sysFields[1] as? Int) == OsEventType.IMU_DATA_REPORT.value && imuData != null) {
|
|
3728
|
+
val imu = parseImuReportData(imuData)
|
|
3729
|
+
if (imu != null) {
|
|
3730
|
+
Bridge.log("G2: IMU data report: ${imu.first}, ${imu.second}, ${imu.third}")
|
|
3731
|
+
Bridge.sendAccelEvent(imu.first, imu.second, imu.third, timestamp)
|
|
3732
|
+
return
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3245
3736
|
val normalType = sysFields[1] as? Int
|
|
3246
3737
|
val eventType: OsEventType? =
|
|
3247
3738
|
if (normalType != null) OsEventType.fromInt(normalType) else OsEventType.CLICK
|
|
@@ -3269,22 +3760,20 @@ class G2 : SGCManager() {
|
|
|
3269
3760
|
if (eventType == OsEventType.DOUBLE_CLICK) {
|
|
3270
3761
|
// trigger dashboard:
|
|
3271
3762
|
val isHeadUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
3272
|
-
|
|
3273
|
-
DeviceStore.
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3763
|
+
|
|
3764
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
3765
|
+
if (useNativeDashboard) {
|
|
3766
|
+
showDashboard()
|
|
3767
|
+
} else {
|
|
3768
|
+
// toggle head up:
|
|
3769
|
+
DeviceStore.apply("glasses", "headUp", !isHeadUp)
|
|
3770
|
+
}
|
|
3278
3771
|
}
|
|
3279
3772
|
|
|
3280
3773
|
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
3281
3774
|
// Reset page state and re-create the page to reclaim EvenHub focus
|
|
3282
3775
|
if (eventType == OsEventType.SYSTEM_EXIT || eventType == OsEventType.ABNORMAL_EXIT) {
|
|
3283
|
-
startupPageCreated = false
|
|
3284
3776
|
pageCreated = false
|
|
3285
|
-
pageHasTextContainer = false
|
|
3286
|
-
currentTextContent = ""
|
|
3287
|
-
currentBitmapBase64 = ""
|
|
3288
3777
|
// Firmware kills the mic on system exit; re-arm it if it should be on
|
|
3289
3778
|
DeviceStore.apply("glasses", "micEnabled", false)
|
|
3290
3779
|
DeviceManager.getInstance().updateMicState()
|
|
@@ -3325,7 +3814,8 @@ class G2 : SGCManager() {
|
|
|
3325
3814
|
OsEventType.FOREGROUND_ENTER -> "foreground_enter"
|
|
3326
3815
|
OsEventType.FOREGROUND_EXIT -> "foreground_exit"
|
|
3327
3816
|
OsEventType.SYSTEM_EXIT -> "system_exit"
|
|
3328
|
-
OsEventType.
|
|
3817
|
+
OsEventType.IMU_DATA_REPORT -> null
|
|
3818
|
+
OsEventType.ABNORMAL_EXIT -> null // don't report abnormal exits as gestures
|
|
3329
3819
|
}
|
|
3330
3820
|
}
|
|
3331
3821
|
|
|
@@ -3337,9 +3827,9 @@ class G2 : SGCManager() {
|
|
|
3337
3827
|
// Ignore heartbeat acks
|
|
3338
3828
|
if (cmdValue == DevCfgCommandId.BASE_CONN_HEART_BEAT.value) return
|
|
3339
3829
|
|
|
3340
|
-
Bridge.log(
|
|
3341
|
-
|
|
3342
|
-
)
|
|
3830
|
+
// Bridge.log(
|
|
3831
|
+
// "G2: DevSettings response: ${payload.take(32).joinToString(":") { String.format("%02X", it) }}"
|
|
3832
|
+
// )
|
|
3343
3833
|
|
|
3344
3834
|
if (cmdValue == DevCfgCommandId.AUTHENTICATION.value) {
|
|
3345
3835
|
// DevCfgDataPackage: field 2 = magicRandom, field 3 = AuthMgr { field 1 = secAuth }
|
|
@@ -3381,7 +3871,7 @@ class G2 : SGCManager() {
|
|
|
3381
3871
|
}
|
|
3382
3872
|
|
|
3383
3873
|
val connStatus = ringFields[4] as? Int ?: -1
|
|
3384
|
-
Bridge.log("G2: Ring connection status: connStatus?=$connStatus")
|
|
3874
|
+
// Bridge.log("G2: Ring connection status: connStatus?=$connStatus")
|
|
3385
3875
|
|
|
3386
3876
|
if (connStatus == 22) {
|
|
3387
3877
|
Bridge.log("G2: Ring disconnected")
|
|
@@ -3445,14 +3935,59 @@ class G2 : SGCManager() {
|
|
|
3445
3935
|
}
|
|
3446
3936
|
|
|
3447
3937
|
private fun handleGestureCtrl(payload: ByteArray) {
|
|
3938
|
+
// Dedup: L and R peripherals both deliver this event, so debounce within 500ms.
|
|
3939
|
+
val timestamp = System.currentTimeMillis()
|
|
3940
|
+
val last = lastGestureCtrlTimestamp
|
|
3941
|
+
if (last != null && timestamp - last < 500) {
|
|
3942
|
+
// Bridge.log("G2: gesture_ctrl dedup")
|
|
3943
|
+
return
|
|
3944
|
+
}
|
|
3945
|
+
lastGestureCtrlTimestamp = timestamp
|
|
3946
|
+
|
|
3448
3947
|
// Dashboard close detection: 08011A00 means dashboard closed
|
|
3449
3948
|
if (payload.contentEquals(byteArrayOf(0x08, 0x01, 0x1A, 0x00))) {
|
|
3450
|
-
Bridge.log("G2:
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3949
|
+
Bridge.log("G2: dashboard closed / shutdown - dashboardShowing=$dashboardShowing")
|
|
3950
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
3951
|
+
if (!useNativeDashboard) {
|
|
3952
|
+
dashboardShowing = 0
|
|
3953
|
+
// Rebuild the page from cached containers, then reconcile against
|
|
3954
|
+
// DeviceManager's authoritative current view so the glasses match the phone
|
|
3955
|
+
// (not just the last-cached G2 containers) after returning from the dashboard.
|
|
3956
|
+
displayScope.launch {
|
|
3957
|
+
displayMutex.withLock { rebuildState() }
|
|
3958
|
+
DeviceManager.getInstance().sendCurrentState()
|
|
3959
|
+
// set the mic back on if it should be on — only after the rebuild
|
|
3960
|
+
// completes, matching iOS (which awaits rebuildState before restartMic).
|
|
3961
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3962
|
+
if (micEnabled) {
|
|
3963
|
+
restartMic()
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
return
|
|
3967
|
+
} else {
|
|
3968
|
+
// if we aren't trying to show the dashboard
|
|
3969
|
+
// then we need to turn the mic back on and display the mentra main page:
|
|
3970
|
+
if (dashboardShowing <= 1) {
|
|
3971
|
+
dashboardShowing = 0
|
|
3972
|
+
// Rebuild the page from cached containers, then reconcile against
|
|
3973
|
+
// DeviceManager's authoritative current view so the glasses match the phone
|
|
3974
|
+
// (not just the last-cached G2 containers) after returning from the dashboard.
|
|
3975
|
+
displayScope.launch {
|
|
3976
|
+
displayMutex.withLock { rebuildState() }
|
|
3977
|
+
// set the mic back on if it should be on
|
|
3978
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3979
|
+
if (micEnabled) {
|
|
3980
|
+
restartMic()
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
return
|
|
3984
|
+
}
|
|
3985
|
+
// do nothing this time since we just closed the dashboard
|
|
3986
|
+
dashboardShowing -= 1
|
|
3987
|
+
if (dashboardShowing < 0) {
|
|
3988
|
+
dashboardShowing = 0
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3456
3991
|
}
|
|
3457
3992
|
}
|
|
3458
3993
|
|
|
@@ -3479,7 +4014,7 @@ class G2 : SGCManager() {
|
|
|
3479
4014
|
// Battery
|
|
3480
4015
|
(fields[12] as? Int)?.let { battery ->
|
|
3481
4016
|
if (battery in 0..100) {
|
|
3482
|
-
Bridge.log("G2: Battery level: $battery%")
|
|
4017
|
+
// Bridge.log("G2: Battery level: $battery%")
|
|
3483
4018
|
batteryLevel_ = battery
|
|
3484
4019
|
}
|
|
3485
4020
|
}
|
|
@@ -3496,13 +4031,13 @@ class G2 : SGCManager() {
|
|
|
3496
4031
|
// Software versions
|
|
3497
4032
|
(fields[5] as? ByteArray)?.let { leftVer ->
|
|
3498
4033
|
val leftVersion = String(leftVer, Charsets.UTF_8)
|
|
3499
|
-
Bridge.log("G2: Left firmware: $leftVersion")
|
|
4034
|
+
// Bridge.log("G2: Left firmware: $leftVersion")
|
|
3500
4035
|
DeviceStore.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
3501
4036
|
}
|
|
3502
4037
|
|
|
3503
4038
|
(fields[6] as? ByteArray)?.let { rightVer ->
|
|
3504
4039
|
val rightVersion = String(rightVer, Charsets.UTF_8)
|
|
3505
|
-
Bridge.log("G2: Right firmware: $rightVersion")
|
|
4040
|
+
// Bridge.log("G2: Right firmware: $rightVersion")
|
|
3506
4041
|
DeviceStore.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
3507
4042
|
DeviceStore.apply("glasses", "firmwareVersion", rightVersion)
|
|
3508
4043
|
}
|
|
@@ -3531,7 +4066,7 @@ class G2 : SGCManager() {
|
|
|
3531
4066
|
return
|
|
3532
4067
|
}
|
|
3533
4068
|
lastAudioFrame = audioData
|
|
3534
|
-
Bridge.log("G2: audio data from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
4069
|
+
// Bridge.log("G2: audio data from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
3535
4070
|
DeviceManager.getInstance().handleGlassesMicData(audioData, 40)
|
|
3536
4071
|
}
|
|
3537
4072
|
|