@mentra/bluetooth-sdk 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -18
- package/android/build.gradle +289 -16
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/build.gradle +95 -6
- package/android/settings.gradle +3 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +42 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
- package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/build/BluetoothSdk.types.d.ts +241 -43
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +11 -10
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +36 -23
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +30 -52
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/photoRequestPayload.d.ts +4 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +25 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +23 -0
- package/build/index.js.map +1 -1
- package/build/react/useMentraBluetooth.d.ts +2 -2
- package/build/react/useMentraBluetooth.d.ts.map +1 -1
- package/build/react/useMentraBluetooth.js +3 -3
- package/build/react/useMentraBluetooth.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +174 -70
- package/ios/LocalPhotoUploadServer.swift +370 -105
- package/ios/MentraBluetoothSDK.podspec +3 -1
- package/ios/MentraPhotoReceiverModule.swift +62 -10
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -0
- package/ios/Source/Bridge.swift +58 -19
- package/ios/Source/DeviceManager.swift +149 -17
- package/ios/Source/DeviceStore.swift +16 -6
- package/ios/Source/MentraBluetoothSDK.swift +803 -47
- package/ios/Source/ObservableStore.swift +8 -2
- package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
- package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
- package/ios/Source/controllers/ControllerManager.swift +4 -3
- package/ios/Source/controllers/R1.swift +3 -3
- package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
- package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
- package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
- package/ios/Source/services/PhoneMic.swift +1 -0
- package/ios/Source/sgcs/G1.swift +2 -2
- package/ios/Source/sgcs/G2.swift +645 -403
- package/ios/Source/sgcs/Mach1.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +812 -319
- package/ios/Source/sgcs/MentraNex.swift +412 -182
- package/ios/Source/sgcs/SGCManager.swift +57 -3
- package/ios/Source/sgcs/Simulated.swift +7 -3
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
- package/ios/Source/status/DeviceStatus.swift +974 -0
- package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
- package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
- package/ios/Source/stt/STTTools.swift +27 -12
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
- package/ios/Source/tts/TTSTools.swift +164 -0
- package/ios/Source/utils/JSCExperiment.swift +7 -7
- package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
- package/ios/Source/utils/MessageChunker.swift +78 -21
- package/ios/Source/utils/TarBz2Extractor.swift +53 -2
- package/package.json +1 -1
- package/plugin/build/withAndroid.js +68 -4
- package/src/BluetoothSdk.types.ts +331 -59
- package/src/_private/BluetoothSdkModule.ts +113 -102
- package/src/_private/photoRequestPayload.ts +28 -0
- package/src/index.ts +58 -15
- package/src/react/useMentraBluetooth.ts +6 -4
- package/android/.project +0 -28
- package/android/lc3Lib/.project +0 -28
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Source/Status/DeviceStatus.swift +0 -471
- /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
- /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
- /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
- /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
- /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
|
@@ -496,16 +496,20 @@ private object DevSettingsProto {
|
|
|
496
496
|
return w.toByteArray()
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
/// DevCfgDataPackage with TIME_SYNC command.
|
|
500
|
+
/// TimeSync submessage: f1 = (Unix seconds + TZ offset seconds) as Int32, no TZ field.
|
|
501
|
+
/// Firmware appears to ignore the TZ field, so we pre-shift the timestamp itself
|
|
502
|
+
/// to make UTC interpretation read as local. Empirically confirmed via probe variants in dbg1().
|
|
499
503
|
fun timeSync(magicRandom: Int): ByteArray {
|
|
500
504
|
val w = ProtobufWriter()
|
|
501
505
|
w.writeInt32Field(1, DevCfgCommandId.TIME_SYNC.value)
|
|
502
506
|
w.writeInt32Field(2, magicRandom)
|
|
503
507
|
|
|
504
508
|
val tsW = ProtobufWriter()
|
|
505
|
-
val
|
|
506
|
-
|
|
507
|
-
val
|
|
508
|
-
tsW.writeInt32Field(
|
|
509
|
+
val nowMs = System.currentTimeMillis()
|
|
510
|
+
val nowSec = nowMs / 1000
|
|
511
|
+
val tzSec = (TimeZone.getDefault().getOffset(nowMs) / 1000).toLong()
|
|
512
|
+
tsW.writeInt32Field(1, (nowSec + tzSec).toInt())
|
|
509
513
|
w.writeMessageField(128, tsW.toByteArray())
|
|
510
514
|
return w.toByteArray()
|
|
511
515
|
}
|
|
@@ -676,6 +680,69 @@ private object EvenAIProto {
|
|
|
676
680
|
w.writeMessageField(13, configW.toByteArray()) // config (field 13)
|
|
677
681
|
return w.toByteArray()
|
|
678
682
|
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* EvenAIDataPackage with CTRL — used to put glasses into / out of an AI session.
|
|
686
|
+
* Mirrors Flutter `sendWakeupResp` which sends EvenAIControl{status=ENTER}.
|
|
687
|
+
* status: 1 WAKE_UP, 2 ENTER, 3 EXIT
|
|
688
|
+
*/
|
|
689
|
+
fun aiCtrl(magicRandom: Int, status: Int): ByteArray {
|
|
690
|
+
val ctrlW = ProtobufWriter()
|
|
691
|
+
ctrlW.writeInt32Field(1, status) // status
|
|
692
|
+
|
|
693
|
+
val w = ProtobufWriter()
|
|
694
|
+
w.writeInt32Field(1, 1) // commandId = CTRL
|
|
695
|
+
w.writeInt32Field(2, magicRandom)
|
|
696
|
+
w.writeMessageField(3, ctrlW.toByteArray()) // ctrl (field 3)
|
|
697
|
+
return w.toByteArray()
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* EvenAIDataPackage with ASK — what the phone sends after cloud ASR resolves the
|
|
702
|
+
* user's audio into text. Mirrors Flutter `sendAsr`: EvenAIAskInfo{text, streamEnable=0}.
|
|
703
|
+
*/
|
|
704
|
+
fun aiAsk(magicRandom: Int, text: String, streamEnable: Int = 0): ByteArray {
|
|
705
|
+
val askW = ProtobufWriter()
|
|
706
|
+
askW.writeInt32Field(2, streamEnable) // streamEnable
|
|
707
|
+
askW.writeBytesField(4, text.toByteArray(Charsets.UTF_8)) // text
|
|
708
|
+
|
|
709
|
+
val w = ProtobufWriter()
|
|
710
|
+
w.writeInt32Field(1, 3) // commandId = ASK
|
|
711
|
+
w.writeInt32Field(2, magicRandom)
|
|
712
|
+
w.writeMessageField(5, askW.toByteArray()) // askInfo (field 5)
|
|
713
|
+
return w.toByteArray()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* EvenAIDataPackage with SKILL — triggers a built-in glasses UI the same way
|
|
718
|
+
* "Hey Even, show X" voice command does.
|
|
719
|
+
* skillId values (per even_ai.proto):
|
|
720
|
+
* 0 SKILL_NONE, 1 BRIGHTNESS, 2 TRANSLATE_CTRL, 3 NOTIFICATION,
|
|
721
|
+
* 4 TELEPROMPT, 5 NAVIGATE, 6 CONVERSATE, 7 QUICKLIST, 8 AUTO_BRIGHTNESS
|
|
722
|
+
*/
|
|
723
|
+
fun triggerSkill(
|
|
724
|
+
magicRandom: Int,
|
|
725
|
+
skillId: Int,
|
|
726
|
+
skillParam: Int = 0,
|
|
727
|
+
text: String = "",
|
|
728
|
+
streamEnable: Int = 1,
|
|
729
|
+
fTextEnd: Int = 1
|
|
730
|
+
): ByteArray {
|
|
731
|
+
// EvenAISkillInfo
|
|
732
|
+
val skillW = ProtobufWriter()
|
|
733
|
+
skillW.writeInt32Field(1, streamEnable) // streamEnable
|
|
734
|
+
skillW.writeInt32Field(2, skillId) // skillId
|
|
735
|
+
skillW.writeInt32Field(3, skillParam) // skillParam — for NOTIFICATION skill this is a NotificationType enum
|
|
736
|
+
skillW.writeBytesField(4, text.toByteArray(Charsets.UTF_8)) // text
|
|
737
|
+
skillW.writeInt32Field(6, fTextEnd) // fTextEnd — 1 signals "this is the final/complete packet"
|
|
738
|
+
|
|
739
|
+
// EvenAIDataPackage
|
|
740
|
+
val w = ProtobufWriter()
|
|
741
|
+
w.writeInt32Field(1, 6) // commandId = SKILL
|
|
742
|
+
w.writeInt32Field(2, magicRandom)
|
|
743
|
+
w.writeMessageField(8, skillW.toByteArray()) // skillInfo (field 8)
|
|
744
|
+
return w.toByteArray()
|
|
745
|
+
}
|
|
679
746
|
}
|
|
680
747
|
|
|
681
748
|
// ---------- Menu Protobuf Builders (menu.proto, service ID 3) ----------
|
|
@@ -757,6 +824,124 @@ private object MenuProto {
|
|
|
757
824
|
}
|
|
758
825
|
}
|
|
759
826
|
|
|
827
|
+
/**
|
|
828
|
+
* Builders for the dashboard calendar widget (service 0x01, command Dashboard_Receive = 2).
|
|
829
|
+
* Field numbers come from the extracted dashboard.proto v2.1.0_beta_v3 and mirror the iOS
|
|
830
|
+
* DashboardProto.calendarPush builder so both platforms produce the same wire payload.
|
|
831
|
+
*/
|
|
832
|
+
private object CalendarProto {
|
|
833
|
+
// Dashboard_Receive — phone → glasses widget/config push.
|
|
834
|
+
const val DASHBOARD_RECEIVE = 2
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Build a Schedule submessage (the calendar event payload).
|
|
838
|
+
* f1 = scheduleId (int32, required)
|
|
839
|
+
* f2 = title (string, optional)
|
|
840
|
+
* f3 = location (string, optional)
|
|
841
|
+
* f4 = time (string, optional — display text e.g. "10:00 AM")
|
|
842
|
+
* f5 = endTimestamp (int32, Unix seconds — pre-shifted by TZ so the glasses,
|
|
843
|
+
* which treat timestamps as already-local, display local time)
|
|
844
|
+
*/
|
|
845
|
+
private fun schedule(
|
|
846
|
+
scheduleId: Int,
|
|
847
|
+
title: String?,
|
|
848
|
+
location: String?,
|
|
849
|
+
time: String?,
|
|
850
|
+
endTimestamp: Int
|
|
851
|
+
): ByteArray {
|
|
852
|
+
val w = ProtobufWriter()
|
|
853
|
+
w.writeInt32Field(1, scheduleId)
|
|
854
|
+
title?.let { w.writeStringField(2, it) }
|
|
855
|
+
location?.let { w.writeStringField(3, it) }
|
|
856
|
+
time?.let { w.writeStringField(4, it) }
|
|
857
|
+
w.writeInt32Field(5, endTimestamp)
|
|
858
|
+
return w.toByteArray()
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Build the full calendar-push DashboardDataPackage:
|
|
863
|
+
* DashboardDataPackage {
|
|
864
|
+
* commandId = Dashboard_Receive (2)
|
|
865
|
+
* magicRandom
|
|
866
|
+
* dashboardReceive = DashboardReceiveFromApp {
|
|
867
|
+
* packageId = 1
|
|
868
|
+
* bashboardConfig = DashboardContent {
|
|
869
|
+
* widgetComponents = rWidgetComponent {
|
|
870
|
+
* schedule = rScheduleWidget {
|
|
871
|
+
* scheduleTotal, scheduleNum (0-based), Schedule, scheduleAuthority
|
|
872
|
+
* }
|
|
873
|
+
* }
|
|
874
|
+
* }
|
|
875
|
+
* }
|
|
876
|
+
* }
|
|
877
|
+
*/
|
|
878
|
+
fun calendarPush(
|
|
879
|
+
magicRandom: Int,
|
|
880
|
+
packageId: Int,
|
|
881
|
+
scheduleId: Int,
|
|
882
|
+
title: String?,
|
|
883
|
+
location: String?,
|
|
884
|
+
time: String?,
|
|
885
|
+
endTimestamp: Int,
|
|
886
|
+
scheduleAuthority: Int,
|
|
887
|
+
scheduleTotal: Int,
|
|
888
|
+
scheduleNum: Int
|
|
889
|
+
): ByteArray {
|
|
890
|
+
val sched = schedule(scheduleId, title, location, time, endTimestamp)
|
|
891
|
+
|
|
892
|
+
// rScheduleWidget { f1 = scheduleTotal, f2 = scheduleNum, f3 = Schedule, f4 = scheduleAuthority }
|
|
893
|
+
val rSchedW = ProtobufWriter()
|
|
894
|
+
rSchedW.writeInt32Field(1, scheduleTotal)
|
|
895
|
+
rSchedW.writeInt32Field(2, scheduleNum)
|
|
896
|
+
rSchedW.writeMessageField(3, sched)
|
|
897
|
+
rSchedW.writeInt32Field(4, scheduleAuthority)
|
|
898
|
+
|
|
899
|
+
// rWidgetComponent { f3 = rScheduleWidget }
|
|
900
|
+
val rWidgetW = ProtobufWriter()
|
|
901
|
+
rWidgetW.writeMessageField(3, rSchedW.toByteArray())
|
|
902
|
+
|
|
903
|
+
// DashboardContent { f2 = rWidgetComponent }
|
|
904
|
+
val contentW = ProtobufWriter()
|
|
905
|
+
contentW.writeMessageField(2, rWidgetW.toByteArray())
|
|
906
|
+
|
|
907
|
+
// DashboardReceiveFromApp { f1 = packageId, f3 = DashboardContent }
|
|
908
|
+
val receiveW = ProtobufWriter()
|
|
909
|
+
receiveW.writeInt32Field(1, packageId)
|
|
910
|
+
receiveW.writeMessageField(3, contentW.toByteArray())
|
|
911
|
+
|
|
912
|
+
// DashboardDataPackage { f1 = commandId, f2 = magicRandom, f4 = dashboardReceive }
|
|
913
|
+
val pkgW = ProtobufWriter()
|
|
914
|
+
pkgW.writeInt32Field(1, DASHBOARD_RECEIVE)
|
|
915
|
+
pkgW.writeInt32Field(2, magicRandom)
|
|
916
|
+
pkgW.writeMessageField(4, receiveW.toByteArray())
|
|
917
|
+
return pkgW.toByteArray()
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
fun calendarClear(magicRandom: Int, packageId: Int, scheduleAuthority: Int): ByteArray {
|
|
921
|
+
// rScheduleWidget with scheduleTotal=0 clears the widget without sending a stale Schedule.
|
|
922
|
+
val rSchedW = ProtobufWriter()
|
|
923
|
+
rSchedW.writeInt32Field(1, 0)
|
|
924
|
+
rSchedW.writeInt32Field(2, 0)
|
|
925
|
+
rSchedW.writeInt32Field(4, scheduleAuthority)
|
|
926
|
+
|
|
927
|
+
val rWidgetW = ProtobufWriter()
|
|
928
|
+
rWidgetW.writeMessageField(3, rSchedW.toByteArray())
|
|
929
|
+
|
|
930
|
+
val contentW = ProtobufWriter()
|
|
931
|
+
contentW.writeMessageField(2, rWidgetW.toByteArray())
|
|
932
|
+
|
|
933
|
+
val receiveW = ProtobufWriter()
|
|
934
|
+
receiveW.writeInt32Field(1, packageId)
|
|
935
|
+
receiveW.writeMessageField(3, contentW.toByteArray())
|
|
936
|
+
|
|
937
|
+
val pkgW = ProtobufWriter()
|
|
938
|
+
pkgW.writeInt32Field(1, DASHBOARD_RECEIVE)
|
|
939
|
+
pkgW.writeInt32Field(2, magicRandom)
|
|
940
|
+
pkgW.writeMessageField(4, receiveW.toByteArray())
|
|
941
|
+
return pkgW.toByteArray()
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
760
945
|
// ---------- EvenBLE Transport Layer ----------
|
|
761
946
|
|
|
762
947
|
private object EvenBLETransport {
|
|
@@ -1050,6 +1235,7 @@ class G2 : SGCManager() {
|
|
|
1050
1235
|
private var leftAuthenticated: Boolean = false
|
|
1051
1236
|
private var rightAuthenticated: Boolean = false
|
|
1052
1237
|
private var currentBitmapBase64: String = ""
|
|
1238
|
+
private var dashboardShowing = 0
|
|
1053
1239
|
|
|
1054
1240
|
// Dashboard menu state
|
|
1055
1241
|
private var menuAppIdToPackageName: MutableMap<Int, String> = mutableMapOf()
|
|
@@ -1057,6 +1243,35 @@ class G2 : SGCManager() {
|
|
|
1057
1243
|
private var activeMenuAppId: Int? = null
|
|
1058
1244
|
private var lastClickTimestamp: Long? = null
|
|
1059
1245
|
private var lastMenuSelectTimestamp: Long? = null
|
|
1246
|
+
private var lastGestureCtrlTimestamp: Long? = null
|
|
1247
|
+
|
|
1248
|
+
/** A tracked image container on the current page. Keyed by its rect for reuse. */
|
|
1249
|
+
private data class ImgContainer(
|
|
1250
|
+
val id: Int,
|
|
1251
|
+
val x: Int,
|
|
1252
|
+
val y: Int,
|
|
1253
|
+
val width: Int,
|
|
1254
|
+
val height: Int
|
|
1255
|
+
) {
|
|
1256
|
+
val name: String
|
|
1257
|
+
get() = "img-$id"
|
|
1258
|
+
|
|
1259
|
+
fun matches(x: Int, y: Int, width: Int, height: Int): Boolean =
|
|
1260
|
+
this.x == x && this.y == y && this.width == width && this.height == height
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Live list of image containers on the page, ordered oldest→newest (for LRU eviction). The page
|
|
1265
|
+
* may hold at most 4 image containers (IDs from the pool below).
|
|
1266
|
+
*/
|
|
1267
|
+
private val imageContainers: MutableList<ImgContainer> = mutableListOf()
|
|
1268
|
+
/** Fixed pool of container IDs the page protocol expects. */
|
|
1269
|
+
private val imageContainerIDPool: List<Int> = listOf(10, 11, 12, 13)
|
|
1270
|
+
/** Default container seeded into every fresh page: 100x100 in the top-left. */
|
|
1271
|
+
private val defaultImgX = 0
|
|
1272
|
+
private val defaultImgY = 0
|
|
1273
|
+
private val defaultImgWidth = 100
|
|
1274
|
+
private val defaultImgHeight = 100
|
|
1060
1275
|
|
|
1061
1276
|
// Battery state
|
|
1062
1277
|
private var _batteryLevel: Int = -1
|
|
@@ -1137,7 +1352,7 @@ class G2 : SGCManager() {
|
|
|
1137
1352
|
payload = payload,
|
|
1138
1353
|
reserveFlag = true
|
|
1139
1354
|
)
|
|
1140
|
-
sendToGlasses(packets)
|
|
1355
|
+
sendToGlasses(packets, left = true, right = true)
|
|
1141
1356
|
}
|
|
1142
1357
|
|
|
1143
1358
|
private fun sendDevSettingsCommand(
|
|
@@ -1303,11 +1518,13 @@ class G2 : SGCManager() {
|
|
|
1303
1518
|
0x10,
|
|
1304
1519
|
0x00, // distanceUnit=0
|
|
1305
1520
|
0x18,
|
|
1306
|
-
|
|
1521
|
+
dashboardHalfDayFormat().toByte(),
|
|
1522
|
+
// timeFormat / halfDayFormat
|
|
1307
1523
|
0x20,
|
|
1308
1524
|
0x00, // dateFormat=0
|
|
1309
1525
|
0x28,
|
|
1310
|
-
|
|
1526
|
+
dashboardTemperatureUnit().toByte(),
|
|
1527
|
+
// temperatureUnit
|
|
1311
1528
|
)
|
|
1312
1529
|
)
|
|
1313
1530
|
sendG2SettingCommand(univW.toByteArray())
|
|
@@ -1435,11 +1652,11 @@ class G2 : SGCManager() {
|
|
|
1435
1652
|
) // widgetDisplayOrder
|
|
1436
1653
|
dashDisplayW.writeInt32Field(
|
|
1437
1654
|
6,
|
|
1438
|
-
|
|
1655
|
+
dashboardHalfDayFormat()
|
|
1439
1656
|
) // halfDayFormat
|
|
1440
1657
|
dashDisplayW.writeInt32Field(
|
|
1441
1658
|
7,
|
|
1442
|
-
|
|
1659
|
+
dashboardTemperatureUnit()
|
|
1443
1660
|
) // temperatureUnit
|
|
1444
1661
|
|
|
1445
1662
|
val dashRecvW = ProtobufWriter()
|
|
@@ -1615,6 +1832,7 @@ class G2 : SGCManager() {
|
|
|
1615
1832
|
requestDeviceInfo()
|
|
1616
1833
|
|
|
1617
1834
|
sendMenuApps()
|
|
1835
|
+
sendStoredCalendarEvents()
|
|
1618
1836
|
},
|
|
1619
1837
|
500
|
|
1620
1838
|
)
|
|
@@ -1653,8 +1871,8 @@ class G2 : SGCManager() {
|
|
|
1653
1871
|
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1654
1872
|
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1655
1873
|
dashDisplayW.writeMessageField(5, byteArrayOf(1, 3, 2, 2)) // widgetDisplayOrder
|
|
1656
|
-
dashDisplayW.writeInt32Field(6,
|
|
1657
|
-
dashDisplayW.writeInt32Field(7,
|
|
1874
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1875
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1658
1876
|
|
|
1659
1877
|
val dashRecvW = ProtobufWriter()
|
|
1660
1878
|
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
@@ -1671,6 +1889,36 @@ class G2 : SGCManager() {
|
|
|
1671
1889
|
)
|
|
1672
1890
|
}
|
|
1673
1891
|
|
|
1892
|
+
private fun dashboardHalfDayFormat(): Int {
|
|
1893
|
+
val twelveHour = DeviceStore.get("bluetooth", "twelve_hour_time") as? Boolean ?: true
|
|
1894
|
+
return if (twelveHour) 1 else 0
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
private fun dashboardTemperatureUnit(): Int {
|
|
1898
|
+
val metric = DeviceStore.get("bluetooth", "metric_system") as? Boolean ?: false
|
|
1899
|
+
return if (metric) 1 else 2
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
override fun sendDashboardDisplaySettings() {
|
|
1903
|
+
val dashDisplayW = ProtobufWriter()
|
|
1904
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1905
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1906
|
+
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1907
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1908
|
+
dashDisplayW.writeMessageField(5, byteArrayOf(1, 3, 2, 2)) // widgetDisplayOrder
|
|
1909
|
+
dashDisplayW.writeInt32Field(6, dashboardHalfDayFormat()) // halfDayFormat
|
|
1910
|
+
dashDisplayW.writeInt32Field(7, dashboardTemperatureUnit()) // temperatureUnit
|
|
1911
|
+
|
|
1912
|
+
val dashRecvW = ProtobufWriter()
|
|
1913
|
+
dashRecvW.writeMessageField(2, dashDisplayW.toByteArray())
|
|
1914
|
+
|
|
1915
|
+
val dashPkgW = ProtobufWriter()
|
|
1916
|
+
dashPkgW.writeInt32Field(1, 2) // Dashboard_Receive
|
|
1917
|
+
dashPkgW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1918
|
+
dashPkgW.writeMessageField(4, dashRecvW.toByteArray())
|
|
1919
|
+
sendDashboardCommand(dashPkgW.toByteArray())
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1674
1922
|
// ---------- Heartbeats ----------
|
|
1675
1923
|
|
|
1676
1924
|
private fun startHeartbeats() {
|
|
@@ -1760,11 +2008,24 @@ class G2 : SGCManager() {
|
|
|
1760
2008
|
}
|
|
1761
2009
|
}
|
|
1762
2010
|
|
|
2011
|
+
private fun sendStoredCalendarEvents() {
|
|
2012
|
+
val calendarEvents =
|
|
2013
|
+
DeviceStore.get("bluetooth", "calendar_events") as? List<Map<String, Any>>
|
|
2014
|
+
?: emptyList()
|
|
2015
|
+
sendCalendarEvents(calendarEvents)
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1763
2018
|
// ---------- SGCManager: Display Control ----------
|
|
1764
2019
|
|
|
1765
2020
|
override fun sendTextWall(text: String) {
|
|
1766
2021
|
// Bridge.log("G2: sendTextWall(${text.take(10)}...)")
|
|
1767
2022
|
|
|
2023
|
+
// ignore events while the ER dashboard is open:
|
|
2024
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
2025
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
2026
|
+
return
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1768
2029
|
if (text.isEmpty()) {
|
|
1769
2030
|
clearDisplay()
|
|
1770
2031
|
return
|
|
@@ -1789,121 +2050,113 @@ class G2 : SGCManager() {
|
|
|
1789
2050
|
}
|
|
1790
2051
|
}
|
|
1791
2052
|
|
|
1792
|
-
|
|
2053
|
+
/**
|
|
2054
|
+
* Display a bitmap inside a positioned image container.
|
|
2055
|
+
*
|
|
2056
|
+
* The page keeps a live list of up to 4 image containers keyed by exact rect:
|
|
2057
|
+
* - If a container with the requested rect already exists, the image is just resent to it (no
|
|
2058
|
+
* page rebuild).
|
|
2059
|
+
* - Otherwise a new container is added (evicting the oldest when the list would exceed 4) and
|
|
2060
|
+
* the page is rebuilt before the image is sent.
|
|
2061
|
+
*
|
|
2062
|
+
* Omitted params default to a 100x100 container in the top-left corner.
|
|
2063
|
+
*/
|
|
2064
|
+
override fun displayBitmap(
|
|
2065
|
+
base64ImageData: String,
|
|
2066
|
+
x: Int?,
|
|
2067
|
+
y: Int?,
|
|
2068
|
+
width: Int?,
|
|
2069
|
+
height: Int?
|
|
2070
|
+
): Boolean {
|
|
1793
2071
|
currentBitmapBase64 = base64ImageData
|
|
1794
2072
|
currentTextContent = ""
|
|
1795
|
-
return displayBitmapQuad(base64ImageData)
|
|
1796
|
-
}
|
|
1797
2073
|
|
|
1798
|
-
|
|
2074
|
+
val rx = x ?: defaultImgX
|
|
2075
|
+
val ry = y ?: defaultImgY
|
|
2076
|
+
val rw = width ?: defaultImgWidth
|
|
2077
|
+
val rh = height ?: defaultImgHeight
|
|
2078
|
+
|
|
1799
2079
|
val rawData =
|
|
1800
2080
|
Base64.decode(base64ImageData, Base64.DEFAULT)
|
|
1801
2081
|
?: run {
|
|
1802
|
-
Bridge.log("G2:
|
|
2082
|
+
Bridge.log("G2: displayBitmap() - failed to decode base64")
|
|
1803
2083
|
return false
|
|
1804
2084
|
}
|
|
1805
2085
|
|
|
1806
|
-
|
|
1807
|
-
|
|
2086
|
+
// Create the startup page lazily, seeded with the default top-left container.
|
|
2087
|
+
val freshPage = !startupPageCreated
|
|
2088
|
+
if (freshPage) {
|
|
2089
|
+
imageContainers.clear()
|
|
2090
|
+
imageContainers.add(
|
|
2091
|
+
ImgContainer(
|
|
2092
|
+
id = imageContainerIDPool[0],
|
|
2093
|
+
x = defaultImgX,
|
|
2094
|
+
y = defaultImgY,
|
|
2095
|
+
width = defaultImgWidth,
|
|
2096
|
+
height = defaultImgHeight
|
|
2097
|
+
)
|
|
2098
|
+
)
|
|
2099
|
+
createPageWithText("")
|
|
2100
|
+
Bridge.log("G2: displayBitmap() - startup page created")
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Reuse an existing container if the rect matches exactly; otherwise add a new one.
|
|
2104
|
+
val existing = imageContainers.firstOrNull { it.matches(rx, ry, rw, rh) }
|
|
2105
|
+
val container: ImgContainer
|
|
2106
|
+
val needsRebuild: Boolean
|
|
2107
|
+
if (existing != null) {
|
|
2108
|
+
container = existing
|
|
2109
|
+
needsRebuild = false
|
|
2110
|
+
Bridge.log("G2: displayBitmap() - reusing container ${existing.id} for rect $rx,$ry ${rw}x$rh")
|
|
2111
|
+
} else {
|
|
2112
|
+
container = addImageContainer(rx, ry, rw, rh)
|
|
2113
|
+
needsRebuild = true
|
|
2114
|
+
Bridge.log("G2: displayBitmap() - added container ${container.id} for rect $rx,$ry ${rw}x$rh, rebuilding page")
|
|
2115
|
+
rebuildPage()
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
val bmpData =
|
|
2119
|
+
convertToG2Bmp(rawData, containerWidth = container.width, containerHeight = container.height)
|
|
1808
2120
|
?: run {
|
|
1809
|
-
Bridge.log(
|
|
1810
|
-
"G2: displayBitmapQuad() - failed to slice image into tiles"
|
|
1811
|
-
)
|
|
2121
|
+
Bridge.log("G2: displayBitmap() - failed to convert image to BMP")
|
|
1812
2122
|
return false
|
|
1813
2123
|
}
|
|
1814
2124
|
|
|
1815
|
-
//
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
val container2 =
|
|
1826
|
-
EvenHubProto.imageContainerProperty(
|
|
1827
|
-
x = 200,
|
|
1828
|
-
y = 0,
|
|
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
|
-
)
|
|
1868
|
-
}
|
|
1869
|
-
sendEvenHubCommand(msg)
|
|
1870
|
-
pageCreated = true
|
|
1871
|
-
pageHasTextContainer = false
|
|
1872
|
-
currentTextContent = ""
|
|
1873
|
-
|
|
1874
|
-
// After the 1s settle delay iOS waits, send each tile's BMP in series. Android's
|
|
1875
|
-
// displayBitmap signature is synchronous Boolean, so this is fire-and-forget — we
|
|
1876
|
-
// chain tiles via callbacks rather than awaiting like iOS.
|
|
1877
|
-
Bridge.log("G2: displayBitmapQuad() - page sent, scheduling fragment send in 1s...")
|
|
1878
|
-
mainHandler.postDelayed(
|
|
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
|
-
)
|
|
2125
|
+
// Fire-and-forget. If the page was just created/rebuilt, defer the BMP send 1s to let the
|
|
2126
|
+
// glasses process the page (matches iOS's await). Reuse path sends immediately.
|
|
2127
|
+
if (freshPage || needsRebuild) {
|
|
2128
|
+
mainHandler.postDelayed(
|
|
2129
|
+
{ sendImageData(container.id, container.name, bmpData) },
|
|
2130
|
+
1000
|
|
2131
|
+
)
|
|
2132
|
+
} else {
|
|
2133
|
+
sendImageData(container.id, container.name, bmpData)
|
|
2134
|
+
}
|
|
1893
2135
|
|
|
1894
2136
|
return true
|
|
1895
2137
|
}
|
|
1896
2138
|
|
|
1897
|
-
/**
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
) {
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2139
|
+
/**
|
|
2140
|
+
* Add a new image container for the rect, evicting the oldest when the list is full (max 4).
|
|
2141
|
+
* Returns the newly tracked container (with an assigned ID from the pool).
|
|
2142
|
+
*/
|
|
2143
|
+
private fun addImageContainer(x: Int, y: Int, width: Int, height: Int): ImgContainer {
|
|
2144
|
+
// Evict the oldest container when at capacity, freeing its ID for reuse.
|
|
2145
|
+
if (imageContainers.size >= imageContainerIDPool.size) {
|
|
2146
|
+
val evicted = imageContainers.removeAt(0)
|
|
2147
|
+
Bridge.log("G2: evicting oldest image container ${evicted.id}")
|
|
1906
2148
|
}
|
|
2149
|
+
// Pick the lowest free ID from the pool.
|
|
2150
|
+
val usedIDs = imageContainers.map { it.id }.toSet()
|
|
2151
|
+
val id = imageContainerIDPool.firstOrNull { it !in usedIDs } ?: imageContainerIDPool[0]
|
|
2152
|
+
val container = ImgContainer(id = id, x = x, y = y, width = width, height = height)
|
|
2153
|
+
imageContainers.add(container)
|
|
2154
|
+
return container
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/** Rebuild the page from the current text + image container list. */
|
|
2158
|
+
private fun rebuildPage() {
|
|
2159
|
+
createPageWithText(currentTextContent)
|
|
1907
2160
|
}
|
|
1908
2161
|
|
|
1909
2162
|
private fun sendImageData(
|
|
@@ -1948,9 +2201,8 @@ class G2 : SGCManager() {
|
|
|
1948
2201
|
fragmentIndex++
|
|
1949
2202
|
offset = end
|
|
1950
2203
|
|
|
1951
|
-
// 200ms between fragments — and also before onComplete
|
|
1952
|
-
//
|
|
1953
|
-
// which awaits 200ms after every fragment including the last).
|
|
2204
|
+
// 200ms between fragments — and also before onComplete (matches iOS, which awaits
|
|
2205
|
+
// 200ms after every fragment including the last).
|
|
1954
2206
|
if (offset < bmpData.size) {
|
|
1955
2207
|
mainHandler.postDelayed({ sendNextFragment() }, 200)
|
|
1956
2208
|
} else {
|
|
@@ -1964,93 +2216,23 @@ class G2 : SGCManager() {
|
|
|
1964
2216
|
sendNextFragment()
|
|
1965
2217
|
}
|
|
1966
2218
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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
|
-
}
|
|
2051
|
-
|
|
2219
|
+
/// Bring the Even Realities dashboard (the OS-level home/idle screen) to
|
|
2220
|
+
/// the foreground by tearing down whatever EvenHub page we currently own.
|
|
2221
|
+
/// The glasses fall back to the dashboard automatically when no page is up.
|
|
2052
2222
|
override fun showDashboard() {
|
|
2053
|
-
|
|
2223
|
+
Bridge.log("G2: showDashboard()")
|
|
2224
|
+
dashboardShowing += 2
|
|
2225
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
2226
|
+
sendEvenHubCommand(msg)
|
|
2227
|
+
pageCreated = false
|
|
2228
|
+
pageHasTextContainer = false
|
|
2229
|
+
currentTextContent = ""
|
|
2230
|
+
currentBitmapBase64 = ""
|
|
2231
|
+
mainHandler.postDelayed({
|
|
2232
|
+
// activate the dashboard by setting depth to the current setting:
|
|
2233
|
+
val currentDepth = DeviceStore.get("bluetooth", "dashboard_depth") as? Int ?: 0
|
|
2234
|
+
setDashboardDepthOnly(currentDepth)
|
|
2235
|
+
}, 500)
|
|
2054
2236
|
}
|
|
2055
2237
|
|
|
2056
2238
|
override fun setDashboardPosition(height: Int, depth: Int) {
|
|
@@ -2091,6 +2273,95 @@ class G2 : SGCManager() {
|
|
|
2091
2273
|
sendMenuCommand(msg)
|
|
2092
2274
|
}
|
|
2093
2275
|
|
|
2276
|
+
/**
|
|
2277
|
+
* Bridge entry for `calendar_events` store updates. Each map is expected to match
|
|
2278
|
+
* the TS `CalendarEvent` shape: { title, location?, time, endDate } where `endDate`
|
|
2279
|
+
* is Unix seconds.
|
|
2280
|
+
*
|
|
2281
|
+
* Sends one BLE push per event, with `scheduleTotal` set to the batch size and
|
|
2282
|
+
* `scheduleNum` set to this event's 0-based slot. The widget pages through them on
|
|
2283
|
+
* the glasses — without paging info the firmware overwrites slot 0 on each push and
|
|
2284
|
+
* only the last event survives.
|
|
2285
|
+
*/
|
|
2286
|
+
override fun sendCalendarEvents(events: List<Map<String, Any>>) {
|
|
2287
|
+
Bridge.log("G2: sendCalendarEvents -- ${events.size} events")
|
|
2288
|
+
if (events.isEmpty()) {
|
|
2289
|
+
sendDashboardCommand(
|
|
2290
|
+
CalendarProto.calendarClear(
|
|
2291
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2292
|
+
packageId = 1,
|
|
2293
|
+
scheduleAuthority = 1
|
|
2294
|
+
)
|
|
2295
|
+
)
|
|
2296
|
+
return
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
val total = events.size
|
|
2300
|
+
// Fold the local TZ offset into the timestamp so the glasses (which treat
|
|
2301
|
+
// timestamps as already-local) display the correct time — same hack as time-sync.
|
|
2302
|
+
val tzSec = TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 1000
|
|
2303
|
+
events.forEachIndexed { i, ev ->
|
|
2304
|
+
val title = ev["title"] as? String ?: return@forEachIndexed
|
|
2305
|
+
val time = ev["time"] as? String ?: return@forEachIndexed
|
|
2306
|
+
val endTs = (ev["endDate"] as? Number)?.toLong() ?: return@forEachIndexed
|
|
2307
|
+
val location = ev["location"] as? String
|
|
2308
|
+
|
|
2309
|
+
val payload =
|
|
2310
|
+
CalendarProto.calendarPush(
|
|
2311
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2312
|
+
packageId = 1,
|
|
2313
|
+
scheduleId = i + 1,
|
|
2314
|
+
title = title,
|
|
2315
|
+
location = location,
|
|
2316
|
+
time = time,
|
|
2317
|
+
endTimestamp = (endTs + tzSec).toInt(),
|
|
2318
|
+
scheduleAuthority = 1,
|
|
2319
|
+
scheduleTotal = total,
|
|
2320
|
+
scheduleNum = i
|
|
2321
|
+
)
|
|
2322
|
+
sendDashboardCommand(payload)
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/**
|
|
2327
|
+
* Open the on-glasses notification panel — same effect as the user saying
|
|
2328
|
+
* "Hey Even, show notifications". Replicates the official-app voice flow:
|
|
2329
|
+
* 1. CTRL{status=ENTER} — puts glasses in AI session
|
|
2330
|
+
* 2. ASK{text=" "} — minimal ASR transcript to seed session context
|
|
2331
|
+
* 3. SKILL{skillId=NOTIFICATION, skillParam=show, ...} — dispatches the intent
|
|
2332
|
+
* The SKILL step alone is ignored by firmware; the preceding ENTER+ASK
|
|
2333
|
+
* supply the session context that lets the glasses act on the SKILL.
|
|
2334
|
+
*/
|
|
2335
|
+
override fun showNotificationsPanel() {
|
|
2336
|
+
Bridge.log("G2: showNotificationsPanel()")
|
|
2337
|
+
val enterPayload = EvenAIProto.aiCtrl(
|
|
2338
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2339
|
+
status = 2 // EVEN_AI_ENTER
|
|
2340
|
+
)
|
|
2341
|
+
sendEvenAICommand(enterPayload)
|
|
2342
|
+
|
|
2343
|
+
mainHandler.postDelayed({
|
|
2344
|
+
val askPayload = EvenAIProto.aiAsk(
|
|
2345
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2346
|
+
text = " ",
|
|
2347
|
+
streamEnable = 0
|
|
2348
|
+
)
|
|
2349
|
+
sendEvenAICommand(askPayload)
|
|
2350
|
+
|
|
2351
|
+
mainHandler.postDelayed({
|
|
2352
|
+
val skillPayload = EvenAIProto.triggerSkill(
|
|
2353
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2354
|
+
skillId = 3, // NOTIFICATION
|
|
2355
|
+
skillParam = 1, // show
|
|
2356
|
+
text = " ",
|
|
2357
|
+
streamEnable = 1,
|
|
2358
|
+
fTextEnd = 1
|
|
2359
|
+
)
|
|
2360
|
+
sendEvenAICommand(skillPayload)
|
|
2361
|
+
}, 400)
|
|
2362
|
+
}, 400)
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2094
2365
|
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
2095
2366
|
Bridge.log("G2: setBrightness($level, auto=$autoMode)")
|
|
2096
2367
|
val msg =
|
|
@@ -2121,12 +2392,26 @@ class G2 : SGCManager() {
|
|
|
2121
2392
|
content = text
|
|
2122
2393
|
)
|
|
2123
2394
|
|
|
2395
|
+
// Build the page's image containers from the live tracked list.
|
|
2396
|
+
val imageContainerProps: List<ByteArray> =
|
|
2397
|
+
imageContainers.map { c ->
|
|
2398
|
+
EvenHubProto.imageContainerProperty(
|
|
2399
|
+
x = c.x,
|
|
2400
|
+
y = c.y,
|
|
2401
|
+
width = c.width,
|
|
2402
|
+
height = c.height,
|
|
2403
|
+
containerID = c.id,
|
|
2404
|
+
containerName = c.name
|
|
2405
|
+
)
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2124
2408
|
val msg: ByteArray
|
|
2125
2409
|
if (!startupPageCreated) {
|
|
2126
2410
|
Bridge.log("G2: createPageWithText - using createPageMessage (first time)")
|
|
2127
2411
|
msg =
|
|
2128
2412
|
EvenHubProto.createPageMessage(
|
|
2129
2413
|
textContainers = listOf(tc),
|
|
2414
|
+
imageContainers = imageContainerProps,
|
|
2130
2415
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2131
2416
|
appId = activeMenuAppId
|
|
2132
2417
|
)
|
|
@@ -2136,6 +2421,7 @@ class G2 : SGCManager() {
|
|
|
2136
2421
|
msg =
|
|
2137
2422
|
EvenHubProto.rebuildPageMessage(
|
|
2138
2423
|
textContainers = listOf(tc),
|
|
2424
|
+
imageContainers = imageContainerProps,
|
|
2139
2425
|
magicRandom = sendManager.nextMagicRandom(),
|
|
2140
2426
|
appId = activeMenuAppId
|
|
2141
2427
|
)
|
|
@@ -2327,21 +2613,33 @@ class G2 : SGCManager() {
|
|
|
2327
2613
|
|
|
2328
2614
|
// ---------- SGCManager: Audio Control ----------
|
|
2329
2615
|
|
|
2616
|
+
fun restartMic() {
|
|
2617
|
+
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2618
|
+
DeviceStore.apply("glasses", "micEnabled", true)
|
|
2619
|
+
val msg = EvenHubProto.audioControlMessage(false)
|
|
2620
|
+
sendEvenHubCommand(msg)
|
|
2621
|
+
mainHandler.postDelayed({
|
|
2622
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
2623
|
+
Bridge.log("G2: setMicEnabled - useNativeDashboard=$useNativeDashboard, dashboardShowing=$dashboardShowing")
|
|
2624
|
+
if (useNativeDashboard && dashboardShowing > 0) {
|
|
2625
|
+
return@postDelayed
|
|
2626
|
+
}
|
|
2627
|
+
if (!pageCreated || !pageHasTextContainer) {
|
|
2628
|
+
DeviceManager.getInstance().sendCurrentState() // should re-create the page if needed
|
|
2629
|
+
}
|
|
2630
|
+
val msg = EvenHubProto.audioControlMessage(true)
|
|
2631
|
+
sendEvenHubCommand(msg)
|
|
2632
|
+
}, 500)
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2330
2635
|
override fun setMicEnabled(enabled: Boolean) {
|
|
2331
2636
|
Bridge.log("G2: setMicEnabled($enabled)")
|
|
2332
2637
|
val currentEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
2333
|
-
|
|
2334
|
-
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2335
2638
|
if (currentEnabled && enabled) {
|
|
2336
|
-
|
|
2337
|
-
val msg = EvenHubProto.audioControlMessage(false)
|
|
2338
|
-
sendEvenHubCommand(msg)
|
|
2339
|
-
mainHandler.postDelayed({
|
|
2340
|
-
val msg = EvenHubProto.audioControlMessage(true)
|
|
2341
|
-
sendEvenHubCommand(msg)
|
|
2342
|
-
}, 500)
|
|
2639
|
+
restartMic()
|
|
2343
2640
|
return
|
|
2344
2641
|
}
|
|
2642
|
+
|
|
2345
2643
|
DeviceStore.apply("glasses", "micEnabled", enabled)
|
|
2346
2644
|
val msg = EvenHubProto.audioControlMessage(enabled)
|
|
2347
2645
|
sendEvenHubCommand(msg)
|
|
@@ -2360,6 +2658,7 @@ class G2 : SGCManager() {
|
|
|
2360
2658
|
authToken: String?,
|
|
2361
2659
|
compress: String?,
|
|
2362
2660
|
flash: Boolean,
|
|
2661
|
+
save: Boolean,
|
|
2363
2662
|
sound: Boolean,
|
|
2364
2663
|
exposureTimeNs: Long?,
|
|
2365
2664
|
iso: Int?,
|
|
@@ -2464,6 +2763,8 @@ class G2 : SGCManager() {
|
|
|
2464
2763
|
startupPageCreated = false
|
|
2465
2764
|
pageCreated = false
|
|
2466
2765
|
pageHasTextContainer = false
|
|
2766
|
+
imageContainers.clear()
|
|
2767
|
+
dashboardShowing = 0
|
|
2467
2768
|
heartbeatCounter = 0
|
|
2468
2769
|
currentBitmapBase64 = ""
|
|
2469
2770
|
menuAppIdToPackageName.clear()
|
|
@@ -2506,7 +2807,7 @@ class G2 : SGCManager() {
|
|
|
2506
2807
|
}
|
|
2507
2808
|
|
|
2508
2809
|
override fun dbg1() {
|
|
2509
|
-
|
|
2810
|
+
showNotificationsPanel()
|
|
2510
2811
|
}
|
|
2511
2812
|
override fun dbg2() {
|
|
2512
2813
|
disconnectController()
|
|
@@ -2606,6 +2907,14 @@ class G2 : SGCManager() {
|
|
|
2606
2907
|
// TODO: Implement via dev_settings
|
|
2607
2908
|
}
|
|
2608
2909
|
|
|
2910
|
+
/// Push the current time to the glasses. Useful after DST transitions,
|
|
2911
|
+
/// time-zone travel, or a long sleep where the glasses' clock has drifted.
|
|
2912
|
+
fun syncTime() {
|
|
2913
|
+
Bridge.log("G2: syncTime()")
|
|
2914
|
+
val msg = DevSettingsProto.timeSync(sendManager.nextMagicRandom())
|
|
2915
|
+
sendDevSettingsCommand(msg, left = true, right = true)
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2609
2918
|
override fun sendRgbLedControl(
|
|
2610
2919
|
requestId: String,
|
|
2611
2920
|
packageName: String?,
|
|
@@ -2940,6 +3249,7 @@ class G2 : SGCManager() {
|
|
|
2940
3249
|
startupPageCreated = false
|
|
2941
3250
|
pageCreated = false
|
|
2942
3251
|
pageHasTextContainer = false
|
|
3252
|
+
dashboardShowing = 0
|
|
2943
3253
|
DeviceStore.apply("glasses", "connected", false)
|
|
2944
3254
|
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2945
3255
|
|
|
@@ -3188,7 +3498,11 @@ class G2 : SGCManager() {
|
|
|
3188
3498
|
}
|
|
3189
3499
|
(resFields[8] as? Int)?.let { errorCode ->
|
|
3190
3500
|
// ImgResCmd has ErrorCode in field 8
|
|
3191
|
-
|
|
3501
|
+
if (errorCode == 4) {
|
|
3502
|
+
Bridge.log("G2: img_success")
|
|
3503
|
+
} else {
|
|
3504
|
+
Bridge.log("G2: EvenHub ImgRes errorCode=$errorCode")
|
|
3505
|
+
}
|
|
3192
3506
|
}
|
|
3193
3507
|
}
|
|
3194
3508
|
|
|
@@ -3269,12 +3583,14 @@ class G2 : SGCManager() {
|
|
|
3269
3583
|
if (eventType == OsEventType.DOUBLE_CLICK) {
|
|
3270
3584
|
// trigger dashboard:
|
|
3271
3585
|
val isHeadUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
3272
|
-
|
|
3273
|
-
DeviceStore.
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3586
|
+
|
|
3587
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
3588
|
+
if (useNativeDashboard) {
|
|
3589
|
+
showDashboard()
|
|
3590
|
+
} else {
|
|
3591
|
+
// toggle head up:
|
|
3592
|
+
DeviceStore.apply("glasses", "headUp", !isHeadUp)
|
|
3593
|
+
}
|
|
3278
3594
|
}
|
|
3279
3595
|
|
|
3280
3596
|
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
@@ -3445,14 +3761,47 @@ class G2 : SGCManager() {
|
|
|
3445
3761
|
}
|
|
3446
3762
|
|
|
3447
3763
|
private fun handleGestureCtrl(payload: ByteArray) {
|
|
3764
|
+
// Dedup: L and R peripherals both deliver this event, so debounce within 500ms.
|
|
3765
|
+
val timestamp = System.currentTimeMillis()
|
|
3766
|
+
val last = lastGestureCtrlTimestamp
|
|
3767
|
+
if (last != null && timestamp - last < 500) {
|
|
3768
|
+
Bridge.log("G2: gesture_ctrl dedup")
|
|
3769
|
+
return
|
|
3770
|
+
}
|
|
3771
|
+
lastGestureCtrlTimestamp = timestamp
|
|
3772
|
+
|
|
3448
3773
|
// Dashboard close detection: 08011A00 means dashboard closed
|
|
3449
3774
|
if (payload.contentEquals(byteArrayOf(0x08, 0x01, 0x1A, 0x00))) {
|
|
3450
|
-
Bridge.log("G2:
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3775
|
+
Bridge.log("G2: dashboard closed / shutdown - dashboardShowing=$dashboardShowing")
|
|
3776
|
+
val useNativeDashboard = DeviceStore.get("bluetooth", "use_native_dashboard") as? Boolean ?: false
|
|
3777
|
+
if (!useNativeDashboard) {
|
|
3778
|
+
// make sure the container exists:
|
|
3779
|
+
DeviceManager.getInstance().sendCurrentState()
|
|
3780
|
+
// re-send mic on / if it's enabled:
|
|
3781
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3782
|
+
if (micEnabled) {
|
|
3783
|
+
restartMic()
|
|
3784
|
+
}
|
|
3785
|
+
} else {
|
|
3786
|
+
// if we aren't trying to show the dashboard
|
|
3787
|
+
// then we need to turn the mic back on and display the mentra main page:
|
|
3788
|
+
if (dashboardShowing <= 1) {
|
|
3789
|
+
dashboardShowing = 0
|
|
3790
|
+
// make sure the container exists:
|
|
3791
|
+
DeviceManager.getInstance().sendCurrentState()
|
|
3792
|
+
// set the mic back on if it should be on
|
|
3793
|
+
val micEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
3794
|
+
if (micEnabled) {
|
|
3795
|
+
restartMic()
|
|
3796
|
+
}
|
|
3797
|
+
return
|
|
3798
|
+
}
|
|
3799
|
+
// do nothing this time since we just closed the dashboard
|
|
3800
|
+
dashboardShowing -= 1
|
|
3801
|
+
if (dashboardShowing < 0) {
|
|
3802
|
+
dashboardShowing = 0
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3456
3805
|
}
|
|
3457
3806
|
}
|
|
3458
3807
|
|