@glassly/bluetooth-sdk 0.1.0-dev.26
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 +563 -0
- package/android/build.gradle +450 -0
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +212 -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/glassly/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 +4 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +50 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/AndroidManifest.xml +42 -0
- package/android/src/main/assets/G2Firmware/README.md +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/app.plugin.js +3 -0
- package/build/BluetoothSdk.types.d.ts +1354 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +41 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/_internal.d.ts +17 -0
- package/build/_internal.d.ts.map +1 -0
- package/build/_internal.js +16 -0
- package/build/_internal.js.map +1 -0
- package/build/_private/BluetoothSdkModule.d.ts +172 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
- package/build/_private/BluetoothSdkModule.js +300 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -0
- package/build/_private/LocalNetworkModule.d.ts +38 -0
- package/build/_private/LocalNetworkModule.d.ts.map +1 -0
- package/build/_private/LocalNetworkModule.js +3 -0
- package/build/_private/LocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +32 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts +6 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +86 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/_private/sdkOtaManifest.d.ts +11 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +15 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +46 -0
- package/build/changelogs.js.map +1 -0
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +9 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +12 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +171 -0
- package/build/index.js.map +1 -0
- package/build/ota-server/OtaServer.types.d.ts +14 -0
- package/build/ota-server/OtaServer.types.d.ts.map +1 -0
- package/build/ota-server/OtaServer.types.js +2 -0
- package/build/ota-server/OtaServer.types.js.map +1 -0
- package/build/ota-server/OtaServerModule.d.ts +21 -0
- package/build/ota-server/OtaServerModule.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.js +3 -0
- package/build/ota-server/OtaServerModule.js.map +1 -0
- package/build/ota-server/OtaServerModule.web.d.ts +10 -0
- package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.web.js +12 -0
- package/build/ota-server/OtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.js +2 -0
- package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.js +3 -0
- package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- package/build/react/index.d.ts +5 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +4 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useBluetooth.d.ts +100 -0
- package/build/react/useBluetooth.d.ts.map +1 -0
- package/build/react/useBluetooth.js +167 -0
- package/build/react/useBluetooth.js.map +1 -0
- package/build/react/useBluetoothEvent.d.ts +6 -0
- package/build/react/useBluetoothEvent.d.ts.map +1 -0
- package/build/react/useBluetoothEvent.js +21 -0
- package/build/react/useBluetoothEvent.js.map +1 -0
- package/build/react/useBluetoothScan.d.ts +22 -0
- package/build/react/useBluetoothScan.d.ts.map +1 -0
- package/build/react/useBluetoothScan.js +135 -0
- package/build/react/useBluetoothScan.js.map +1 -0
- package/build/react/useBluetoothStatus.d.ts +16 -0
- package/build/react/useBluetoothStatus.d.ts.map +1 -0
- package/build/react/useBluetoothStatus.js +137 -0
- package/build/react/useBluetoothStatus.js.map +1 -0
- package/build/react/useGlassesConnection.d.ts +29 -0
- package/build/react/useGlassesConnection.d.ts.map +1 -0
- package/build/react/useGlassesConnection.js +153 -0
- package/build/react/useGlassesConnection.js.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/expo-module.config.json +14 -0
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +1034 -0
- package/ios/GlasslyBluetoothSDK.podspec +106 -0
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/LocalPhotoUploadServer.swift +754 -0
- package/ios/OtaServerModule.swift +147 -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/include/PcmConverter.h +22 -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 +2383 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/PhotoReceiverModule.swift +129 -0
- package/ios/Source/BluetoothSDK.swift +2303 -0
- package/ios/Source/BluetoothSDKDebug.swift +12 -0
- package/ios/Source/BluetoothSdkDefaults.swift +29 -0
- package/ios/Source/Bridge.swift +708 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +2109 -0
- package/ios/Source/DeviceStore.swift +336 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/ObservableStore.swift +118 -0
- package/ios/Source/OtaManifest.swift +211 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/Resources/G2Firmware/README.md +10 -0
- package/ios/Source/audio/AudioModels.swift +178 -0
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +866 -0
- package/ios/Source/connection/ScanSession.swift +27 -0
- package/ios/Source/controllers/ControllerManager.swift +184 -0
- package/ios/Source/controllers/R1.swift +829 -0
- package/ios/Source/errors/BluetoothSdkError.swift +19 -0
- package/ios/Source/events/BluetoothEvents.swift +342 -0
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/internal/BluetoothAvailability.swift +78 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
- package/ios/Source/internal/ValueParsing.swift +99 -0
- package/ios/Source/requests/DisplayRequests.swift +82 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/Frame.swift +649 -0
- package/ios/Source/sgcs/G1.swift +2554 -0
- package/ios/Source/sgcs/G2.swift +6887 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
- package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
- package/ios/Source/sgcs/G2Flasher.swift +773 -0
- package/ios/Source/sgcs/Mach1.swift +541 -0
- package/ios/Source/sgcs/MentraLive.swift +7011 -0
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/MentraNex.swift +3221 -0
- package/ios/Source/sgcs/Nimo.swift +1886 -0
- package/ios/Source/sgcs/SGCManager.swift +608 -0
- package/ios/Source/sgcs/Simulated.swift +274 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
- package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
- package/ios/Source/status/DeviceStatus.swift +981 -0
- package/ios/Source/status/RuntimeState.swift +359 -0
- package/ios/Source/status/WifiHotspotStatus.swift +345 -0
- package/ios/Source/streaming/StreamModels.swift +693 -0
- package/ios/Source/stt/STTTools.swift +191 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
- package/ios/Source/tts/TTSTools.swift +171 -0
- package/ios/Source/types/DeviceModels.swift +177 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/BinaryCodec.swift +61 -0
- package/ios/Source/utils/BleJsonCompact.swift +405 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/Constants.swift +78 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2096 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
- package/ios/Source/utils/MessageChunker.swift +294 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
- package/ios/Source/utils/TarBz2Extractor.swift +257 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +129 -0
- package/plugin/build/index.d.ts +10 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +3 -0
- package/plugin/build/withAndroid.js +190 -0
- package/plugin/build/withIos.d.ts +5 -0
- package/plugin/build/withIos.js +108 -0
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +45 -0
- package/scripts/write-release-metadata.mjs +152 -0
- package/scripts/write-release-metadata.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +1623 -0
- package/src/_internal.ts +16 -0
- package/src/_private/BluetoothSdkModule.ts +684 -0
- package/src/_private/LocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +34 -0
- package/src/_private/photoRequestPayload.ts +90 -0
- package/src/_private/sdkOtaManifest.ts +18 -0
- package/src/changelogs.ts +46 -0
- package/src/debug.ts +9 -0
- package/src/generated/changelogCatalog.ts +13 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +321 -0
- package/src/ota-server/OtaServer.types.ts +10 -0
- package/src/ota-server/OtaServerModule.ts +23 -0
- package/src/ota-server/OtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
- package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
- package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
- package/src/photo-receiver/index.ts +2 -0
- package/src/react/index.ts +21 -0
- package/src/react/useBluetooth.ts +309 -0
- package/src/react/useBluetoothEvent.ts +35 -0
- package/src/react/useBluetoothScan.ts +173 -0
- package/src/react/useBluetoothStatus.ts +180 -0
- package/src/react/useGlassesConnection.ts +202 -0
- package/src/types/index.ts +12 -0
|
@@ -0,0 +1,2011 @@
|
|
|
1
|
+
package com.glassly.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import com.glassly.bluetoothsdk.BluetoothSdkDefaults
|
|
4
|
+
import com.glassly.bluetoothsdk.DeviceManager
|
|
5
|
+
import com.glassly.bluetoothsdk.PhotoRequest
|
|
6
|
+
import com.glassly.bluetoothsdk.DeviceStore
|
|
7
|
+
|
|
8
|
+
import android.graphics.Bitmap
|
|
9
|
+
import android.graphics.BitmapFactory
|
|
10
|
+
import android.graphics.Canvas
|
|
11
|
+
import android.graphics.ColorMatrix
|
|
12
|
+
import android.graphics.ColorMatrixColorFilter
|
|
13
|
+
import android.graphics.Paint
|
|
14
|
+
import android.bluetooth.BluetoothAdapter
|
|
15
|
+
import android.bluetooth.BluetoothDevice
|
|
16
|
+
import android.bluetooth.BluetoothGatt
|
|
17
|
+
import android.bluetooth.BluetoothGattCallback
|
|
18
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
19
|
+
import android.bluetooth.BluetoothGattService
|
|
20
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
21
|
+
import android.bluetooth.BluetoothProfile
|
|
22
|
+
import android.bluetooth.le.BluetoothLeScanner
|
|
23
|
+
import android.bluetooth.le.ScanCallback
|
|
24
|
+
import android.bluetooth.le.ScanFilter
|
|
25
|
+
import android.bluetooth.le.ScanResult
|
|
26
|
+
import android.bluetooth.le.ScanSettings
|
|
27
|
+
import android.os.Message
|
|
28
|
+
import android.os.Handler
|
|
29
|
+
import android.os.Looper
|
|
30
|
+
import android.util.Log
|
|
31
|
+
import android.content.Context
|
|
32
|
+
|
|
33
|
+
import glassly.ble.GlasslyBle.GlassesToPhone
|
|
34
|
+
import glassly.ble.GlasslyBle.PhoneToGlasses
|
|
35
|
+
import glassly.ble.GlasslyBle.ChargingState
|
|
36
|
+
import glassly.ble.GlasslyBle.BatteryStatus
|
|
37
|
+
import glassly.ble.GlasslyBle.HeadGesture
|
|
38
|
+
import glassly.ble.GlasslyBle.ButtonEvent
|
|
39
|
+
import glassly.ble.GlasslyBle.ImuData
|
|
40
|
+
import glassly.ble.GlasslyBle.ImageTransferComplete
|
|
41
|
+
import glassly.ble.GlasslyBle.HeadUpAngleResponse
|
|
42
|
+
import glassly.ble.GlasslyBle.HeadPosition
|
|
43
|
+
import glassly.ble.GlasslyBle.DeviceInfo
|
|
44
|
+
|
|
45
|
+
import com.glassly.bluetoothsdk.sgcs.SGCManager
|
|
46
|
+
import com.glassly.bluetoothsdk.Bridge
|
|
47
|
+
import com.glassly.bluetoothsdk.utils.DeviceTypes
|
|
48
|
+
import com.glassly.bluetoothsdk.utils.NexProtobufUtils
|
|
49
|
+
import com.glassly.bluetoothsdk.utils.NexEventUtils
|
|
50
|
+
import com.glassly.bluetoothsdk.utils.NexBluetoothConstants
|
|
51
|
+
import com.glassly.bluetoothsdk.utils.NexDisplayConstants
|
|
52
|
+
import com.glassly.bluetoothsdk.utils.NexBluetoothPacketTypes
|
|
53
|
+
import com.glassly.bluetoothsdk.utils.BitmapJavaUtils
|
|
54
|
+
import com.glassly.bluetoothsdk.utils.G1FontLoaderKt
|
|
55
|
+
import com.glassly.bluetoothsdk.utils.G1Text
|
|
56
|
+
import com.glassly.bluetoothsdk.utils.ConnTypes
|
|
57
|
+
import com.glassly.lc3Lib.Lc3Cpp
|
|
58
|
+
import com.glassly.bluetoothsdk.utils.audio.Lc3Player
|
|
59
|
+
|
|
60
|
+
import java.util.UUID
|
|
61
|
+
import java.util.concurrent.LinkedBlockingQueue
|
|
62
|
+
import java.nio.charset.Charset
|
|
63
|
+
import java.util.Random
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
import org.json.JSONObject
|
|
67
|
+
|
|
68
|
+
import com.google.protobuf.InvalidProtocolBufferException
|
|
69
|
+
|
|
70
|
+
class MentraNex : SGCManager() {
|
|
71
|
+
companion object {
|
|
72
|
+
private const val TAG = "MentraNex";
|
|
73
|
+
|
|
74
|
+
private const val MTU_517: Int = 517
|
|
75
|
+
private const val MTU_DEFAULT: Int = 23
|
|
76
|
+
|
|
77
|
+
private const val MAX_CHUNK_SIZE_DEFAULT: Int = 176 // Maximum chunk size for BLE packets
|
|
78
|
+
private const val DELAY_BETWEEN_CHUNKS_SEND: Long = 5 // Adjust this value as needed
|
|
79
|
+
// Upper bound on waiting for onCharacteristicWrite. A no-response write's
|
|
80
|
+
// callback is normally near-instant; this only guards against a callback that
|
|
81
|
+
// never arrives, so a dropped write can't wedge the send queue forever.
|
|
82
|
+
private const val WRITE_CALLBACK_TIMEOUT_MS: Long = 10
|
|
83
|
+
// Upper bound for an acked (write-with-response) fragment. The callback only
|
|
84
|
+
// fires after the firmware's ATT Write Response — at least one connection
|
|
85
|
+
// interval — so 10ms is far too short here: timing out early would let the
|
|
86
|
+
// worker issue the next write while this one is still outstanding, which the
|
|
87
|
+
// stack rejects (one GATT op at a time) and the fragment is dropped. This is a
|
|
88
|
+
// safety bound only; the ack normally arrives within an interval or two.
|
|
89
|
+
private const val WRITE_ACK_TIMEOUT_MS: Long = 400
|
|
90
|
+
|
|
91
|
+
private const val INITIAL_CONNECTION_DELAY_MS = 350L // Adjust this value as needed
|
|
92
|
+
// Window to let a queued DisconnectRequest flush to the glasses before we close GATT.
|
|
93
|
+
private const val DISCONNECT_FLUSH_DELAY_MS = 250L
|
|
94
|
+
private const val MICBEAT_INTERVAL_MS: Long = (1000 * 60) * 30; // micbeat every 30 minutes
|
|
95
|
+
|
|
96
|
+
private const val MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED: Int = 110
|
|
97
|
+
private const val MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES: Int = 120
|
|
98
|
+
private const val MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED: Int = 210
|
|
99
|
+
|
|
100
|
+
// actions of device or gatt
|
|
101
|
+
private const val MAIN_TASK_HANDLER_CODE_CONNECT_DEVICE: Int = 310
|
|
102
|
+
private const val MAIN_TASK_HANDLER_CODE_DISCONNECT_DEVICE: Int = 320
|
|
103
|
+
private const val MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE: Int = 350
|
|
104
|
+
private const val MAIN_TASK_HANDLER_CODE_CANCEL_RECONNECT_DEVICE: Int = 360
|
|
105
|
+
private const val MAIN_TASK_HANDLER_CODE_RECONNECT_GATT: Int = 370
|
|
106
|
+
private const val MAIN_TASK_HANDLER_CODE_SCAN_START: Int = 410
|
|
107
|
+
private const val MAIN_TASK_HANDLER_CODE_SCAN_END: Int = 420
|
|
108
|
+
|
|
109
|
+
// actions of NEX Glasses
|
|
110
|
+
private const val MAIN_TASK_HANDLER_CODE_BATTERY_QUERY: Int = 620
|
|
111
|
+
private const val MAIN_TASK_HANDLER_CODE_HEART_BEAT: Int = 630
|
|
112
|
+
|
|
113
|
+
// Fixed rect for the live-captions box (G2-style): a single full-canvas text container
|
|
114
|
+
// reused across caption updates, so each caption is an in-place text change that coexists
|
|
115
|
+
// with any bitmap/other components. Matches the 500x220 Glassly display canvas.
|
|
116
|
+
private const val CAPTION_BOX_X: Int = 0
|
|
117
|
+
private const val CAPTION_BOX_Y: Int = 0
|
|
118
|
+
private const val CAPTION_BOX_W: Int = 500
|
|
119
|
+
private const val CAPTION_BOX_H: Int = 220
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private var heartbeatCount: Int = 0;
|
|
123
|
+
private var micBeatCount: Int = 0;
|
|
124
|
+
|
|
125
|
+
private var context: Context? = null
|
|
126
|
+
// private var isDebug: Boolean = true
|
|
127
|
+
|
|
128
|
+
// Off by default; toggled from Nex Developer Settings via the nex_lc3_audio_playback flag.
|
|
129
|
+
private val isLc3AudioEnabled: Boolean
|
|
130
|
+
get() = DeviceStore.get("bluetooth", "nex_lc3_audio_playback") as? Boolean ?: false
|
|
131
|
+
private var lc3AudioPlayer: Lc3Player? = null
|
|
132
|
+
|
|
133
|
+
private var lc3DecoderPtr: Long = 0
|
|
134
|
+
|
|
135
|
+
@Volatile private var isImageSendProgressing: Boolean = false
|
|
136
|
+
|
|
137
|
+
private var mainDevice: BluetoothDevice? = null
|
|
138
|
+
private var bluetoothAdapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
139
|
+
|
|
140
|
+
private var isScanningForCompatibleDevices: Boolean = false
|
|
141
|
+
|
|
142
|
+
private var isMainConnected = false
|
|
143
|
+
private var isKilled: Boolean = false
|
|
144
|
+
private var lastConnectionTimestamp: Long = 0
|
|
145
|
+
private var lastSendTimestamp: Long = 0
|
|
146
|
+
private var mainReconnectAttempts: Int = 0
|
|
147
|
+
|
|
148
|
+
private var lastReceivedLc3Sequence: Int = -1
|
|
149
|
+
|
|
150
|
+
private val fontLoader: G1FontLoaderKt = G1FontLoaderKt()
|
|
151
|
+
private val textRenderer: G1Text = G1Text()
|
|
152
|
+
private var preferredMainDeviceId: String? = null
|
|
153
|
+
private var savedNexMainName: String? = null
|
|
154
|
+
private var savedNexMainAddress: String? = null
|
|
155
|
+
|
|
156
|
+
private var mainGlassGatt: BluetoothGatt? = null
|
|
157
|
+
private var mainWriteChar: BluetoothGattCharacteristic? = null
|
|
158
|
+
private var mainNotifyChar: BluetoothGattCharacteristic? = null
|
|
159
|
+
private var currentMTU: Int = 0
|
|
160
|
+
private var deviceMaxMTU: Int = 0
|
|
161
|
+
|
|
162
|
+
private var maxChunkSize: Int = MAX_CHUNK_SIZE_DEFAULT // Maximum chunk size for BLE packets
|
|
163
|
+
private var bmpChunkSize: Int = 194 // BMP chunk size
|
|
164
|
+
private var protobufSeq: Int = 0 // Rolling sequence for fragmented control messages
|
|
165
|
+
|
|
166
|
+
@Volatile private var isWorkerRunning = false
|
|
167
|
+
// Queue to hold pending requests
|
|
168
|
+
private val sendQueue = LinkedBlockingQueue<Array<SendRequest>>()
|
|
169
|
+
private val mainWaiter = BooleanWaiter()
|
|
170
|
+
private val mainServicesWaiter = BooleanWaiter()
|
|
171
|
+
|
|
172
|
+
private var shouldUseGlassesMic: Boolean = false
|
|
173
|
+
private var microphoneStateBeforeDisconnection: Boolean = false
|
|
174
|
+
|
|
175
|
+
private var whiteListedAlready: Boolean = false
|
|
176
|
+
private var isMicrophoneEnabled: Boolean = true
|
|
177
|
+
private var lastThingDisplayedWasAnImage: Boolean = false
|
|
178
|
+
private var isScanning: Boolean = false
|
|
179
|
+
|
|
180
|
+
private var protobufVersionPosted: Boolean = false
|
|
181
|
+
|
|
182
|
+
private var bleScanCallback: ScanCallback? = null
|
|
183
|
+
|
|
184
|
+
private val modernScanCallback = object : ScanCallback() {
|
|
185
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
186
|
+
val device = result.getDevice()
|
|
187
|
+
val name = device.getName()
|
|
188
|
+
|
|
189
|
+
// Now you can reference the bluetoothAdapter field if needed:
|
|
190
|
+
if (!bluetoothAdapter.isEnabled) {
|
|
191
|
+
Log.e(TAG, "Bluetooth is disabled")
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
Bridge.log("=== New Device is Found ===$name ${device.address}")
|
|
195
|
+
Bridge.log("=== New Glasses Device Information ===")
|
|
196
|
+
|
|
197
|
+
// Log all available device information for debugging
|
|
198
|
+
Bridge.log("=== Device Information ===")
|
|
199
|
+
Bridge.log("Device Name: $name")
|
|
200
|
+
Bridge.log("Device Address: ${device.address}")
|
|
201
|
+
Bridge.log("Device Type: ${device.type}")
|
|
202
|
+
Bridge.log("Device Class: ${device.bluetoothClass}")
|
|
203
|
+
Bridge.log("Bond State: ${device.bondState}")
|
|
204
|
+
|
|
205
|
+
if (name == null || (!name.contains("Nex1-") && !name.contains("MENTRA_DISPLAY_"))) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// If we already have saved device names for main...
|
|
210
|
+
if (name != null && preferredMainDeviceId != null) {
|
|
211
|
+
if (!name.contains(preferredMainDeviceId!!)) {
|
|
212
|
+
return // Not a matching device
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Identify which side (main)
|
|
217
|
+
stopScan()
|
|
218
|
+
mainDevice = device
|
|
219
|
+
mainTaskHandler.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0) // 1 second delay
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
override fun onScanFailed(errorCode: Int) {
|
|
223
|
+
Log.e(TAG,"Scan failed with error: $errorCode")
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
init {
|
|
228
|
+
context = Bridge.getContext()
|
|
229
|
+
// isDebug = isDebug(context)
|
|
230
|
+
type = DeviceTypes.NEX
|
|
231
|
+
hasMic = true
|
|
232
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
233
|
+
preferredMainDeviceId = DeviceManager.getInstance().deviceName
|
|
234
|
+
|
|
235
|
+
// Initialize LC3 audio player
|
|
236
|
+
lc3AudioPlayer = Lc3Player(context)
|
|
237
|
+
lc3AudioPlayer?.init()
|
|
238
|
+
if (isLc3AudioEnabled) {
|
|
239
|
+
lc3AudioPlayer?.startPlay()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Start/stop the LC3 player when the nex_lc3_audio_playback flag changes. */
|
|
244
|
+
override fun applyNexAudioPlaybackSetting() {
|
|
245
|
+
if (isLc3AudioEnabled) {
|
|
246
|
+
Bridge.log("Nex: LC3 audio playback enabled - starting player")
|
|
247
|
+
lc3AudioPlayer?.startPlay()
|
|
248
|
+
} else {
|
|
249
|
+
Bridge.log("Nex: LC3 audio playback disabled - stopping player")
|
|
250
|
+
lc3AudioPlayer?.stopPlay()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private val random: Random = Random()
|
|
255
|
+
|
|
256
|
+
private val mainGattCallback: BluetoothGattCallback = createGattCallback()
|
|
257
|
+
|
|
258
|
+
private var mainTaskHandler: Handler = Handler(Looper.getMainLooper(), Handler.Callback(::handleMainTaskMessage))
|
|
259
|
+
private var whiteListHandler: Handler = Handler()
|
|
260
|
+
private var micEnableHandler: Handler = Handler()
|
|
261
|
+
private var micBeatHandler: Handler = Handler()
|
|
262
|
+
private var micBeatRunnable: Runnable? = null
|
|
263
|
+
private var notificationHandler: Handler = Handler()
|
|
264
|
+
private var notificationRunnable: Runnable? = null
|
|
265
|
+
private var textWallHandler: Handler = Handler()
|
|
266
|
+
private val textWallRunnable: Runnable? = null
|
|
267
|
+
private var findCompatibleDevicesHandler: Handler? = null
|
|
268
|
+
|
|
269
|
+
private var lastHeartbeatSentTime: Long = 0
|
|
270
|
+
private var lastHeartbeatReceivedTime: Long = 0
|
|
271
|
+
|
|
272
|
+
private var currentImageChunks: MutableList<ByteArray> = mutableListOf()
|
|
273
|
+
|
|
274
|
+
// Data class to represent a send request
|
|
275
|
+
// ack=true issues the fragment as a write-WITH-response (WRITE_TYPE_DEFAULT): the
|
|
276
|
+
// firmware ATT-acks each write, so the worker knows it landed. ack=false is a
|
|
277
|
+
// no-response write (WRITE_TYPE_NO_RESPONSE) — fire-and-forget, used for captions
|
|
278
|
+
// where the newest frame supersedes the last and throughput matters more than
|
|
279
|
+
// per-packet delivery. See processQueue() for how the write type is applied.
|
|
280
|
+
private data class SendRequest( val data: ByteArray, var waitTime: Int = -1, val ack: Boolean = true) {
|
|
281
|
+
override fun equals(other: Any?): Boolean {
|
|
282
|
+
if (this === other) return true
|
|
283
|
+
if (javaClass != other?.javaClass) return false
|
|
284
|
+
|
|
285
|
+
other as SendRequest
|
|
286
|
+
|
|
287
|
+
if (!data.contentEquals(other.data)) return false
|
|
288
|
+
if (waitTime != other.waitTime) return false
|
|
289
|
+
if (ack != other.ack) return false
|
|
290
|
+
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
override fun hashCode(): Int {
|
|
295
|
+
var result = data.contentHashCode()
|
|
296
|
+
result = 31 * result + waitTime
|
|
297
|
+
result = 31 * result + ack.hashCode()
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private class BooleanWaiter {
|
|
303
|
+
@Volatile
|
|
304
|
+
private var flag = true // initially true
|
|
305
|
+
|
|
306
|
+
@Synchronized
|
|
307
|
+
fun waitWhileTrue() {
|
|
308
|
+
while (flag) {
|
|
309
|
+
(this as Object).wait()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Bounded wait: returns early if the flag is still set after [timeoutMs]. */
|
|
314
|
+
@Synchronized
|
|
315
|
+
fun waitWhileTrue(timeoutMs: Long) {
|
|
316
|
+
val deadline = System.currentTimeMillis() + timeoutMs
|
|
317
|
+
while (flag) {
|
|
318
|
+
val remaining = deadline - System.currentTimeMillis()
|
|
319
|
+
if (remaining <= 0) break
|
|
320
|
+
(this as Object).wait(remaining)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@Synchronized
|
|
325
|
+
fun setTrue() {
|
|
326
|
+
flag = true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@Synchronized
|
|
330
|
+
fun setFalse() {
|
|
331
|
+
flag = false
|
|
332
|
+
(this as Object).notifyAll()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Network Management
|
|
337
|
+
override fun requestWifiScan (scanId: String?) { Bridge.log("Nex: requestWifiScan operation not supported") }
|
|
338
|
+
override fun sendWifiCredentials(ssid: String, password: String) { Bridge.log("Nex: sendWifiCredentials operation not supported") }
|
|
339
|
+
override fun forgetWifiNetwork(ssid: String) { }
|
|
340
|
+
override fun sendHotspotState(enabled: Boolean) { Bridge.log("Nex: sendHotspotState operation not supported") }
|
|
341
|
+
|
|
342
|
+
// Gallery: Not supported on Nex (No camera)
|
|
343
|
+
override fun queryGalleryStatus() { Bridge.log("Nex: queryGalleryStatus operation not supported") }
|
|
344
|
+
override fun sendGalleryMode() { Bridge.log("Nex: sendGalleryMode operation not supported") }
|
|
345
|
+
|
|
346
|
+
override fun sendVoiceActivityDetectionSetting() {
|
|
347
|
+
val enabled =
|
|
348
|
+
DeviceStore.get("bluetooth", "voice_activity_detection_enabled") as? Boolean
|
|
349
|
+
?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
|
|
350
|
+
Bridge.log("Nex: 🎤 Sending Voice Activity Detection setting to glasses: $enabled")
|
|
351
|
+
|
|
352
|
+
if (connectionState != ConnTypes.CONNECTED) {
|
|
353
|
+
Bridge.log("Nex: Cannot send VAD setting - not connected")
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
val bytes = NexProtobufUtils.generateVadEnabledRequestCommandBytes(enabled)
|
|
358
|
+
sendDataSequentially(bytes)
|
|
359
|
+
Bridge.sendVoiceActivityDetectionStatus(enabled)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Version info: Not supported on Nex (uses protobuf for version info)
|
|
363
|
+
override fun requestVersionInfo() { Bridge.log("Nex: requestVersionInfo operation not supported") }
|
|
364
|
+
|
|
365
|
+
// Camera & Media: Not supported on Nex (No camera)
|
|
366
|
+
override fun requestPhoto(request: PhotoRequest) {
|
|
367
|
+
Bridge.log("Nex: requestPhoto operation not supported")
|
|
368
|
+
}
|
|
369
|
+
override fun startStream(message: MutableMap<String, Any>) { Bridge.log("Nex: startStream operation not supported") }
|
|
370
|
+
override fun stopStream() { Bridge.log("Nex: stopStream operation not supported") }
|
|
371
|
+
override fun sendStreamKeepAlive(message: MutableMap<String, Any>) { Bridge.log("Nex: sendStreamKeepAlive operation not supported") }
|
|
372
|
+
override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) { Bridge.log("Nex: startVideoRecording operation not supported") }
|
|
373
|
+
override fun stopVideoRecording(requestId: String) { Bridge.log("Nex: stopVideoRecording operation not supported") }
|
|
374
|
+
|
|
375
|
+
// Button Settings: Not supported on Nex
|
|
376
|
+
override fun sendButtonPhotoSettings() { Bridge.log("Nex: sendButtonPhotoSettings operation not supported") }
|
|
377
|
+
override fun sendButtonVideoRecordingSettings() { Bridge.log("Nex: sendButtonVideoRecordingSettings operation not supported") }
|
|
378
|
+
override fun sendButtonMaxRecordingTime() { Bridge.log("Nex: sendButtonMaxRecordingTime operation not supported") }
|
|
379
|
+
|
|
380
|
+
override fun sendCameraFovSetting() { Bridge.log("Nex: sendCameraFovSetting operation not supported") }
|
|
381
|
+
|
|
382
|
+
override fun sendUserEmailToGlasses(email: String) { Bridge.log("Nex: sendUserEmailToGlasses operation not supported") }
|
|
383
|
+
|
|
384
|
+
override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
|
|
385
|
+
Bridge.log("Nex: sendIncidentId operation not supported")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Connection
|
|
389
|
+
override fun findCompatibleDevices() {
|
|
390
|
+
if (isScanningForCompatibleDevices) {
|
|
391
|
+
Bridge.log("Scan already in progress, skipping...")
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
isScanningForCompatibleDevices = true
|
|
396
|
+
val scanner = bluetoothAdapter.bluetoothLeScanner ?: run {
|
|
397
|
+
Log.e(TAG, "BluetoothLeScanner not available")
|
|
398
|
+
isScanningForCompatibleDevices = false
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
val foundDeviceNames = mutableListOf<String>()
|
|
403
|
+
if (findCompatibleDevicesHandler == null) {
|
|
404
|
+
findCompatibleDevicesHandler = Handler(Looper.getMainLooper())
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Optional: add filters if you want to narrow the scan
|
|
408
|
+
val filters = mutableListOf<ScanFilter>()
|
|
409
|
+
val settings = ScanSettings.Builder()
|
|
410
|
+
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
|
|
411
|
+
.build()
|
|
412
|
+
|
|
413
|
+
// Create a modern ScanCallback instead of the deprecated LeScanCallback
|
|
414
|
+
bleScanCallback = object : ScanCallback() {
|
|
415
|
+
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
416
|
+
val device = result.device
|
|
417
|
+
val name = device.name
|
|
418
|
+
val address = device.address
|
|
419
|
+
|
|
420
|
+
name?.let {
|
|
421
|
+
if (it.startsWith("Nex1-") || it.startsWith("MENTRA_DISPLAY_")) {
|
|
422
|
+
Bridge.log("bleScanCallback onScanResult: $name address $address")
|
|
423
|
+
synchronized(foundDeviceNames) {
|
|
424
|
+
if (!foundDeviceNames.contains(it)) {
|
|
425
|
+
foundDeviceNames.add(it)
|
|
426
|
+
Bridge.log("Found smart glasses: $name")
|
|
427
|
+
Bridge.sendDiscoveredDevice(DeviceTypes.NEX, it);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
override fun onBatchScanResults(results: List<ScanResult>) {
|
|
435
|
+
// If needed, handle batch results here
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
override fun onScanFailed(errorCode: Int) {
|
|
439
|
+
Log.e(TAG, "BLE scan failed with code: $errorCode")
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Start scanning
|
|
444
|
+
scanner.startScan(filters, settings, bleScanCallback)
|
|
445
|
+
Bridge.log("Started scanning for smart glasses with BluetoothLeScanner...")
|
|
446
|
+
scanner.flushPendingScanResults(bleScanCallback)
|
|
447
|
+
|
|
448
|
+
// Stop scanning after 10 seconds (adjust as needed)
|
|
449
|
+
findCompatibleDevicesHandler?.postDelayed({
|
|
450
|
+
bleScanCallback?.let { scanner.stopScan(it) }
|
|
451
|
+
isScanningForCompatibleDevices = false
|
|
452
|
+
bleScanCallback = null
|
|
453
|
+
Bridge.log("Stopped scanning for smart glasses.")
|
|
454
|
+
}, 10000)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
override fun connectById(id: String) {
|
|
458
|
+
preferredMainDeviceId = id
|
|
459
|
+
connectToSmartGlasses()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
override fun getConnectedBluetoothName(): String {
|
|
463
|
+
return mainDevice?.getName() ?: ""
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
override fun disconnect() {
|
|
467
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
468
|
+
destroy()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
override fun forget() {
|
|
472
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
473
|
+
destroy()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
override fun cleanup() {
|
|
477
|
+
// TODO: Later
|
|
478
|
+
Bridge.log("To be implemented")
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Device
|
|
482
|
+
override fun setHeadUpAngle(angle: Int) {
|
|
483
|
+
// Validate headUpAngle range (0 ~ 60)
|
|
484
|
+
val validatedAngle = angle.coerceIn(0, 60)
|
|
485
|
+
Bridge.log("Nex: === SENDING HEAD UP ANGLE COMMAND TO GLASSES ===")
|
|
486
|
+
Bridge.log("Nex: Head Up Angle: $validatedAngle degrees (validated range: 0-60)")
|
|
487
|
+
val cmdBytes = NexProtobufUtils.generateHeadUpAngleConfigCommandBytes(angle)
|
|
488
|
+
sendProtobuf(cmdBytes, 10)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
override fun getBatteryStatus() {
|
|
492
|
+
queryBatteryStatus()
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
override fun exit() {
|
|
496
|
+
sendDataSequentially(byteArrayOf(0x18.toByte()), 100)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
override fun sendShutdown() {
|
|
500
|
+
Bridge.log("sendShutdown - not supported on Nex")
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
override fun sendReboot() {
|
|
504
|
+
Bridge.log("sendReboot - not supported on Nex")
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
override fun sendRgbLedControl(requestId: String, packageName: String?, action: String, color: String?, onDurationMs: Int, offDurationMs: Int, count: Int) {
|
|
508
|
+
Bridge.log("sendRgbLedControl - not supported on Nex");
|
|
509
|
+
Bridge.sendRgbLedControlResponse(requestId, false, "device_not_supported");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Not supported in Nex
|
|
513
|
+
override fun setSilentMode(enabled: Boolean) { Bridge.log("Nex: setSilentMode operation not supported") }
|
|
514
|
+
|
|
515
|
+
// Display
|
|
516
|
+
override fun setBrightness(level: Int, autoMode: Boolean) {
|
|
517
|
+
// TODO: test this logic. Is it correct? Should we send both or just one?
|
|
518
|
+
Bridge.log("Nex: setBrightness() - level: " + level + "%, autoMode: " + autoMode);
|
|
519
|
+
val brightnessCmdBytes = NexProtobufUtils.generateBrightnessConfigCommandBytes(level)
|
|
520
|
+
sendProtobuf(brightnessCmdBytes, 10)
|
|
521
|
+
|
|
522
|
+
val autoBrightnessCmdBytes = NexProtobufUtils.generateAutoBrightnessConfigCommandBytes(autoMode)
|
|
523
|
+
sendProtobuf(autoBrightnessCmdBytes, 10)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
override fun clearDisplay() {
|
|
527
|
+
Bridge.log("Nex: clearDisplay() - clearing canvas + caption");
|
|
528
|
+
// Tear down any canvas components (bitmap + positioned text) and forget the element mapping
|
|
529
|
+
// + active layout, then clear the caption text path too so either kind of screen is wiped.
|
|
530
|
+
canvasElements.clear()
|
|
531
|
+
currentLayoutId = null
|
|
532
|
+
sendProtobuf(NexProtobufUtils.generateCanvasClearCommandBytes(), 10)
|
|
533
|
+
val clearDisplayPackets = NexProtobufUtils.generateClearDisplayRequestCommandBytes()
|
|
534
|
+
sendProtobuf(clearDisplayPackets, 10)
|
|
535
|
+
Bridge.log("Nex: clearDisplay() - sent");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
override fun sendText(text: String) {
|
|
540
|
+
Bridge.log("Nex: sendText() - text: " + text);
|
|
541
|
+
showCaptionText(text)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
override fun sendTextWall(text: String) {
|
|
545
|
+
Bridge.log("Nex: sendTextWall() - text: " + text);
|
|
546
|
+
showCaptionText(text)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
override fun sendDoubleTextWall(top: String, bottom: String) {
|
|
550
|
+
Bridge.log("Nex: sendDoubleTextWall() - top: " + top + ", bottom: " + bottom);
|
|
551
|
+
val finalText: String = listOf(top, bottom).filter { it.isNotEmpty() }.joinToString("\n")
|
|
552
|
+
showCaptionText(finalText)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Live captions / text walls render into a single reused canvas text container (the caption
|
|
556
|
+
// box), like Even G2: the first call creates it, each subsequent call is an in-place text
|
|
557
|
+
// update on the same component (reused by rect via sendPositionedText), so captions can sit
|
|
558
|
+
// alongside a navigation minimap or other canvas components instead of owning the screen.
|
|
559
|
+
private fun showCaptionText(text: String) {
|
|
560
|
+
// Captions are high-frequency, replace-in-place updates where the newest frame
|
|
561
|
+
// supersedes the last, so they go out WITHOUT ATT acks (write-no-response) for
|
|
562
|
+
// throughput — every other command uses acked writes. Route through upsertText
|
|
563
|
+
// directly (rather than the public sendPositionedText override, which can't take
|
|
564
|
+
// an ack flag) using the same rect-synthesized element id so reuse still works.
|
|
565
|
+
val elementId = "text@$CAPTION_BOX_X,$CAPTION_BOX_Y,${CAPTION_BOX_W}x$CAPTION_BOX_H,0,0"
|
|
566
|
+
upsertText(elementId, null, text, CAPTION_BOX_X, CAPTION_BOX_Y, CAPTION_BOX_W, CAPTION_BOX_H, 0, 0, ack = false)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---- Canvas text containers (G2-style) -------------------------------------------------
|
|
570
|
+
// Positioned text is drawn as canvas TEXTBOX components so it coexists with the bitmap
|
|
571
|
+
// component (a navigation screen = minimap + several text boxes shown together). Components
|
|
572
|
+
// are keyed by their rect+border and reused so repeated updates don't recreate the LVGL
|
|
573
|
+
// object; ids come from the firmware's text pool (1..6) with oldest-out eviction.
|
|
574
|
+
// Retained-mode canvas registry: elementId -> firmware component. The layout path (nav HUD)
|
|
575
|
+
// passes explicit element ids; legacy callers (captions, one-off bitmaps) synthesize an id from
|
|
576
|
+
// their rect, so reuse/update works for them through the same path. Text ids come from the
|
|
577
|
+
// firmware pool 1..6, bitmaps from 10..13.
|
|
578
|
+
private data class CanvasElement(val firmwareId: Int, val isBitmap: Boolean, val rect: String)
|
|
579
|
+
private val canvasElements = LinkedHashMap<String, CanvasElement>()
|
|
580
|
+
private val canvasTextIdPool: List<Int> = listOf(1, 2, 3, 4, 5, 6)
|
|
581
|
+
private val canvasBitmapIdPool: List<Int> = listOf(10, 11, 12, 13)
|
|
582
|
+
// Active layout/scene id. A display call carrying a DIFFERENT layoutId clears the previous scene
|
|
583
|
+
// first (retained-mode contract); legacy calls (null layoutId) leave it untouched.
|
|
584
|
+
private var currentLayoutId: String? = null
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Handle a scene/layout change. DeviceManager sweeps the previous app's
|
|
588
|
+
* elements before a cross-app frame arrives, so the registry is usually
|
|
589
|
+
* empty here and switching costs nothing. Only when stale components remain
|
|
590
|
+
* (e.g. legacy callers with no sweep) do we delete them — individually, NOT
|
|
591
|
+
* via CanvasClear: clear also exits the canvas VIEW (the first create
|
|
592
|
+
* re-activates it), which reads as a full-screen flash.
|
|
593
|
+
*/
|
|
594
|
+
private fun ensureLayout(layoutId: String?) {
|
|
595
|
+
if (layoutId != null && layoutId != currentLayoutId) {
|
|
596
|
+
if (canvasElements.isNotEmpty()) {
|
|
597
|
+
for (el in canvasElements.values) {
|
|
598
|
+
sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(el.firmwareId))
|
|
599
|
+
}
|
|
600
|
+
canvasElements.clear()
|
|
601
|
+
}
|
|
602
|
+
currentLayoutId = layoutId
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* A replay frame (reconnect / firmware recovery / re-dispatch) repaints from
|
|
608
|
+
* scratch: forget the registry so every element takes the CREATE path.
|
|
609
|
+
* Create-on-existing-id is replace in firmware, so repainting over a live
|
|
610
|
+
* canvas is safe, and updates-to-dead-ids (which firmware drops silently)
|
|
611
|
+
* can never happen.
|
|
612
|
+
*/
|
|
613
|
+
override fun onSceneReplay(appId: String) {
|
|
614
|
+
canvasElements.clear()
|
|
615
|
+
currentLayoutId = appId
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Free firmware id from the type's pool. When the pool is exhausted the
|
|
620
|
+
* OLDEST registered element of that type is evicted for real: its firmware
|
|
621
|
+
* component is deleted and its id reused (LinkedHashMap preserves insertion
|
|
622
|
+
* order).
|
|
623
|
+
*/
|
|
624
|
+
private fun allocFirmwareId(isBitmap: Boolean): Int? {
|
|
625
|
+
val pool = if (isBitmap) canvasBitmapIdPool else canvasTextIdPool
|
|
626
|
+
val used = canvasElements.values.filter { it.isBitmap == isBitmap }.map { it.firmwareId }.toSet()
|
|
627
|
+
pool.firstOrNull { it !in used }?.let {
|
|
628
|
+
return it
|
|
629
|
+
}
|
|
630
|
+
// Pool full — evict the oldest same-type element.
|
|
631
|
+
val oldest = canvasElements.entries.firstOrNull { it.value.isBitmap == isBitmap } ?: return null
|
|
632
|
+
Bridge.log("Nex: pool full — evicting oldest ${if (isBitmap) "bitmap" else "text"} element '${oldest.key}' (fw id ${oldest.value.firmwareId})")
|
|
633
|
+
sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(oldest.value.firmwareId))
|
|
634
|
+
canvasElements.remove(oldest.key)
|
|
635
|
+
return oldest.value.firmwareId
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Legacy positioned text: identity is the rect, so repeated updates reuse the component.
|
|
639
|
+
override fun sendPositionedText(
|
|
640
|
+
text: String, x: Int, y: Int, width: Int, height: Int, borderWidth: Int, borderRadius: Int
|
|
641
|
+
) {
|
|
642
|
+
upsertText("text@$x,$y,${width}x$height,$borderWidth,$borderRadius", null,
|
|
643
|
+
text, x, y, width, height, borderWidth, borderRadius)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
override fun drawLayoutText(
|
|
647
|
+
text: String, x: Int, y: Int, width: Int, height: Int,
|
|
648
|
+
borderWidth: Int, borderRadius: Int, elementId: String, layoutId: String?
|
|
649
|
+
) {
|
|
650
|
+
upsertText(elementId, layoutId, text, x, y, width, height, borderWidth, borderRadius)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private fun upsertText(
|
|
654
|
+
elementId: String, layoutId: String?, text: String,
|
|
655
|
+
x: Int, y: Int, width: Int, height: Int, borderWidth: Int, borderRadius: Int,
|
|
656
|
+
ack: Boolean = true
|
|
657
|
+
) {
|
|
658
|
+
ensureLayout(layoutId)
|
|
659
|
+
val rect = "$x,$y,${width}x$height,$borderWidth,$borderRadius"
|
|
660
|
+
val existing = canvasElements[elementId]
|
|
661
|
+
if (existing != null && !existing.isBitmap) {
|
|
662
|
+
if (existing.rect != rect) {
|
|
663
|
+
// Geometry moved/resized — recreate the box at the SAME firmware id, then set text.
|
|
664
|
+
sendProtobuf(NexProtobufUtils.generateCanvasCreateTextboxCommandBytes(
|
|
665
|
+
existing.firmwareId, x, y, width, height, borderWidth, borderRadius), ack = ack)
|
|
666
|
+
canvasElements[elementId] = existing.copy(rect = rect)
|
|
667
|
+
}
|
|
668
|
+
sendProtobuf(NexProtobufUtils.generateCanvasUpdateTextCommandBytes(existing.firmwareId, text), ack = ack)
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
val fid = allocFirmwareId(false)
|
|
672
|
+
if (fid == null) {
|
|
673
|
+
Bridge.log("Nex: text pool full (>6) — dropping element '$elementId'")
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
canvasElements[elementId] = CanvasElement(fid, false, rect)
|
|
677
|
+
sendProtobuf(NexProtobufUtils.generateCanvasCreateTextboxCommandBytes(
|
|
678
|
+
fid, x, y, width, height, borderWidth, borderRadius), ack = ack)
|
|
679
|
+
sendProtobuf(NexProtobufUtils.generateCanvasUpdateTextCommandBytes(fid, text), ack = ack)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
override fun removeLayoutElement(elementId: String, layoutId: String?) {
|
|
683
|
+
val slot = canvasElements.remove(elementId) ?: return
|
|
684
|
+
sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(slot.firmwareId))
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private data class NexBmp(val data: ByteArray, val w: Int, val h: Int)
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Decode a base64 image to the glasses-native 1-bit BMP at the requested size. The panel is
|
|
691
|
+
* 1-bpp and renders white-on-black, so we scale to the rect, invert, then Floyd–Steinberg dither
|
|
692
|
+
* (preserving gradients as dot patterns instead of a hard 50% threshold) before the 1-bit encode.
|
|
693
|
+
*/
|
|
694
|
+
private fun decodeBitmapForNex(base64ImageData: String, width: Int?, height: Int?): NexBmp? {
|
|
695
|
+
return try {
|
|
696
|
+
val bmpData = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)
|
|
697
|
+
if (bmpData == null || bmpData.isEmpty()) {
|
|
698
|
+
Log.e(TAG, "Failed to decode base64 image data")
|
|
699
|
+
return null
|
|
700
|
+
}
|
|
701
|
+
val bmp = BitmapFactory.decodeByteArray(bmpData, 0, bmpData.size)
|
|
702
|
+
val destW = width ?: bmp.width
|
|
703
|
+
val destH = height ?: bmp.height
|
|
704
|
+
val scaled = Bitmap.createScaledBitmap(bmp, destW, destH, true)
|
|
705
|
+
val inverted = invertBitmap(scaled)
|
|
706
|
+
val dithered = floydSteinbergDither(inverted)
|
|
707
|
+
NexBmp(BitmapJavaUtils.convertBitmapTo1BitBmpBytes(dithered, false), destW, destH)
|
|
708
|
+
} catch (e: Exception) {
|
|
709
|
+
Log.e(TAG, "Error decoding bitmap: " + e.message)
|
|
710
|
+
null
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Legacy one-off bitmap: identity is the rect (no layout scene).
|
|
715
|
+
override fun displayBitmap(
|
|
716
|
+
base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?
|
|
717
|
+
): Boolean {
|
|
718
|
+
val nb = decodeBitmapForNex(base64ImageData, width, height) ?: return false
|
|
719
|
+
upsertBitmap("bmp@${x ?: 0},${y ?: 0},${nb.w}x${nb.h}", null, nb.data, x ?: 0, y ?: 0, nb.w, nb.h)
|
|
720
|
+
return true
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
override fun drawLayoutBitmap(
|
|
724
|
+
base64ImageData: String, x: Int, y: Int, width: Int, height: Int,
|
|
725
|
+
elementId: String, layoutId: String?
|
|
726
|
+
): Boolean {
|
|
727
|
+
val nb = decodeBitmapForNex(base64ImageData, width, height) ?: return false
|
|
728
|
+
upsertBitmap(elementId, layoutId, nb.data, x, y, nb.w, nb.h)
|
|
729
|
+
return true
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private fun upsertBitmap(
|
|
733
|
+
elementId: String, layoutId: String?, bmpData: ByteArray, x: Int, y: Int, w: Int, h: Int
|
|
734
|
+
) {
|
|
735
|
+
ensureLayout(layoutId)
|
|
736
|
+
val rect = "$x,$y,${w}x$h"
|
|
737
|
+
val existing = canvasElements[elementId]
|
|
738
|
+
val fid: Int
|
|
739
|
+
if (existing != null && existing.isBitmap) {
|
|
740
|
+
fid = existing.firmwareId
|
|
741
|
+
if (existing.rect != rect) {
|
|
742
|
+
sendProtobuf(NexProtobufUtils.generateCanvasCreateBitmapCommandBytes(fid, x, y, w, h))
|
|
743
|
+
canvasElements[elementId] = existing.copy(rect = rect)
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
val alloc = allocFirmwareId(true)
|
|
747
|
+
if (alloc == null) {
|
|
748
|
+
Bridge.log("Nex: bitmap pool full (>4) — dropping element '$elementId'")
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
fid = alloc
|
|
752
|
+
canvasElements[elementId] = CanvasElement(fid, true, rect)
|
|
753
|
+
sendProtobuf(NexProtobufUtils.generateCanvasCreateBitmapCommandBytes(fid, x, y, w, h))
|
|
754
|
+
}
|
|
755
|
+
streamBitmapPixels(fid, bmpData)
|
|
756
|
+
}
|
|
757
|
+
override fun showDashboard() {
|
|
758
|
+
exit()
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
override fun setDashboardPosition(height: Int, depth: Int) {
|
|
762
|
+
Bridge.log("Nex: setDashboardPosition() - height: " + height + ", depth: " + depth)
|
|
763
|
+
// Send display_height then display_distance; adjust order if Nex firmware requires otherwise.
|
|
764
|
+
val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
|
|
765
|
+
sendProtobuf(heightBytes, 10)
|
|
766
|
+
val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
|
|
767
|
+
val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
|
|
768
|
+
sendProtobuf(distanceBytes, 10)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
override fun setDashboardHeightOnly(height: Int) {
|
|
772
|
+
Bridge.log("Nex: setDashboardHeightOnly() - height: $height")
|
|
773
|
+
val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
|
|
774
|
+
sendProtobuf(heightBytes, 10)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
override fun setDashboardDepthOnly(depth: Int) {
|
|
778
|
+
Bridge.log("Nex: setDashboardDepthOnly() - depth: $depth")
|
|
779
|
+
val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
|
|
780
|
+
val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
|
|
781
|
+
sendProtobuf(distanceBytes, 10)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
override fun ping() {
|
|
785
|
+
Bridge.log("Nex: ping()");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
override fun dbg1() {}
|
|
789
|
+
override fun dbg2() {}
|
|
790
|
+
|
|
791
|
+
// Audio Control
|
|
792
|
+
// TODO: Validate this logic. looks weird.
|
|
793
|
+
override fun setMicEnabled(enabled: Boolean) {
|
|
794
|
+
Bridge.log("Nex: setMicEnabled(): $enabled")
|
|
795
|
+
// Update the shouldUseGlassesMic flag to reflect the current state
|
|
796
|
+
shouldUseGlassesMic = enabled
|
|
797
|
+
Bridge.log("Nex: Updated shouldUseGlassesMic to: $shouldUseGlassesMic")
|
|
798
|
+
if (enabled) {
|
|
799
|
+
Bridge.log("Nex: Microphone enabled, starting audio input handling")
|
|
800
|
+
startMicBeat(MICBEAT_INTERVAL_MS.toInt())
|
|
801
|
+
} else {
|
|
802
|
+
Bridge.log("Nex: Microphone disabled, stopping audio input handling")
|
|
803
|
+
stopMicBeat()
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
override fun sortMicRanking(list: MutableList<String>): MutableList<String> {
|
|
808
|
+
return list
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
//==================== HELPER FUNCTIONS =================================================
|
|
812
|
+
private fun createGattCallback(): BluetoothGattCallback {
|
|
813
|
+
return object : BluetoothGattCallback() {
|
|
814
|
+
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
815
|
+
Bridge.log("onConnectionStateChange status State ${status == BluetoothGatt.GATT_SUCCESS}")
|
|
816
|
+
Bridge.log("onConnectionStateChange connected State ${newState == BluetoothProfile.STATE_CONNECTED}")
|
|
817
|
+
|
|
818
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
819
|
+
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
|
820
|
+
Bridge.log("glass connected, discovering services...")
|
|
821
|
+
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
|
|
822
|
+
gatt.setPreferredPhy(
|
|
823
|
+
BluetoothDevice.PHY_LE_2M_MASK,
|
|
824
|
+
BluetoothDevice.PHY_LE_2M_MASK,
|
|
825
|
+
BluetoothDevice.PHY_OPTION_NO_PREFERRED
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
isMainConnected = true
|
|
829
|
+
mainReconnectAttempts = 0
|
|
830
|
+
Bridge.log("Both glasses connected. Stopping BLE scan.")
|
|
831
|
+
stopScan()
|
|
832
|
+
|
|
833
|
+
if (!isWorkerRunning) {
|
|
834
|
+
Bridge.log("Worker thread is not running. Starting it.")
|
|
835
|
+
startWorkerIfNeeded()
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
Bridge.log("Discover services calling...")
|
|
839
|
+
gatt.discoverServices()
|
|
840
|
+
updateConnectionState()
|
|
841
|
+
mainDevice?.let { device ->
|
|
842
|
+
savedNexMainName = device.name
|
|
843
|
+
savedNexMainAddress = device.address
|
|
844
|
+
// savePairedDeviceNames()
|
|
845
|
+
// savePairedDeviceAddress()
|
|
846
|
+
}
|
|
847
|
+
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
|
848
|
+
handleDisconnection(gatt)
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
handleConnectionFailure(gatt, status)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private fun handleDisconnection(gatt: BluetoothGatt) {
|
|
856
|
+
Bridge.log("glass disconnected, stopping heartbeats")
|
|
857
|
+
Bridge.log("Entering STATE_DISCONNECTED branch")
|
|
858
|
+
|
|
859
|
+
// The glasses lose their canvas on disconnect — forget the
|
|
860
|
+
// component registry so post-reconnect frames take the CREATE
|
|
861
|
+
// path (firmware silently drops updates to dead component ids).
|
|
862
|
+
// The host replays the current scene after reconnect.
|
|
863
|
+
canvasElements.clear()
|
|
864
|
+
currentLayoutId = null
|
|
865
|
+
|
|
866
|
+
// Save current microphone state before disconnection
|
|
867
|
+
microphoneStateBeforeDisconnection = isMicrophoneEnabled
|
|
868
|
+
Bridge.log("Saved microphone state before disconnection: $microphoneStateBeforeDisconnection")
|
|
869
|
+
|
|
870
|
+
// Reset protobuf version posted flag for next connection
|
|
871
|
+
protobufVersionPosted = false
|
|
872
|
+
|
|
873
|
+
// Reset chunk sizes to defaults
|
|
874
|
+
maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
875
|
+
bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
876
|
+
mainServicesWaiter.setTrue()
|
|
877
|
+
mainWaiter.setFalse()
|
|
878
|
+
Bridge.log("[BLE] Disconnect: reset mainServicesWaiter=true, mainWaiter=false (unblock any in-flight write). isWorkerRunning=$isWorkerRunning")
|
|
879
|
+
forceSideDisconnection()
|
|
880
|
+
Bridge.log("Called forceSideDisconnection()")
|
|
881
|
+
currentMTU = 0
|
|
882
|
+
|
|
883
|
+
// Stop any periodic transmissions
|
|
884
|
+
stopMicBeat()
|
|
885
|
+
sendQueue.clear()
|
|
886
|
+
Bridge.log("Stopped heartbeat monitoring and mic beat; cleared sendQueue")
|
|
887
|
+
updateConnectionState()
|
|
888
|
+
Bridge.log("Updated connection state after disconnection")
|
|
889
|
+
|
|
890
|
+
// gatt.device?.let {
|
|
891
|
+
// Bridge.log("Closing GATT connection for device: ${it.address}")
|
|
892
|
+
// gatt.disconnect()
|
|
893
|
+
// gatt.close()
|
|
894
|
+
// Bridge.log("GATT connection closed")
|
|
895
|
+
// } ?: run {
|
|
896
|
+
// Bridge.log("No GATT device available to disconnect")
|
|
897
|
+
// }
|
|
898
|
+
|
|
899
|
+
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 2000)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private fun handleConnectionFailure(gatt: BluetoothGatt, status: Int) {
|
|
903
|
+
// Same as handleDisconnection: the glasses lose their canvas —
|
|
904
|
+
// forget the component registry so post-reconnect frames take
|
|
905
|
+
// the CREATE path (fw silently drops updates to dead ids).
|
|
906
|
+
canvasElements.clear()
|
|
907
|
+
currentLayoutId = null
|
|
908
|
+
|
|
909
|
+
// Save current microphone state before connection failure
|
|
910
|
+
microphoneStateBeforeDisconnection = isMicrophoneEnabled
|
|
911
|
+
Bridge.log("Saved microphone state before connection failure: $microphoneStateBeforeDisconnection")
|
|
912
|
+
|
|
913
|
+
currentMTU = 0
|
|
914
|
+
maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
915
|
+
bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
916
|
+
Bridge.log("Unexpected connection state encountered for glass: $status")
|
|
917
|
+
stopMicBeat()
|
|
918
|
+
sendQueue.clear()
|
|
919
|
+
|
|
920
|
+
// Mark as not ready
|
|
921
|
+
mainServicesWaiter.setTrue()
|
|
922
|
+
mainWaiter.setFalse()
|
|
923
|
+
Bridge.log("[BLE] ConnectionFailure: reset mainServicesWaiter=true, mainWaiter=false (unblock any in-flight write). isWorkerRunning=$isWorkerRunning")
|
|
924
|
+
Bridge.log("Stopped heartbeat monitoring and mic beat; cleared sendQueue due to connection failure")
|
|
925
|
+
Bridge.log("glass connection failed with status: $status")
|
|
926
|
+
|
|
927
|
+
isMainConnected = false
|
|
928
|
+
mainReconnectAttempts++
|
|
929
|
+
|
|
930
|
+
mainGlassGatt?.let {
|
|
931
|
+
it.disconnect()
|
|
932
|
+
it.close()
|
|
933
|
+
mainGlassGatt = null
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
forceSideDisconnection()
|
|
937
|
+
Bridge.log("Called forceSideDisconnection() after connection failure")
|
|
938
|
+
Bridge.log("GATT connection disconnected and closed due to failure")
|
|
939
|
+
updateConnectionState()
|
|
940
|
+
|
|
941
|
+
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 2000)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private fun forceSideDisconnection() {
|
|
945
|
+
Bridge.log("forceSideDisconnection() called")
|
|
946
|
+
isMainConnected = false
|
|
947
|
+
mainReconnectAttempts++
|
|
948
|
+
Bridge.log("Main glass: Marked as disconnected and incremented mainReconnectAttempts to $mainReconnectAttempts")
|
|
949
|
+
|
|
950
|
+
mainGlassGatt?.let {
|
|
951
|
+
Bridge.log("Main glass GATT exists. Disconnecting and closing mainGlassGatt")
|
|
952
|
+
it.disconnect()
|
|
953
|
+
it.close()
|
|
954
|
+
mainGlassGatt = null
|
|
955
|
+
} ?: run {
|
|
956
|
+
Bridge.log("Main glass GATT is already null")
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
961
|
+
val statusBool = status == BluetoothGatt.GATT_SUCCESS
|
|
962
|
+
mainTaskHandler?.obtainMessage(
|
|
963
|
+
MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES,
|
|
964
|
+
statusBool
|
|
965
|
+
)?.sendToTarget()
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
override fun onCharacteristicWrite(
|
|
969
|
+
gatt: BluetoothGatt,
|
|
970
|
+
characteristic: BluetoothGattCharacteristic,
|
|
971
|
+
status: Int
|
|
972
|
+
) {
|
|
973
|
+
mainWaiter.setFalse()
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
|
|
977
|
+
val statusStr = if (status == BluetoothGatt.GATT_SUCCESS) "SUCCESS" else "FAILED($status)"
|
|
978
|
+
Bridge.log("[BLE] onDescriptorWrite: $statusStr — releasing mainServicesWaiter, worker can now send")
|
|
979
|
+
mainServicesWaiter.setFalse()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
983
|
+
val statusBool = status == BluetoothGatt.GATT_SUCCESS
|
|
984
|
+
Bridge.log("🔄 MTU Negotiation Result: Success=$statusBool, Device MTU=$mtu, Status=$status")
|
|
985
|
+
|
|
986
|
+
if (statusBool) {
|
|
987
|
+
// Store device capability and calculate actual negotiated MTU
|
|
988
|
+
deviceMaxMTU = mtu // Record what device actually supports
|
|
989
|
+
// The actual negotiated MTU is the minimum of our request and device capability
|
|
990
|
+
currentMTU = MTU_517.coerceAtMost(mtu)
|
|
991
|
+
|
|
992
|
+
Bridge.log("🎯 MTU Negotiation Complete:")
|
|
993
|
+
Bridge.log(" 📱 App Requested: $MTU_517 bytes")
|
|
994
|
+
Bridge.log(" 📡 Device Supports: $deviceMaxMTU bytes")
|
|
995
|
+
Bridge.log(" 🤝 Negotiated MTU: $currentMTU bytes")
|
|
996
|
+
|
|
997
|
+
// Calculate optimal chunk sizes based on negotiated MTU
|
|
998
|
+
maxChunkSize = currentMTU - 10
|
|
999
|
+
bmpChunkSize = currentMTU - 20 // BMP has more config bytes
|
|
1000
|
+
|
|
1001
|
+
Bridge.log("✅ MTU Configuration Complete:")
|
|
1002
|
+
Bridge.log(" 📊 Final MTU: $currentMTU bytes")
|
|
1003
|
+
Bridge.log(" 📦 Data Chunk Size: $maxChunkSize bytes")
|
|
1004
|
+
Bridge.log(" 🖼️ Image Chunk Size: $bmpChunkSize bytes")
|
|
1005
|
+
Bridge.log(" 🔧 Device Maximum: $deviceMaxMTU bytes")
|
|
1006
|
+
} else {
|
|
1007
|
+
Bridge.log("❌ MTU Request Failed - Status: $status, Requested: $mtu")
|
|
1008
|
+
|
|
1009
|
+
// Simple fallback strategy: 247 → 23
|
|
1010
|
+
if (mtu == MTU_517) {
|
|
1011
|
+
Bridge.log("🔄 247 bytes failed, trying default: $MTU_DEFAULT bytes...")
|
|
1012
|
+
Bridge.log("📤 Requesting MTU: $MTU_DEFAULT bytes (fallback)")
|
|
1013
|
+
gatt.requestMtu(MTU_DEFAULT)
|
|
1014
|
+
} else {
|
|
1015
|
+
Bridge.log("⚠️ All MTU requests failed, using defaults")
|
|
1016
|
+
currentMTU = MTU_DEFAULT
|
|
1017
|
+
deviceMaxMTU = MTU_DEFAULT
|
|
1018
|
+
maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
1019
|
+
bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
|
|
1020
|
+
|
|
1021
|
+
Bridge.log("📋 Fallback Configuration:")
|
|
1022
|
+
Bridge.log(" 📊 Default MTU: $MTU_DEFAULT bytes")
|
|
1023
|
+
Bridge.log(" 📦 Data Chunk Size: $maxChunkSize bytes")
|
|
1024
|
+
Bridge.log(" 🖼️ Image Chunk Size: $bmpChunkSize bytes")
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
|
1030
|
+
val msg = Message.obtain().apply {
|
|
1031
|
+
what = MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED
|
|
1032
|
+
obj = characteristic // Attach any object you want
|
|
1033
|
+
}
|
|
1034
|
+
mainTaskHandler.sendMessage(msg)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private fun handleMainTaskMessage(msg: Message): Boolean {
|
|
1040
|
+
val msgCode = msg.what
|
|
1041
|
+
// NOTE: do not log here. Every incoming BLE notification (including
|
|
1042
|
+
// ~50 audio packets/sec) is dispatched through this handler on the
|
|
1043
|
+
// main looper, and Bridge.log marshals an event across the RN bridge
|
|
1044
|
+
// to JS. Per-message logging here floods the main thread and backs up
|
|
1045
|
+
// the audio queue when the CPU downclocks at screen-off. (Matches G1,
|
|
1046
|
+
// which does not log per audio packet.)
|
|
1047
|
+
when (msgCode) {
|
|
1048
|
+
MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED -> {}
|
|
1049
|
+
MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES -> {
|
|
1050
|
+
val statusBool = msg.obj as? Boolean ?: false
|
|
1051
|
+
if (statusBool) {
|
|
1052
|
+
mainGlassGatt?.let { initNexGlasses(it) }
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED -> {
|
|
1056
|
+
val characteristic = msg.obj as? BluetoothGattCharacteristic
|
|
1057
|
+
characteristic?.let { onCharacteristicChangedHandler(it) }
|
|
1058
|
+
}
|
|
1059
|
+
MAIN_TASK_HANDLER_CODE_CONNECT_DEVICE -> {}
|
|
1060
|
+
MAIN_TASK_HANDLER_CODE_DISCONNECT_DEVICE -> {}
|
|
1061
|
+
MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE -> {
|
|
1062
|
+
mainDevice?.let { attemptGattConnection(it) }
|
|
1063
|
+
}
|
|
1064
|
+
MAIN_TASK_HANDLER_CODE_CANCEL_RECONNECT_DEVICE -> {}
|
|
1065
|
+
MAIN_TASK_HANDLER_CODE_RECONNECT_GATT -> {}
|
|
1066
|
+
MAIN_TASK_HANDLER_CODE_SCAN_START -> {}
|
|
1067
|
+
MAIN_TASK_HANDLER_CODE_SCAN_END -> {}
|
|
1068
|
+
MAIN_TASK_HANDLER_CODE_BATTERY_QUERY -> {
|
|
1069
|
+
queryBatteryStatus()
|
|
1070
|
+
}
|
|
1071
|
+
MAIN_TASK_HANDLER_CODE_HEART_BEAT -> {
|
|
1072
|
+
// Note: Heartbeat is now handled by receiving ping from glasses
|
|
1073
|
+
// This case is kept for backward compatibility but no longer used
|
|
1074
|
+
}
|
|
1075
|
+
else -> { }
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return true
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private fun initNexGlasses(gatt: BluetoothGatt) {
|
|
1082
|
+
// Start MTU discovery with our maximum target
|
|
1083
|
+
Bridge.log("Nex: 🔍 MTU Discovery: Requesting maximum MTU size: $MTU_517")
|
|
1084
|
+
Bridge.log("Nex: 🎯 Target: Use $MTU_517 bytes max, or $MTU_DEFAULT bytes default")
|
|
1085
|
+
Bridge.log("Nex: 📤 Requesting MTU: $MTU_517 bytes")
|
|
1086
|
+
gatt.requestMtu(MTU_517) // Request our maximum MTU
|
|
1087
|
+
|
|
1088
|
+
val uartService: BluetoothGattService? = gatt.getService(NexBluetoothConstants.MAIN_SERVICE_UUID)
|
|
1089
|
+
|
|
1090
|
+
if (uartService != null) {
|
|
1091
|
+
val writeChar = uartService.getCharacteristic(NexBluetoothConstants.WRITE_CHAR_UUID)
|
|
1092
|
+
val notifyChar = uartService.getCharacteristic(NexBluetoothConstants.NOTIFY_CHAR_UUID)
|
|
1093
|
+
|
|
1094
|
+
writeChar?.let {
|
|
1095
|
+
mainWriteChar = it
|
|
1096
|
+
// Write type is now chosen PER FRAGMENT in processQueue (acked for
|
|
1097
|
+
// everything except captions), so it's set there right before each
|
|
1098
|
+
// write rather than once here. Captions still use write-without-
|
|
1099
|
+
// response — which unblocks TX from the ~1-packet-per-connection-
|
|
1100
|
+
// interval ATT round-trip when the phone sleeps and the interval is
|
|
1101
|
+
// relaxed — while every other command is acked for reliable delivery.
|
|
1102
|
+
Bridge.log("Nex: glass TX characteristic found (write type set per-fragment)")
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
notifyChar?.let {
|
|
1106
|
+
mainNotifyChar = it
|
|
1107
|
+
enableNotification(gatt, it)
|
|
1108
|
+
Bridge.log("Nex: glass RX characteristic found")
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Mark as connected but wait for setup below to update connection state
|
|
1112
|
+
isMainConnected = true
|
|
1113
|
+
Bridge.log("Nex: PROC_QUEUE - left side setup complete")
|
|
1114
|
+
|
|
1115
|
+
if (isMainConnected) {
|
|
1116
|
+
// Do first battery status query
|
|
1117
|
+
mainTaskHandler.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_BATTERY_QUERY, 10)
|
|
1118
|
+
|
|
1119
|
+
// Restore previous microphone state or disable if this is the first connection
|
|
1120
|
+
val shouldRestoreMic = microphoneStateBeforeDisconnection
|
|
1121
|
+
Bridge.log("Nex: `Restoring microphone state to: $shouldRestoreMic (previous state: $microphoneStateBeforeDisconnection)")
|
|
1122
|
+
|
|
1123
|
+
if (shouldRestoreMic) {
|
|
1124
|
+
startMicBeat(MICBEAT_INTERVAL_MS.toInt())
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Enable our AugmentOS notification key
|
|
1128
|
+
sendWhiteListCommand(10)
|
|
1129
|
+
|
|
1130
|
+
showHomeScreen() // Turn on the NexGlasses display
|
|
1131
|
+
updateConnectionState()
|
|
1132
|
+
|
|
1133
|
+
// Post protobuf schema version information (only once)
|
|
1134
|
+
if (!protobufVersionPosted) {
|
|
1135
|
+
postProtobufSchemaVersionInfo()
|
|
1136
|
+
protobufVersionPosted = true
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Query glasses protobuf version from firmware
|
|
1140
|
+
Bridge.log("=== SENDING GLASSES PROTOBUF VERSION REQUEST ===")
|
|
1141
|
+
val versionQueryPacket = NexProtobufUtils.generateVersionRequestCommandBytes()
|
|
1142
|
+
if (versionQueryPacket.isNotEmpty()) {
|
|
1143
|
+
sendProtobuf(versionQueryPacket, 100)
|
|
1144
|
+
Bridge.log("Sent glasses protobuf version request")
|
|
1145
|
+
} else {
|
|
1146
|
+
Bridge.log("Skipping version request: schema removed VersionRequest")
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Push current glasses-side Voice Activity Detection setting
|
|
1150
|
+
sendVoiceActivityDetectionSetting()
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
Log.e(TAG, " glass UART service not found")
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
|
1158
|
+
Bridge.log("Nex: PROC_QUEUE - Starting notification setup")
|
|
1159
|
+
|
|
1160
|
+
// Enable notifications
|
|
1161
|
+
val result = gatt.setCharacteristicNotification(characteristic, true)
|
|
1162
|
+
Bridge.log("Nex: PROC_QUEUE - setCharacteristicNotification result: $result")
|
|
1163
|
+
|
|
1164
|
+
// NOTE: write type is NOT set here. This receives the *notify*
|
|
1165
|
+
// characteristic, which the phone never writes to — setting writeType
|
|
1166
|
+
// on it had no effect. The write characteristic (mainWriteChar) is
|
|
1167
|
+
// configured for WRITE_TYPE_NO_RESPONSE at its assignment instead.
|
|
1168
|
+
|
|
1169
|
+
// Add delay
|
|
1170
|
+
Bridge.log("Nex: PROC_QUEUE - waiting to enable notification...")
|
|
1171
|
+
try {
|
|
1172
|
+
Thread.sleep(100)
|
|
1173
|
+
} catch (e: InterruptedException) {
|
|
1174
|
+
Log.e(TAG, "Error sending data: ${e.message}")
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Get and configure descriptor
|
|
1178
|
+
val descriptor = characteristic.getDescriptor(NexBluetoothConstants.CLIENT_CHARACTERISTIC_CONFIG_UUID)
|
|
1179
|
+
descriptor?.let {
|
|
1180
|
+
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
1181
|
+
val writeResult = gatt.writeDescriptor(it)
|
|
1182
|
+
Bridge.log("Nex: PROC_QUEUE - set descriptor with result: $writeResult")
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private fun onCharacteristicChangedHandler(characteristic: BluetoothGattCharacteristic) {
|
|
1187
|
+
if (characteristic.uuid == NexBluetoothConstants.NOTIFY_CHAR_UUID) {
|
|
1188
|
+
val data = characteristic.value
|
|
1189
|
+
val deviceName = mainGlassGatt?.device?.name ?: return
|
|
1190
|
+
// NOTE: this runs on the main looper for every notification, audio
|
|
1191
|
+
// included (~50/sec). Do not log or stringify the whole packet to
|
|
1192
|
+
// hex on this hot path — Bridge.log crosses the RN bridge to JS and
|
|
1193
|
+
// packetHex is O(n) per packet. Both back up the audio queue at
|
|
1194
|
+
// screen-off. packetHex is only needed by the protobuf branch, so
|
|
1195
|
+
// compute it lazily there.
|
|
1196
|
+
if (data.isEmpty()) return
|
|
1197
|
+
|
|
1198
|
+
val packetType = data[0]
|
|
1199
|
+
|
|
1200
|
+
when (packetType) {
|
|
1201
|
+
NexBluetoothPacketTypes.PACKET_TYPE_JSON -> {
|
|
1202
|
+
val jsonData = data.copyOfRange(1, data.size)
|
|
1203
|
+
decodeJsons(jsonData)
|
|
1204
|
+
}
|
|
1205
|
+
NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF -> {
|
|
1206
|
+
val protobufData = data.copyOfRange(1, data.size)
|
|
1207
|
+
val packetHex = data.joinToString("") { "%02X".format(it) }
|
|
1208
|
+
decodeProtobufs(protobufData, packetHex)
|
|
1209
|
+
}
|
|
1210
|
+
NexBluetoothPacketTypes.PACKET_TYPE_AUDIO -> {
|
|
1211
|
+
// Check for audio packet header
|
|
1212
|
+
if (data[0] == 0xA0.toByte()) {
|
|
1213
|
+
val sequenceNumber = data[1]
|
|
1214
|
+
val receiveTime = System.currentTimeMillis()
|
|
1215
|
+
|
|
1216
|
+
// Basic sequence validation
|
|
1217
|
+
if (lastReceivedLc3Sequence != -1 && (lastReceivedLc3Sequence + 1).toByte() != sequenceNumber) {
|
|
1218
|
+
Bridge.log("LC3 packet sequence mismatch. Expected: ${lastReceivedLc3Sequence + 1}, Got: $sequenceNumber")
|
|
1219
|
+
}
|
|
1220
|
+
lastReceivedLc3Sequence = sequenceNumber.toInt()
|
|
1221
|
+
|
|
1222
|
+
val lc3Data = data.copyOfRange(2, data.size)
|
|
1223
|
+
|
|
1224
|
+
// No per-packet log here: this fires ~50x/sec and each
|
|
1225
|
+
// Bridge.log crosses the RN bridge to JS. (Matches G1.)
|
|
1226
|
+
|
|
1227
|
+
// Play LC3 audio directly through LC3 player.
|
|
1228
|
+
// No logging in any branch: this runs per audio packet
|
|
1229
|
+
// (~50/sec) and Bridge.log crosses the RN bridge to JS.
|
|
1230
|
+
// With isLc3AudioEnabled hardcoded off, the disabled
|
|
1231
|
+
// branch would otherwise log on every single packet.
|
|
1232
|
+
if (lc3AudioPlayer != null && isLc3AudioEnabled) {
|
|
1233
|
+
// Use the original packet format that LC3 player expects
|
|
1234
|
+
lc3AudioPlayer?.write(data, 0, data.size)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
DeviceManager.getInstance().handleGlassesMicData(lc3Data);
|
|
1238
|
+
|
|
1239
|
+
// Still decode for callback compatibility
|
|
1240
|
+
// TODO: (Verify) Commenting because commented in G1
|
|
1241
|
+
// if (lc3DecoderPtr != 0L) {
|
|
1242
|
+
// val pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, lc3Data)
|
|
1243
|
+
// Bridge.log("pcmData size:${pcmData?.size ?: 0}")
|
|
1244
|
+
// audioProcessingCallback?.let { callback ->
|
|
1245
|
+
// if (!pcmData.isNullOrEmpty()) {
|
|
1246
|
+
// callback.onAudioDataAvailable(pcmData)
|
|
1247
|
+
// }
|
|
1248
|
+
// } ?: run {
|
|
1249
|
+
// // If we get here, it means the callback wasn't properly registered
|
|
1250
|
+
// Log.e(TAG,"Audio processing callback is null - callback registration failed!")
|
|
1251
|
+
// }
|
|
1252
|
+
// }
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
NexBluetoothPacketTypes.PACKET_TYPE_IMAGE -> {
|
|
1256
|
+
// Handle image packet if needed
|
|
1257
|
+
}
|
|
1258
|
+
else -> {
|
|
1259
|
+
Bridge.log("unknown packetType: ${String.format("%02X ", packetType)}")
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
private fun attemptGattConnection(device: BluetoothDevice) {
|
|
1266
|
+
if (device == null) {
|
|
1267
|
+
Bridge.log("Cannot connect to GATT: Device is null")
|
|
1268
|
+
return
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
val deviceName = device.name
|
|
1272
|
+
if (deviceName.isNullOrEmpty()) {
|
|
1273
|
+
Log.e(TAG,"Skipping null/empty device name: ${device.address}... this means something horrific has occurred. Look into this.")
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
Bridge.log("attemptGattConnection called for device: $deviceName (${device.address})")
|
|
1278
|
+
|
|
1279
|
+
DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTING)
|
|
1280
|
+
Bridge.log("Setting connectionState to CONNECTING. Notifying connectionEvent.")
|
|
1281
|
+
// connectionEvent(connectionState)
|
|
1282
|
+
|
|
1283
|
+
if (mainGlassGatt == null) {
|
|
1284
|
+
Bridge.log("Attempting GATT connection for Main Glass...")
|
|
1285
|
+
mainGlassGatt = device.connectGatt(context, false, mainGattCallback)
|
|
1286
|
+
isMainConnected = false
|
|
1287
|
+
Bridge.log("Main GATT connection initiated. isMainConnected set to false.")
|
|
1288
|
+
} else {
|
|
1289
|
+
Bridge.log("Main Glass GATT already exists")
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private fun postProtobufSchemaVersionInfo() {
|
|
1294
|
+
try {
|
|
1295
|
+
// Call the version method only once
|
|
1296
|
+
val schemaVersion = NexProtobufUtils.getProtobufSchemaVersion(context!!)
|
|
1297
|
+
|
|
1298
|
+
// Build the info string directly instead of calling getProtobufBuildInfo()
|
|
1299
|
+
val fileDescriptorName = glassly.ble.GlasslyBle.getDescriptor().file.name
|
|
1300
|
+
val buildInfo = "Schema v$schemaVersion | $fileDescriptorName"
|
|
1301
|
+
|
|
1302
|
+
// protobufSchemaVersion = schemaVersion.toString()
|
|
1303
|
+
|
|
1304
|
+
// val event = ProtobufSchemaVersionEvent(
|
|
1305
|
+
// schemaVersion,
|
|
1306
|
+
// buildInfo,
|
|
1307
|
+
// smartGlassesDevice?.deviceModelName ?: "Unknown"
|
|
1308
|
+
// )
|
|
1309
|
+
|
|
1310
|
+
// EventBus.getDefault().post(event)
|
|
1311
|
+
Bridge.log("Posted protobuf schema version event: $buildInfo")
|
|
1312
|
+
} catch (e: Exception) {
|
|
1313
|
+
Log.e(TAG,"Error posting protobuf schema version event: ${e.message}", e)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
private fun connectToSmartGlasses() {
|
|
1318
|
+
val deviceModelName = DeviceManager.getInstance().deviceName
|
|
1319
|
+
val deviceAddress = DeviceManager.getInstance().deviceAddress
|
|
1320
|
+
|
|
1321
|
+
// Register bonding receiver
|
|
1322
|
+
Bridge.log("connectToSmartGlasses start")
|
|
1323
|
+
Bridge.log("try to ConnectToSmartGlassesing deviceModelName: ${deviceModelName} deviceAddress: ${deviceAddress}")
|
|
1324
|
+
preferredMainDeviceId = DeviceManager.getInstance().deviceName
|
|
1325
|
+
if (!bluetoothAdapter.isEnabled) {
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
when {
|
|
1329
|
+
!deviceModelName.isNullOrEmpty() && !deviceAddress.isNullOrEmpty() -> {
|
|
1330
|
+
stopScan()
|
|
1331
|
+
mainDevice = bluetoothAdapter.getRemoteDevice(deviceAddress)
|
|
1332
|
+
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
|
|
1333
|
+
}
|
|
1334
|
+
savedNexMainAddress != null -> {
|
|
1335
|
+
mainDevice = bluetoothAdapter.getRemoteDevice(savedNexMainAddress)
|
|
1336
|
+
mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
|
|
1337
|
+
}
|
|
1338
|
+
else -> {
|
|
1339
|
+
// Start scanning for devices
|
|
1340
|
+
stopScan()
|
|
1341
|
+
DeviceStore.apply("glasses", "connectionState", ConnTypes.SCANNING)
|
|
1342
|
+
// connectionEvent(connectionState) // TODO: Figure out where is connection event defined????
|
|
1343
|
+
startScan()
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private fun destroy() {
|
|
1349
|
+
// Tell the glasses this teardown is intentional so they return to the welcome
|
|
1350
|
+
// screen immediately instead of holding the last frame through the firmware's
|
|
1351
|
+
// unexpected-disconnect grace period. Best-effort: if the write can't be sent
|
|
1352
|
+
// or doesn't flush in time, the firmware grace period is the safety net.
|
|
1353
|
+
val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
|
|
1354
|
+
if (!canSend) {
|
|
1355
|
+
performDestroy()
|
|
1356
|
+
return
|
|
1357
|
+
}
|
|
1358
|
+
Bridge.log("Nex: Sending DisconnectRequest before teardown")
|
|
1359
|
+
sendProtobuf(NexProtobufUtils.generateDisconnectRequestCommandBytes(), 100)
|
|
1360
|
+
mainTaskHandler.postDelayed({ performDestroy() }, DISCONNECT_FLUSH_DELAY_MS)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private fun performDestroy() {
|
|
1364
|
+
Bridge.log("Nex: MentraNexSGC ONDESTROY")
|
|
1365
|
+
showHomeScreen()
|
|
1366
|
+
isKilled = true
|
|
1367
|
+
// stop BLE scanning
|
|
1368
|
+
stopScan()
|
|
1369
|
+
// Save current microphone state before destroying
|
|
1370
|
+
microphoneStateBeforeDisconnection = isMicrophoneEnabled
|
|
1371
|
+
Bridge.log("Nex: Saved microphone state during destroy: $microphoneStateBeforeDisconnection")
|
|
1372
|
+
// disable the microphone and stop sending micbeat
|
|
1373
|
+
stopMicBeat()
|
|
1374
|
+
// Stop periodic notifications
|
|
1375
|
+
stopPeriodicNotifications()
|
|
1376
|
+
mainGlassGatt?.let { gatt ->
|
|
1377
|
+
gatt.disconnect()
|
|
1378
|
+
gatt.close()
|
|
1379
|
+
mainGlassGatt = null
|
|
1380
|
+
}
|
|
1381
|
+
lc3AudioPlayer?.let { player ->
|
|
1382
|
+
try {
|
|
1383
|
+
player.stopPlay()
|
|
1384
|
+
Bridge.log("Nex: LC3 audio player stopped and cleaned up")
|
|
1385
|
+
} catch (e: Exception) {
|
|
1386
|
+
Log.e(TAG,"Nex: Error stopping LC3 audio player during destroy: ${e.message}")
|
|
1387
|
+
} finally {
|
|
1388
|
+
lc3AudioPlayer = null
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Clean up handlers
|
|
1392
|
+
mainTaskHandler?.removeCallbacksAndMessages(null)
|
|
1393
|
+
whiteListHandler?.removeCallbacksAndMessages(null)
|
|
1394
|
+
micEnableHandler?.removeCallbacksAndMessages(null)
|
|
1395
|
+
notificationRunnable?.let {
|
|
1396
|
+
notificationHandler?.removeCallbacks(it)
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
textWallRunnable?.let {
|
|
1400
|
+
textWallHandler?.removeCallbacks(it)
|
|
1401
|
+
}
|
|
1402
|
+
findCompatibleDevicesHandler?.removeCallbacksAndMessages(null)
|
|
1403
|
+
// Free LC3 decoder
|
|
1404
|
+
if (lc3DecoderPtr != 0L) {
|
|
1405
|
+
Lc3Cpp.freeDecoder(lc3DecoderPtr)
|
|
1406
|
+
lc3DecoderPtr = 0L
|
|
1407
|
+
}
|
|
1408
|
+
currentImageChunks.clear()
|
|
1409
|
+
isImageSendProgressing = false
|
|
1410
|
+
sendQueue.clear()
|
|
1411
|
+
// Add a dummy element to unblock the take() call if needed
|
|
1412
|
+
sendQueue.offer(emptyArray()) // is this needed?
|
|
1413
|
+
isWorkerRunning = false
|
|
1414
|
+
isMainConnected = false
|
|
1415
|
+
Bridge.log("Nex: MentraNexSGC cleanup complete")
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private fun startScan() {
|
|
1419
|
+
val scanner: BluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
|
|
1420
|
+
if (scanner == null) {
|
|
1421
|
+
Log.e(TAG,"BluetoothLeScanner not available.")
|
|
1422
|
+
return
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Optionally, define filters if needed
|
|
1426
|
+
val filters = mutableListOf<ScanFilter>()
|
|
1427
|
+
// For example, to filter by device name:
|
|
1428
|
+
// filters.add(ScanFilter.Builder().setDeviceName("Even G1_").build())
|
|
1429
|
+
|
|
1430
|
+
// Set desired scan settings
|
|
1431
|
+
val settings = ScanSettings.Builder()
|
|
1432
|
+
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
|
1433
|
+
.build()
|
|
1434
|
+
|
|
1435
|
+
// Start scanning
|
|
1436
|
+
isScanning = true
|
|
1437
|
+
scanner.startScan(filters, settings, modernScanCallback)
|
|
1438
|
+
scanner.flushPendingScanResults(modernScanCallback)
|
|
1439
|
+
Bridge.log("CALL START SCAN - Started scanning for devices...")
|
|
1440
|
+
|
|
1441
|
+
// Ensure scanning state is immediately communicated to UI
|
|
1442
|
+
DeviceStore.apply("glasses", "connectionState", ConnTypes.SCANNING)
|
|
1443
|
+
// connectionEvent(connectionState)
|
|
1444
|
+
|
|
1445
|
+
// Stop the scan after some time (e.g., 10-15s instead of 60 to avoid
|
|
1446
|
+
// throttling)
|
|
1447
|
+
// handler.postDelayed({ stopScan() }, 10000)
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
override fun stopScan() {
|
|
1451
|
+
val scanner = bluetoothAdapter.bluetoothLeScanner
|
|
1452
|
+
scanner?.stopScan(modernScanCallback)
|
|
1453
|
+
isScanning = false
|
|
1454
|
+
Bridge.log("Stopped scanning for devices")
|
|
1455
|
+
|
|
1456
|
+
if (bleScanCallback != null && isScanningForCompatibleDevices) {
|
|
1457
|
+
scanner?.stopScan(bleScanCallback)
|
|
1458
|
+
isScanningForCompatibleDevices = false
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Returns a colour-inverted copy of [src] (white <-> black). The firmware decoder normalises
|
|
1464
|
+
* BMP palette polarity, so the only way to flip the on-glass result from the SGC is to invert
|
|
1465
|
+
* the actual pixel content here.
|
|
1466
|
+
*/
|
|
1467
|
+
private fun invertBitmap(src: Bitmap): Bitmap {
|
|
1468
|
+
val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
|
|
1469
|
+
val paint = Paint().apply {
|
|
1470
|
+
colorFilter = ColorMatrixColorFilter(
|
|
1471
|
+
ColorMatrix(
|
|
1472
|
+
floatArrayOf(
|
|
1473
|
+
-1f, 0f, 0f, 0f, 255f,
|
|
1474
|
+
0f, -1f, 0f, 0f, 255f,
|
|
1475
|
+
0f, 0f, -1f, 0f, 255f,
|
|
1476
|
+
0f, 0f, 0f, 1f, 0f
|
|
1477
|
+
)
|
|
1478
|
+
)
|
|
1479
|
+
)
|
|
1480
|
+
}
|
|
1481
|
+
Canvas(out).drawBitmap(src, 0f, 0f, paint)
|
|
1482
|
+
return out
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Floyd-Steinberg error-diffusion dithering to monochrome. Returns a copy of [src] whose pixels
|
|
1487
|
+
* are pure black or white but whose dot density approximates the original luminance, preserving
|
|
1488
|
+
* far more perceived detail on a 1-bpp panel than a flat threshold. Luminance uses Rec. 601
|
|
1489
|
+
* weights; quantization error is pushed to the right/below neighbours (7/3/5/1 over 16).
|
|
1490
|
+
*/
|
|
1491
|
+
private fun floydSteinbergDither(src: Bitmap): Bitmap {
|
|
1492
|
+
val w = src.width
|
|
1493
|
+
val h = src.height
|
|
1494
|
+
val px = IntArray(w * h)
|
|
1495
|
+
src.getPixels(px, 0, w, 0, 0, w, h)
|
|
1496
|
+
|
|
1497
|
+
// Working luminance buffer (float so diffused error can over/undershoot 0..255).
|
|
1498
|
+
val lum = FloatArray(w * h)
|
|
1499
|
+
for (i in 0 until w * h) {
|
|
1500
|
+
val c = px[i]
|
|
1501
|
+
val r = (c shr 16) and 0xFF
|
|
1502
|
+
val g = (c shr 8) and 0xFF
|
|
1503
|
+
val b = c and 0xFF
|
|
1504
|
+
lum[i] = 0.299f * r + 0.587f * g + 0.114f * b
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
for (y in 0 until h) {
|
|
1508
|
+
for (x in 0 until w) {
|
|
1509
|
+
val idx = y * w + x
|
|
1510
|
+
val old = lum[idx]
|
|
1511
|
+
val newVal = if (old < 128f) 0f else 255f
|
|
1512
|
+
val err = old - newVal
|
|
1513
|
+
px[idx] = if (newVal >= 128f) 0xFFFFFFFF.toInt() else 0xFF000000.toInt()
|
|
1514
|
+
|
|
1515
|
+
if (x + 1 < w) lum[idx + 1] += err * 7f / 16f
|
|
1516
|
+
if (y + 1 < h) {
|
|
1517
|
+
if (x - 1 >= 0) lum[idx + w - 1] += err * 3f / 16f
|
|
1518
|
+
lum[idx + w] += err * 5f / 16f
|
|
1519
|
+
if (x + 1 < w) lum[idx + w + 1] += err * 1f / 16f
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
val out = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
1525
|
+
out.setPixels(px, 0, w, 0, 0, w, h)
|
|
1526
|
+
return out
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/** Stream 1-bit BMP pixels into an already-created canvas bitmap component [componentId]. */
|
|
1530
|
+
private fun streamBitmapPixels(componentId: Int, bmpData: ByteArray) {
|
|
1531
|
+
try {
|
|
1532
|
+
if (bmpData.isEmpty()) {
|
|
1533
|
+
Log.e(TAG, "Invalid BMP data provided")
|
|
1534
|
+
return
|
|
1535
|
+
}
|
|
1536
|
+
// 4-digit hex stream id per the canvas image protocol.
|
|
1537
|
+
val totalChunks = (bmpData.size + bmpChunkSize - 1) / bmpChunkSize
|
|
1538
|
+
val streamId = "%04X".format(random.nextInt(0x10000))
|
|
1539
|
+
|
|
1540
|
+
sendProtobuf(NexProtobufUtils.generateCanvasUpdateImageCommandBytes(
|
|
1541
|
+
componentId, streamId, totalChunks))
|
|
1542
|
+
|
|
1543
|
+
val chunks = NexProtobufUtils.createBmpChunksForNexGlasses(streamId, bmpData, totalChunks, bmpChunkSize)
|
|
1544
|
+
currentImageChunks.clear()
|
|
1545
|
+
currentImageChunks.addAll(chunks)
|
|
1546
|
+
sendDataSequentially(chunks)
|
|
1547
|
+
} catch (e: Exception) {
|
|
1548
|
+
Log.e(TAG, "Error streaming bitmap pixels: ${e.message}")
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
private fun stopPeriodicNotifications() {
|
|
1553
|
+
notificationRunnable?.let { runnable ->
|
|
1554
|
+
notificationHandler?.removeCallbacks(runnable)
|
|
1555
|
+
Bridge.log("Stopped periodic notifications")
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
private fun startMicBeat(delay: Int) {
|
|
1559
|
+
Bridge.log("Nex: Starting micbeat")
|
|
1560
|
+
if (micBeatCount > 0) {
|
|
1561
|
+
stopMicBeat()
|
|
1562
|
+
}
|
|
1563
|
+
sendSetMicEnabled(true, 10)
|
|
1564
|
+
micBeatRunnable = Runnable {
|
|
1565
|
+
sendSetMicEnabled(shouldUseGlassesMic, 1)
|
|
1566
|
+
micBeatHandler.postDelayed(micBeatRunnable!!, MICBEAT_INTERVAL_MS)
|
|
1567
|
+
}
|
|
1568
|
+
micBeatHandler.postDelayed(micBeatRunnable!!, delay.toLong())
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
private fun stopMicBeat() {
|
|
1572
|
+
sendSetMicEnabled(false, 10)
|
|
1573
|
+
if (micBeatHandler != null) {
|
|
1574
|
+
micBeatHandler.removeCallbacksAndMessages(null);
|
|
1575
|
+
micBeatHandler.removeCallbacksAndMessages(micBeatRunnable);
|
|
1576
|
+
micBeatRunnable = null;
|
|
1577
|
+
micBeatCount = 0;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private fun sendWhiteListCommand(delay: Int) {
|
|
1582
|
+
if (whiteListedAlready) return
|
|
1583
|
+
|
|
1584
|
+
whiteListedAlready = true
|
|
1585
|
+
Bridge.log("Nex: Sending whitelist command")
|
|
1586
|
+
|
|
1587
|
+
whiteListHandler.postDelayed({
|
|
1588
|
+
val chunks = NexProtobufUtils.getWhitelistChunks(maxChunkSize)
|
|
1589
|
+
sendDataSequentially(chunks)
|
|
1590
|
+
|
|
1591
|
+
// Uncomment if needed for debugging:
|
|
1592
|
+
// chunks.forEach { chunk ->
|
|
1593
|
+
// Bridge.log("Nex: Sending this chunk for white list: ${bytesToUtf8(chunk)}")
|
|
1594
|
+
// sendDataSequentially(chunk)
|
|
1595
|
+
// // Thread.sleep(150) // Uncomment if delay between chunks is needed
|
|
1596
|
+
// }
|
|
1597
|
+
}, delay.toLong())
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
private fun showHomeScreen() {
|
|
1601
|
+
Bridge.log("Nex: showHomeScreen")
|
|
1602
|
+
|
|
1603
|
+
if (lastThingDisplayedWasAnImage) {
|
|
1604
|
+
// clearNexScreen()
|
|
1605
|
+
lastThingDisplayedWasAnImage = false
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
private fun updateConnectionState() {
|
|
1610
|
+
if (isMainConnected) {
|
|
1611
|
+
DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTED)
|
|
1612
|
+
Bridge.log("Nex: Main glasses connected")
|
|
1613
|
+
lastConnectionTimestamp = System.currentTimeMillis()
|
|
1614
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
1615
|
+
DeviceStore.apply("glasses", "connected", true)
|
|
1616
|
+
// Removed commented sleep code as it's not needed
|
|
1617
|
+
// connectionEvent(it)
|
|
1618
|
+
} else {
|
|
1619
|
+
DeviceStore.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
1620
|
+
Bridge.log("Nex: No Main glasses connected")
|
|
1621
|
+
DeviceStore.apply("glasses", "fullyBooted", false)
|
|
1622
|
+
DeviceStore.apply("glasses", "connected", false)
|
|
1623
|
+
// connectionEvent(it)
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
private fun decodeJsons(jsonBytes: ByteArray) {
|
|
1628
|
+
val jsonString = String(jsonBytes, Charset.defaultCharset())
|
|
1629
|
+
try {
|
|
1630
|
+
val commandObject = JSONObject(jsonString)
|
|
1631
|
+
when (val type = commandObject.getString("type")) {
|
|
1632
|
+
"image_transfer_complete" -> {
|
|
1633
|
+
// Handle image transfer complete
|
|
1634
|
+
}
|
|
1635
|
+
"disconnect" -> {
|
|
1636
|
+
// Handle disconnect
|
|
1637
|
+
}
|
|
1638
|
+
"request_battery_state" -> {
|
|
1639
|
+
// Handle battery state request
|
|
1640
|
+
}
|
|
1641
|
+
"charging_state" -> {
|
|
1642
|
+
// Handle charging state
|
|
1643
|
+
}
|
|
1644
|
+
"device_info" -> {
|
|
1645
|
+
// val deviceInfo = gson.fromJson(jsonString, DeviceInfo::class.java)
|
|
1646
|
+
}
|
|
1647
|
+
"enter_pairing_mode" -> {
|
|
1648
|
+
// Handle pairing mode
|
|
1649
|
+
}
|
|
1650
|
+
"request_head_position" -> {
|
|
1651
|
+
// Handle head position request
|
|
1652
|
+
}
|
|
1653
|
+
"set_head_up_angle" -> {
|
|
1654
|
+
// Handle head up angle
|
|
1655
|
+
}
|
|
1656
|
+
"ping" -> {
|
|
1657
|
+
// Handle ping
|
|
1658
|
+
}
|
|
1659
|
+
"vad_event" -> {
|
|
1660
|
+
// Handle VAD event
|
|
1661
|
+
}
|
|
1662
|
+
"imu_data" -> {
|
|
1663
|
+
// Handle IMU data
|
|
1664
|
+
}
|
|
1665
|
+
"button_event" -> {
|
|
1666
|
+
// Handle button event
|
|
1667
|
+
}
|
|
1668
|
+
"head_gesture" -> {
|
|
1669
|
+
// Handle head gesture
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
} catch (e: Exception) {
|
|
1673
|
+
Log.e(TAG,"Error decoding JSON: ${e.message}")
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private fun decodeProtobufs(protobufBytes: ByteArray, packetHex: String) {
|
|
1678
|
+
try {
|
|
1679
|
+
val glassesToPhone = GlassesToPhone.parseFrom(protobufBytes)
|
|
1680
|
+
val payloadCase = glassesToPhone.payloadCase.toString()
|
|
1681
|
+
|
|
1682
|
+
Bridge.log("decodeProtobufs glassesToPhone: $glassesToPhone")
|
|
1683
|
+
Bridge.log("decodeProtobufs glassesToPhone payloadCase: $payloadCase")
|
|
1684
|
+
|
|
1685
|
+
NexEventUtils.sendBleCommandReceivedEvent(payloadCase, packetHex, System.currentTimeMillis())
|
|
1686
|
+
// if (isDebugMode) {
|
|
1687
|
+
// EventBus.getDefault().post(BleCommandReceiver(payloadCase, packetHex))
|
|
1688
|
+
// }
|
|
1689
|
+
|
|
1690
|
+
when (glassesToPhone.payloadCase) {
|
|
1691
|
+
GlassesToPhone.PayloadCase.BATTERY_STATUS -> {
|
|
1692
|
+
val batteryStatus: BatteryStatus = glassesToPhone.batteryStatus
|
|
1693
|
+
DeviceStore.apply("glasses", "batteryLevel", batteryStatus.level)
|
|
1694
|
+
DeviceStore.apply("glasses", "charging", batteryStatus.charging)
|
|
1695
|
+
}
|
|
1696
|
+
GlassesToPhone.PayloadCase.CHARGING_STATE -> {
|
|
1697
|
+
val chargingState: ChargingState = glassesToPhone.chargingState
|
|
1698
|
+
DeviceStore.apply(
|
|
1699
|
+
"glasses",
|
|
1700
|
+
"charging",
|
|
1701
|
+
chargingState.state == ChargingState.State.CHARGING
|
|
1702
|
+
)
|
|
1703
|
+
}
|
|
1704
|
+
GlassesToPhone.PayloadCase.DEVICE_INFO -> {
|
|
1705
|
+
val deviceInfo: DeviceInfo = glassesToPhone.deviceInfo
|
|
1706
|
+
Bridge.log("deviceInfo: $deviceInfo")
|
|
1707
|
+
DeviceStore.apply("glasses", "firmwareVersion", deviceInfo.fwVersion)
|
|
1708
|
+
}
|
|
1709
|
+
GlassesToPhone.PayloadCase.HEAD_POSITION -> {
|
|
1710
|
+
val headPosition: HeadPosition = glassesToPhone.headPosition
|
|
1711
|
+
// EventBus.getDefault().post(HeadUpAngleEvent(headPosition.angle))
|
|
1712
|
+
Bridge.log("headPosition: $headPosition")
|
|
1713
|
+
}
|
|
1714
|
+
GlassesToPhone.PayloadCase.HEAD_UP_ANGLE_SET -> {
|
|
1715
|
+
val headUpAngleResponse: HeadUpAngleResponse = glassesToPhone.headUpAngleSet
|
|
1716
|
+
Bridge.log("headUpAngleResponse: $headUpAngleResponse")
|
|
1717
|
+
}
|
|
1718
|
+
GlassesToPhone.PayloadCase.VAD_EVENT -> {
|
|
1719
|
+
// val vadEvent = glassesToPhone.vadEvent
|
|
1720
|
+
// EventBus.getDefault().post(VadEvent(vadEvent.vad))
|
|
1721
|
+
}
|
|
1722
|
+
GlassesToPhone.PayloadCase.IMAGE_TRANSFER_COMPLETE -> {
|
|
1723
|
+
val transferComplete: ImageTransferComplete = glassesToPhone.imageTransferComplete
|
|
1724
|
+
Bridge.log("transferComplete: $transferComplete")
|
|
1725
|
+
|
|
1726
|
+
when (transferComplete.status) {
|
|
1727
|
+
ImageTransferComplete.Status.OK -> {
|
|
1728
|
+
currentImageChunks.clear()
|
|
1729
|
+
isImageSendProgressing = false
|
|
1730
|
+
}
|
|
1731
|
+
ImageTransferComplete.Status.INCOMPLETE -> {
|
|
1732
|
+
val missingChunksList = transferComplete.missingChunksList
|
|
1733
|
+
reSendImageMissingChunks(missingChunksList)
|
|
1734
|
+
}
|
|
1735
|
+
else -> {}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
GlassesToPhone.PayloadCase.IMU_DATA -> {
|
|
1739
|
+
val imuData: ImuData = glassesToPhone.imuData
|
|
1740
|
+
Bridge.log("imuData: $imuData")
|
|
1741
|
+
}
|
|
1742
|
+
GlassesToPhone.PayloadCase.BUTTON_EVENT -> {
|
|
1743
|
+
val buttonEvent: ButtonEvent = glassesToPhone.buttonEvent
|
|
1744
|
+
Bridge.log("buttonEvent: $buttonEvent")
|
|
1745
|
+
// buttonEvent.button.number
|
|
1746
|
+
// EventBus.getDefault().post(ButtonPressEvent(
|
|
1747
|
+
// smartGlassesDevice.deviceModelName,
|
|
1748
|
+
// buttonId,
|
|
1749
|
+
// pressType,
|
|
1750
|
+
// timestamp
|
|
1751
|
+
// ))
|
|
1752
|
+
}
|
|
1753
|
+
GlassesToPhone.PayloadCase.HEAD_GESTURE -> {
|
|
1754
|
+
val headGesture: HeadGesture = glassesToPhone.headGesture
|
|
1755
|
+
Bridge.log("headGesture: $headGesture")
|
|
1756
|
+
// Matches iOS: only the head-up gesture reaches the shared
|
|
1757
|
+
// store (iOS has no clearing path either).
|
|
1758
|
+
if (headGesture.gesture == HeadGesture.Gesture.HEAD_UP) {
|
|
1759
|
+
DeviceStore.apply("glasses", "headUp", true)
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
GlassesToPhone.PayloadCase.CANVAS_RESULT -> {
|
|
1763
|
+
// Ack for CanvasCreateComponent / CanvasClear (updates are
|
|
1764
|
+
// unacked). Firmware codes: OK / INVALID (bad id for type) /
|
|
1765
|
+
// OVERSIZE (bitmap > 200x200) / OOM (no backing storage).
|
|
1766
|
+
// Consumed for observability — a silent blank screen becomes
|
|
1767
|
+
// a diagnosable log line. Non-OK codes also invalidate the
|
|
1768
|
+
// registry entry so the next frame recreates the component.
|
|
1769
|
+
val canvasResult = glassesToPhone.canvasResult
|
|
1770
|
+
if (canvasResult.code != glassly.ble.GlasslyBle.CanvasResult.ResultCode.OK) {
|
|
1771
|
+
Bridge.log("Nex: CANVAS_RESULT id=${canvasResult.id} code=${canvasResult.code} — dropping registry entry for recreate")
|
|
1772
|
+
canvasElements.entries.removeAll { it.value.firmwareId == canvasResult.id.toInt() }
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
GlassesToPhone.PayloadCase.PAYLOAD_NOT_SET,
|
|
1776
|
+
null -> {
|
|
1777
|
+
// Do nothing
|
|
1778
|
+
}
|
|
1779
|
+
else -> {
|
|
1780
|
+
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
} catch (e: InvalidProtocolBufferException) {
|
|
1784
|
+
Log.e(TAG,"Error decoding protobuf: ${e.message}")
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private fun decodeProtobufsByWrite(protobufBytes: ByteArray, packetHex: String) {
|
|
1789
|
+
try {
|
|
1790
|
+
val phoneToGlasses = PhoneToGlasses.parseFrom(protobufBytes)
|
|
1791
|
+
Bridge.log("decodeProtobufsByWrite phoneToGlasses: $phoneToGlasses")
|
|
1792
|
+
Bridge.log("decodeProtobufsByWrite phoneToGlasses payloadCase: ${phoneToGlasses.payloadCase}")
|
|
1793
|
+
val payloadCase = phoneToGlasses.payloadCase.toString()
|
|
1794
|
+
NexEventUtils.sendBleCommandSentEvent(payloadCase, packetHex, System.currentTimeMillis())
|
|
1795
|
+
// if (isDebugMode) {
|
|
1796
|
+
// EventBus.getDefault().post(BleCommandSender(payloadCase, packetHex))
|
|
1797
|
+
// }
|
|
1798
|
+
} catch (e: Exception) {
|
|
1799
|
+
Log.e(TAG,"Error in decodeProtobufsByWrite: ${e.message}")
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
private fun reSendImageMissingChunks(missingChunksIndexList: List<Int>) {
|
|
1804
|
+
if (!isImageSendProgressing || currentImageChunks.isEmpty() || missingChunksIndexList.isEmpty()) {
|
|
1805
|
+
return
|
|
1806
|
+
}
|
|
1807
|
+
val missingChunks = missingChunksIndexList.map { index ->
|
|
1808
|
+
currentImageChunks[index]
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
sendDataSequentially(missingChunks)
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
private fun createTextWallChunksForNex(text: String): ByteArray {
|
|
1815
|
+
// Create the PhoneToGlasses using its builder and set the DisplayText
|
|
1816
|
+
return NexProtobufUtils.generateDisplayTextCommandBytes(text)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
private fun sendSetMicEnabled(enable: Boolean, delay: Long) {
|
|
1820
|
+
Bridge.log("Nex: setMicEnabled called with enable: $enable and delay: $delay")
|
|
1821
|
+
Bridge.log("Nex: Running set mic enabled: $enable")
|
|
1822
|
+
isMicrophoneEnabled = enable // Update the state tracker
|
|
1823
|
+
DeviceStore.apply("glasses", "micEnabled", enable)
|
|
1824
|
+
|
|
1825
|
+
micEnableHandler?.postDelayed({
|
|
1826
|
+
if (connectionState != ConnTypes.CONNECTED) {
|
|
1827
|
+
Bridge.log("Nex: Tryna start mic: Not connected to glasses")
|
|
1828
|
+
return@postDelayed
|
|
1829
|
+
}
|
|
1830
|
+
Bridge.log("Nex: === SENDING MICROPHONE STATE COMMAND TO GLASSES ===")
|
|
1831
|
+
val micConfigBytes = NexProtobufUtils.generateMicStateConfigCommandBytes(enable)
|
|
1832
|
+
sendProtobuf(micConfigBytes, 10) // wait some time to setup the mic
|
|
1833
|
+
Bridge.log("Nex: Sent MIC command: ${micConfigBytes.joinToString("") { "%02x".format(it) }}")
|
|
1834
|
+
}, delay)
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
private fun queryBatteryStatus() {
|
|
1838
|
+
val batteryQueryPacket = NexProtobufUtils.generateBatteryStateRequestCommandBytes()
|
|
1839
|
+
sendProtobuf(batteryQueryPacket, 250)
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
///// PROCESSING THREAD /////////////////
|
|
1843
|
+
|
|
1844
|
+
// Start the worker thread if it's not already running
|
|
1845
|
+
@Synchronized
|
|
1846
|
+
private fun startWorkerIfNeeded() {
|
|
1847
|
+
if (!isWorkerRunning) {
|
|
1848
|
+
isWorkerRunning = true
|
|
1849
|
+
Thread({ processQueue() }, "MentraNexSGCProcessQueue").start()
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Non-blocking function to add new send request
|
|
1854
|
+
private fun sendDataSequentially(data: ByteArray) {
|
|
1855
|
+
val chunks = arrayOf(SendRequest(data))
|
|
1856
|
+
sendQueue.offer(chunks)
|
|
1857
|
+
startWorkerIfNeeded()
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// Non-blocking function to add new send request with wait time
|
|
1861
|
+
private fun sendDataSequentially(data: ByteArray, waitTime: Int) {
|
|
1862
|
+
val chunks = arrayOf(SendRequest(data, waitTime))
|
|
1863
|
+
sendQueue.offer(chunks)
|
|
1864
|
+
startWorkerIfNeeded()
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
private fun sendDataSequentially(data: List<ByteArray>) {
|
|
1868
|
+
val chunks = Array(data.size) { i -> SendRequest(data[i]) }
|
|
1869
|
+
sendQueue.offer(chunks)
|
|
1870
|
+
startWorkerIfNeeded()
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/**
|
|
1874
|
+
* Frames a serialized protobuf message onto the 0x02 control channel and sends it.
|
|
1875
|
+
*
|
|
1876
|
+
* The message is split into fragments that each fit the negotiated MTU, prefixed with a
|
|
1877
|
+
* 4-byte transport header: [0x02][seq][totalChunks][chunkIndex]. The firmware reassembles
|
|
1878
|
+
* fragments by seq/index before decoding. Single-fragment messages (totalChunks == 1) are
|
|
1879
|
+
* decoded directly by the firmware fast path.
|
|
1880
|
+
*
|
|
1881
|
+
* @param protobufBytes raw serialized PhoneToGlasses bytes (no framing)
|
|
1882
|
+
* @param waitTime optional post-message delay in ms applied after the last fragment (-1 = none)
|
|
1883
|
+
* @param ack when true (default) each fragment is a write-with-response; captions pass false
|
|
1884
|
+
*/
|
|
1885
|
+
private fun sendProtobuf(protobufBytes: ByteArray, waitTime: Int = -1, ack: Boolean = true) {
|
|
1886
|
+
val headerSize = 4 // [0x02][seq][totalChunks][chunkIndex]
|
|
1887
|
+
val maxFragment = (maxChunkSize - headerSize).coerceAtLeast(1)
|
|
1888
|
+
val totalChunks =
|
|
1889
|
+
if (protobufBytes.isEmpty()) 1 else (protobufBytes.size + maxFragment - 1) / maxFragment
|
|
1890
|
+
|
|
1891
|
+
if (totalChunks > 255) {
|
|
1892
|
+
Log.e(TAG, "Nex: Protobuf message too large to fragment ($totalChunks chunks) - dropping")
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
val seq = protobufSeq
|
|
1897
|
+
protobufSeq = (protobufSeq + 1) and 0xFF
|
|
1898
|
+
|
|
1899
|
+
val frames = Array(totalChunks) { index ->
|
|
1900
|
+
val start = index * maxFragment
|
|
1901
|
+
val end = minOf(start + maxFragment, protobufBytes.size)
|
|
1902
|
+
val fragLen = end - start
|
|
1903
|
+
val frame = ByteArray(headerSize + fragLen)
|
|
1904
|
+
frame[0] = NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF
|
|
1905
|
+
frame[1] = seq.toByte()
|
|
1906
|
+
frame[2] = totalChunks.toByte()
|
|
1907
|
+
frame[3] = index.toByte()
|
|
1908
|
+
if (fragLen > 0) {
|
|
1909
|
+
System.arraycopy(protobufBytes, start, frame, headerSize, fragLen)
|
|
1910
|
+
}
|
|
1911
|
+
// Carry the post-message delay on the final fragment only.
|
|
1912
|
+
SendRequest(frame, if (index == totalChunks - 1) waitTime else -1, ack)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
sendQueue.offer(frames)
|
|
1916
|
+
startWorkerIfNeeded()
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
private fun processQueue() {
|
|
1920
|
+
// First wait until the services are ready to receive data
|
|
1921
|
+
Bridge.log("[BLE] PROC_QUEUE started — waiting for descriptor write (mainServicesWaiter)")
|
|
1922
|
+
try {
|
|
1923
|
+
mainServicesWaiter.waitWhileTrue()
|
|
1924
|
+
} catch (e: InterruptedException) {
|
|
1925
|
+
Log.e(TAG,"Nex: Interrupted waiting for descriptor writes: $e")
|
|
1926
|
+
}
|
|
1927
|
+
Bridge.log("[BLE] PROC_QUEUE ready — descriptor write done, entering send loop")
|
|
1928
|
+
|
|
1929
|
+
while (!isKilled) {
|
|
1930
|
+
try {
|
|
1931
|
+
// Make sure services are ready before processing requests
|
|
1932
|
+
mainServicesWaiter.waitWhileTrue()
|
|
1933
|
+
|
|
1934
|
+
// This will block until data is available
|
|
1935
|
+
val requests = sendQueue.take()
|
|
1936
|
+
|
|
1937
|
+
for (request in requests) {
|
|
1938
|
+
if (isKilled) {
|
|
1939
|
+
isWorkerRunning = false
|
|
1940
|
+
break
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
try {
|
|
1944
|
+
// Force an initial delay so BLE gets all setup
|
|
1945
|
+
val timeSinceConnection = System.currentTimeMillis() - lastConnectionTimestamp
|
|
1946
|
+
if (timeSinceConnection < INITIAL_CONNECTION_DELAY_MS) {
|
|
1947
|
+
Thread.sleep(INITIAL_CONNECTION_DELAY_MS - timeSinceConnection)
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// Send to main glass
|
|
1951
|
+
val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
|
|
1952
|
+
var writeStarted = false
|
|
1953
|
+
if (canSend) {
|
|
1954
|
+
mainWaiter.setTrue()
|
|
1955
|
+
// Per-request write type: acked fragments go out WITH response
|
|
1956
|
+
// (firmware ATT-acks each), captions go out WITHOUT response for
|
|
1957
|
+
// throughput. Set on the shared char just before the write — the
|
|
1958
|
+
// worker is single-threaded so there's no concurrent write to race.
|
|
1959
|
+
mainWriteChar?.writeType = if (request.ack) {
|
|
1960
|
+
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
1961
|
+
} else {
|
|
1962
|
+
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
1963
|
+
}
|
|
1964
|
+
mainWriteChar?.value = request.data
|
|
1965
|
+
// Honor the return value: writeCharacteristic returns false when the
|
|
1966
|
+
// stack is busy / out of buffers, and in that case no callback fires.
|
|
1967
|
+
// Only wait for the callback if the write actually started, otherwise
|
|
1968
|
+
// the waiter would block until the next disconnect.
|
|
1969
|
+
val issued = mainGlassGatt?.writeCharacteristic(mainWriteChar) ?: false
|
|
1970
|
+
if (issued) {
|
|
1971
|
+
writeStarted = true
|
|
1972
|
+
lastSendTimestamp = System.currentTimeMillis()
|
|
1973
|
+
} else {
|
|
1974
|
+
mainWaiter.setFalse()
|
|
1975
|
+
Log.e(TAG, "[BLE] PROC_QUEUE writeCharacteristic returned false — stack busy, dropping fragment")
|
|
1976
|
+
}
|
|
1977
|
+
} else {
|
|
1978
|
+
Bridge.log("[BLE] PROC_QUEUE skipping write — not ready, packet dropped")
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
if (writeStarted) {
|
|
1982
|
+
// Acked writes need to wait for the real ATT response (a
|
|
1983
|
+
// connection interval or two); no-response writes get the
|
|
1984
|
+
// near-instant fast-path bound.
|
|
1985
|
+
val timeout = if (request.ack) WRITE_ACK_TIMEOUT_MS else WRITE_CALLBACK_TIMEOUT_MS
|
|
1986
|
+
mainWaiter.waitWhileTrue(timeout)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
// If the packet asked us to do a delay, then do it
|
|
1991
|
+
if (request.waitTime != -1) {
|
|
1992
|
+
Thread.sleep(request.waitTime.toLong())
|
|
1993
|
+
}
|
|
1994
|
+
} catch (e: InterruptedException) {
|
|
1995
|
+
Log.e(TAG,"Nex: Error sending data: ${e.message}")
|
|
1996
|
+
if (isKilled) break
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
} catch (e: InterruptedException) {
|
|
2000
|
+
if (isKilled) {
|
|
2001
|
+
Bridge.log("Nex: Process queue thread interrupted - shutting down")
|
|
2002
|
+
break
|
|
2003
|
+
}
|
|
2004
|
+
Log.e(TAG,"Nex: Error in queue processing: ${e.message}")
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
Bridge.log("Nex: Process queue thread exiting")
|
|
2009
|
+
isWorkerRunning = false
|
|
2010
|
+
}
|
|
2011
|
+
}
|