@mentra/bluetooth-sdk 0.1.0
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 +278 -0
- package/android/build.gradle +167 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +109 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/app.plugin.js +1 -0
- package/build/BluetoothSdk.types.d.ts +498 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +23 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/BluetoothSdkModule.d.ts +71 -0
- package/build/BluetoothSdkModule.d.ts.map +1 -0
- package/build/BluetoothSdkModule.js +106 -0
- package/build/BluetoothSdkModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BluetoothSdkModule.swift +665 -0
- package/ios/MentraBluetoothSDK.podspec +77 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- 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/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
- package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
- package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
- package/ios/Packages/VAD/Data/Configuration.swift +68 -0
- package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
- package/ios/Packages/VAD/Data/Record.swift +13 -0
- package/ios/Packages/VAD/Data/Result.swift +22 -0
- package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
- package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/Source/Bridge.swift +353 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +1478 -0
- package/ios/Source/DeviceStore.swift +295 -0
- package/ios/Source/MentraBluetoothSDK.swift +2439 -0
- package/ios/Source/ObservableStore.swift +88 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/controllers/ControllerManager.swift +187 -0
- package/ios/Source/controllers/R1.swift +827 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Frame.swift +651 -0
- package/ios/Source/sgcs/G1.swift +2505 -0
- package/ios/Source/sgcs/G2.swift +3730 -0
- package/ios/Source/sgcs/Mach1.swift +543 -0
- package/ios/Source/sgcs/MentraLive.swift +4708 -0
- package/ios/Source/sgcs/MentraNex.swift +2479 -0
- package/ios/Source/sgcs/SGCManager.swift +243 -0
- package/ios/Source/sgcs/Simulated.swift +270 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
- package/ios/Source/stt/STTTools.swift +154 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/Constants.swift +74 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2067 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunker.swift +164 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
- package/ios/Source/utils/TarBz2Extractor.swift +206 -0
- package/package.json +78 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +4 -0
- package/plugin/build/withAndroid.js +80 -0
- package/plugin/build/withIos.d.ts +4 -0
- package/plugin/build/withIos.js +64 -0
- package/src/BluetoothSdk.types.ts +581 -0
- package/src/BluetoothSdkModule.ts +259 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,3551 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothGatt
|
|
5
|
+
import android.bluetooth.BluetoothGattCallback
|
|
6
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
7
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
8
|
+
import android.bluetooth.BluetoothProfile
|
|
9
|
+
import android.bluetooth.le.ScanCallback
|
|
10
|
+
import android.bluetooth.le.ScanResult
|
|
11
|
+
import android.bluetooth.le.ScanSettings
|
|
12
|
+
import android.content.Context
|
|
13
|
+
import android.graphics.Bitmap
|
|
14
|
+
import android.graphics.BitmapFactory
|
|
15
|
+
import android.graphics.Canvas
|
|
16
|
+
import android.graphics.Color
|
|
17
|
+
import android.graphics.Paint
|
|
18
|
+
import android.graphics.Rect
|
|
19
|
+
import android.os.Handler
|
|
20
|
+
import android.os.Looper
|
|
21
|
+
import android.util.Base64
|
|
22
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
23
|
+
import com.mentra.bluetoothsdk.DeviceManager
|
|
24
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
25
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
26
|
+
import java.io.ByteArrayOutputStream
|
|
27
|
+
import java.util.TimeZone
|
|
28
|
+
import java.util.UUID
|
|
29
|
+
import java.util.regex.Pattern
|
|
30
|
+
|
|
31
|
+
// ---------- G2 Protocol Constants ----------
|
|
32
|
+
|
|
33
|
+
private object G2BLE {
|
|
34
|
+
// EvenHub BLE characteristic UUIDs (NOT the G1 UART UUIDs!)
|
|
35
|
+
val CHAR_WRITE: UUID = UUID.fromString("00002760-08C2-11E1-9073-0E8AC72E5401")
|
|
36
|
+
val CHAR_NOTIFY: UUID = UUID.fromString("00002760-08C2-11E1-9073-0E8AC72E5402")
|
|
37
|
+
val AUDIO_NOTIFY: UUID = UUID.fromString("00002760-08C2-11E1-9073-0E8AC72E6402")
|
|
38
|
+
val SERVICE_UUID: UUID = UUID.fromString("00002760-08C2-11E1-9073-0E8AC72E0000")
|
|
39
|
+
val CLIENT_CHARACTERISTIC_CONFIG: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
40
|
+
|
|
41
|
+
const val HEADER_BYTE: Byte = 0xAA.toByte()
|
|
42
|
+
const val SOURCE_PHONE: Byte = 1
|
|
43
|
+
const val DEST_GLASSES: Byte = 2
|
|
44
|
+
const val MAX_PACKET_PAYLOAD: Int = 236
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Service IDs from service_id_def.proto
|
|
48
|
+
private enum class ServiceID(val value: Byte) {
|
|
49
|
+
DASHBOARD(0x01), // UI_BACKGROUND_DASHBOARD_APP_ID
|
|
50
|
+
MENU(0x03), // UI_FOREGROUND_MEUN_ID (typo is intentional — matches Even's proto)
|
|
51
|
+
EVEN_AI(0x07), // UI_FOREGROUND_EVEN_AI_ID
|
|
52
|
+
G2_SETTING(0x09), // UI_SETTING_APP_ID
|
|
53
|
+
GESTURE_CTRL(0x0D), // gesture_ctrl lifecycle signals
|
|
54
|
+
ONBOARDING(0x10), // UI_ONBOARDING_APP_ID
|
|
55
|
+
DEVICE_SETTINGS(0x80.toByte()), // UX_DEVICE_SETTINGS_APP_ID
|
|
56
|
+
EVEN_HUB_CTRL(0x81.toByte()), // EvenHub CTRL channel (init/registration)
|
|
57
|
+
EVEN_HUB(0xE0.toByte()); // UI_BACKGROUND_EVENHUB_APP_ID
|
|
58
|
+
|
|
59
|
+
companion object {
|
|
60
|
+
fun fromByte(b: Byte): ServiceID? = entries.find { it.value == b }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// EvenHub command IDs from EvenHub.proto
|
|
65
|
+
private enum class EvenHubCmd(val value: Int) {
|
|
66
|
+
CREATE_STARTUP_PAGE(0),
|
|
67
|
+
UPDATE_IMAGE_RAW_DATA(3),
|
|
68
|
+
UPDATE_TEXT_DATA(5),
|
|
69
|
+
REBUILD_PAGE(7),
|
|
70
|
+
SHUTDOWN_PAGE(9),
|
|
71
|
+
HEARTBEAT(12),
|
|
72
|
+
AUDIO_CONTROL(15)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// EvenHub response command IDs (glasses → phone)
|
|
76
|
+
private enum class EvenHubResponseCmd(val value: Int) {
|
|
77
|
+
OS_NOTIFY_EVENT_TO_APP(2)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// OsEventTypeList from EvenHub.proto
|
|
81
|
+
private enum class OsEventType(val value: Int) {
|
|
82
|
+
CLICK(0),
|
|
83
|
+
SCROLL_TOP(1),
|
|
84
|
+
SCROLL_BOTTOM(2),
|
|
85
|
+
DOUBLE_CLICK(3),
|
|
86
|
+
FOREGROUND_ENTER(4),
|
|
87
|
+
FOREGROUND_EXIT(5),
|
|
88
|
+
ABNORMAL_EXIT(6),
|
|
89
|
+
SYSTEM_EXIT(7);
|
|
90
|
+
|
|
91
|
+
companion object {
|
|
92
|
+
fun fromInt(v: Int): OsEventType? = entries.find { it.value == v }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// g2_settingCommandId from g2_setting.proto
|
|
97
|
+
private enum class G2SettingCommandId(val value: Int) {
|
|
98
|
+
NONE(0),
|
|
99
|
+
DEVICE_RECEIVE_INFO(1),
|
|
100
|
+
DEVICE_RECEIVE_REQUEST(2),
|
|
101
|
+
DEVICE_SEND_TO_APP(3),
|
|
102
|
+
DEVICE_RESPOND_TO_APP(4)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// DevCfgCommandId from dev_config_protocol.proto
|
|
106
|
+
private enum class DevCfgCommandId(val value: Int) {
|
|
107
|
+
AUTHENTICATION(4),
|
|
108
|
+
PIPE_ROLE_CHANGE(5),
|
|
109
|
+
RING_CONNECT_INFO(6),
|
|
110
|
+
TIME_SYNC(128),
|
|
111
|
+
BASE_CONN_HEART_BEAT(14)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------- CRC16 ----------
|
|
115
|
+
|
|
116
|
+
private fun calcCRC16(data: ByteArray): Int {
|
|
117
|
+
var crc = 0xFFFF
|
|
118
|
+
for (byte in data) {
|
|
119
|
+
val b = byte.toInt() and 0xFF
|
|
120
|
+
crc = ((crc shr 8) or ((crc shl 8) and 0xFF00)) xor b
|
|
121
|
+
crc = crc xor ((crc and 0xFF) shr 4)
|
|
122
|
+
crc = crc xor ((crc shl 12) and 0xFFFF)
|
|
123
|
+
crc = crc xor (((crc and 0xFF) shl 5) and 0xFFFF)
|
|
124
|
+
}
|
|
125
|
+
return crc and 0xFFFF
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------- Minimal Protobuf Encoding ----------
|
|
129
|
+
|
|
130
|
+
private class ProtobufWriter {
|
|
131
|
+
private val stream = ByteArrayOutputStream()
|
|
132
|
+
|
|
133
|
+
fun writeVarint(value: Long) {
|
|
134
|
+
var v = value
|
|
135
|
+
// Use unsigned comparison so negative values (sign-extended) produce 10-byte varints
|
|
136
|
+
while (v.toULong() > 0x7FuL) {
|
|
137
|
+
stream.write(((v and 0x7F) or 0x80).toInt())
|
|
138
|
+
v = v ushr 7
|
|
139
|
+
}
|
|
140
|
+
stream.write((v and 0x7F).toInt())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fun writeInt32Field(fieldNumber: Int, value: Int) {
|
|
144
|
+
val tag = (fieldNumber shl 3).toLong() // wire type 0 = varint
|
|
145
|
+
writeVarint(tag)
|
|
146
|
+
// Kotlin Int.toLong() sign-extends, which is correct for protobuf int32
|
|
147
|
+
// Negative values produce 10-byte varints via unsigned comparison in writeVarint
|
|
148
|
+
writeVarint(value.toLong())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fun writeStringField(fieldNumber: Int, value: String) {
|
|
152
|
+
val tag = ((fieldNumber shl 3) or 2).toLong() // wire type 2 = length-delimited
|
|
153
|
+
writeVarint(tag)
|
|
154
|
+
val utf8 = value.toByteArray(Charsets.UTF_8)
|
|
155
|
+
writeVarint(utf8.size.toLong())
|
|
156
|
+
stream.write(utf8)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fun writeBytesField(fieldNumber: Int, value: ByteArray) {
|
|
160
|
+
val tag = ((fieldNumber shl 3) or 2).toLong()
|
|
161
|
+
writeVarint(tag)
|
|
162
|
+
writeVarint(value.size.toLong())
|
|
163
|
+
stream.write(value)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fun writeMessageField(fieldNumber: Int, subMessage: ByteArray) {
|
|
167
|
+
val tag = ((fieldNumber shl 3) or 2).toLong()
|
|
168
|
+
writeVarint(tag)
|
|
169
|
+
writeVarint(subMessage.size.toLong())
|
|
170
|
+
stream.write(subMessage)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fun writeBoolField(fieldNumber: Int, value: Boolean) {
|
|
174
|
+
writeInt32Field(fieldNumber, if (value) 1 else 0)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fun toByteArray(): ByteArray = stream.toByteArray()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------- Minimal Protobuf Decoding ----------
|
|
181
|
+
|
|
182
|
+
private class ProtobufReader(private val data: ByteArray) {
|
|
183
|
+
private var offset: Int = 0
|
|
184
|
+
|
|
185
|
+
val hasMore: Boolean
|
|
186
|
+
get() = offset < data.size
|
|
187
|
+
|
|
188
|
+
fun readVarint(): Long? {
|
|
189
|
+
var result: Long = 0
|
|
190
|
+
var shift = 0
|
|
191
|
+
while (offset < data.size) {
|
|
192
|
+
val byte = data[offset].toInt() and 0xFF
|
|
193
|
+
offset++
|
|
194
|
+
result = result or ((byte.toLong() and 0x7F) shl shift)
|
|
195
|
+
if (byte and 0x80 == 0) return result
|
|
196
|
+
shift += 7
|
|
197
|
+
if (shift > 63) return null
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun readTag(): Pair<Int, Int>? {
|
|
203
|
+
val tag = readVarint() ?: return null
|
|
204
|
+
return Pair((tag shr 3).toInt(), (tag and 0x07).toInt())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun readInt32(): Int? {
|
|
208
|
+
val v = readVarint() ?: return null
|
|
209
|
+
return v.toInt()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fun readBytes(): ByteArray? {
|
|
213
|
+
val len = readVarint()?.toInt() ?: return null
|
|
214
|
+
if (offset + len > data.size) return null
|
|
215
|
+
val result = data.copyOfRange(offset, offset + len)
|
|
216
|
+
offset += len
|
|
217
|
+
return result
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fun readString(): String? {
|
|
221
|
+
val bytes = readBytes() ?: return null
|
|
222
|
+
return String(bytes, Charsets.UTF_8)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fun skipField(wireType: Int) {
|
|
226
|
+
when (wireType) {
|
|
227
|
+
0 -> readVarint() // varint
|
|
228
|
+
1 -> offset += 8 // 64-bit
|
|
229
|
+
2 -> readBytes() // length-delimited
|
|
230
|
+
5 -> offset += 4 // 32-bit
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun parseFields(): Map<Int, Any> {
|
|
235
|
+
val fields = mutableMapOf<Int, Any>()
|
|
236
|
+
while (hasMore) {
|
|
237
|
+
val (fieldNum, wireType) = readTag() ?: break
|
|
238
|
+
when (wireType) {
|
|
239
|
+
0 -> {
|
|
240
|
+
val v = readVarint()
|
|
241
|
+
if (v != null) fields[fieldNum] = v.toInt()
|
|
242
|
+
}
|
|
243
|
+
2 -> {
|
|
244
|
+
val d = readBytes()
|
|
245
|
+
if (d != null) fields[fieldNum] = d
|
|
246
|
+
}
|
|
247
|
+
else -> skipField(wireType)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return fields
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------- EvenHub Protobuf Message Builders ----------
|
|
255
|
+
|
|
256
|
+
private object EvenHubProto {
|
|
257
|
+
fun textContainerProperty(
|
|
258
|
+
x: Int,
|
|
259
|
+
y: Int,
|
|
260
|
+
width: Int,
|
|
261
|
+
height: Int,
|
|
262
|
+
borderWidth: Int = 0,
|
|
263
|
+
borderColor: Int = 0,
|
|
264
|
+
borderRadius: Int = 0,
|
|
265
|
+
paddingLength: Int = 0,
|
|
266
|
+
containerID: Int,
|
|
267
|
+
containerName: String? = null,
|
|
268
|
+
isEventCapture: Boolean = false,
|
|
269
|
+
content: String? = null
|
|
270
|
+
): ByteArray {
|
|
271
|
+
val w = ProtobufWriter()
|
|
272
|
+
w.writeInt32Field(1, x)
|
|
273
|
+
w.writeInt32Field(2, y)
|
|
274
|
+
w.writeInt32Field(3, width)
|
|
275
|
+
w.writeInt32Field(4, height)
|
|
276
|
+
w.writeInt32Field(5, borderWidth)
|
|
277
|
+
w.writeInt32Field(6, borderColor)
|
|
278
|
+
w.writeInt32Field(7, borderRadius)
|
|
279
|
+
w.writeInt32Field(8, paddingLength)
|
|
280
|
+
w.writeInt32Field(9, containerID)
|
|
281
|
+
containerName?.let { w.writeStringField(10, it) }
|
|
282
|
+
w.writeInt32Field(11, if (isEventCapture) 1 else 0)
|
|
283
|
+
content?.let { w.writeStringField(12, it) }
|
|
284
|
+
return w.toByteArray()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fun imageContainerProperty(
|
|
288
|
+
x: Int,
|
|
289
|
+
y: Int,
|
|
290
|
+
width: Int,
|
|
291
|
+
height: Int,
|
|
292
|
+
containerID: Int,
|
|
293
|
+
containerName: String? = null
|
|
294
|
+
): ByteArray {
|
|
295
|
+
val w = ProtobufWriter()
|
|
296
|
+
w.writeInt32Field(1, x)
|
|
297
|
+
w.writeInt32Field(2, y)
|
|
298
|
+
w.writeInt32Field(3, width)
|
|
299
|
+
w.writeInt32Field(4, height)
|
|
300
|
+
w.writeInt32Field(5, containerID)
|
|
301
|
+
containerName?.let { w.writeStringField(6, it) }
|
|
302
|
+
return w.toByteArray()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fun imageRawDataUpdate(
|
|
306
|
+
containerID: Int,
|
|
307
|
+
containerName: String? = null,
|
|
308
|
+
mapSessionId: Int,
|
|
309
|
+
mapTotalSize: Int,
|
|
310
|
+
compressMode: Int = 0,
|
|
311
|
+
mapFragmentIndex: Int,
|
|
312
|
+
mapFragmentPacketSize: Int,
|
|
313
|
+
mapRawData: ByteArray
|
|
314
|
+
): ByteArray {
|
|
315
|
+
val w = ProtobufWriter()
|
|
316
|
+
w.writeInt32Field(1, containerID)
|
|
317
|
+
containerName?.let { w.writeStringField(2, it) }
|
|
318
|
+
w.writeInt32Field(3, mapSessionId)
|
|
319
|
+
w.writeInt32Field(4, mapTotalSize)
|
|
320
|
+
w.writeInt32Field(5, compressMode)
|
|
321
|
+
w.writeInt32Field(6, mapFragmentIndex)
|
|
322
|
+
w.writeInt32Field(7, mapFragmentPacketSize)
|
|
323
|
+
w.writeBytesField(8, mapRawData)
|
|
324
|
+
return w.toByteArray()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fun createStartupPageContainer(
|
|
328
|
+
containerTotalNum: Int,
|
|
329
|
+
textContainers: List<ByteArray> = emptyList(),
|
|
330
|
+
imageContainers: List<ByteArray> = emptyList()
|
|
331
|
+
): ByteArray {
|
|
332
|
+
val w = ProtobufWriter()
|
|
333
|
+
w.writeInt32Field(1, containerTotalNum)
|
|
334
|
+
for (tc in textContainers) w.writeMessageField(3, tc)
|
|
335
|
+
for (ic in imageContainers) w.writeMessageField(4, ic)
|
|
336
|
+
return w.toByteArray()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fun textContainerUpgrade(
|
|
340
|
+
containerID: Int,
|
|
341
|
+
contentOffset: Int = 0,
|
|
342
|
+
contentLength: Int,
|
|
343
|
+
content: String
|
|
344
|
+
): ByteArray {
|
|
345
|
+
val w = ProtobufWriter()
|
|
346
|
+
w.writeInt32Field(1, containerID)
|
|
347
|
+
w.writeInt32Field(3, contentOffset)
|
|
348
|
+
w.writeInt32Field(4, contentLength)
|
|
349
|
+
w.writeStringField(5, content)
|
|
350
|
+
return w.toByteArray()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fun shutdownContainer(exitMode: Int = 0): ByteArray {
|
|
354
|
+
val w = ProtobufWriter()
|
|
355
|
+
w.writeInt32Field(1, exitMode)
|
|
356
|
+
return w.toByteArray()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fun heartbeatPacket(cnt: Int = 0): ByteArray {
|
|
360
|
+
val w = ProtobufWriter()
|
|
361
|
+
if (cnt != 0) w.writeInt32Field(1, cnt)
|
|
362
|
+
return w.toByteArray()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fun audioCtrCmd(enable: Boolean): ByteArray {
|
|
366
|
+
val w = ProtobufWriter()
|
|
367
|
+
w.writeInt32Field(1, if (enable) 1 else 0)
|
|
368
|
+
return w.toByteArray()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fun evenHubMessage(
|
|
372
|
+
cmd: EvenHubCmd,
|
|
373
|
+
subFieldNumber: Int,
|
|
374
|
+
subMessage: ByteArray,
|
|
375
|
+
magicRandom: Int = 0,
|
|
376
|
+
appId: Int? = null
|
|
377
|
+
): ByteArray {
|
|
378
|
+
val w = ProtobufWriter()
|
|
379
|
+
w.writeInt32Field(1, cmd.value) // Cmd (field 1, enum)
|
|
380
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom (field 2)
|
|
381
|
+
w.writeMessageField(subFieldNumber, subMessage) // the actual command payload
|
|
382
|
+
appId?.let { w.writeInt32Field(5, it) } // Associate page with a menu item appId
|
|
383
|
+
return w.toByteArray()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fun createPageMessage(
|
|
387
|
+
textContainers: List<ByteArray> = emptyList(),
|
|
388
|
+
imageContainers: List<ByteArray> = emptyList(),
|
|
389
|
+
magicRandom: Int = 0,
|
|
390
|
+
appId: Int? = null
|
|
391
|
+
): ByteArray {
|
|
392
|
+
val total = textContainers.size + imageContainers.size
|
|
393
|
+
val createMsg = createStartupPageContainer(total, textContainers, imageContainers)
|
|
394
|
+
return evenHubMessage(
|
|
395
|
+
EvenHubCmd.CREATE_STARTUP_PAGE,
|
|
396
|
+
3,
|
|
397
|
+
createMsg,
|
|
398
|
+
magicRandom = magicRandom,
|
|
399
|
+
appId = null
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fun rebuildPageMessage(
|
|
404
|
+
textContainers: List<ByteArray> = emptyList(),
|
|
405
|
+
imageContainers: List<ByteArray> = emptyList(),
|
|
406
|
+
magicRandom: Int = 0,
|
|
407
|
+
appId: Int? = null
|
|
408
|
+
): ByteArray {
|
|
409
|
+
val total = textContainers.size + imageContainers.size
|
|
410
|
+
val rebuildMsg = createStartupPageContainer(total, textContainers, imageContainers)
|
|
411
|
+
return evenHubMessage(
|
|
412
|
+
EvenHubCmd.REBUILD_PAGE,
|
|
413
|
+
7,
|
|
414
|
+
rebuildMsg,
|
|
415
|
+
magicRandom = magicRandom,
|
|
416
|
+
appId = appId
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fun updateImageRawDataMessage(
|
|
421
|
+
containerID: Int,
|
|
422
|
+
containerName: String? = null,
|
|
423
|
+
mapSessionId: Int,
|
|
424
|
+
mapTotalSize: Int,
|
|
425
|
+
compressMode: Int = 0,
|
|
426
|
+
mapFragmentIndex: Int,
|
|
427
|
+
mapFragmentPacketSize: Int,
|
|
428
|
+
mapRawData: ByteArray
|
|
429
|
+
): ByteArray {
|
|
430
|
+
val updateMsg =
|
|
431
|
+
imageRawDataUpdate(
|
|
432
|
+
containerID,
|
|
433
|
+
containerName,
|
|
434
|
+
mapSessionId,
|
|
435
|
+
mapTotalSize,
|
|
436
|
+
compressMode,
|
|
437
|
+
mapFragmentIndex,
|
|
438
|
+
mapFragmentPacketSize,
|
|
439
|
+
mapRawData
|
|
440
|
+
)
|
|
441
|
+
return evenHubMessage(EvenHubCmd.UPDATE_IMAGE_RAW_DATA, 5, updateMsg)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fun updateTextMessage(
|
|
445
|
+
containerID: Int,
|
|
446
|
+
contentOffset: Int = 0,
|
|
447
|
+
contentLength: Int,
|
|
448
|
+
content: String
|
|
449
|
+
): ByteArray {
|
|
450
|
+
val upgradeMsg = textContainerUpgrade(containerID, contentOffset, contentLength, content)
|
|
451
|
+
return evenHubMessage(EvenHubCmd.UPDATE_TEXT_DATA, 9, upgradeMsg)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fun shutdownMessage(exitMode: Int = 0): ByteArray {
|
|
455
|
+
val msg = shutdownContainer(exitMode)
|
|
456
|
+
return evenHubMessage(EvenHubCmd.SHUTDOWN_PAGE, 11, msg)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fun heartbeatMessage(magicRandom: Int = 0): ByteArray {
|
|
460
|
+
val msg = heartbeatPacket()
|
|
461
|
+
return evenHubMessage(EvenHubCmd.HEARTBEAT, 14, msg, magicRandom = magicRandom)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fun audioControlMessage(enable: Boolean, magicRandom: Int = 0): ByteArray {
|
|
465
|
+
val msg = audioCtrCmd(enable)
|
|
466
|
+
return evenHubMessage(EvenHubCmd.AUDIO_CONTROL, 18, msg, magicRandom = magicRandom)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------- DevSettings Auth Protobuf Builders ----------
|
|
471
|
+
|
|
472
|
+
private object DevSettingsProto {
|
|
473
|
+
fun authCmd(magicRandom: Int): ByteArray {
|
|
474
|
+
val w = ProtobufWriter()
|
|
475
|
+
w.writeInt32Field(1, DevCfgCommandId.AUTHENTICATION.value)
|
|
476
|
+
w.writeInt32Field(2, magicRandom)
|
|
477
|
+
|
|
478
|
+
// AuthMgr sub-message
|
|
479
|
+
val authW = ProtobufWriter()
|
|
480
|
+
authW.writeBoolField(1, true) // secAuth
|
|
481
|
+
authW.writeInt32Field(2, 4) // phoneType = PHONE_ANDROID (4)
|
|
482
|
+
|
|
483
|
+
w.writeMessageField(3, authW.toByteArray())
|
|
484
|
+
return w.toByteArray()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
fun pipeRoleChange(magicRandom: Int): ByteArray {
|
|
488
|
+
val w = ProtobufWriter()
|
|
489
|
+
w.writeInt32Field(1, DevCfgCommandId.PIPE_ROLE_CHANGE.value)
|
|
490
|
+
w.writeInt32Field(2, magicRandom)
|
|
491
|
+
|
|
492
|
+
// PipeRoleChange: field 1 = asCmdRole (GlassesLR.RIGHT=1)
|
|
493
|
+
val roleW = ProtobufWriter()
|
|
494
|
+
roleW.writeInt32Field(1, 1) // RIGHT
|
|
495
|
+
w.writeMessageField(4, roleW.toByteArray())
|
|
496
|
+
return w.toByteArray()
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
fun timeSync(magicRandom: Int): ByteArray {
|
|
500
|
+
val w = ProtobufWriter()
|
|
501
|
+
w.writeInt32Field(1, DevCfgCommandId.TIME_SYNC.value)
|
|
502
|
+
w.writeInt32Field(2, magicRandom)
|
|
503
|
+
|
|
504
|
+
val tsW = ProtobufWriter()
|
|
505
|
+
val timestamp = (System.currentTimeMillis() / 1000).toInt()
|
|
506
|
+
tsW.writeInt32Field(1, timestamp)
|
|
507
|
+
val tz = TimeZone.getDefault().getOffset(System.currentTimeMillis()) / 3600000
|
|
508
|
+
tsW.writeInt32Field(2, tz)
|
|
509
|
+
w.writeMessageField(128, tsW.toByteArray())
|
|
510
|
+
return w.toByteArray()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fun baseHeartbeat(magicRandom: Int): ByteArray {
|
|
514
|
+
val w = ProtobufWriter()
|
|
515
|
+
w.writeInt32Field(1, DevCfgCommandId.BASE_CONN_HEART_BEAT.value)
|
|
516
|
+
w.writeInt32Field(2, magicRandom)
|
|
517
|
+
|
|
518
|
+
// BaseConnHeartBeat: empty message
|
|
519
|
+
val hbW = ProtobufWriter()
|
|
520
|
+
w.writeMessageField(13, hbW.toByteArray())
|
|
521
|
+
return w.toByteArray()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
fun ringConnectInfo(
|
|
525
|
+
magicRandom: Int,
|
|
526
|
+
connect: Boolean,
|
|
527
|
+
ringMac: ByteArray,
|
|
528
|
+
ringName: String = ""
|
|
529
|
+
): ByteArray {
|
|
530
|
+
val w = ProtobufWriter()
|
|
531
|
+
w.writeInt32Field(
|
|
532
|
+
1,
|
|
533
|
+
DevCfgCommandId.RING_CONNECT_INFO.value
|
|
534
|
+
) // commandId = RING_CONNECT_INFO (6)
|
|
535
|
+
w.writeInt32Field(2, magicRandom)
|
|
536
|
+
|
|
537
|
+
// RingInfo sub-message (field 5 in DevCfgDataPackage)
|
|
538
|
+
val ringW = ProtobufWriter()
|
|
539
|
+
ringW.writeBoolField(1, connect) // connectRing
|
|
540
|
+
ringW.writeBytesField(2, ringMac) // ringMac (6 bytes)
|
|
541
|
+
if (ringName.isNotEmpty()) {
|
|
542
|
+
ringW.writeBytesField(3, ringName.toByteArray(Charsets.UTF_8)) // ringName
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
w.writeMessageField(5, ringW.toByteArray()) // ringInfo (field 5)
|
|
546
|
+
return w.toByteArray()
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ---------- G2 Settings Protobuf Builders ----------
|
|
551
|
+
|
|
552
|
+
private object G2SettingProto {
|
|
553
|
+
fun setBrightness(magicRandom: Int, level: Int, autoAdjust: Boolean): ByteArray {
|
|
554
|
+
val brightnessW = ProtobufWriter()
|
|
555
|
+
brightnessW.writeInt32Field(1, if (autoAdjust) 1 else 0)
|
|
556
|
+
brightnessW.writeInt32Field(2, level)
|
|
557
|
+
|
|
558
|
+
val infoW = ProtobufWriter()
|
|
559
|
+
infoW.writeMessageField(1, brightnessW.toByteArray())
|
|
560
|
+
|
|
561
|
+
val w = ProtobufWriter()
|
|
562
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_INFO.value)
|
|
563
|
+
w.writeInt32Field(2, magicRandom)
|
|
564
|
+
w.writeMessageField(3, infoW.toByteArray())
|
|
565
|
+
return w.toByteArray()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
fun requestInfo(magicRandom: Int): ByteArray {
|
|
569
|
+
val reqW = ProtobufWriter()
|
|
570
|
+
reqW.writeInt32Field(1, 1) // settingInfoType = APP_REQUIRE_BASIC_SETTING
|
|
571
|
+
|
|
572
|
+
val w = ProtobufWriter()
|
|
573
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_REQUEST.value)
|
|
574
|
+
w.writeInt32Field(2, magicRandom)
|
|
575
|
+
w.writeMessageField(4, reqW.toByteArray())
|
|
576
|
+
return w.toByteArray()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
fun setHeadUpSwitch(magicRandom: Int, enabled: Boolean): ByteArray {
|
|
580
|
+
// DeviceReceive_Head_UP_Setting
|
|
581
|
+
val headUpW = ProtobufWriter()
|
|
582
|
+
headUpW.writeInt32Field(1, if (enabled) 1 else 0) // headUpSwitch
|
|
583
|
+
|
|
584
|
+
// DeviceReceiveInfoFromAPP
|
|
585
|
+
val infoW = ProtobufWriter()
|
|
586
|
+
infoW.writeMessageField(4, headUpW.toByteArray()) // deviceReceiveHeadUpSetting (field 4)
|
|
587
|
+
|
|
588
|
+
// G2SettingPackage
|
|
589
|
+
val w = ProtobufWriter()
|
|
590
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_INFO.value)
|
|
591
|
+
w.writeInt32Field(2, magicRandom)
|
|
592
|
+
w.writeMessageField(3, infoW.toByteArray()) // deviceReceiveInfoFromApp (field 3)
|
|
593
|
+
return w.toByteArray()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
fun setHeadUpAngle(magicRandom: Int, angle: Int): ByteArray {
|
|
597
|
+
// DeviceReceive_Head_UP_Setting
|
|
598
|
+
val headUpW = ProtobufWriter()
|
|
599
|
+
headUpW.writeInt32Field(2, angle) // headUpAngle (field 2)
|
|
600
|
+
|
|
601
|
+
// DeviceReceiveInfoFromAPP
|
|
602
|
+
val infoW = ProtobufWriter()
|
|
603
|
+
infoW.writeMessageField(4, headUpW.toByteArray()) // deviceReceiveHeadUpSetting (field 4)
|
|
604
|
+
|
|
605
|
+
// G2SettingPackage
|
|
606
|
+
val w = ProtobufWriter()
|
|
607
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_INFO.value)
|
|
608
|
+
w.writeInt32Field(2, magicRandom)
|
|
609
|
+
w.writeMessageField(3, infoW.toByteArray())
|
|
610
|
+
return w.toByteArray()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
fun setScreenHeight(magicRandom: Int, level: Int): ByteArray {
|
|
614
|
+
// DeviceReceive_Y_Coordinate
|
|
615
|
+
val yW = ProtobufWriter()
|
|
616
|
+
yW.writeInt32Field(1, level) // yCoordinateLevel
|
|
617
|
+
|
|
618
|
+
// DeviceReceiveInfoFromAPP
|
|
619
|
+
val infoW = ProtobufWriter()
|
|
620
|
+
infoW.writeMessageField(2, yW.toByteArray()) // deviceReceiveYCoordinate (field 2)
|
|
621
|
+
|
|
622
|
+
// G2SettingPackage
|
|
623
|
+
val w = ProtobufWriter()
|
|
624
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_INFO.value)
|
|
625
|
+
w.writeInt32Field(2, magicRandom)
|
|
626
|
+
w.writeMessageField(3, infoW.toByteArray())
|
|
627
|
+
return w.toByteArray()
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fun setScreenDepth(magicRandom: Int, level: Int): ByteArray {
|
|
631
|
+
// DeviceReceive_X_Coordinate
|
|
632
|
+
val xW = ProtobufWriter()
|
|
633
|
+
xW.writeInt32Field(1, level) // xCoordinateLevel
|
|
634
|
+
|
|
635
|
+
// DeviceReceiveInfoFromAPP
|
|
636
|
+
val infoW = ProtobufWriter()
|
|
637
|
+
infoW.writeMessageField(3, xW.toByteArray()) // deviceReceiveXCoordinate (field 3)
|
|
638
|
+
|
|
639
|
+
// G2SettingPackage
|
|
640
|
+
val w = ProtobufWriter()
|
|
641
|
+
w.writeInt32Field(1, G2SettingCommandId.DEVICE_RECEIVE_INFO.value)
|
|
642
|
+
w.writeInt32Field(2, magicRandom)
|
|
643
|
+
w.writeMessageField(3, infoW.toByteArray())
|
|
644
|
+
return w.toByteArray()
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---------- Onboarding Protobuf Builders ----------
|
|
649
|
+
|
|
650
|
+
private object OnboardingProto {
|
|
651
|
+
fun skipOnboarding(magicRandom: Int): ByteArray {
|
|
652
|
+
val configW = ProtobufWriter()
|
|
653
|
+
configW.writeInt32Field(1, 4) // processId = FINISH
|
|
654
|
+
|
|
655
|
+
val w = ProtobufWriter()
|
|
656
|
+
w.writeInt32Field(1, 1) // commandId = CONFIG
|
|
657
|
+
w.writeInt32Field(2, magicRandom)
|
|
658
|
+
w.writeMessageField(3, configW.toByteArray())
|
|
659
|
+
return w.toByteArray()
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ---------- EvenAI Protobuf Builders (even_ai.proto, service ID 7) ----------
|
|
664
|
+
|
|
665
|
+
private object EvenAIProto {
|
|
666
|
+
fun setHeyEven(magicRandom: Int, enabled: Boolean): ByteArray {
|
|
667
|
+
// EvenAIConfig
|
|
668
|
+
val configW = ProtobufWriter()
|
|
669
|
+
configW.writeInt32Field(1, if (enabled) 1 else 0) // voiceSwitch
|
|
670
|
+
configW.writeInt32Field(2, 80) // streamSpeed (always sent)
|
|
671
|
+
|
|
672
|
+
// EvenAIDataPackage
|
|
673
|
+
val w = ProtobufWriter()
|
|
674
|
+
w.writeInt32Field(1, 10) // commandId = CONFIG
|
|
675
|
+
w.writeInt32Field(2, magicRandom)
|
|
676
|
+
w.writeMessageField(13, configW.toByteArray()) // config (field 13)
|
|
677
|
+
return w.toByteArray()
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------- Menu Protobuf Builders (menu.proto, service ID 3) ----------
|
|
682
|
+
|
|
683
|
+
private object MenuProto {
|
|
684
|
+
data class MenuItem(val packageName: String, val name: String, val running: Boolean)
|
|
685
|
+
|
|
686
|
+
const val MIN_MENU_SIZE = 5
|
|
687
|
+
const val MAX_MENU_SIZE = 10
|
|
688
|
+
const val MAX_NAME_LENGTH = 15 // 17 char limit minus 2 for running indicator prefix
|
|
689
|
+
val PLACEHOLDER_APP_IDS = listOf(10535, 10536, 10537, 10538, 10539)
|
|
690
|
+
|
|
691
|
+
/** Deterministic hash of packageName -> numeric appId in range 10029-10534 */
|
|
692
|
+
fun packageNameToAppId(packageName: String): Int {
|
|
693
|
+
var hash = 0
|
|
694
|
+
for (char in packageName) {
|
|
695
|
+
hash = (hash shl 5) - hash + char.code
|
|
696
|
+
}
|
|
697
|
+
// 506 values: 10029-10534 (reserve 10535-10539 for placeholders)
|
|
698
|
+
return 10029 + (kotlin.math.abs(hash) % 506)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* meun_main_msg_ctx with APP_SEND_MENU_INFO command Handles: name truncation (15 chars),
|
|
703
|
+
* running prefix, padding to 5, cap at 10 Always prepends the built-in Notification item as the
|
|
704
|
+
* first entry. Returns (protobuf data, appId->packageName mapping for reverse lookup)
|
|
705
|
+
*/
|
|
706
|
+
fun sendMenuInfo(magicRandom: Int, items: List<MenuItem>): Pair<ByteArray, Map<Int, String>> {
|
|
707
|
+
val appIdMap = mutableMapOf<Int, String>()
|
|
708
|
+
|
|
709
|
+
data class WireItem(val displayName: String?, val appId: Int, val isBuiltIn: Boolean)
|
|
710
|
+
val wireItems = mutableListOf<WireItem>()
|
|
711
|
+
|
|
712
|
+
// Always first: built-in Notification (SID=4)
|
|
713
|
+
wireItems.add(WireItem(null, 4, true))
|
|
714
|
+
|
|
715
|
+
// Third-party items — leave room for the built-in
|
|
716
|
+
for (item in items.take(MAX_MENU_SIZE - 1)) {
|
|
717
|
+
val appId = packageNameToAppId(item.packageName)
|
|
718
|
+
appIdMap[appId] = item.packageName
|
|
719
|
+
|
|
720
|
+
val truncated =
|
|
721
|
+
if (item.name.length > MAX_NAME_LENGTH) item.name.take(MAX_NAME_LENGTH)
|
|
722
|
+
else item.name
|
|
723
|
+
val prefix = if (item.running) "● " else " "
|
|
724
|
+
wireItems.add(WireItem(prefix + truncated, appId, false))
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Pad to MIN_MENU_SIZE with placeholder third-party items
|
|
728
|
+
while (wireItems.size < MIN_MENU_SIZE) {
|
|
729
|
+
val idx = wireItems.size - 1 // -1 because built-in occupies slot 0
|
|
730
|
+
wireItems.add(WireItem(" ---", PLACEHOLDER_APP_IDS[idx], false))
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// MenuInfoSend
|
|
734
|
+
val menuW = ProtobufWriter()
|
|
735
|
+
menuW.writeInt32Field(1, wireItems.size) // itemTotalNum
|
|
736
|
+
|
|
737
|
+
for (item in wireItems) {
|
|
738
|
+
val itemW = ProtobufWriter()
|
|
739
|
+
if (item.isBuiltIn) {
|
|
740
|
+
itemW.writeInt32Field(1, 0) // itemType = 0 (built-in)
|
|
741
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId = SID
|
|
742
|
+
} else {
|
|
743
|
+
itemW.writeInt32Field(1, 1) // itemType = 1 (third-party)
|
|
744
|
+
itemW.writeInt32Field(2, 1) // iconNum = 1
|
|
745
|
+
itemW.writeStringField(3, item.displayName ?: "") // itemName
|
|
746
|
+
itemW.writeInt32Field(4, item.appId) // itemAppId
|
|
747
|
+
}
|
|
748
|
+
menuW.writeMessageField(2, itemW.toByteArray()) // repeated item (field 2)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// meun_main_msg_ctx
|
|
752
|
+
val w = ProtobufWriter()
|
|
753
|
+
w.writeInt32Field(1, 0) // Cmd = APP_SEND_MENU_INFO (0)
|
|
754
|
+
w.writeInt32Field(2, magicRandom) // MagicRandom
|
|
755
|
+
w.writeMessageField(3, menuW.toByteArray()) // sendData (field 3)
|
|
756
|
+
return Pair(w.toByteArray(), appIdMap)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ---------- EvenBLE Transport Layer ----------
|
|
761
|
+
|
|
762
|
+
private object EvenBLETransport {
|
|
763
|
+
fun buildPackets(
|
|
764
|
+
syncId: Byte,
|
|
765
|
+
serviceId: Byte,
|
|
766
|
+
payload: ByteArray,
|
|
767
|
+
reserveFlag: Boolean = false
|
|
768
|
+
): List<ByteArray> {
|
|
769
|
+
val maxPayload = G2BLE.MAX_PACKET_PAYLOAD
|
|
770
|
+
|
|
771
|
+
// Split payload into chunks
|
|
772
|
+
val chunks = mutableListOf<ByteArray>()
|
|
773
|
+
var offset = 0
|
|
774
|
+
while (offset < payload.size) {
|
|
775
|
+
val end = minOf(offset + maxPayload, payload.size)
|
|
776
|
+
chunks.add(payload.copyOfRange(offset, end))
|
|
777
|
+
offset = end
|
|
778
|
+
}
|
|
779
|
+
if (chunks.isEmpty()) {
|
|
780
|
+
chunks.add(ByteArray(0))
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// If last chunk is exactly max size, need extra packet for CRC
|
|
784
|
+
if (chunks.last().size == maxPayload) {
|
|
785
|
+
chunks.add(ByteArray(0))
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
val totalPackets = chunks.size.toByte()
|
|
789
|
+
val crc = calcCRC16(payload)
|
|
790
|
+
|
|
791
|
+
val packets = mutableListOf<ByteArray>()
|
|
792
|
+
for ((i, chunk) in chunks.withIndex()) {
|
|
793
|
+
val serialNum = (i + 1).toByte()
|
|
794
|
+
val isLast = serialNum == totalPackets
|
|
795
|
+
|
|
796
|
+
// status byte: bit5=reserveFlag
|
|
797
|
+
val status: Byte = if (reserveFlag) 0x20 else 0x00
|
|
798
|
+
|
|
799
|
+
// payload length includes CRC if last packet
|
|
800
|
+
val payloadLen = (chunk.size + if (isLast) 2 else 0).toByte()
|
|
801
|
+
|
|
802
|
+
val packet = ByteArrayOutputStream()
|
|
803
|
+
packet.write(G2BLE.HEADER_BYTE.toInt() and 0xFF)
|
|
804
|
+
packet.write(
|
|
805
|
+
((G2BLE.DEST_GLASSES.toInt() shl 4) or G2BLE.SOURCE_PHONE.toInt()) and 0xFF
|
|
806
|
+
)
|
|
807
|
+
packet.write(syncId.toInt() and 0xFF)
|
|
808
|
+
packet.write(payloadLen.toInt() and 0xFF)
|
|
809
|
+
packet.write(totalPackets.toInt() and 0xFF)
|
|
810
|
+
packet.write(serialNum.toInt() and 0xFF)
|
|
811
|
+
packet.write(serviceId.toInt() and 0xFF)
|
|
812
|
+
packet.write(status.toInt() and 0xFF)
|
|
813
|
+
|
|
814
|
+
packet.write(chunk)
|
|
815
|
+
|
|
816
|
+
if (isLast) {
|
|
817
|
+
packet.write(crc and 0xFF)
|
|
818
|
+
packet.write((crc shr 8) and 0xFF)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
packets.add(packet.toByteArray())
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return packets
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ---------- G2 Send Manager ----------
|
|
829
|
+
|
|
830
|
+
private class G2SendManager {
|
|
831
|
+
private var syncId: Byte = 0
|
|
832
|
+
private var magicRandom: Byte = 0
|
|
833
|
+
|
|
834
|
+
fun nextSyncId(): Byte {
|
|
835
|
+
val id = syncId
|
|
836
|
+
syncId = (syncId + 1).toByte()
|
|
837
|
+
return id
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
fun nextMagicRandom(): Int {
|
|
841
|
+
val v = magicRandom
|
|
842
|
+
magicRandom = (magicRandom + 1).toByte()
|
|
843
|
+
return v.toInt() and 0xFF
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fun buildPackets(
|
|
847
|
+
serviceId: Byte,
|
|
848
|
+
payload: ByteArray,
|
|
849
|
+
reserveFlag: Boolean = false
|
|
850
|
+
): List<ByteArray> {
|
|
851
|
+
val sid = nextSyncId()
|
|
852
|
+
return EvenBLETransport.buildPackets(sid, serviceId, payload, reserveFlag)
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ---------- G2 Receive Manager ----------
|
|
857
|
+
|
|
858
|
+
private class G2ReceiveManager {
|
|
859
|
+
private val partials = mutableMapOf<String, Pair<ByteArrayOutputStream, Byte>>()
|
|
860
|
+
|
|
861
|
+
fun handlePacket(rawData: ByteArray, sourceKey: String = ""): Pair<Byte, ByteArray>? {
|
|
862
|
+
if (rawData.size < 8) return null
|
|
863
|
+
if (rawData[0] != G2BLE.HEADER_BYTE) return null
|
|
864
|
+
|
|
865
|
+
val payloadLen = rawData[3].toInt() and 0xFF
|
|
866
|
+
val expectedLen = payloadLen + 8
|
|
867
|
+
if (rawData.size < expectedLen) return null
|
|
868
|
+
|
|
869
|
+
val totalPackets = rawData[4]
|
|
870
|
+
val serialNum = rawData[5]
|
|
871
|
+
val serviceId = rawData[6]
|
|
872
|
+
val status = rawData[7].toInt() and 0xFF
|
|
873
|
+
val resultCode = (status shr 1) and 0x0F
|
|
874
|
+
|
|
875
|
+
if (resultCode != 0) return null
|
|
876
|
+
|
|
877
|
+
val isLast = serialNum == totalPackets
|
|
878
|
+
val hasCrc = isLast
|
|
879
|
+
val payloadEnd = 8 + payloadLen - if (hasCrc) 2 else 0
|
|
880
|
+
val payload = rawData.copyOfRange(8, payloadEnd)
|
|
881
|
+
|
|
882
|
+
val syncId = rawData[2]
|
|
883
|
+
// Include sourceKey so concurrent multi-packet responses from the L and R lenses with
|
|
884
|
+
// the same syncId don't cross-merge into one broken payload.
|
|
885
|
+
val key = "$sourceKey-${serviceId.toInt() and 0xFF}-${syncId.toInt() and 0xFF}"
|
|
886
|
+
|
|
887
|
+
if ((serialNum.toInt() and 0xFF) > 1) {
|
|
888
|
+
val existing = partials[key] ?: return null
|
|
889
|
+
existing.first.write(payload)
|
|
890
|
+
partials[key] = Pair(existing.first, serialNum)
|
|
891
|
+
} else if ((totalPackets.toInt() and 0xFF) > 1) {
|
|
892
|
+
val baos = ByteArrayOutputStream()
|
|
893
|
+
baos.write(payload)
|
|
894
|
+
partials[key] = Pair(baos, serialNum)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!isLast) return null
|
|
898
|
+
|
|
899
|
+
val fullPayload: ByteArray
|
|
900
|
+
val existing = partials[key]
|
|
901
|
+
if (existing != null) {
|
|
902
|
+
fullPayload = existing.first.toByteArray()
|
|
903
|
+
partials.remove(key)
|
|
904
|
+
} else {
|
|
905
|
+
fullPayload = payload
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return Pair(serviceId, fullPayload)
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ---------- G2 Reconnection Manager ----------
|
|
913
|
+
|
|
914
|
+
private class G2ReconnectionManager(
|
|
915
|
+
private val intervalMs: Long = 30_000L,
|
|
916
|
+
private val maxAttempts: Int = -1 // -1 for unlimited
|
|
917
|
+
) {
|
|
918
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
919
|
+
private var runnable: Runnable? = null
|
|
920
|
+
private var attempts = 0
|
|
921
|
+
val isRunning: Boolean
|
|
922
|
+
get() = runnable != null
|
|
923
|
+
|
|
924
|
+
fun start(onAttempt: () -> Boolean) {
|
|
925
|
+
stop()
|
|
926
|
+
attempts = 0
|
|
927
|
+
|
|
928
|
+
val r =
|
|
929
|
+
object : Runnable {
|
|
930
|
+
override fun run() {
|
|
931
|
+
if (maxAttempts > 0 && attempts >= maxAttempts) {
|
|
932
|
+
Bridge.log("G2: Max reconnection attempts ($maxAttempts) reached")
|
|
933
|
+
stop()
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
attempts++
|
|
938
|
+
Bridge.log("G2: Reconnection attempt $attempts")
|
|
939
|
+
|
|
940
|
+
val shouldStop = onAttempt()
|
|
941
|
+
if (shouldStop) {
|
|
942
|
+
Bridge.log("G2: Reconnection successful, stopping")
|
|
943
|
+
stop()
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
handler.postDelayed(this, intervalMs)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
runnable = r
|
|
951
|
+
handler.postDelayed(r, intervalMs)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
fun stop() {
|
|
955
|
+
runnable?.let { handler.removeCallbacks(it) }
|
|
956
|
+
runnable = null
|
|
957
|
+
attempts = 0
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// ---------- G2 Class ----------
|
|
962
|
+
|
|
963
|
+
class G2 : SGCManager() {
|
|
964
|
+
|
|
965
|
+
companion object {
|
|
966
|
+
private const val PREFS_NAME = "G2Prefs"
|
|
967
|
+
private const val KEY_LEFT_ADDRESS = "g2_leftGlassAddress"
|
|
968
|
+
private const val KEY_RIGHT_ADDRESS = "g2_rightGlassAddress"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
init {
|
|
972
|
+
type = DeviceTypes.G2
|
|
973
|
+
hasMic = true
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// BLE
|
|
977
|
+
private val context: Context
|
|
978
|
+
get() = Bridge.getContext()
|
|
979
|
+
private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
|
|
980
|
+
private var leftGatt: BluetoothGatt? = null
|
|
981
|
+
private var rightGatt: BluetoothGatt? = null
|
|
982
|
+
private var leftWriteChar: BluetoothGattCharacteristic? = null
|
|
983
|
+
private var rightWriteChar: BluetoothGattCharacteristic? = null
|
|
984
|
+
private var leftNotifyChar: BluetoothGattCharacteristic? = null
|
|
985
|
+
private var rightNotifyChar: BluetoothGattCharacteristic? = null
|
|
986
|
+
private var leftAudioChar: BluetoothGattCharacteristic? = null
|
|
987
|
+
private var rightAudioChar: BluetoothGattCharacteristic? = null
|
|
988
|
+
private var leftInitialized: Boolean = false
|
|
989
|
+
private var rightInitialized: Boolean = false
|
|
990
|
+
private var isDisconnecting = false
|
|
991
|
+
private var pairingTimeoutRunnable: Runnable? = null
|
|
992
|
+
|
|
993
|
+
// Device search
|
|
994
|
+
private var DEVICE_SEARCH_ID = "NOT_SET"
|
|
995
|
+
// Map device names to serial numbers (populated from manufacturer data during scan)
|
|
996
|
+
private val deviceNameToSerialNumber = mutableMapOf<String, String>()
|
|
997
|
+
|
|
998
|
+
// Saved addresses for reconnection
|
|
999
|
+
private var leftGlassAddress: String?
|
|
1000
|
+
get() =
|
|
1001
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1002
|
+
.getString(KEY_LEFT_ADDRESS, null)
|
|
1003
|
+
set(value) {
|
|
1004
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1005
|
+
.edit()
|
|
1006
|
+
.apply {
|
|
1007
|
+
if (value != null) putString(KEY_LEFT_ADDRESS, value)
|
|
1008
|
+
else remove(KEY_LEFT_ADDRESS)
|
|
1009
|
+
}
|
|
1010
|
+
.apply()
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private var rightGlassAddress: String?
|
|
1014
|
+
get() =
|
|
1015
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1016
|
+
.getString(KEY_RIGHT_ADDRESS, null)
|
|
1017
|
+
set(value) {
|
|
1018
|
+
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
1019
|
+
.edit()
|
|
1020
|
+
.apply {
|
|
1021
|
+
if (value != null) putString(KEY_RIGHT_ADDRESS, value)
|
|
1022
|
+
else remove(KEY_RIGHT_ADDRESS)
|
|
1023
|
+
}
|
|
1024
|
+
.apply()
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Reconnection
|
|
1028
|
+
private val reconnectionManager = G2ReconnectionManager()
|
|
1029
|
+
|
|
1030
|
+
// Protocol state
|
|
1031
|
+
private val sendManager = G2SendManager()
|
|
1032
|
+
private val receiveManager = G2ReceiveManager()
|
|
1033
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
1034
|
+
private var heartbeatRunnable: Runnable? = null
|
|
1035
|
+
private var devSettingsHeartbeatRunnable: Runnable? = null
|
|
1036
|
+
private var evenHubQueueRunnable: Runnable? = null
|
|
1037
|
+
private var pendingTextMsg: ByteArray? = null
|
|
1038
|
+
private var lastEvenHubMsg: ByteArray? = null
|
|
1039
|
+
private var lastEvenHubResendsRemaining: Int = 0
|
|
1040
|
+
private val EVEN_HUB_RESEND_COUNT: Int = 1
|
|
1041
|
+
private val EVEN_HUB_QUEUE_TICK_MS = 100L
|
|
1042
|
+
private var startupPageCreated: Boolean = false
|
|
1043
|
+
private var pageCreated: Boolean = false
|
|
1044
|
+
private var pageHasTextContainer: Boolean = false
|
|
1045
|
+
private var currentTextContent: String = ""
|
|
1046
|
+
private var textContainerID: Int = 1
|
|
1047
|
+
private var imageSessionCounter: Int = 0
|
|
1048
|
+
private var heartbeatCounter: Int = 0
|
|
1049
|
+
private var authStarted: Boolean = false
|
|
1050
|
+
private var leftAuthenticated: Boolean = false
|
|
1051
|
+
private var rightAuthenticated: Boolean = false
|
|
1052
|
+
private var currentBitmapBase64: String = ""
|
|
1053
|
+
|
|
1054
|
+
// Dashboard menu state
|
|
1055
|
+
private var menuAppIdToPackageName: MutableMap<Int, String> = mutableMapOf()
|
|
1056
|
+
private var dashboardMenuItems: MutableList<MenuProto.MenuItem> = mutableListOf()
|
|
1057
|
+
private var activeMenuAppId: Int? = null
|
|
1058
|
+
private var lastClickTimestamp: Long? = null
|
|
1059
|
+
private var lastMenuSelectTimestamp: Long? = null
|
|
1060
|
+
|
|
1061
|
+
// Battery state
|
|
1062
|
+
private var _batteryLevel: Int = -1
|
|
1063
|
+
private var batteryLevel_: Int
|
|
1064
|
+
get() = _batteryLevel
|
|
1065
|
+
set(value) {
|
|
1066
|
+
val old = _batteryLevel
|
|
1067
|
+
_batteryLevel = value
|
|
1068
|
+
if (value != old && value >= 0) {
|
|
1069
|
+
DeviceStore.apply("glasses", "batteryLevel", value)
|
|
1070
|
+
Bridge.sendBatteryStatus(value, isCharging)
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
private var isCharging: Boolean = false
|
|
1074
|
+
|
|
1075
|
+
// Scanning
|
|
1076
|
+
private var scanCallback: ScanCallback? = null
|
|
1077
|
+
|
|
1078
|
+
// GATT operation queue for descriptor writes
|
|
1079
|
+
private val gattOpQueue = mutableListOf<() -> Unit>()
|
|
1080
|
+
private var gattOpInProgress = false
|
|
1081
|
+
|
|
1082
|
+
// ---------- BLE Sending ----------
|
|
1083
|
+
|
|
1084
|
+
// Min gap between BLE packets when bursting many in a row. Android serializes one in-flight
|
|
1085
|
+
// GATT op at a time even for WRITE_TYPE_NO_RESPONSE, so back-to-back writeCharacteristic() in
|
|
1086
|
+
// a tight loop drops packets silently. iOS gets this for free via CoreBluetooth; we don't.
|
|
1087
|
+
// Matches the 8 ms G1.java uses for its bitmap chunk loop (ANDROID_CHUNK_DELAY_MS).
|
|
1088
|
+
private val BLE_PACKET_GAP_MS = 8L
|
|
1089
|
+
|
|
1090
|
+
@Suppress("deprecation")
|
|
1091
|
+
private fun writeOnePacket(packet: ByteArray, left: Boolean, right: Boolean) {
|
|
1092
|
+
if (right) {
|
|
1093
|
+
rightWriteChar?.let { char ->
|
|
1094
|
+
rightGatt?.let { gatt ->
|
|
1095
|
+
char.value = packet
|
|
1096
|
+
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1097
|
+
gatt.writeCharacteristic(char)
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (left) {
|
|
1102
|
+
leftWriteChar?.let { char ->
|
|
1103
|
+
leftGatt?.let { gatt ->
|
|
1104
|
+
char.value = packet
|
|
1105
|
+
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1106
|
+
gatt.writeCharacteristic(char)
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private fun sendToGlasses(
|
|
1113
|
+
packets: List<ByteArray>,
|
|
1114
|
+
left: Boolean = false,
|
|
1115
|
+
right: Boolean = true
|
|
1116
|
+
) {
|
|
1117
|
+
if (packets.isEmpty()) return
|
|
1118
|
+
// Single-packet sends (the common case for text/settings) go straight through.
|
|
1119
|
+
if (packets.size == 1) {
|
|
1120
|
+
writeOnePacket(packets[0], left, right)
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
// Multi-packet bursts (bitmaps, large protobufs): write the first packet immediately,
|
|
1124
|
+
// then schedule the rest with BLE_PACKET_GAP_MS spacing so the Android BLE stack can
|
|
1125
|
+
// actually drain each write before the next one is queued.
|
|
1126
|
+
writeOnePacket(packets[0], left, right)
|
|
1127
|
+
for (i in 1 until packets.size) {
|
|
1128
|
+
val packet = packets[i]
|
|
1129
|
+
mainHandler.postDelayed({ writeOnePacket(packet, left, right) }, BLE_PACKET_GAP_MS * i)
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private fun sendEvenHubCommand(payload: ByteArray) {
|
|
1134
|
+
val packets =
|
|
1135
|
+
sendManager.buildPackets(
|
|
1136
|
+
serviceId = ServiceID.EVEN_HUB.value,
|
|
1137
|
+
payload = payload,
|
|
1138
|
+
reserveFlag = true
|
|
1139
|
+
)
|
|
1140
|
+
sendToGlasses(packets)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
private fun sendDevSettingsCommand(
|
|
1144
|
+
payload: ByteArray,
|
|
1145
|
+
left: Boolean = false,
|
|
1146
|
+
right: Boolean = true
|
|
1147
|
+
) {
|
|
1148
|
+
val packets =
|
|
1149
|
+
sendManager.buildPackets(
|
|
1150
|
+
serviceId = ServiceID.DEVICE_SETTINGS.value,
|
|
1151
|
+
payload = payload
|
|
1152
|
+
)
|
|
1153
|
+
sendToGlasses(packets, left = left, right = right)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private fun sendG2SettingCommand(payload: ByteArray) {
|
|
1157
|
+
val packets =
|
|
1158
|
+
sendManager.buildPackets(
|
|
1159
|
+
serviceId = ServiceID.G2_SETTING.value,
|
|
1160
|
+
payload = payload,
|
|
1161
|
+
reserveFlag = true
|
|
1162
|
+
)
|
|
1163
|
+
sendToGlasses(packets)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private fun sendOnboardingCommand(payload: ByteArray) {
|
|
1167
|
+
val packets =
|
|
1168
|
+
sendManager.buildPackets(
|
|
1169
|
+
serviceId = ServiceID.ONBOARDING.value,
|
|
1170
|
+
payload = payload,
|
|
1171
|
+
reserveFlag = true
|
|
1172
|
+
)
|
|
1173
|
+
sendToGlasses(packets)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private fun sendEvenAICommand(payload: ByteArray) {
|
|
1177
|
+
val packets =
|
|
1178
|
+
sendManager.buildPackets(
|
|
1179
|
+
serviceId = ServiceID.EVEN_AI.value,
|
|
1180
|
+
payload = payload,
|
|
1181
|
+
reserveFlag = true
|
|
1182
|
+
)
|
|
1183
|
+
sendToGlasses(packets)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private fun sendMenuCommand(payload: ByteArray) {
|
|
1187
|
+
val packets =
|
|
1188
|
+
sendManager.buildPackets(
|
|
1189
|
+
serviceId = ServiceID.MENU.value,
|
|
1190
|
+
payload = payload,
|
|
1191
|
+
reserveFlag = true
|
|
1192
|
+
)
|
|
1193
|
+
sendToGlasses(packets)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private fun sendGestureCtrlCommand(payload: ByteArray) {
|
|
1197
|
+
val packets =
|
|
1198
|
+
sendManager.buildPackets(
|
|
1199
|
+
serviceId = ServiceID.GESTURE_CTRL.value,
|
|
1200
|
+
payload = payload,
|
|
1201
|
+
reserveFlag = true
|
|
1202
|
+
)
|
|
1203
|
+
sendToGlasses(packets)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
private fun sendEvenHubCtrlCommand(payload: ByteArray) {
|
|
1207
|
+
val packets =
|
|
1208
|
+
sendManager.buildPackets(
|
|
1209
|
+
serviceId = ServiceID.EVEN_HUB_CTRL.value,
|
|
1210
|
+
payload = payload,
|
|
1211
|
+
reserveFlag = true
|
|
1212
|
+
)
|
|
1213
|
+
sendToGlasses(packets)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private fun sendDashboardCommand(payload: ByteArray) {
|
|
1217
|
+
val packets =
|
|
1218
|
+
sendManager.buildPackets(
|
|
1219
|
+
serviceId = ServiceID.DASHBOARD.value,
|
|
1220
|
+
payload = payload,
|
|
1221
|
+
reserveFlag = true
|
|
1222
|
+
)
|
|
1223
|
+
sendToGlasses(packets)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ---------- Authentication Sequence ----------
|
|
1227
|
+
|
|
1228
|
+
private fun runAuthSequence() {
|
|
1229
|
+
Bridge.log("G2: Running auth sequence")
|
|
1230
|
+
|
|
1231
|
+
// Auth to left side
|
|
1232
|
+
if (leftGatt != null && leftWriteChar != null) {
|
|
1233
|
+
val authL = DevSettingsProto.authCmd(sendManager.nextMagicRandom())
|
|
1234
|
+
sendDevSettingsCommand(authL, left = true, right = false)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Small delay then auth right + pipe role change + time sync
|
|
1238
|
+
mainHandler.postDelayed(
|
|
1239
|
+
{
|
|
1240
|
+
val authR = DevSettingsProto.authCmd(sendManager.nextMagicRandom())
|
|
1241
|
+
sendDevSettingsCommand(authR, left = false, right = true)
|
|
1242
|
+
|
|
1243
|
+
mainHandler.postDelayed(
|
|
1244
|
+
{
|
|
1245
|
+
val roleChange =
|
|
1246
|
+
DevSettingsProto.pipeRoleChange(
|
|
1247
|
+
sendManager.nextMagicRandom()
|
|
1248
|
+
)
|
|
1249
|
+
sendDevSettingsCommand(roleChange, left = false, right = true)
|
|
1250
|
+
|
|
1251
|
+
mainHandler.postDelayed(
|
|
1252
|
+
{
|
|
1253
|
+
val timeSync =
|
|
1254
|
+
DevSettingsProto.timeSync(
|
|
1255
|
+
sendManager.nextMagicRandom()
|
|
1256
|
+
)
|
|
1257
|
+
sendDevSettingsCommand(timeSync)
|
|
1258
|
+
|
|
1259
|
+
// Skip onboarding on connect
|
|
1260
|
+
mainHandler.postDelayed(
|
|
1261
|
+
{
|
|
1262
|
+
val onboarding =
|
|
1263
|
+
OnboardingProto.skipOnboarding(
|
|
1264
|
+
sendManager
|
|
1265
|
+
.nextMagicRandom()
|
|
1266
|
+
)
|
|
1267
|
+
sendOnboardingCommand(onboarding)
|
|
1268
|
+
Bridge.log(
|
|
1269
|
+
"G2: Sent onboarding skip (FINISH)"
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
// Disable "Hey Even" wakeword on connect
|
|
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
|
|
1629
|
+
)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private fun runDashboardSequence() {
|
|
1633
|
+
Bridge.log("G2: Running dashboard sequence")
|
|
1634
|
+
|
|
1635
|
+
// Send the shutdown command to the glasses
|
|
1636
|
+
val msg = EvenHubProto.shutdownMessage()
|
|
1637
|
+
sendEvenHubCommand(msg)
|
|
1638
|
+
pageCreated = false
|
|
1639
|
+
currentTextContent = ""
|
|
1640
|
+
|
|
1641
|
+
mainHandler.postDelayed(
|
|
1642
|
+
{
|
|
1643
|
+
// 1. gesture_ctrl init (field1=0, field2=magicRandom)
|
|
1644
|
+
val gestureInitW = ProtobufWriter()
|
|
1645
|
+
gestureInitW.writeInt32Field(1, 0)
|
|
1646
|
+
gestureInitW.writeInt32Field(2, sendManager.nextMagicRandom())
|
|
1647
|
+
sendGestureCtrlCommand(gestureInitW.toByteArray())
|
|
1648
|
+
|
|
1649
|
+
// 6. Dashboard init (0x01) — display settings
|
|
1650
|
+
val dashDisplayW = ProtobufWriter()
|
|
1651
|
+
dashDisplayW.writeInt32Field(1, 4) // displayMode
|
|
1652
|
+
dashDisplayW.writeInt32Field(2, 3) // statusDisplayCount
|
|
1653
|
+
dashDisplayW.writeMessageField(3, byteArrayOf(1, 2, 3)) // statusDisplayOrder
|
|
1654
|
+
dashDisplayW.writeInt32Field(4, 4) // widgetDisplayCount
|
|
1655
|
+
dashDisplayW.writeMessageField(5, byteArrayOf(1, 3, 2, 2)) // widgetDisplayOrder
|
|
1656
|
+
dashDisplayW.writeInt32Field(6, 1) // halfDayFormat
|
|
1657
|
+
dashDisplayW.writeInt32Field(7, 1) // temperatureUnit
|
|
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
|
+
)
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// ---------- Heartbeats ----------
|
|
1675
|
+
|
|
1676
|
+
private fun startHeartbeats() {
|
|
1677
|
+
// EvenHub heartbeat every 5 seconds
|
|
1678
|
+
stopHeartbeats()
|
|
1679
|
+
|
|
1680
|
+
val hbRunnable =
|
|
1681
|
+
object : Runnable {
|
|
1682
|
+
override fun run() {
|
|
1683
|
+
sendEvenHubHeartbeat()
|
|
1684
|
+
mainHandler.postDelayed(this, 5000)
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
heartbeatRunnable = hbRunnable
|
|
1688
|
+
mainHandler.postDelayed(hbRunnable, 5000)
|
|
1689
|
+
|
|
1690
|
+
// DevSettings heartbeat every 5 seconds
|
|
1691
|
+
val dsRunnable =
|
|
1692
|
+
object : Runnable {
|
|
1693
|
+
override fun run() {
|
|
1694
|
+
sendDevSettingsHeartbeat()
|
|
1695
|
+
mainHandler.postDelayed(this, 5000)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
devSettingsHeartbeatRunnable = dsRunnable
|
|
1699
|
+
mainHandler.postDelayed(dsRunnable, 5000)
|
|
1700
|
+
|
|
1701
|
+
// EvenHub text command queue: drain the most recent pending updateText every 100ms
|
|
1702
|
+
val queueRunnable =
|
|
1703
|
+
object : Runnable {
|
|
1704
|
+
override fun run() {
|
|
1705
|
+
drainEvenHubQueue()
|
|
1706
|
+
mainHandler.postDelayed(this, EVEN_HUB_QUEUE_TICK_MS)
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
evenHubQueueRunnable = queueRunnable
|
|
1710
|
+
mainHandler.postDelayed(queueRunnable, EVEN_HUB_QUEUE_TICK_MS)
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
private fun stopHeartbeats() {
|
|
1714
|
+
heartbeatRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1715
|
+
heartbeatRunnable = null
|
|
1716
|
+
devSettingsHeartbeatRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1717
|
+
devSettingsHeartbeatRunnable = null
|
|
1718
|
+
evenHubQueueRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
1719
|
+
evenHubQueueRunnable = null
|
|
1720
|
+
pendingTextMsg = null
|
|
1721
|
+
lastEvenHubMsg = null
|
|
1722
|
+
lastEvenHubResendsRemaining = 0
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
private fun sendEvenHubHeartbeat() {
|
|
1726
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
1727
|
+
if (!isFullyBooted) {
|
|
1728
|
+
return
|
|
1729
|
+
}
|
|
1730
|
+
val msg = EvenHubProto.heartbeatMessage()
|
|
1731
|
+
sendEvenHubCommand(msg)
|
|
1732
|
+
|
|
1733
|
+
// Poll battery every 10 heartbeats (~50 seconds)
|
|
1734
|
+
heartbeatCounter++
|
|
1735
|
+
if (heartbeatCounter % 10 == 0) {
|
|
1736
|
+
requestDeviceInfo()
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private fun sendDevSettingsHeartbeat() {
|
|
1741
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
1742
|
+
if (!isFullyBooted) {
|
|
1743
|
+
return
|
|
1744
|
+
}
|
|
1745
|
+
val msg = DevSettingsProto.baseHeartbeat(sendManager.nextMagicRandom())
|
|
1746
|
+
sendDevSettingsCommand(msg)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private fun requestDeviceInfo() {
|
|
1750
|
+
val msg = G2SettingProto.requestInfo(sendManager.nextMagicRandom())
|
|
1751
|
+
sendG2SettingCommand(msg)
|
|
1752
|
+
Bridge.log("G2: Requested device info (battery/version)")
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
private fun sendMenuApps() {
|
|
1756
|
+
val menuItems =
|
|
1757
|
+
DeviceStore.get("bluetooth", "menu_apps") as? List<Map<String, Any>> ?: emptyList()
|
|
1758
|
+
if (menuItems.isNotEmpty()) {
|
|
1759
|
+
setDashboardMenu(menuItems)
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// ---------- SGCManager: Display Control ----------
|
|
1764
|
+
|
|
1765
|
+
override fun sendTextWall(text: String) {
|
|
1766
|
+
// Bridge.log("G2: sendTextWall(${text.take(10)}...)")
|
|
1767
|
+
|
|
1768
|
+
if (text.isEmpty()) {
|
|
1769
|
+
clearDisplay()
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (!pageCreated || !pageHasTextContainer) {
|
|
1774
|
+
createPageWithText(text)
|
|
1775
|
+
} else {
|
|
1776
|
+
updateText(text)
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
1781
|
+
val combined = "$top\n$bottom"
|
|
1782
|
+
sendTextWall(combined)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
override fun clearDisplay() {
|
|
1786
|
+
Bridge.log("G2: clearDisplay()")
|
|
1787
|
+
if (pageCreated) {
|
|
1788
|
+
sendTextWall(" ")
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
override fun displayBitmap(base64ImageData: String): Boolean {
|
|
1793
|
+
currentBitmapBase64 = base64ImageData
|
|
1794
|
+
currentTextContent = ""
|
|
1795
|
+
return displayBitmapQuad(base64ImageData)
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
private fun displayBitmapQuad(base64ImageData: String): Boolean {
|
|
1799
|
+
val rawData =
|
|
1800
|
+
Base64.decode(base64ImageData, Base64.DEFAULT)
|
|
1801
|
+
?: run {
|
|
1802
|
+
Bridge.log("G2: displayBitmapQuad() - failed to decode base64")
|
|
1803
|
+
return false
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
val tiles =
|
|
1807
|
+
renderAndSliceTo4Tiles(rawData)
|
|
1808
|
+
?: run {
|
|
1809
|
+
Bridge.log(
|
|
1810
|
+
"G2: displayBitmapQuad() - failed to slice image into tiles"
|
|
1811
|
+
)
|
|
1812
|
+
return false
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// 2x2 grid of 200x100 tiles covering 400x200 (matches G2.swift:1729-1745)
|
|
1816
|
+
val container1 =
|
|
1817
|
+
EvenHubProto.imageContainerProperty(
|
|
1818
|
+
x = 0,
|
|
1819
|
+
y = 0,
|
|
1820
|
+
width = 200,
|
|
1821
|
+
height = 100,
|
|
1822
|
+
containerID = 10,
|
|
1823
|
+
containerName = "img-10"
|
|
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
|
+
)
|
|
1893
|
+
|
|
1894
|
+
return true
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
/** Send the next tile's BMP in series; recurse to the next tile after this one finishes. */
|
|
1898
|
+
private fun sendImageDataChained(
|
|
1899
|
+
tiles: List<Triple<Int, String, ByteArray>>,
|
|
1900
|
+
index: Int
|
|
1901
|
+
) {
|
|
1902
|
+
if (index >= tiles.size) return
|
|
1903
|
+
val (containerID, containerName, bmpData) = tiles[index]
|
|
1904
|
+
sendImageData(containerID, containerName, bmpData) {
|
|
1905
|
+
sendImageDataChained(tiles, index + 1)
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
private fun sendImageData(
|
|
1910
|
+
containerID: Int,
|
|
1911
|
+
containerName: String,
|
|
1912
|
+
bmpData: ByteArray,
|
|
1913
|
+
onComplete: (() -> Unit)? = null
|
|
1914
|
+
) {
|
|
1915
|
+
val fragmentSize = 4096
|
|
1916
|
+
imageSessionCounter++
|
|
1917
|
+
val sessionId = imageSessionCounter
|
|
1918
|
+
val totalSize = bmpData.size
|
|
1919
|
+
var fragmentIndex = 0
|
|
1920
|
+
var offset = 0
|
|
1921
|
+
|
|
1922
|
+
fun sendNextFragment() {
|
|
1923
|
+
if (offset >= bmpData.size) {
|
|
1924
|
+
Bridge.log(
|
|
1925
|
+
"G2: sendImageData($containerName) - $fragmentIndex fragments, ${bmpData.size} bytes"
|
|
1926
|
+
)
|
|
1927
|
+
onComplete?.invoke()
|
|
1928
|
+
return
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
val end = minOf(offset + fragmentSize, bmpData.size)
|
|
1932
|
+
val fragment = bmpData.copyOfRange(offset, end)
|
|
1933
|
+
|
|
1934
|
+
val msg =
|
|
1935
|
+
EvenHubProto.updateImageRawDataMessage(
|
|
1936
|
+
containerID = containerID,
|
|
1937
|
+
containerName = containerName,
|
|
1938
|
+
mapSessionId = sessionId,
|
|
1939
|
+
mapTotalSize = totalSize,
|
|
1940
|
+
compressMode = 0,
|
|
1941
|
+
mapFragmentIndex = fragmentIndex,
|
|
1942
|
+
mapFragmentPacketSize = fragment.size,
|
|
1943
|
+
mapRawData = fragment
|
|
1944
|
+
)
|
|
1945
|
+
sendEvenHubCommand(msg)
|
|
1946
|
+
Bridge.log("G2: sendImageData($containerName) - sent fragment $fragmentIndex")
|
|
1947
|
+
|
|
1948
|
+
fragmentIndex++
|
|
1949
|
+
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
|
+
}
|
|
1962
|
+
}
|
|
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
|
+
}
|
|
2051
|
+
|
|
2052
|
+
override fun showDashboard() {
|
|
2053
|
+
// G2 doesn't have a native dashboard concept via EvenHub
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
override fun setDashboardPosition(height: Int, depth: Int) {
|
|
2057
|
+
Bridge.log("G2: setDashboardPosition(height=$height, depth=$depth)")
|
|
2058
|
+
setDashboardHeightOnly(height)
|
|
2059
|
+
setDashboardDepthOnly(depth)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
override fun setDashboardHeightOnly(height: Int) {
|
|
2063
|
+
val clamped = height.coerceIn(0, 12)
|
|
2064
|
+
Bridge.log("G2: setDashboardHeightOnly($clamped)")
|
|
2065
|
+
val msg = G2SettingProto.setScreenHeight(sendManager.nextMagicRandom(), clamped)
|
|
2066
|
+
sendG2SettingCommand(msg)
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
override fun setDashboardDepthOnly(depth: Int) {
|
|
2070
|
+
val clamped = depth.coerceIn(0, 2)
|
|
2071
|
+
Bridge.log("G2: setDashboardDepthOnly($clamped)")
|
|
2072
|
+
val msg = G2SettingProto.setScreenDepth(sendManager.nextMagicRandom(), clamped)
|
|
2073
|
+
sendG2SettingCommand(msg)
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
override fun setDashboardMenu(items: List<Map<String, Any>>) {
|
|
2077
|
+
Bridge.log("G2: setDashboardMenu -- items: $items")
|
|
2078
|
+
val menuItems =
|
|
2079
|
+
items.mapNotNull { dict ->
|
|
2080
|
+
val name = dict["name"] as? String ?: return@mapNotNull null
|
|
2081
|
+
val packageName = dict["packageName"] as? String ?: return@mapNotNull null
|
|
2082
|
+
val running = dict["running"] as? Boolean ?: false
|
|
2083
|
+
MenuProto.MenuItem(packageName, name, running)
|
|
2084
|
+
}
|
|
2085
|
+
dashboardMenuItems.clear()
|
|
2086
|
+
dashboardMenuItems.addAll(menuItems)
|
|
2087
|
+
Bridge.log("G2: setDashboardMenu -- sending ${menuItems.size} items")
|
|
2088
|
+
val (msg, appIdMap) = MenuProto.sendMenuInfo(sendManager.nextMagicRandom(), menuItems)
|
|
2089
|
+
menuAppIdToPackageName = appIdMap.toMutableMap()
|
|
2090
|
+
activeMenuAppId = appIdMap.keys.sorted().firstOrNull()
|
|
2091
|
+
sendMenuCommand(msg)
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
2095
|
+
Bridge.log("G2: setBrightness($level, auto=$autoMode)")
|
|
2096
|
+
val msg =
|
|
2097
|
+
G2SettingProto.setBrightness(
|
|
2098
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2099
|
+
level = level,
|
|
2100
|
+
autoAdjust = autoMode
|
|
2101
|
+
)
|
|
2102
|
+
sendG2SettingCommand(msg)
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// ---------- Private Display Helpers ----------
|
|
2106
|
+
|
|
2107
|
+
private fun createPageWithText(text: String) {
|
|
2108
|
+
val tc =
|
|
2109
|
+
EvenHubProto.textContainerProperty(
|
|
2110
|
+
x = 0,
|
|
2111
|
+
y = 0,
|
|
2112
|
+
width = 576,
|
|
2113
|
+
height = 288,
|
|
2114
|
+
borderWidth = 0,
|
|
2115
|
+
borderColor = 0,
|
|
2116
|
+
borderRadius = 0,
|
|
2117
|
+
paddingLength = 4,
|
|
2118
|
+
containerID = textContainerID,
|
|
2119
|
+
containerName = "text-main",
|
|
2120
|
+
isEventCapture = true,
|
|
2121
|
+
content = text
|
|
2122
|
+
)
|
|
2123
|
+
|
|
2124
|
+
val msg: ByteArray
|
|
2125
|
+
if (!startupPageCreated) {
|
|
2126
|
+
Bridge.log("G2: createPageWithText - using createPageMessage (first time)")
|
|
2127
|
+
msg =
|
|
2128
|
+
EvenHubProto.createPageMessage(
|
|
2129
|
+
textContainers = listOf(tc),
|
|
2130
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2131
|
+
appId = activeMenuAppId
|
|
2132
|
+
)
|
|
2133
|
+
startupPageCreated = true
|
|
2134
|
+
} else {
|
|
2135
|
+
Bridge.log("G2: createPageWithText - using rebuildPageMessage")
|
|
2136
|
+
msg =
|
|
2137
|
+
EvenHubProto.rebuildPageMessage(
|
|
2138
|
+
textContainers = listOf(tc),
|
|
2139
|
+
magicRandom = sendManager.nextMagicRandom(),
|
|
2140
|
+
appId = activeMenuAppId
|
|
2141
|
+
)
|
|
2142
|
+
}
|
|
2143
|
+
sendEvenHubCommand(msg)
|
|
2144
|
+
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
|
+
}
|
|
2162
|
+
|
|
2163
|
+
@Synchronized
|
|
2164
|
+
private fun queueEvenHubCommand(payload: ByteArray) {
|
|
2165
|
+
pendingTextMsg = payload
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
@Synchronized
|
|
2169
|
+
private fun drainEvenHubQueue() {
|
|
2170
|
+
val msg = pendingTextMsg
|
|
2171
|
+
pendingTextMsg = null
|
|
2172
|
+
val toSend: ByteArray? = if (msg != null) {
|
|
2173
|
+
lastEvenHubMsg = msg
|
|
2174
|
+
lastEvenHubResendsRemaining = EVEN_HUB_RESEND_COUNT
|
|
2175
|
+
msg
|
|
2176
|
+
} else if (lastEvenHubResendsRemaining > 0 && lastEvenHubMsg != null) {
|
|
2177
|
+
lastEvenHubResendsRemaining -= 1
|
|
2178
|
+
lastEvenHubMsg
|
|
2179
|
+
} else {
|
|
2180
|
+
null
|
|
2181
|
+
}
|
|
2182
|
+
toSend?.let { sendEvenHubCommand(it) }
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// ---------- Bitmap Conversion ----------
|
|
2186
|
+
|
|
2187
|
+
private fun convertToG2Bmp(
|
|
2188
|
+
data: ByteArray,
|
|
2189
|
+
containerWidth: Int,
|
|
2190
|
+
containerHeight: Int
|
|
2191
|
+
): ByteArray? {
|
|
2192
|
+
val srcBitmap =
|
|
2193
|
+
BitmapFactory.decodeByteArray(data, 0, data.size)
|
|
2194
|
+
?: run {
|
|
2195
|
+
Bridge.log("G2: convertToG2Bmp - could not decode image")
|
|
2196
|
+
return null
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
val srcWidth = srcBitmap.width
|
|
2200
|
+
val srcHeight = srcBitmap.height
|
|
2201
|
+
|
|
2202
|
+
// Scale to fit within container (maintain aspect ratio)
|
|
2203
|
+
val scale =
|
|
2204
|
+
minOf(containerWidth.toDouble() / srcWidth, containerHeight.toDouble() / srcHeight)
|
|
2205
|
+
val scaledW = maxOf(1, (srcWidth * scale).toInt())
|
|
2206
|
+
val scaledH = maxOf(1, (srcHeight * scale).toInt())
|
|
2207
|
+
val offsetX = (containerWidth - scaledW) / 2
|
|
2208
|
+
val offsetY = (containerHeight - scaledH) / 2
|
|
2209
|
+
|
|
2210
|
+
Bridge.log(
|
|
2211
|
+
"G2: convertToG2Bmp - input ${srcWidth}x${srcHeight} → scaled ${scaledW}x${scaledH} in ${containerWidth}x${containerHeight}"
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
// Render to container-sized bitmap with black background
|
|
2215
|
+
val destBitmap =
|
|
2216
|
+
Bitmap.createBitmap(containerWidth, containerHeight, Bitmap.Config.ARGB_8888)
|
|
2217
|
+
val canvas = Canvas(destBitmap)
|
|
2218
|
+
canvas.drawColor(Color.BLACK)
|
|
2219
|
+
|
|
2220
|
+
val srcRect = Rect(0, 0, srcWidth, srcHeight)
|
|
2221
|
+
val dstRect = Rect(offsetX, offsetY, offsetX + scaledW, offsetY + scaledH)
|
|
2222
|
+
val paint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
2223
|
+
canvas.drawBitmap(srcBitmap, srcRect, dstRect, paint)
|
|
2224
|
+
|
|
2225
|
+
// Extract grayscale pixels
|
|
2226
|
+
val grayscalePixels = ByteArray(containerWidth * containerHeight)
|
|
2227
|
+
for (y in 0 until containerHeight) {
|
|
2228
|
+
for (x in 0 until containerWidth) {
|
|
2229
|
+
val pixel = destBitmap.getPixel(x, y)
|
|
2230
|
+
val r = Color.red(pixel)
|
|
2231
|
+
val g = Color.green(pixel)
|
|
2232
|
+
val b = Color.blue(pixel)
|
|
2233
|
+
val gray = (r * 299 + g * 587 + b * 114) / 1000
|
|
2234
|
+
grayscalePixels[y * containerWidth + x] = gray.toByte()
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
srcBitmap.recycle()
|
|
2239
|
+
destBitmap.recycle()
|
|
2240
|
+
|
|
2241
|
+
return build4BitBmp(grayscalePixels, containerWidth, containerHeight)
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
private fun build4BitBmp(grayscalePixels: ByteArray, width: Int, height: Int): ByteArray? {
|
|
2245
|
+
// 4-bit: 2 pixels per byte, rows padded to 4-byte boundary
|
|
2246
|
+
val bytesPerRow4bit = (width + 1) / 2
|
|
2247
|
+
val paddedRowSize = (bytesPerRow4bit + 3) and 3.inv()
|
|
2248
|
+
val pixelDataSize = paddedRowSize * height
|
|
2249
|
+
|
|
2250
|
+
// BMP file header (14) + DIB header (40) + color table (16 * 4 = 64)
|
|
2251
|
+
val headerSize = 14 + 40 + 64
|
|
2252
|
+
val fileSize = headerSize + pixelDataSize
|
|
2253
|
+
|
|
2254
|
+
val bmp = ByteArrayOutputStream(fileSize)
|
|
2255
|
+
|
|
2256
|
+
// --- BMP File Header (14 bytes) ---
|
|
2257
|
+
bmp.write(0x42)
|
|
2258
|
+
bmp.write(0x4D) // "BM"
|
|
2259
|
+
writeLittleEndianInt(bmp, fileSize)
|
|
2260
|
+
writeLittleEndianShort(bmp, 0) // Reserved1
|
|
2261
|
+
writeLittleEndianShort(bmp, 0) // Reserved2
|
|
2262
|
+
writeLittleEndianInt(bmp, headerSize) // Pixel data offset
|
|
2263
|
+
|
|
2264
|
+
// --- DIB Header (BITMAPINFOHEADER, 40 bytes) ---
|
|
2265
|
+
writeLittleEndianInt(bmp, 40) // DIB header size
|
|
2266
|
+
writeLittleEndianInt(bmp, width) // Width
|
|
2267
|
+
writeLittleEndianInt(bmp, height) // Height (positive = bottom-up)
|
|
2268
|
+
writeLittleEndianShort(bmp, 1) // Color planes
|
|
2269
|
+
writeLittleEndianShort(bmp, 4) // Bits per pixel (4-bit)
|
|
2270
|
+
writeLittleEndianInt(bmp, 0) // Compression (none)
|
|
2271
|
+
writeLittleEndianInt(bmp, pixelDataSize) // Image size
|
|
2272
|
+
writeLittleEndianInt(bmp, 2835) // X pixels/meter (~72 DPI)
|
|
2273
|
+
writeLittleEndianInt(bmp, 2835) // Y pixels/meter
|
|
2274
|
+
writeLittleEndianInt(bmp, 16) // Colors used
|
|
2275
|
+
writeLittleEndianInt(bmp, 0) // Important colors (0 = all)
|
|
2276
|
+
|
|
2277
|
+
// --- Color Table (16 entries, 4 bytes each: B, G, R, 0) ---
|
|
2278
|
+
for (i in 0 until 16) {
|
|
2279
|
+
val v = i * 17 // 0, 17, 34, ... 255
|
|
2280
|
+
bmp.write(v)
|
|
2281
|
+
bmp.write(v)
|
|
2282
|
+
bmp.write(v)
|
|
2283
|
+
bmp.write(0) // B, G, R, Reserved
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// --- Pixel Data (bottom-up rows, 4-bit packed) ---
|
|
2287
|
+
for (row in 0 until height) {
|
|
2288
|
+
// BMP is bottom-up: row 0 in BMP = last row of image
|
|
2289
|
+
val srcRow = height - 1 - row
|
|
2290
|
+
val srcOffset = srcRow * width
|
|
2291
|
+
val rowBuf = ByteArray(paddedRowSize)
|
|
2292
|
+
|
|
2293
|
+
for (col in 0 until width) {
|
|
2294
|
+
val pixelIndex = srcOffset + col
|
|
2295
|
+
if (pixelIndex >= grayscalePixels.size) continue
|
|
2296
|
+
|
|
2297
|
+
val gray8 = grayscalePixels[pixelIndex].toInt() and 0xFF
|
|
2298
|
+
val index4 = gray8 shr 4
|
|
2299
|
+
|
|
2300
|
+
val bytePos = col / 2
|
|
2301
|
+
if (col % 2 == 0) {
|
|
2302
|
+
rowBuf[bytePos] = (index4 shl 4).toByte()
|
|
2303
|
+
} else {
|
|
2304
|
+
rowBuf[bytePos] = (rowBuf[bytePos].toInt() or index4).toByte()
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
bmp.write(rowBuf)
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
Bridge.log(
|
|
2311
|
+
"G2: build4BitBmp - ${bmp.size()} bytes (header=$headerSize, pixels=$pixelDataSize, rows=${paddedRowSize}x$height)"
|
|
2312
|
+
)
|
|
2313
|
+
return bmp.toByteArray()
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
private fun writeLittleEndianInt(out: ByteArrayOutputStream, value: Int) {
|
|
2317
|
+
out.write(value and 0xFF)
|
|
2318
|
+
out.write((value shr 8) and 0xFF)
|
|
2319
|
+
out.write((value shr 16) and 0xFF)
|
|
2320
|
+
out.write((value shr 24) and 0xFF)
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
private fun writeLittleEndianShort(out: ByteArrayOutputStream, value: Int) {
|
|
2324
|
+
out.write(value and 0xFF)
|
|
2325
|
+
out.write((value shr 8) and 0xFF)
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// ---------- SGCManager: Audio Control ----------
|
|
2329
|
+
|
|
2330
|
+
override fun setMicEnabled(enabled: Boolean) {
|
|
2331
|
+
Bridge.log("G2: setMicEnabled($enabled)")
|
|
2332
|
+
val currentEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
|
|
2333
|
+
|
|
2334
|
+
// if already enabled, set to disabled, then send enabled after 500ms:
|
|
2335
|
+
if (currentEnabled && enabled) {
|
|
2336
|
+
DeviceStore.apply("glasses", "micEnabled", true)
|
|
2337
|
+
val msg = EvenHubProto.audioControlMessage(false)
|
|
2338
|
+
sendEvenHubCommand(msg)
|
|
2339
|
+
mainHandler.postDelayed({
|
|
2340
|
+
val msg = EvenHubProto.audioControlMessage(true)
|
|
2341
|
+
sendEvenHubCommand(msg)
|
|
2342
|
+
}, 500)
|
|
2343
|
+
return
|
|
2344
|
+
}
|
|
2345
|
+
DeviceStore.apply("glasses", "micEnabled", enabled)
|
|
2346
|
+
val msg = EvenHubProto.audioControlMessage(enabled)
|
|
2347
|
+
sendEvenHubCommand(msg)
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
override fun sortMicRanking(list: MutableList<String>): MutableList<String> {
|
|
2351
|
+
return list
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// Camera & Media - G2 has no camera
|
|
2355
|
+
override fun requestPhoto(
|
|
2356
|
+
requestId: String,
|
|
2357
|
+
appId: String,
|
|
2358
|
+
size: String,
|
|
2359
|
+
webhookUrl: String?,
|
|
2360
|
+
authToken: String?,
|
|
2361
|
+
compress: String?,
|
|
2362
|
+
flash: Boolean,
|
|
2363
|
+
sound: Boolean
|
|
2364
|
+
) {
|
|
2365
|
+
Bridge.log("G2: requestPhoto - not supported (no camera)")
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
override fun startStream(message: MutableMap<String, Any>) {
|
|
2369
|
+
Bridge.log("G2: startStream - not supported")
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
override fun stopStream() {
|
|
2373
|
+
Bridge.log("G2: stopStream - not supported")
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
override fun sendStreamKeepAlive(message: MutableMap<String, Any>) {
|
|
2377
|
+
Bridge.log("G2: sendStreamKeepAlive - not supported")
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
override fun startVideoRecording(
|
|
2381
|
+
requestId: String,
|
|
2382
|
+
save: Boolean,
|
|
2383
|
+
flash: Boolean,
|
|
2384
|
+
sound: Boolean
|
|
2385
|
+
) {
|
|
2386
|
+
Bridge.log("G2: startVideoRecording - not supported")
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
override fun stopVideoRecording(requestId: String) {
|
|
2390
|
+
Bridge.log("G2: stopVideoRecording - not supported")
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Button Settings
|
|
2394
|
+
override fun sendButtonPhotoSettings() {
|
|
2395
|
+
Bridge.log("G2: sendButtonPhotoSettings")
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
override fun sendButtonVideoRecordingSettings() {
|
|
2399
|
+
Bridge.log("G2: sendButtonVideoRecordingSettings")
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
override fun sendButtonMaxRecordingTime() {
|
|
2403
|
+
Bridge.log("G2: sendButtonMaxRecordingTime")
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
override fun sendButtonCameraLedSetting() {
|
|
2407
|
+
Bridge.log("G2: sendButtonCameraLedSetting")
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
override fun sendCameraFovSetting() {
|
|
2411
|
+
Bridge.log("G2: sendCameraFovSetting")
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
override fun findCompatibleDevices() {
|
|
2415
|
+
Bridge.log("G2: findCompatibleDevices()")
|
|
2416
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
2417
|
+
startScan()
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
override fun connectById(id: String) {
|
|
2421
|
+
Bridge.log("G2: connectById($id)")
|
|
2422
|
+
DEVICE_SEARCH_ID = id
|
|
2423
|
+
startScan()
|
|
2424
|
+
startPairingTimeout()
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
private fun startPairingTimeout() {
|
|
2428
|
+
pairingTimeoutRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
2429
|
+
val work = Runnable {
|
|
2430
|
+
if (leftGatt != null && rightGatt == null) {
|
|
2431
|
+
Bridge.log("G2: pairing timeout — found LEFT but not RIGHT")
|
|
2432
|
+
Bridge.sendPairFailureEvent("errors:pairNeedDisconnect")
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
pairingTimeoutRunnable = work
|
|
2436
|
+
mainHandler.postDelayed(work, 10_000)
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
private fun cancelPairingTimeout() {
|
|
2440
|
+
pairingTimeoutRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
2441
|
+
pairingTimeoutRunnable = null
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
override fun disconnect() {
|
|
2445
|
+
Bridge.log("G2: disconnect()")
|
|
2446
|
+
isDisconnecting = true
|
|
2447
|
+
cancelPairingTimeout()
|
|
2448
|
+
stopScan()
|
|
2449
|
+
stopHeartbeats()
|
|
2450
|
+
reconnectionManager.stop()
|
|
2451
|
+
|
|
2452
|
+
leftGatt?.disconnect()
|
|
2453
|
+
leftGatt?.close()
|
|
2454
|
+
rightGatt?.disconnect()
|
|
2455
|
+
rightGatt?.close()
|
|
2456
|
+
|
|
2457
|
+
leftInitialized = false
|
|
2458
|
+
rightInitialized = false
|
|
2459
|
+
authStarted = false
|
|
2460
|
+
leftAuthenticated = false
|
|
2461
|
+
rightAuthenticated = false
|
|
2462
|
+
startupPageCreated = false
|
|
2463
|
+
pageCreated = false
|
|
2464
|
+
pageHasTextContainer = false
|
|
2465
|
+
heartbeatCounter = 0
|
|
2466
|
+
currentBitmapBase64 = ""
|
|
2467
|
+
menuAppIdToPackageName.clear()
|
|
2468
|
+
activeMenuAppId = null
|
|
2469
|
+
lastClickTimestamp = null
|
|
2470
|
+
lastMenuSelectTimestamp = null
|
|
2471
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
2472
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
override fun forget() {
|
|
2476
|
+
Bridge.log("G2: forget()")
|
|
2477
|
+
stopHeartbeats()
|
|
2478
|
+
reconnectionManager.stop()
|
|
2479
|
+
disconnect()
|
|
2480
|
+
leftGlassAddress = null
|
|
2481
|
+
rightGlassAddress = null
|
|
2482
|
+
leftGatt = null
|
|
2483
|
+
rightGatt = null
|
|
2484
|
+
leftWriteChar = null
|
|
2485
|
+
rightWriteChar = null
|
|
2486
|
+
leftNotifyChar = null
|
|
2487
|
+
rightNotifyChar = null
|
|
2488
|
+
leftAudioChar = null
|
|
2489
|
+
rightAudioChar = null
|
|
2490
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
2491
|
+
dashboardMenuItems.clear()
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
override fun cleanup() {
|
|
2495
|
+
disconnect()
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
override fun getConnectedBluetoothName(): String {
|
|
2499
|
+
return rightGatt?.device?.name ?: leftGatt?.device?.name ?: ""
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
override fun ping() {
|
|
2503
|
+
sendEvenHubHeartbeat()
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
override fun dbg1() {
|
|
2507
|
+
connectController()
|
|
2508
|
+
}
|
|
2509
|
+
override fun dbg2() {
|
|
2510
|
+
disconnectController()
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
fun reconnectController() {
|
|
2514
|
+
val mac = DeviceStore.get("glasses", "controllerMacAddress") as? String
|
|
2515
|
+
if (mac.isNullOrEmpty()) {
|
|
2516
|
+
Bridge.log("G2: reconnectController - no MAC address found")
|
|
2517
|
+
return
|
|
2518
|
+
}
|
|
2519
|
+
connectController()
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
override fun connectController() {
|
|
2523
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
2524
|
+
if (!isFullyBooted) {
|
|
2525
|
+
Bridge.log("G2: connectController - g2 not fully booted, ignoring")
|
|
2526
|
+
return
|
|
2527
|
+
}
|
|
2528
|
+
val mac = DeviceStore.get("glasses", "controllerMacAddress") as? String
|
|
2529
|
+
if (mac.isNullOrEmpty()) {
|
|
2530
|
+
Bridge.log("G2: connectController - no MAC address found")
|
|
2531
|
+
return
|
|
2532
|
+
}
|
|
2533
|
+
val hexParts = mac.split(":").mapNotNull { it.toIntOrNull(16)?.toByte() }
|
|
2534
|
+
if (hexParts.size != 6) {
|
|
2535
|
+
Bridge.log("G2: connectController - invalid MAC format: $mac")
|
|
2536
|
+
return
|
|
2537
|
+
}
|
|
2538
|
+
Bridge.log("G2: connectController() - MAC: $mac")
|
|
2539
|
+
val macData = hexParts.toByteArray()
|
|
2540
|
+
val msg = DevSettingsProto.ringConnectInfo(sendManager.nextMagicRandom(), true, macData)
|
|
2541
|
+
sendDevSettingsCommand(msg)
|
|
2542
|
+
Bridge.log("G2: Sent RING_CONNECT_INFO for MAC $mac")
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
override fun disconnectController() {
|
|
2546
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
2547
|
+
if (!isFullyBooted) {
|
|
2548
|
+
Bridge.log("G2: disconnectController - g2 not fully booted, ignoring")
|
|
2549
|
+
return
|
|
2550
|
+
}
|
|
2551
|
+
val mac = DeviceStore.get("glasses", "controllerMacAddress") as? String
|
|
2552
|
+
if (mac.isNullOrEmpty()) {
|
|
2553
|
+
Bridge.log("G2: disconnectController - no MAC address found")
|
|
2554
|
+
return
|
|
2555
|
+
}
|
|
2556
|
+
val hexParts = mac.split(":").mapNotNull { it.toIntOrNull(16)?.toByte() }
|
|
2557
|
+
if (hexParts.size != 6) {
|
|
2558
|
+
Bridge.log("G2: disconnectController - invalid MAC format: $mac")
|
|
2559
|
+
return
|
|
2560
|
+
}
|
|
2561
|
+
val macData = hexParts.toByteArray()
|
|
2562
|
+
val msg = DevSettingsProto.ringConnectInfo(sendManager.nextMagicRandom(), false, macData)
|
|
2563
|
+
sendDevSettingsCommand(msg)
|
|
2564
|
+
// DeviceStore.apply("glasses", "controllerMacAddress", "")
|
|
2565
|
+
DeviceStore.apply("glasses", "controllerConnected", false)
|
|
2566
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", false)
|
|
2567
|
+
Bridge.log("G2: Sent RING_DISCONNECT_INFO for MAC $mac")
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// ---------- SGCManager: Device Control ----------
|
|
2571
|
+
|
|
2572
|
+
override fun setHeadUpAngle(angle: Int) {
|
|
2573
|
+
val clamped = angle.coerceIn(0, 60)
|
|
2574
|
+
Bridge.log("G2: setHeadUpAngle($clamped)")
|
|
2575
|
+
|
|
2576
|
+
// Enable head-up display
|
|
2577
|
+
val enableMsg = G2SettingProto.setHeadUpSwitch(sendManager.nextMagicRandom(), true)
|
|
2578
|
+
sendG2SettingCommand(enableMsg)
|
|
2579
|
+
|
|
2580
|
+
// Set the angle
|
|
2581
|
+
val angleMsg = G2SettingProto.setHeadUpAngle(sendManager.nextMagicRandom(), clamped)
|
|
2582
|
+
sendG2SettingCommand(angleMsg)
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
override fun getBatteryStatus() {
|
|
2586
|
+
Bridge.log("G2: getBatteryStatus()")
|
|
2587
|
+
requestDeviceInfo()
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
override fun setSilentMode(enabled: Boolean) {
|
|
2591
|
+
// TODO: Implement
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
override fun exit() {
|
|
2595
|
+
clearDisplay()
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
override fun sendShutdown() {
|
|
2599
|
+
clearDisplay()
|
|
2600
|
+
disconnect()
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
override fun sendReboot() {
|
|
2604
|
+
// TODO: Implement via dev_settings
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
override fun sendRgbLedControl(
|
|
2608
|
+
requestId: String,
|
|
2609
|
+
packageName: String?,
|
|
2610
|
+
action: String,
|
|
2611
|
+
color: String?,
|
|
2612
|
+
ontime: Int,
|
|
2613
|
+
offtime: Int,
|
|
2614
|
+
count: Int
|
|
2615
|
+
) {
|
|
2616
|
+
// G2 doesn't have RGB LEDs
|
|
2617
|
+
Bridge.sendRgbLedControlResponse(requestId, false, "device_not_supported")
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// ---------- SGCManager: Network (G2 has no WiFi) ----------
|
|
2621
|
+
|
|
2622
|
+
override fun requestWifiScan() {}
|
|
2623
|
+
override fun sendWifiCredentials(ssid: String, password: String) {}
|
|
2624
|
+
override fun forgetWifiNetwork(ssid: String) {}
|
|
2625
|
+
override fun sendHotspotState(enabled: Boolean) {}
|
|
2626
|
+
|
|
2627
|
+
// ---------- SGCManager: User Context ----------
|
|
2628
|
+
|
|
2629
|
+
override fun sendUserEmailToGlasses(email: String) {
|
|
2630
|
+
// TODO: Could send via dev_settings
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// ---------- SGCManager: Gallery ----------
|
|
2634
|
+
|
|
2635
|
+
override fun queryGalleryStatus() {}
|
|
2636
|
+
override fun sendGalleryMode() {}
|
|
2637
|
+
|
|
2638
|
+
// ---------- SGCManager: Version Info ----------
|
|
2639
|
+
|
|
2640
|
+
override fun requestVersionInfo() {
|
|
2641
|
+
Bridge.log("G2: requestVersionInfo()")
|
|
2642
|
+
requestDeviceInfo()
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {}
|
|
2646
|
+
|
|
2647
|
+
// ---------- BLE Scanning ----------
|
|
2648
|
+
|
|
2649
|
+
private fun startScan(): Boolean {
|
|
2650
|
+
Bridge.log("G2: startScan()")
|
|
2651
|
+
|
|
2652
|
+
stopScan()
|
|
2653
|
+
|
|
2654
|
+
val adapter =
|
|
2655
|
+
bluetoothAdapter
|
|
2656
|
+
?: run {
|
|
2657
|
+
Bridge.log("G2: BluetoothAdapter not available")
|
|
2658
|
+
return false
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
if (!adapter.isEnabled) {
|
|
2662
|
+
Bridge.log("G2: Bluetooth not enabled")
|
|
2663
|
+
return false
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
isDisconnecting = false
|
|
2667
|
+
|
|
2668
|
+
// Try address-based reconnection first
|
|
2669
|
+
if (connectByAddress()) {
|
|
2670
|
+
return true
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
val scanner =
|
|
2674
|
+
adapter.bluetoothLeScanner
|
|
2675
|
+
?: run {
|
|
2676
|
+
Bridge.log("G2: BluetoothLeScanner not available")
|
|
2677
|
+
return false
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
val settings =
|
|
2681
|
+
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
|
|
2682
|
+
|
|
2683
|
+
val callback =
|
|
2684
|
+
object : ScanCallback() {
|
|
2685
|
+
override fun onScanResult(callbackType: Int, result: ScanResult?) {
|
|
2686
|
+
result ?: return
|
|
2687
|
+
val device = result.device ?: return
|
|
2688
|
+
val name = device.name ?: return
|
|
2689
|
+
|
|
2690
|
+
if (!name.contains("G2")) return
|
|
2691
|
+
|
|
2692
|
+
mainHandler.post {
|
|
2693
|
+
// Extract serial number from manufacturer data (like iOS)
|
|
2694
|
+
val serialNumber = extractSNFromScanRecord(result)
|
|
2695
|
+
if (serialNumber == null) {
|
|
2696
|
+
Bridge.log("G2: Discovered: $name but no SN in mfg data")
|
|
2697
|
+
return@post
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
val mfgFirst = result.scanRecord?.manufacturerSpecificData?.valueAt(0)
|
|
2701
|
+
val mfgHex =
|
|
2702
|
+
mfgFirst?.joinToString(" ") { String.format("%02X", it) }
|
|
2703
|
+
?: "none"
|
|
2704
|
+
Bridge.log(
|
|
2705
|
+
"G2: Discovered: $name (SN: $serialNumber) mfgData[${mfgFirst?.size ?: 0}]: $mfgHex"
|
|
2706
|
+
)
|
|
2707
|
+
deviceNameToSerialNumber[name] = serialNumber
|
|
2708
|
+
|
|
2709
|
+
// Save MAC per side; ring's advStart needs the left lens MAC.
|
|
2710
|
+
val mac = extractMacFromScanRecord(result)
|
|
2711
|
+
if (mac != null) {
|
|
2712
|
+
if (name.contains("_L_")) {
|
|
2713
|
+
DeviceStore.apply("glasses", "leftMacAddress", mac)
|
|
2714
|
+
DeviceStore.apply("glasses", "btMacAddress", mac)
|
|
2715
|
+
} else if (name.contains("_R_")) {
|
|
2716
|
+
DeviceStore.apply("glasses", "rightMacAddress", mac)
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
// Stop scanning once we have both
|
|
2720
|
+
if (leftGatt != null && rightGatt != null) {
|
|
2721
|
+
stopScan()
|
|
2722
|
+
Bridge.log("G2: Stopped scan after discovering both devices")
|
|
2723
|
+
return@post
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// Always emit discovered device to frontend
|
|
2727
|
+
emitDiscoveredDevice(serialNumber)
|
|
2728
|
+
|
|
2729
|
+
// If scan-only mode, don't auto-connect
|
|
2730
|
+
if (DEVICE_SEARCH_ID == "NOT_SET") return@post
|
|
2731
|
+
|
|
2732
|
+
// Only connect to devices matching our search ID
|
|
2733
|
+
if (!serialNumber.contains(DEVICE_SEARCH_ID)) return@post
|
|
2734
|
+
|
|
2735
|
+
if (name.contains("_L_")) {
|
|
2736
|
+
if (leftGatt == null) {
|
|
2737
|
+
Bridge.log("G2: Connecting to LEFT: $name")
|
|
2738
|
+
leftGatt = device.connectGatt(context, false, leftGattCallback)
|
|
2739
|
+
}
|
|
2740
|
+
} else if (name.contains("_R_")) {
|
|
2741
|
+
if (rightGatt == null) {
|
|
2742
|
+
Bridge.log("G2: Connecting to RIGHT: $name")
|
|
2743
|
+
rightGatt =
|
|
2744
|
+
device.connectGatt(context, false, rightGattCallback)
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// Stop scanning once we have both
|
|
2749
|
+
if (leftGatt != null && rightGatt != null) {
|
|
2750
|
+
stopScan()
|
|
2751
|
+
cancelPairingTimeout()
|
|
2752
|
+
Bridge.log("G2: Stopped scan after discovering both devices2")
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
override fun onScanFailed(errorCode: Int) {
|
|
2758
|
+
Bridge.log("G2: Scan failed with error code: $errorCode")
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
scanCallback = callback
|
|
2763
|
+
try {
|
|
2764
|
+
scanner.startScan(null, settings, callback)
|
|
2765
|
+
} catch (e: SecurityException) {
|
|
2766
|
+
// Auto-reconnect paths may fire before BLUETOOTH_SCAN is granted on Android 12+
|
|
2767
|
+
Bridge.log("G2: startScan SecurityException — bluetooth permission missing: ${e.message}")
|
|
2768
|
+
scanCallback = null
|
|
2769
|
+
return false
|
|
2770
|
+
} catch (e: Exception) {
|
|
2771
|
+
Bridge.log("G2: startScan failed: ${e.message}")
|
|
2772
|
+
scanCallback = null
|
|
2773
|
+
return false
|
|
2774
|
+
}
|
|
2775
|
+
return true
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
override fun stopScan() {
|
|
2779
|
+
scanCallback?.let { cb -> bluetoothAdapter?.bluetoothLeScanner?.stopScan(cb) }
|
|
2780
|
+
scanCallback = null
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
private fun connectByAddress(): Boolean {
|
|
2784
|
+
if (DEVICE_SEARCH_ID == "NOT_SET" || DEVICE_SEARCH_ID.isEmpty()) {
|
|
2785
|
+
Bridge.log("G2: No DEVICE_SEARCH_ID set, skipping connect by address")
|
|
2786
|
+
return false
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
val leftAddr = leftGlassAddress ?: return false
|
|
2790
|
+
val rightAddr = rightGlassAddress ?: return false
|
|
2791
|
+
|
|
2792
|
+
val adapter = bluetoothAdapter ?: return false
|
|
2793
|
+
|
|
2794
|
+
try {
|
|
2795
|
+
val leftDevice = adapter.getRemoteDevice(leftAddr)
|
|
2796
|
+
val rightDevice = adapter.getRemoteDevice(rightAddr)
|
|
2797
|
+
|
|
2798
|
+
Bridge.log(
|
|
2799
|
+
"G2: connectByAddress - left: ${leftDevice.name ?: leftAddr}, right: ${rightDevice.name ?: rightAddr}"
|
|
2800
|
+
)
|
|
2801
|
+
|
|
2802
|
+
leftGatt = leftDevice.connectGatt(context, false, leftGattCallback)
|
|
2803
|
+
rightGatt = rightDevice.connectGatt(context, false, rightGattCallback)
|
|
2804
|
+
return true
|
|
2805
|
+
} catch (e: Exception) {
|
|
2806
|
+
Bridge.log("G2: connectByAddress failed: ${e.message}")
|
|
2807
|
+
return false
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* Extract serial number from BLE scan record manufacturer data. The SN is embedded in the
|
|
2813
|
+
* manufacturer-specific data payload. iOS: skip 2 bytes ("ER" prefix), read 14 bytes of ASCII
|
|
2814
|
+
* SN. Android: same approach on the manufacturer-specific data bytes.
|
|
2815
|
+
*/
|
|
2816
|
+
private fun extractSNFromScanRecord(result: ScanResult): String? {
|
|
2817
|
+
val scanRecord = result.scanRecord ?: return null
|
|
2818
|
+
|
|
2819
|
+
// Get manufacturer-specific data
|
|
2820
|
+
// Android strips the 2-byte company ID (0x4552 = "ER"), so the SN starts at offset 0.
|
|
2821
|
+
// iOS keeps the "ER" prefix so it skips 2 bytes — we don't need to skip on Android.
|
|
2822
|
+
val mfgData = scanRecord.manufacturerSpecificData
|
|
2823
|
+
if (mfgData == null || mfgData.size() == 0) return null
|
|
2824
|
+
|
|
2825
|
+
val data = mfgData.valueAt(0) ?: return null
|
|
2826
|
+
if (data.size < 14) return null
|
|
2827
|
+
|
|
2828
|
+
// Read 14 bytes of ASCII SN starting at offset 0
|
|
2829
|
+
val snBytes = data.copyOfRange(0, minOf(14, data.size))
|
|
2830
|
+
val sn =
|
|
2831
|
+
String(snBytes, Charsets.US_ASCII)
|
|
2832
|
+
.replace(Regex("[\\x00-\\x1F\\x7F]"), "") // Strip control chars
|
|
2833
|
+
return if (sn.isNotEmpty()) sn else null
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
/**
|
|
2837
|
+
* Extract the BLE MAC from the G2 scan record manufacturer data. Layout (after Android strips
|
|
2838
|
+
* the 2-byte company ID): SN(14) + MAC(6, little-endian) + flag(1) Returns "AA:BB:CC:DD:EE:FF"
|
|
2839
|
+
* (big-endian, colon-separated).
|
|
2840
|
+
*/
|
|
2841
|
+
private fun extractMacFromScanRecord(result: ScanResult): String? {
|
|
2842
|
+
val scanRecord = result.scanRecord ?: return null
|
|
2843
|
+
val mfgData = scanRecord.manufacturerSpecificData
|
|
2844
|
+
if (mfgData == null || mfgData.size() == 0) return null
|
|
2845
|
+
val data = mfgData.valueAt(0) ?: return null
|
|
2846
|
+
if (data.size < 20) return null
|
|
2847
|
+
val macLE = data.copyOfRange(14, 20)
|
|
2848
|
+
return macLE.reversed().joinToString(":") { String.format("%02X", it.toInt() and 0xFF) }
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
private fun emitDiscoveredDevice(serialNumber: String) {
|
|
2852
|
+
Bridge.sendDiscoveredDevice(DeviceTypes.G2, serialNumber)
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
private fun extractIdNumber(name: String): Int? {
|
|
2856
|
+
val pattern = Pattern.compile("G2_(\\d+)_")
|
|
2857
|
+
val matcher = pattern.matcher(name)
|
|
2858
|
+
if (matcher.find()) {
|
|
2859
|
+
return matcher.group(1)?.toIntOrNull()
|
|
2860
|
+
}
|
|
2861
|
+
return null
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// ---------- GATT Callbacks ----------
|
|
2865
|
+
|
|
2866
|
+
private val leftGattCallback = createGattCallback("LEFT")
|
|
2867
|
+
private val rightGattCallback = createGattCallback("RIGHT")
|
|
2868
|
+
|
|
2869
|
+
@Suppress("deprecation")
|
|
2870
|
+
private fun createGattCallback(side: String): BluetoothGattCallback {
|
|
2871
|
+
return object : BluetoothGattCallback() {
|
|
2872
|
+
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
2873
|
+
mainHandler.post {
|
|
2874
|
+
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
2875
|
+
Bridge.log("G2: Connected to $side: ${gatt.device?.name ?: "unknown"}")
|
|
2876
|
+
|
|
2877
|
+
// Save address for reconnection
|
|
2878
|
+
val address = gatt.device?.address
|
|
2879
|
+
if (side == "LEFT") {
|
|
2880
|
+
leftGlassAddress = address
|
|
2881
|
+
} else {
|
|
2882
|
+
rightGlassAddress = address
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// Request a larger MTU so 200-byte audio notifications aren't fragmented.
|
|
2886
|
+
// Default ATT MTU is 23 → max payload 20 bytes, which would chop each audio
|
|
2887
|
+
// chunk into 10+ pieces. We ask for 247 (max for BLE 4.2+ data length ext).
|
|
2888
|
+
// discoverServices is deferred to onMtuChanged so the larger MTU is in
|
|
2889
|
+
// effect for the rest of the setup.
|
|
2890
|
+
val mtuRequested =
|
|
2891
|
+
try {
|
|
2892
|
+
gatt.requestMtu(247)
|
|
2893
|
+
} catch (e: SecurityException) {
|
|
2894
|
+
Bridge.log(
|
|
2895
|
+
"G2: requestMtu SecurityException on $side: ${e.message}"
|
|
2896
|
+
)
|
|
2897
|
+
false
|
|
2898
|
+
}
|
|
2899
|
+
if (!mtuRequested) {
|
|
2900
|
+
Bridge.log(
|
|
2901
|
+
"G2: requestMtu returned false on $side, proceeding without MTU bump"
|
|
2902
|
+
)
|
|
2903
|
+
gatt.discoverServices()
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// Ask for high connection priority so the link can sustain 16 kHz / 10 ms
|
|
2907
|
+
// audio without dropped notifications. Caller is responsible for dropping
|
|
2908
|
+
// back to BALANCED later if power becomes a concern.
|
|
2909
|
+
try {
|
|
2910
|
+
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
|
|
2911
|
+
} catch (e: SecurityException) {
|
|
2912
|
+
Bridge.log(
|
|
2913
|
+
"G2: requestConnectionPriority SecurityException on $side: ${e.message}"
|
|
2914
|
+
)
|
|
2915
|
+
}
|
|
2916
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
2917
|
+
Bridge.log("G2: Disconnected $side")
|
|
2918
|
+
|
|
2919
|
+
if (isDisconnecting) return@post
|
|
2920
|
+
|
|
2921
|
+
// Clear both sides to force re-discovery
|
|
2922
|
+
leftGatt?.close()
|
|
2923
|
+
rightGatt?.close()
|
|
2924
|
+
leftGatt = null
|
|
2925
|
+
rightGatt = null
|
|
2926
|
+
leftInitialized = false
|
|
2927
|
+
rightInitialized = false
|
|
2928
|
+
leftWriteChar = null
|
|
2929
|
+
rightWriteChar = null
|
|
2930
|
+
leftNotifyChar = null
|
|
2931
|
+
rightNotifyChar = null
|
|
2932
|
+
leftAudioChar = null
|
|
2933
|
+
rightAudioChar = null
|
|
2934
|
+
authStarted = false
|
|
2935
|
+
leftAuthenticated = false
|
|
2936
|
+
rightAuthenticated = false
|
|
2937
|
+
|
|
2938
|
+
startupPageCreated = false
|
|
2939
|
+
pageCreated = false
|
|
2940
|
+
pageHasTextContainer = false
|
|
2941
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
2942
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
2943
|
+
|
|
2944
|
+
startReconnectionTimer()
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
2950
|
+
Bridge.log("G2: onMtuChanged $side mtu=$mtu status=$status")
|
|
2951
|
+
mainHandler.post {
|
|
2952
|
+
// discoverServices was deferred until MTU negotiation finishes (success or
|
|
2953
|
+
// not).
|
|
2954
|
+
try {
|
|
2955
|
+
gatt.discoverServices()
|
|
2956
|
+
} catch (e: SecurityException) {
|
|
2957
|
+
Bridge.log("G2: discoverServices SecurityException on $side: ${e.message}")
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
2963
|
+
if (status != BluetoothGatt.GATT_SUCCESS) return
|
|
2964
|
+
|
|
2965
|
+
mainHandler.post {
|
|
2966
|
+
val services = gatt.services ?: return@post
|
|
2967
|
+
|
|
2968
|
+
for (service in services) {
|
|
2969
|
+
for (char in service.characteristics) {
|
|
2970
|
+
val uuid = char.uuid
|
|
2971
|
+
val props = char.properties
|
|
2972
|
+
|
|
2973
|
+
var propStr = mutableListOf<String>()
|
|
2974
|
+
if (props and BluetoothGattCharacteristic.PROPERTY_READ != 0)
|
|
2975
|
+
propStr.add("read")
|
|
2976
|
+
if (props and BluetoothGattCharacteristic.PROPERTY_WRITE != 0)
|
|
2977
|
+
propStr.add("write")
|
|
2978
|
+
if (props and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE !=
|
|
2979
|
+
0
|
|
2980
|
+
)
|
|
2981
|
+
propStr.add("writeNoResp")
|
|
2982
|
+
if (props and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
|
|
2983
|
+
propStr.add("notify")
|
|
2984
|
+
if (props and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0)
|
|
2985
|
+
propStr.add("indicate")
|
|
2986
|
+
Bridge.log("G2: $side char $uuid props=[${propStr.joinToString(",")}]")
|
|
2987
|
+
|
|
2988
|
+
when (uuid) {
|
|
2989
|
+
G2BLE.CHAR_WRITE -> {
|
|
2990
|
+
Bridge.log("G2: Found WRITE char on $side")
|
|
2991
|
+
if (side == "LEFT") leftWriteChar = char
|
|
2992
|
+
else rightWriteChar = char
|
|
2993
|
+
}
|
|
2994
|
+
G2BLE.CHAR_NOTIFY -> {
|
|
2995
|
+
Bridge.log("G2: Found NOTIFY char on $side")
|
|
2996
|
+
if (side == "LEFT") leftNotifyChar = char
|
|
2997
|
+
else rightNotifyChar = char
|
|
2998
|
+
enqueueGattOp { enableNotifications(gatt, char) }
|
|
2999
|
+
}
|
|
3000
|
+
G2BLE.AUDIO_NOTIFY -> {
|
|
3001
|
+
Bridge.log("G2: Found AUDIO char on $side")
|
|
3002
|
+
if (side == "LEFT") leftAudioChar = char
|
|
3003
|
+
else rightAudioChar = char
|
|
3004
|
+
enqueueGattOp { enableNotifications(gatt, char) }
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// Check if this side is fully initialized
|
|
3011
|
+
if (side == "LEFT" && leftWriteChar != null) {
|
|
3012
|
+
leftInitialized = true
|
|
3013
|
+
Bridge.log("G2: LEFT initialized")
|
|
3014
|
+
} else if (side == "RIGHT" && rightWriteChar != null && rightNotifyChar != null
|
|
3015
|
+
) {
|
|
3016
|
+
rightInitialized = true
|
|
3017
|
+
Bridge.log("G2: RIGHT initialized")
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Both sides ready -> run auth (once)
|
|
3021
|
+
if (leftInitialized && rightInitialized && !authStarted) {
|
|
3022
|
+
// stop scanning
|
|
3023
|
+
stopScan()
|
|
3024
|
+
authStarted = true
|
|
3025
|
+
Bridge.log("G2: Both sides initialized, starting auth sequence")
|
|
3026
|
+
runAuthSequence()
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
@Deprecated("Deprecated in API level 33")
|
|
3032
|
+
override fun onCharacteristicChanged(
|
|
3033
|
+
gatt: BluetoothGatt,
|
|
3034
|
+
characteristic: BluetoothGattCharacteristic
|
|
3035
|
+
) {
|
|
3036
|
+
val data = characteristic.value ?: return
|
|
3037
|
+
|
|
3038
|
+
val sourceKey = if (side == "LEFT") "L" else "R"
|
|
3039
|
+
when (characteristic.uuid) {
|
|
3040
|
+
G2BLE.AUDIO_NOTIFY -> handleAudioData(data, sourceKey)
|
|
3041
|
+
G2BLE.CHAR_NOTIFY -> mainHandler.post { handleNotifyData(data, sourceKey) }
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
override fun onDescriptorWrite(
|
|
3046
|
+
gatt: BluetoothGatt,
|
|
3047
|
+
descriptor: BluetoothGattDescriptor,
|
|
3048
|
+
status: Int
|
|
3049
|
+
) {
|
|
3050
|
+
mainHandler.post {
|
|
3051
|
+
// Process next queued GATT operation
|
|
3052
|
+
gattOpInProgress = false
|
|
3053
|
+
processGattOpQueue()
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// GATT operation queue (Android only allows one outstanding GATT op at a time)
|
|
3060
|
+
private fun enqueueGattOp(op: () -> Unit) {
|
|
3061
|
+
gattOpQueue.add(op)
|
|
3062
|
+
if (!gattOpInProgress) {
|
|
3063
|
+
processGattOpQueue()
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
private fun processGattOpQueue() {
|
|
3068
|
+
if (gattOpQueue.isEmpty()) return
|
|
3069
|
+
gattOpInProgress = true
|
|
3070
|
+
val op = gattOpQueue.removeAt(0)
|
|
3071
|
+
op()
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
@Suppress("deprecation")
|
|
3075
|
+
private fun enableNotifications(
|
|
3076
|
+
gatt: BluetoothGatt,
|
|
3077
|
+
characteristic: BluetoothGattCharacteristic
|
|
3078
|
+
) {
|
|
3079
|
+
gatt.setCharacteristicNotification(characteristic, true)
|
|
3080
|
+
val descriptor = characteristic.getDescriptor(G2BLE.CLIENT_CHARACTERISTIC_CONFIG)
|
|
3081
|
+
if (descriptor != null) {
|
|
3082
|
+
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
3083
|
+
gatt.writeDescriptor(descriptor)
|
|
3084
|
+
} else {
|
|
3085
|
+
// No descriptor, move to next op
|
|
3086
|
+
gattOpInProgress = false
|
|
3087
|
+
processGattOpQueue()
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// ---------- Incoming Data Handling ----------
|
|
3092
|
+
|
|
3093
|
+
private fun handleNotifyData(data: ByteArray, sourceKey: String) {
|
|
3094
|
+
val result = receiveManager.handlePacket(data, sourceKey) ?: return
|
|
3095
|
+
|
|
3096
|
+
val serviceId = result.first
|
|
3097
|
+
val payload = result.second
|
|
3098
|
+
|
|
3099
|
+
when (serviceId) {
|
|
3100
|
+
ServiceID.EVEN_HUB.value -> handleEvenHubResponse(payload)
|
|
3101
|
+
ServiceID.DEVICE_SETTINGS.value -> handleDevSettingsResponse(payload, sourceKey)
|
|
3102
|
+
ServiceID.G2_SETTING.value -> handleG2SettingResponse(payload)
|
|
3103
|
+
ServiceID.MENU.value -> handleMenuResponse(payload)
|
|
3104
|
+
ServiceID.DASHBOARD.value -> handleDashboardResponse(payload)
|
|
3105
|
+
ServiceID.GESTURE_CTRL.value -> handleGestureCtrl(payload)
|
|
3106
|
+
ServiceID.EVEN_HUB_CTRL.value -> handleEvenHubCtrlResponse(payload)
|
|
3107
|
+
else -> {
|
|
3108
|
+
Bridge.log(
|
|
3109
|
+
"G2: Unhandled service ${serviceId.toInt() and 0xFF} (${payload.size} bytes): ${
|
|
3110
|
+
payload.take(32).joinToString("") { String.format("%02X", it) }
|
|
3111
|
+
}"
|
|
3112
|
+
)
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
private fun handleEvenHubResponse(payload: ByteArray) {
|
|
3118
|
+
val reader = ProtobufReader(payload)
|
|
3119
|
+
val fields = reader.parseFields()
|
|
3120
|
+
|
|
3121
|
+
val cmdValue =
|
|
3122
|
+
fields[1] as? Int
|
|
3123
|
+
?: run {
|
|
3124
|
+
Bridge.log(
|
|
3125
|
+
"G2: EvenHub response - no cmd field, ${payload.size} bytes: ${
|
|
3126
|
+
payload.joinToString("") { String.format("%02X", it) }
|
|
3127
|
+
}"
|
|
3128
|
+
)
|
|
3129
|
+
return
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
if (cmdValue == EvenHubResponseCmd.OS_NOTIFY_EVENT_TO_APP.value) {
|
|
3133
|
+
// Touch/gesture event from glasses
|
|
3134
|
+
val devEventData = fields[13] as? ByteArray ?: return
|
|
3135
|
+
val timestamp = System.currentTimeMillis()
|
|
3136
|
+
val last = lastClickTimestamp
|
|
3137
|
+
if (last != null && timestamp - last < 250) {
|
|
3138
|
+
return
|
|
3139
|
+
}
|
|
3140
|
+
lastClickTimestamp = timestamp
|
|
3141
|
+
handleTouchEvent(devEventData)
|
|
3142
|
+
} else if (cmdValue == 17) {
|
|
3143
|
+
// Miniapp selection from glasses dashboard menu (cmdId=17)
|
|
3144
|
+
// Dedup: L and R peripherals both deliver this event, so debounce or
|
|
3145
|
+
// MantleManager toggles start→stop in quick succession.
|
|
3146
|
+
val timestamp = System.currentTimeMillis()
|
|
3147
|
+
val lastMenu = lastMenuSelectTimestamp
|
|
3148
|
+
if (lastMenu != null && timestamp - lastMenu < 500) {
|
|
3149
|
+
return
|
|
3150
|
+
}
|
|
3151
|
+
lastMenuSelectTimestamp = timestamp
|
|
3152
|
+
// field 20 contains sub-message with field 1 = itemAppId
|
|
3153
|
+
val selectData = fields[20] as? ByteArray ?: return
|
|
3154
|
+
val selectReader = ProtobufReader(selectData)
|
|
3155
|
+
val selectFields = selectReader.parseFields()
|
|
3156
|
+
val appId = selectFields[1] as? Int ?: return
|
|
3157
|
+
// Resolve appId → packageName using our stored mapping
|
|
3158
|
+
val packageName = menuAppIdToPackageName[appId]
|
|
3159
|
+
if (packageName != null) {
|
|
3160
|
+
Bridge.log("G2: Menu miniapp selected — $packageName")
|
|
3161
|
+
Bridge.sendMiniappSelected(packageName)
|
|
3162
|
+
mainHandler.postDelayed({ clearDisplay() }, 500)
|
|
3163
|
+
} else {
|
|
3164
|
+
Bridge.log("G2: Menu selection ignored — placeholder or unknown appId=$appId")
|
|
3165
|
+
}
|
|
3166
|
+
} else {
|
|
3167
|
+
// Parse error codes from responses
|
|
3168
|
+
// field 4 = StartupResCmd, field 6 = ImgResCmd, field 8 = RebuildResCmd, field 10 =
|
|
3169
|
+
// TextResCmd
|
|
3170
|
+
for (resField in listOf(4, 6, 8, 10)) {
|
|
3171
|
+
val resData = fields[resField] as? ByteArray ?: continue
|
|
3172
|
+
val resReader = ProtobufReader(resData)
|
|
3173
|
+
val resFields = resReader.parseFields()
|
|
3174
|
+
(resFields[1] as? Int)?.let { errorCode ->
|
|
3175
|
+
// 0=page_success, 4=img_success, 5=img_failed, 6=rebuild_success,
|
|
3176
|
+
// 7=rebuild_failed, 8=text_success, 9=text_failed
|
|
3177
|
+
if (errorCode == 9) {
|
|
3178
|
+
Bridge.log(
|
|
3179
|
+
"G2: WARN: Glasses shutdown our EvenHub page — resetting page state"
|
|
3180
|
+
)
|
|
3181
|
+
startupPageCreated = false
|
|
3182
|
+
pageCreated = false
|
|
3183
|
+
pageHasTextContainer = false
|
|
3184
|
+
currentTextContent = ""
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
(resFields[8] as? Int)?.let { errorCode ->
|
|
3188
|
+
// ImgResCmd has ErrorCode in field 8
|
|
3189
|
+
Bridge.log("G2: EvenHub ImgRes errorCode=$errorCode")
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// If glasses sent a shutdown (cmd=9/10), our page is gone — reset state
|
|
3194
|
+
if (cmdValue == 9 || cmdValue == 10) {
|
|
3195
|
+
Bridge.log("G2: ERROR: Glasses shutdown our EvenHub page — resetting page state")
|
|
3196
|
+
startupPageCreated = false
|
|
3197
|
+
pageCreated = false
|
|
3198
|
+
pageHasTextContainer = false
|
|
3199
|
+
currentTextContent = ""
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
private fun setFullyConnected() {
|
|
3205
|
+
val isFullyConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
|
|
3206
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
3207
|
+
if (!isFullyConnected) {
|
|
3208
|
+
DeviceStore.apply("glasses", "connected", true)
|
|
3209
|
+
}
|
|
3210
|
+
if (!isFullyBooted) {
|
|
3211
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
private fun setControllerFullyConnected() {
|
|
3216
|
+
val isControllerConnected =
|
|
3217
|
+
DeviceStore.get("glasses", "controllerConnected") as? Boolean ?: false
|
|
3218
|
+
val isControllerFullyBooted =
|
|
3219
|
+
DeviceStore.get("glasses", "controllerFullyBooted") as? Boolean ?: false
|
|
3220
|
+
if (!isControllerConnected) {
|
|
3221
|
+
DeviceStore.apply("glasses", "controllerConnected", true)
|
|
3222
|
+
}
|
|
3223
|
+
if (!isControllerFullyBooted) {
|
|
3224
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", true)
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
private fun handleTouchEvent(devEventData: ByteArray) {
|
|
3229
|
+
// Parse SendDeviceEvent: field 1=ListEvent, field 2=TextEvent, field 3=SysEvent
|
|
3230
|
+
val reader = ProtobufReader(devEventData)
|
|
3231
|
+
val fields = reader.parseFields()
|
|
3232
|
+
|
|
3233
|
+
val timestamp = System.currentTimeMillis()
|
|
3234
|
+
|
|
3235
|
+
// if we are receiving touch events we are fully booted:
|
|
3236
|
+
setFullyConnected()
|
|
3237
|
+
|
|
3238
|
+
// SysEvent (field 3) - system-level gestures
|
|
3239
|
+
(fields[3] as? ByteArray)?.let { sysData ->
|
|
3240
|
+
val sysReader = ProtobufReader(sysData)
|
|
3241
|
+
val sysFields = sysReader.parseFields()
|
|
3242
|
+
|
|
3243
|
+
val normalType = sysFields[1] as? Int
|
|
3244
|
+
val eventType: OsEventType? =
|
|
3245
|
+
if (normalType != null) OsEventType.fromInt(normalType) else OsEventType.CLICK
|
|
3246
|
+
val eventSource: Int? = sysFields[2] as? Int
|
|
3247
|
+
|
|
3248
|
+
if (eventType == null) {
|
|
3249
|
+
Bridge.log("G2: unknown event type: $sysFields")
|
|
3250
|
+
return@let
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
val gestureName = mapEventTypeToGesture(eventType)
|
|
3254
|
+
if (gestureName == null) {
|
|
3255
|
+
Bridge.log("G2: no gesture mapping for $eventType $sysFields")
|
|
3256
|
+
return@let
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
Bridge.sendTouchEvent(DeviceTypes.G2, gestureName, timestamp, eventSource)
|
|
3260
|
+
Bridge.log("G2: SysEvent → $eventType $eventSource")
|
|
3261
|
+
|
|
3262
|
+
if (eventSource == 2) {
|
|
3263
|
+
// controller must be connected and fully booted:
|
|
3264
|
+
setControllerFullyConnected()
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
if (eventType == OsEventType.DOUBLE_CLICK) {
|
|
3268
|
+
// trigger dashboard:
|
|
3269
|
+
val isHeadUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
|
|
3270
|
+
// toggle head up:
|
|
3271
|
+
DeviceStore.apply("glasses", "headUp", !isHeadUp)
|
|
3272
|
+
// if (isHeadUp) {
|
|
3273
|
+
// // clear the display after a delay:
|
|
3274
|
+
// mainHandler.postDelayed({ clearDisplay() }, 500)
|
|
3275
|
+
// }
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// System exit: glasses killed our EvenHub page (user opened menu or another app)
|
|
3279
|
+
// Reset page state and re-create the page to reclaim EvenHub focus
|
|
3280
|
+
if (eventType == OsEventType.SYSTEM_EXIT || eventType == OsEventType.ABNORMAL_EXIT) {
|
|
3281
|
+
startupPageCreated = false
|
|
3282
|
+
pageCreated = false
|
|
3283
|
+
pageHasTextContainer = false
|
|
3284
|
+
currentTextContent = ""
|
|
3285
|
+
currentBitmapBase64 = ""
|
|
3286
|
+
// Firmware kills the mic on system exit; re-arm it if it should be on
|
|
3287
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
3288
|
+
DeviceManager.getInstance().updateMicState()
|
|
3289
|
+
}
|
|
3290
|
+
return
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// TextEvent (field 2) - tap on text container
|
|
3294
|
+
(fields[2] as? ByteArray)?.let { textData ->
|
|
3295
|
+
val textReader = ProtobufReader(textData)
|
|
3296
|
+
val textFields = textReader.parseFields()
|
|
3297
|
+
val eventTypeRaw = textFields[3] as? Int ?: return@let
|
|
3298
|
+
val eventType = OsEventType.fromInt(eventTypeRaw) ?: return@let
|
|
3299
|
+
val gestureName = mapEventTypeToGesture(eventType)
|
|
3300
|
+
// log raw event data:
|
|
3301
|
+
// Bridge.log("G2: TextEvent raw data: ${textData.joinToString("") {
|
|
3302
|
+
// String.format("%02X", it) }}")
|
|
3303
|
+
// Bridge.log("G2: TextEvent fields: $textFields")
|
|
3304
|
+
|
|
3305
|
+
if (gestureName == null) {
|
|
3306
|
+
Bridge.log("G2: no gesture mapping for $eventType $textFields")
|
|
3307
|
+
return@let
|
|
3308
|
+
}
|
|
3309
|
+
Bridge.sendTouchEvent(DeviceTypes.G2, gestureName, timestamp)
|
|
3310
|
+
Bridge.log("G2: TextEvent → $gestureName")
|
|
3311
|
+
return
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// ListEvent (field 1) - interaction with list container (not currently handled)
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
private fun mapEventTypeToGesture(eventType: OsEventType): String? {
|
|
3318
|
+
return when (eventType) {
|
|
3319
|
+
OsEventType.CLICK -> "single_tap"
|
|
3320
|
+
OsEventType.DOUBLE_CLICK -> "double_tap"
|
|
3321
|
+
OsEventType.SCROLL_TOP -> "swipe_up"
|
|
3322
|
+
OsEventType.SCROLL_BOTTOM -> "swipe_down"
|
|
3323
|
+
OsEventType.FOREGROUND_ENTER -> "foreground_enter"
|
|
3324
|
+
OsEventType.FOREGROUND_EXIT -> "foreground_exit"
|
|
3325
|
+
OsEventType.SYSTEM_EXIT -> "system_exit"
|
|
3326
|
+
OsEventType.ABNORMAL_EXIT -> null
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
private fun handleDevSettingsResponse(payload: ByteArray, sourceKey: String) {
|
|
3331
|
+
val reader = ProtobufReader(payload)
|
|
3332
|
+
val fields = reader.parseFields()
|
|
3333
|
+
val cmdValue = fields[1] as? Int ?: -1
|
|
3334
|
+
|
|
3335
|
+
// Ignore heartbeat acks
|
|
3336
|
+
if (cmdValue == DevCfgCommandId.BASE_CONN_HEART_BEAT.value) return
|
|
3337
|
+
|
|
3338
|
+
Bridge.log(
|
|
3339
|
+
"G2: DevSettings response: ${payload.take(32).joinToString(":") { String.format("%02X", it) }}"
|
|
3340
|
+
)
|
|
3341
|
+
|
|
3342
|
+
if (cmdValue == DevCfgCommandId.AUTHENTICATION.value) {
|
|
3343
|
+
// DevCfgDataPackage: field 2 = magicRandom, field 3 = AuthMgr { field 1 = secAuth }
|
|
3344
|
+
var secAuth: Boolean? = null
|
|
3345
|
+
(fields[3] as? ByteArray)?.let { authData ->
|
|
3346
|
+
val authReader = ProtobufReader(authData)
|
|
3347
|
+
val authFields = authReader.parseFields()
|
|
3348
|
+
(authFields[1] as? Int)?.let { secAuth = (it != 0) }
|
|
3349
|
+
}
|
|
3350
|
+
val secAuthStr = secAuth?.toString() ?: "?"
|
|
3351
|
+
Bridge.log("G2: Authentication response: $sourceKey secAuth=$secAuthStr")
|
|
3352
|
+
if (secAuth == true) {
|
|
3353
|
+
if (sourceKey == "L") {
|
|
3354
|
+
leftAuthenticated = true
|
|
3355
|
+
} else if (sourceKey == "R") {
|
|
3356
|
+
rightAuthenticated = true
|
|
3357
|
+
}
|
|
3358
|
+
if (leftAuthenticated && rightAuthenticated) {
|
|
3359
|
+
Bridge.log("G2: Both sides authenticated, setting fully booted and connected")
|
|
3360
|
+
setFullyConnected()
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// RING_CONNECT_INFO response (cmd 6)
|
|
3366
|
+
if (cmdValue == DevCfgCommandId.RING_CONNECT_INFO.value) {
|
|
3367
|
+
(fields[5] as? ByteArray)?.let { ringData ->
|
|
3368
|
+
val ringReader = ProtobufReader(ringData)
|
|
3369
|
+
val ringFields = ringReader.parseFields()
|
|
3370
|
+
|
|
3371
|
+
if ((ringFields[1] as? Int ?: 0) == 1) {
|
|
3372
|
+
Bridge.log("G2: Ring maybe connected?")
|
|
3373
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", true)
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
if ((ringFields[4] as? Int ?: 0) == 62) {
|
|
3377
|
+
Bridge.log("G2: Ring maybe reconnected?")
|
|
3378
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", true)
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
val connStatus = ringFields[4] as? Int ?: -1
|
|
3382
|
+
Bridge.log("G2: Ring connection status: connStatus?=$connStatus")
|
|
3383
|
+
|
|
3384
|
+
if (connStatus == 22) {
|
|
3385
|
+
Bridge.log("G2: Ring disconnected")
|
|
3386
|
+
DeviceStore.apply("glasses", "controllerFullyBooted", false)
|
|
3387
|
+
DeviceStore.apply("glasses", "controllerSearching", true)
|
|
3388
|
+
reconnectController()
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
if (connStatus == 8) {
|
|
3392
|
+
Bridge.log("G2: Ring maybe disconnected?")
|
|
3393
|
+
// DeviceStore.apply("glasses", "controllerFullyBooted", false)
|
|
3394
|
+
// DeviceStore.apply("glasses", "controllerSearching", true)
|
|
3395
|
+
// reconnectController()
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
private fun handleMenuResponse(payload: ByteArray) {
|
|
3402
|
+
Bridge.log(
|
|
3403
|
+
"G2: menu response: ${payload.take(32).joinToString("") { String.format("%02X", it) }}"
|
|
3404
|
+
)
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
private fun handleDashboardResponse(payload: ByteArray) {
|
|
3408
|
+
Bridge.log(
|
|
3409
|
+
"G2: dashboard response: ${payload.take(32).joinToString("") { String.format("%02X", it) }}"
|
|
3410
|
+
)
|
|
3411
|
+
val reader = ProtobufReader(payload)
|
|
3412
|
+
val fields = reader.parseFields()
|
|
3413
|
+
val cmd = fields[1] as? Int ?: -1
|
|
3414
|
+
val magicRandom = fields[2] as? Int ?: 0
|
|
3415
|
+
|
|
3416
|
+
// Parse field 6 (DashboardSendToApp) if present
|
|
3417
|
+
var packageId = 0
|
|
3418
|
+
(fields[6] as? ByteArray)?.let { f6 ->
|
|
3419
|
+
val subReader = ProtobufReader(f6)
|
|
3420
|
+
val sub = subReader.parseFields()
|
|
3421
|
+
packageId = sub[1] as? Int ?: 0
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
// cmd=3 is APP_Respond — glasses sending us info, we should respond with cmd=4
|
|
3425
|
+
// (APP_RECEIVE)
|
|
3426
|
+
if (cmd == 3) {
|
|
3427
|
+
val appRespW = ProtobufWriter()
|
|
3428
|
+
appRespW.writeInt32Field(1, packageId) // packageId
|
|
3429
|
+
appRespW.writeInt32Field(2, 0) // flag = APP_RECEIVED_SUCCESS
|
|
3430
|
+
|
|
3431
|
+
val pkgW = ProtobufWriter()
|
|
3432
|
+
pkgW.writeInt32Field(1, 4) // commandId = APP_RECEIVE
|
|
3433
|
+
pkgW.writeInt32Field(2, magicRandom)
|
|
3434
|
+
pkgW.writeMessageField(5, appRespW.toByteArray()) // field5 = appRespond
|
|
3435
|
+
sendDashboardCommand(pkgW.toByteArray())
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
private fun handleEvenHubCtrlResponse(payload: ByteArray) {
|
|
3440
|
+
Bridge.log(
|
|
3441
|
+
"G2: evenHubCtrl response: ${payload.take(8).joinToString("") { String.format("%02X", it) }}"
|
|
3442
|
+
)
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
private fun handleGestureCtrl(payload: ByteArray) {
|
|
3446
|
+
// Dashboard close detection: 08011A00 means dashboard closed
|
|
3447
|
+
if (payload.contentEquals(byteArrayOf(0x08, 0x01, 0x1A, 0x00))) {
|
|
3448
|
+
Bridge.log("G2: gesture_ctrl response: dashboard closed")
|
|
3449
|
+
// Re-send mic on / update mic state
|
|
3450
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
3451
|
+
DeviceManager.getInstance().updateMicState()
|
|
3452
|
+
// Reset the text container
|
|
3453
|
+
sendTextWall(" ")
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
private fun handleG2SettingResponse(payload: ByteArray) {
|
|
3458
|
+
val reader = ProtobufReader(payload)
|
|
3459
|
+
val fields = reader.parseFields()
|
|
3460
|
+
|
|
3461
|
+
val cmdValue = fields[1] as? Int ?: return
|
|
3462
|
+
|
|
3463
|
+
if (cmdValue == G2SettingCommandId.DEVICE_RECEIVE_REQUEST.value ||
|
|
3464
|
+
cmdValue == G2SettingCommandId.DEVICE_SEND_TO_APP.value
|
|
3465
|
+
) {
|
|
3466
|
+
(fields[4] as? ByteArray)?.let { parseDeviceRequestResponse(it) }
|
|
3467
|
+
(fields[5] as? ByteArray)?.let { parseDeviceSendToApp(it) }
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
private fun parseDeviceRequestResponse(data: ByteArray) {
|
|
3472
|
+
val reader = ProtobufReader(data)
|
|
3473
|
+
val fields = reader.parseFields()
|
|
3474
|
+
|
|
3475
|
+
setFullyConnected()
|
|
3476
|
+
|
|
3477
|
+
// Battery
|
|
3478
|
+
(fields[12] as? Int)?.let { battery ->
|
|
3479
|
+
if (battery in 0..100) {
|
|
3480
|
+
Bridge.log("G2: Battery level: $battery%")
|
|
3481
|
+
batteryLevel_ = battery
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
// Charging status
|
|
3486
|
+
(fields[13] as? Int)?.let { charging ->
|
|
3487
|
+
isCharging = charging != 0
|
|
3488
|
+
Bridge.log("G2: Charging: $isCharging")
|
|
3489
|
+
if (_batteryLevel >= 0) {
|
|
3490
|
+
Bridge.sendBatteryStatus(_batteryLevel, isCharging)
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
// Software versions
|
|
3495
|
+
(fields[5] as? ByteArray)?.let { leftVer ->
|
|
3496
|
+
val leftVersion = String(leftVer, Charsets.UTF_8)
|
|
3497
|
+
Bridge.log("G2: Left firmware: $leftVersion")
|
|
3498
|
+
DeviceStore.apply("glasses", "leftFirmwareVersion", leftVersion)
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
(fields[6] as? ByteArray)?.let { rightVer ->
|
|
3502
|
+
val rightVersion = String(rightVer, Charsets.UTF_8)
|
|
3503
|
+
Bridge.log("G2: Right firmware: $rightVersion")
|
|
3504
|
+
DeviceStore.apply("glasses", "rightFirmwareVersion", rightVersion)
|
|
3505
|
+
DeviceStore.apply("glasses", "firmwareVersion", rightVersion)
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
private fun parseDeviceSendToApp(data: ByteArray) {
|
|
3510
|
+
val reader = ProtobufReader(data)
|
|
3511
|
+
val fields = reader.parseFields()
|
|
3512
|
+
(fields[2] as? Int)?.let { silentMode -> Bridge.log("G2: Silent mode: ${silentMode != 0}") }
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
// ---------- Audio Handling ----------
|
|
3516
|
+
|
|
3517
|
+
private var lastAudioFrame: ByteArray? = null
|
|
3518
|
+
|
|
3519
|
+
private fun handleAudioData(data: ByteArray, sourceKey: String) {
|
|
3520
|
+
// Diagnostic: if BLE notifications are arriving fragmented (MTU too small), data.size
|
|
3521
|
+
// will be consistently < 200. Expected: ~200-byte chunks (5 × 40-byte LC3 frames).
|
|
3522
|
+
|
|
3523
|
+
val usableLength = minOf(data.size, 200)
|
|
3524
|
+
if (usableLength < 40) return
|
|
3525
|
+
|
|
3526
|
+
val audioData = data.copyOfRange(0, usableLength)
|
|
3527
|
+
if (lastAudioFrame?.contentEquals(audioData) == true) {
|
|
3528
|
+
// Bridge.log("G2: audio dup from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
3529
|
+
return
|
|
3530
|
+
}
|
|
3531
|
+
lastAudioFrame = audioData
|
|
3532
|
+
Bridge.log("G2: audio data from $sourceKey: ${data.take(10).joinToString("") { String.format("%02X", it) }}")
|
|
3533
|
+
DeviceManager.getInstance().handleGlassesMicData(audioData, 40)
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// ---------- Reconnection ----------
|
|
3537
|
+
|
|
3538
|
+
private fun startReconnectionTimer() {
|
|
3539
|
+
reconnectionManager.start {
|
|
3540
|
+
val isFullyBooted = DeviceStore.get("glasses", "fullyBooted") as? Boolean ?: false
|
|
3541
|
+
if (isFullyBooted) {
|
|
3542
|
+
Bridge.log("G2: Already connected, stopping reconnection")
|
|
3543
|
+
return@start true
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
Bridge.log("G2: Attempting reconnection...")
|
|
3547
|
+
startScan()
|
|
3548
|
+
return@start false
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|