@mentra/bluetooth-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/android/build.gradle +167 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +109 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/app.plugin.js +1 -0
- package/build/BluetoothSdk.types.d.ts +498 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +23 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/BluetoothSdkModule.d.ts +71 -0
- package/build/BluetoothSdkModule.d.ts.map +1 -0
- package/build/BluetoothSdkModule.js +106 -0
- package/build/BluetoothSdkModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/BluetoothSdkModule.swift +665 -0
- package/ios/MentraBluetoothSDK.podspec +77 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
- package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
- package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
- package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
- package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
- package/ios/Packages/VAD/Data/Configuration.swift +68 -0
- package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
- package/ios/Packages/VAD/Data/Record.swift +13 -0
- package/ios/Packages/VAD/Data/Result.swift +22 -0
- package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
- package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
- package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
- package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/Source/Bridge.swift +353 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +1478 -0
- package/ios/Source/DeviceStore.swift +295 -0
- package/ios/Source/MentraBluetoothSDK.swift +2439 -0
- package/ios/Source/ObservableStore.swift +88 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/controllers/ControllerManager.swift +187 -0
- package/ios/Source/controllers/R1.swift +827 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Frame.swift +651 -0
- package/ios/Source/sgcs/G1.swift +2505 -0
- package/ios/Source/sgcs/G2.swift +3730 -0
- package/ios/Source/sgcs/Mach1.swift +543 -0
- package/ios/Source/sgcs/MentraLive.swift +4708 -0
- package/ios/Source/sgcs/MentraNex.swift +2479 -0
- package/ios/Source/sgcs/SGCManager.swift +243 -0
- package/ios/Source/sgcs/Simulated.swift +270 -0
- package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
- package/ios/Source/stt/STTTools.swift +154 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/Constants.swift +74 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2067 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunker.swift +164 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
- package/ios/Source/utils/TarBz2Extractor.swift +206 -0
- package/package.json +78 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +4 -0
- package/plugin/build/withAndroid.js +80 -0
- package/plugin/build/withIos.d.ts +4 -0
- package/plugin/build/withIos.js +64 -0
- package/src/BluetoothSdk.types.ts +581 -0
- package/src/BluetoothSdkModule.ts +259 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,2479 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MentraNex.swift
|
|
3
|
+
// MentraOS_Manager
|
|
4
|
+
//
|
|
5
|
+
// Created by Gemini on 2024-07-29.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Combine
|
|
9
|
+
import CoreBluetooth
|
|
10
|
+
import Foundation
|
|
11
|
+
import SwiftProtobuf
|
|
12
|
+
import UIKit
|
|
13
|
+
|
|
14
|
+
/// Helper extension for debugging
|
|
15
|
+
extension Data {
|
|
16
|
+
func toHexString() -> String {
|
|
17
|
+
map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Nex firmware expects tier 1–3 in protobuf `DisplayDistanceConfig.distance_cm` (name is legacy, not cm).
|
|
22
|
+
/// Keep in sync with `NexProtobufUtils.dashboardDepthToDistanceCm` (Android `NexSGCUtils.kt`).
|
|
23
|
+
enum NexDashboardDisplayWire {
|
|
24
|
+
static let depthMin = 1
|
|
25
|
+
static let depthMax = 3
|
|
26
|
+
|
|
27
|
+
static func depthToWireTier(_ depth: Int) -> UInt32 {
|
|
28
|
+
UInt32(min(max(depth, depthMin), depthMax))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Read an `Any?` value from the store, default to `depthMin`, and clamp to valid range.
|
|
32
|
+
static func clampDepthFromStore(_ value: Any?) -> Int {
|
|
33
|
+
let raw = value as? Int ?? depthMin
|
|
34
|
+
return min(max(raw, depthMin), depthMax)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@MainActor
|
|
39
|
+
@objc(MentraNexSGC)
|
|
40
|
+
class MentraNexSGC: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SGCManager {
|
|
41
|
+
func sendIncidentId(_: String, apiBaseUrl _: String?) {}
|
|
42
|
+
|
|
43
|
+
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
44
|
+
|
|
45
|
+
func setMicEnabled(_: Bool) {}
|
|
46
|
+
|
|
47
|
+
func requestPhoto(
|
|
48
|
+
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
49
|
+
compress _: String?, flash _: Bool, sound _: Bool
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
func startStream(_: [String: Any]) {}
|
|
53
|
+
|
|
54
|
+
func stopStream() {}
|
|
55
|
+
|
|
56
|
+
func sendStreamKeepAlive(_: [String: Any]) {}
|
|
57
|
+
|
|
58
|
+
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
59
|
+
|
|
60
|
+
func stopVideoRecording(requestId _: String) {}
|
|
61
|
+
|
|
62
|
+
func sendButtonPhotoSettings() {}
|
|
63
|
+
|
|
64
|
+
func sendButtonVideoRecordingSettings() {}
|
|
65
|
+
|
|
66
|
+
func sendButtonMaxRecordingTime() {}
|
|
67
|
+
|
|
68
|
+
func sendButtonCameraLedSetting() {}
|
|
69
|
+
|
|
70
|
+
func sendCameraFovSetting() {}
|
|
71
|
+
|
|
72
|
+
func setBrightness(_: Int, autoMode _: Bool) {}
|
|
73
|
+
|
|
74
|
+
func sendDoubleTextWall(_: String, _: String) {}
|
|
75
|
+
|
|
76
|
+
func displayBitmap(base64ImageData _: String) async -> Bool {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func showDashboard() {}
|
|
81
|
+
|
|
82
|
+
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
83
|
+
// Same order as Android MentraNex: display_height then display_distance.
|
|
84
|
+
updateGlassesDisplayHeight(height)
|
|
85
|
+
updateGlassesDisplayDistance(depth: depth)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func setDashboardHeightOnly(_ height: Int) {
|
|
89
|
+
updateGlassesDisplayHeight(height)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func setDashboardDepthOnly(_ depth: Int) {
|
|
93
|
+
updateGlassesDisplayDistance(depth: depth)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func setHeadUpAngle(_: Int) {}
|
|
97
|
+
|
|
98
|
+
func getBatteryStatus() {}
|
|
99
|
+
|
|
100
|
+
func setSilentMode(_: Bool) {}
|
|
101
|
+
|
|
102
|
+
func exit() {}
|
|
103
|
+
|
|
104
|
+
func sendShutdown() {}
|
|
105
|
+
|
|
106
|
+
func sendReboot() {}
|
|
107
|
+
|
|
108
|
+
func sendRgbLedControl(
|
|
109
|
+
requestId _: String, packageName _: String?, action _: String, color _: String?, ontime _: Int,
|
|
110
|
+
offtime _: Int, count _: Int
|
|
111
|
+
) {}
|
|
112
|
+
|
|
113
|
+
func forget() {}
|
|
114
|
+
|
|
115
|
+
func connectById(_: String) {}
|
|
116
|
+
|
|
117
|
+
func getConnectedBluetoothName() -> String? {
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func cleanup() {}
|
|
122
|
+
|
|
123
|
+
func ping() {}
|
|
124
|
+
func connectController() {}
|
|
125
|
+
func disconnectController() {}
|
|
126
|
+
|
|
127
|
+
func dbg1() {}
|
|
128
|
+
func dbg2() {}
|
|
129
|
+
|
|
130
|
+
func requestWifiScan() {}
|
|
131
|
+
|
|
132
|
+
func sendWifiCredentials(_: String, _: String) {}
|
|
133
|
+
|
|
134
|
+
func forgetWifiNetwork(_: String) {}
|
|
135
|
+
|
|
136
|
+
func sendHotspotState(_: Bool) {}
|
|
137
|
+
|
|
138
|
+
func sendOtaStart() {}
|
|
139
|
+
func sendOtaQueryStatus() {}
|
|
140
|
+
|
|
141
|
+
func sendUserEmailToGlasses(_: String) {}
|
|
142
|
+
|
|
143
|
+
func queryGalleryStatus() {}
|
|
144
|
+
|
|
145
|
+
@objc static func requiresMainQueueSetup() -> Bool {
|
|
146
|
+
true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func sendGalleryMode() {}
|
|
150
|
+
|
|
151
|
+
func requestVersionInfo() {
|
|
152
|
+
Bridge.log("MentraNex: requestVersionInfo - not supported on MentraNex")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - Properties
|
|
156
|
+
|
|
157
|
+
private var centralManager: CBCentralManager?
|
|
158
|
+
|
|
159
|
+
private var peripheral: CBPeripheral?
|
|
160
|
+
private var writeCharacteristic: CBCharacteristic?
|
|
161
|
+
private var notifyCharacteristic: CBCharacteristic?
|
|
162
|
+
private var _isScanning = false
|
|
163
|
+
private var isConnecting = false
|
|
164
|
+
private var nexReady = false
|
|
165
|
+
private var isDisconnecting = false
|
|
166
|
+
private var reconnectionTimer: Timer?
|
|
167
|
+
private var reconnectionAttempts = 0
|
|
168
|
+
// TODO: change this
|
|
169
|
+
private let maxReconnectionAttempts = -1 // -1 for unlimited
|
|
170
|
+
private let reconnectionInterval: TimeInterval = 5.0 // 5 seconds
|
|
171
|
+
private var peripheralToConnectName: String?
|
|
172
|
+
|
|
173
|
+
// Heartbeat tracking (like Java implementation)
|
|
174
|
+
private var heartbeatCount = 0
|
|
175
|
+
private var lastHeartbeatSentTime: TimeInterval = 0
|
|
176
|
+
private var lastHeartbeatReceivedTime: TimeInterval = 0
|
|
177
|
+
|
|
178
|
+
// Microphone beat system (like Java implementation)
|
|
179
|
+
private var micBeatTimer: Timer?
|
|
180
|
+
private var micBeatCount = 0
|
|
181
|
+
private let MICBEAT_INTERVAL_MS: TimeInterval = 30 * 60 // 30 minutes like Java
|
|
182
|
+
private var shouldUseGlassesMic = true
|
|
183
|
+
private var microphoneStateBeforeDisconnection = false
|
|
184
|
+
|
|
185
|
+
// Whitelist system (like Java implementation)
|
|
186
|
+
private var whiteListedAlready = false
|
|
187
|
+
private let WHITELIST_CMD: UInt8 = 0x04
|
|
188
|
+
|
|
189
|
+
/// Protobuf version tracking (like Java implementation)
|
|
190
|
+
private var protobufVersionPosted = false
|
|
191
|
+
|
|
192
|
+
/// Device discovery cache (like MentraLive)
|
|
193
|
+
private var discoveredPeripherals = [String: CBPeripheral]() // name -> peripheral
|
|
194
|
+
|
|
195
|
+
// MARK: - Published Properties (G1-compatible)
|
|
196
|
+
|
|
197
|
+
@Published var vadActive: Bool = false
|
|
198
|
+
@Published var deviceReady: Bool = false
|
|
199
|
+
|
|
200
|
+
// Audio properties (G1-compatible)
|
|
201
|
+
@Published var compressedVoiceData: Data = .init()
|
|
202
|
+
@Published var aiListening: Bool = false
|
|
203
|
+
|
|
204
|
+
// Device info properties
|
|
205
|
+
@Published var deviceFirmwareVersion: String = ""
|
|
206
|
+
@Published var deviceHardwareModel: String = ""
|
|
207
|
+
|
|
208
|
+
// IMU data properties
|
|
209
|
+
@Published var accelerometer: [Float] = [0.0, 0.0, 0.0]
|
|
210
|
+
@Published var gyroscope: [Float] = [0.0, 0.0, 0.0]
|
|
211
|
+
@Published var magnetometer: [Float] = [0.0, 0.0, 0.0]
|
|
212
|
+
|
|
213
|
+
// Button state properties
|
|
214
|
+
@Published var lastButtonPressed: Int = -1
|
|
215
|
+
@Published var lastButtonState: String = ""
|
|
216
|
+
|
|
217
|
+
// Head gesture properties
|
|
218
|
+
@Published var lastHeadGesture: String = ""
|
|
219
|
+
@Published var headUpAngle: Int = 0
|
|
220
|
+
|
|
221
|
+
// Enhanced device persistence (from Java implementation)
|
|
222
|
+
private let PREFS_DEVICE_NAME = "MentraNexLastConnectedDeviceName"
|
|
223
|
+
private let PREFS_DEVICE_ADDRESS = "MentraNexLastConnectedDeviceAddress"
|
|
224
|
+
private let PREFS_DEVICE_ID = "SavedNexIdKey"
|
|
225
|
+
private let SHARED_PREFS_NAME = "NexGlassesPrefs"
|
|
226
|
+
|
|
227
|
+
// Device state tracking (ported from Java)
|
|
228
|
+
private var savedDeviceName: String?
|
|
229
|
+
private var savedDeviceAddress: String?
|
|
230
|
+
private var preferredDeviceId: String?
|
|
231
|
+
private var isKilled = false
|
|
232
|
+
private var scanOnPowerOn = false
|
|
233
|
+
|
|
234
|
+
private let bluetoothQueue = DispatchQueue(label: "MentraNexBluetooth", qos: .userInitiated)
|
|
235
|
+
|
|
236
|
+
/// Protocol-required connectionState (String)
|
|
237
|
+
var connectionState: String = ConnTypes.DISCONNECTED
|
|
238
|
+
|
|
239
|
+
// Protocol-required properties
|
|
240
|
+
var type: String = DeviceTypes.NEX
|
|
241
|
+
var ready: Bool {
|
|
242
|
+
get { nexReady }
|
|
243
|
+
set { nexReady = newValue }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
var hasMic: Bool = true
|
|
247
|
+
|
|
248
|
+
private var peripheralUUID: UUID? {
|
|
249
|
+
get {
|
|
250
|
+
if let uuidString = UserDefaults.standard.string(forKey: "nexPeripheralUUID") {
|
|
251
|
+
return UUID(uuidString: uuidString)
|
|
252
|
+
}
|
|
253
|
+
return nil
|
|
254
|
+
}
|
|
255
|
+
set {
|
|
256
|
+
if let newValue {
|
|
257
|
+
UserDefaults.standard.set(newValue.uuidString, forKey: "nexPeripheralUUID")
|
|
258
|
+
} else {
|
|
259
|
+
UserDefaults.standard.removeObject(forKey: "nexPeripheralUUID")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Custom Bluetooth queue for better performance (like G1)
|
|
265
|
+
private static let _bluetoothQueue = DispatchQueue(
|
|
266
|
+
label: "com.mentra.nex.bluetooth", qos: .background
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
static var instance: MentraNexSGC?
|
|
270
|
+
|
|
271
|
+
// MARK: - Singleton Access
|
|
272
|
+
|
|
273
|
+
@objc static func getInstance() -> MentraNexSGC {
|
|
274
|
+
if instance == nil {
|
|
275
|
+
instance = MentraNexSGC()
|
|
276
|
+
}
|
|
277
|
+
return instance!
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// UUIDs from MentraNexSGC.java
|
|
281
|
+
private let MAIN_SERVICE_UUID = CBUUID(string: "00004860-0000-1000-8000-00805f9b34fb")
|
|
282
|
+
private let WRITE_CHAR_UUID = CBUUID(string: "000071FF-0000-1000-8000-00805f9b34fb")
|
|
283
|
+
private let NOTIFY_CHAR_UUID = CBUUID(string: "000070FF-0000-1000-8000-00805f9b34fb")
|
|
284
|
+
|
|
285
|
+
// Packet types from MentraNexSGC.java
|
|
286
|
+
private let PACKET_TYPE_JSON: UInt8 = 0x01
|
|
287
|
+
private let PACKET_TYPE_PROTOBUF: UInt8 = 0x02
|
|
288
|
+
private let PACKET_TYPE_AUDIO: UInt8 = 0xA0
|
|
289
|
+
private let PACKET_TYPE_IMAGE: UInt8 = 0xB0
|
|
290
|
+
|
|
291
|
+
// MTU Configuration (iOS-optimized)
|
|
292
|
+
private let MTU_MAX_IOS = 185 // iOS maximum (platform limitation)
|
|
293
|
+
private let MTU_DEFAULT = 23 // Default BLE MTU
|
|
294
|
+
private var currentMTU = 23 // Currently negotiated MTU
|
|
295
|
+
private var deviceMaxMTU = 23 // Device's maximum capability
|
|
296
|
+
private var maxChunkSize = 176 // Calculated optimal chunk size
|
|
297
|
+
private var bmpChunkSize = 176 // Image chunk size (iOS-optimized)
|
|
298
|
+
|
|
299
|
+
// MARK: - Command Queue (modeled after ERG1Manager)
|
|
300
|
+
|
|
301
|
+
private struct BufferedCommand {
|
|
302
|
+
let chunks: [[UInt8]]
|
|
303
|
+
let waitTimeMs: Int
|
|
304
|
+
let chunkDelayMs: Int
|
|
305
|
+
|
|
306
|
+
init(chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int = 8) {
|
|
307
|
+
self.chunks = chunks
|
|
308
|
+
self.waitTimeMs = waitTimeMs
|
|
309
|
+
self.chunkDelayMs = chunkDelayMs
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private actor CommandQueue {
|
|
314
|
+
private var commands: [BufferedCommand] = []
|
|
315
|
+
private var continuations: [CheckedContinuation<BufferedCommand, Never>] = []
|
|
316
|
+
|
|
317
|
+
func enqueue(_ command: BufferedCommand) {
|
|
318
|
+
if let continuation = continuations.first {
|
|
319
|
+
continuations.removeFirst()
|
|
320
|
+
continuation.resume(returning: command)
|
|
321
|
+
} else {
|
|
322
|
+
commands.append(command)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func dequeue() async -> BufferedCommand {
|
|
327
|
+
if let command = commands.first {
|
|
328
|
+
commands.removeFirst()
|
|
329
|
+
return command
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return await withCheckedContinuation { continuation in
|
|
333
|
+
continuations.append(continuation)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private let commandQueue = CommandQueue()
|
|
339
|
+
private var isQueueWorkerRunning = false
|
|
340
|
+
|
|
341
|
+
// MARK: - Initialization
|
|
342
|
+
|
|
343
|
+
override private init() {
|
|
344
|
+
super.init()
|
|
345
|
+
Bridge.log("NEX: 🚀 MentraNexSGC initialization started")
|
|
346
|
+
|
|
347
|
+
// Load saved device information (from Java implementation)
|
|
348
|
+
loadSavedDeviceInfo()
|
|
349
|
+
|
|
350
|
+
// Using custom Bluetooth queue for better performance (like G1)
|
|
351
|
+
Bridge.log("NEX: 📱 Creating CBCentralManager with custom Bluetooth queue")
|
|
352
|
+
centralManager = CBCentralManager(delegate: self, queue: MentraNexSGC._bluetoothQueue)
|
|
353
|
+
|
|
354
|
+
Bridge.log("NEX: ✅ MentraNexSGC initialization completed")
|
|
355
|
+
Bridge.log("NEX: 📱 Central Manager created: \(centralManager != nil ? "YES" : "NO")")
|
|
356
|
+
if let centralManager {
|
|
357
|
+
Bridge.log("NEX: 📱 Initial Bluetooth State: \(centralManager.state.rawValue)")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
Bridge.log(
|
|
361
|
+
"NEX: 💾 Loaded saved device - Name: \(savedDeviceName ?? "None"), Address: \(savedDeviceAddress ?? "None")"
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private func setupCommandQueue() {
|
|
366
|
+
if isQueueWorkerRunning { return }
|
|
367
|
+
isQueueWorkerRunning = true
|
|
368
|
+
|
|
369
|
+
Task.detached { [weak self] in
|
|
370
|
+
guard let self else { return }
|
|
371
|
+
while true {
|
|
372
|
+
let command = await self.commandQueue.dequeue()
|
|
373
|
+
await self.processCommand(command)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0, chunkDelayMs: Int = 8) {
|
|
379
|
+
let cmd = BufferedCommand(
|
|
380
|
+
chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: chunkDelayMs
|
|
381
|
+
)
|
|
382
|
+
Task { [weak self] in
|
|
383
|
+
await self?.commandQueue.enqueue(cmd)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Enhanced method that uses MTU-optimized chunking
|
|
388
|
+
private func queueDataWithOptimalChunking(
|
|
389
|
+
_ data: Data, packetType: UInt8 = 0x02, waitTimeMs: Int = 0
|
|
390
|
+
) {
|
|
391
|
+
var chunks: [[UInt8]] = []
|
|
392
|
+
let effectiveChunkSize = maxChunkSize - 1 // Reserve 1 byte for packet type
|
|
393
|
+
|
|
394
|
+
// Add packet type as first byte
|
|
395
|
+
var packetData = Data([packetType])
|
|
396
|
+
packetData.append(data)
|
|
397
|
+
|
|
398
|
+
// Split into MTU-optimized chunks
|
|
399
|
+
var offset = 0
|
|
400
|
+
while offset < packetData.count {
|
|
401
|
+
let chunkSize = min(effectiveChunkSize, packetData.count - offset)
|
|
402
|
+
let chunkData = packetData.subdata(in: offset ..< (offset + chunkSize))
|
|
403
|
+
chunks.append(Array(chunkData))
|
|
404
|
+
offset += chunkSize
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
Bridge.log(
|
|
408
|
+
"NEX: 📦 Created \(chunks.count) MTU-optimized chunks (max size: \(effectiveChunkSize) bytes)"
|
|
409
|
+
)
|
|
410
|
+
queueChunks(chunks, waitTimeMs: waitTimeMs)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/// Helper method for queueing chunks with optional wait time
|
|
414
|
+
private func queueChunks(_ chunks: [[UInt8]], waitTimeMs: Int = 0) {
|
|
415
|
+
let cmd = BufferedCommand(chunks: chunks, waitTimeMs: waitTimeMs, chunkDelayMs: 8)
|
|
416
|
+
Task { [weak self] in
|
|
417
|
+
await self?.commandQueue.enqueue(cmd)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private func processCommand(_ command: BufferedCommand) async {
|
|
422
|
+
guard let peripheral, let writeCharacteristic else {
|
|
423
|
+
Bridge.log("NEX: ⚠️ processCommand: peripheral/characteristic not ready")
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Send each chunk sequentially
|
|
428
|
+
for (index, chunk) in command.chunks.enumerated() {
|
|
429
|
+
let data = Data(chunk)
|
|
430
|
+
Bridge.log(
|
|
431
|
+
"NEX: 📦 Sending chunk \(index) of \(command.chunks.count) to \(peripheral.name ?? "Unknown")"
|
|
432
|
+
)
|
|
433
|
+
Bridge.log("NEX: 📦 Chunk data: \(data.toHexString())")
|
|
434
|
+
peripheral.writeValue(data, for: writeCharacteristic, type: .withResponse)
|
|
435
|
+
|
|
436
|
+
// Delay between chunks except maybe after the last chunk if waitTime will handle it
|
|
437
|
+
if index < command.chunks.count - 1 {
|
|
438
|
+
try? await Task.sleep(nanoseconds: UInt64(command.chunkDelayMs) * 1_000_000)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Optional wait after the command
|
|
443
|
+
if command.waitTimeMs > 0 {
|
|
444
|
+
try? await Task.sleep(nanoseconds: UInt64(command.waitTimeMs) * 1_000_000)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// MARK: - Device Persistence (ported from Java)
|
|
449
|
+
|
|
450
|
+
private func loadSavedDeviceInfo() {
|
|
451
|
+
savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME)
|
|
452
|
+
savedDeviceAddress = UserDefaults.standard.string(forKey: PREFS_DEVICE_ADDRESS)
|
|
453
|
+
preferredDeviceId = UserDefaults.standard.string(forKey: PREFS_DEVICE_ID)
|
|
454
|
+
|
|
455
|
+
Bridge.log(
|
|
456
|
+
"NEX: 💾 Loaded device info - Name: \(savedDeviceName ?? "None"), Address: \(savedDeviceAddress ?? "None"), ID: \(preferredDeviceId ?? "None")"
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private func savePairedDeviceInfo(name: String?, address: String?) {
|
|
461
|
+
if let name {
|
|
462
|
+
UserDefaults.standard.set(name, forKey: PREFS_DEVICE_NAME)
|
|
463
|
+
savedDeviceName = name
|
|
464
|
+
Bridge.log("NEX: 💾 Saved device name: \(name)")
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if let address {
|
|
468
|
+
UserDefaults.standard.set(address, forKey: PREFS_DEVICE_ADDRESS)
|
|
469
|
+
savedDeviceAddress = address
|
|
470
|
+
Bridge.log("NEX: 💾 Saved device address: \(address)")
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@objc func savePreferredDeviceId(_ deviceId: String) {
|
|
475
|
+
UserDefaults.standard.set(deviceId, forKey: PREFS_DEVICE_ID)
|
|
476
|
+
preferredDeviceId = deviceId
|
|
477
|
+
Bridge.log("NEX: 💾 Saved preferred device ID: \(deviceId)")
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@objc func clearSavedDeviceInfo() {
|
|
481
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_NAME)
|
|
482
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_ADDRESS)
|
|
483
|
+
UserDefaults.standard.removeObject(forKey: PREFS_DEVICE_ID)
|
|
484
|
+
|
|
485
|
+
savedDeviceName = nil
|
|
486
|
+
savedDeviceAddress = nil
|
|
487
|
+
preferredDeviceId = nil
|
|
488
|
+
peripheralUUID = nil
|
|
489
|
+
|
|
490
|
+
Bridge.log("NEX: 🗑️ Cleared all saved device information")
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// MARK: - Enhanced Device Filtering (ported from Java)
|
|
494
|
+
|
|
495
|
+
private func isCompatibleNexDevice(_ deviceName: String) -> Bool {
|
|
496
|
+
// Enhanced filtering logic from Java implementation
|
|
497
|
+
let compatiblePrefixes = [
|
|
498
|
+
"NexSim",
|
|
499
|
+
// "MENTRA",
|
|
500
|
+
// "NEX",
|
|
501
|
+
// "Nex",
|
|
502
|
+
// "MentraNex",
|
|
503
|
+
// "MENTRA_NEX",
|
|
504
|
+
// "Xy_A", // Legacy support
|
|
505
|
+
// "XyBLE_", // Legacy support
|
|
506
|
+
// "MENTRA_LIVE", // Cross-compatibility
|
|
507
|
+
]
|
|
508
|
+
|
|
509
|
+
for prefix in compatiblePrefixes {
|
|
510
|
+
if deviceName.hasPrefix(prefix) || deviceName.contains(prefix) {
|
|
511
|
+
Bridge.log("NEX: ✅ Device '\(deviceName)' matches compatible prefix: \(prefix)")
|
|
512
|
+
return true
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return false
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private func extractDeviceId(from deviceName: String) -> String? {
|
|
520
|
+
// Extract device ID pattern similar to Java implementation
|
|
521
|
+
let patterns = [
|
|
522
|
+
"Mentra_([0-9A-Fa-f]+)",
|
|
523
|
+
"NEX_([0-9A-Fa-f]+)",
|
|
524
|
+
"MENTRA_NEX_([0-9A-Fa-f]+)",
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
for pattern in patterns {
|
|
528
|
+
let regex = try? NSRegularExpression(pattern: pattern)
|
|
529
|
+
let range = NSRange(deviceName.startIndex ..< deviceName.endIndex, in: deviceName)
|
|
530
|
+
if let match = regex?.firstMatch(in: deviceName, options: [], range: range),
|
|
531
|
+
let matchRange = Range(match.range(at: 1), in: deviceName)
|
|
532
|
+
{
|
|
533
|
+
let deviceId = String(deviceName[matchRange])
|
|
534
|
+
Bridge.log("NEX: 🏷️ Extracted device ID: \(deviceId) from \(deviceName)")
|
|
535
|
+
return deviceId
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
Bridge.log("NEX: ⚠️ Could not extract device ID from: \(deviceName)")
|
|
540
|
+
return nil
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// MARK: - Connection Logic (enhanced from G1)
|
|
544
|
+
|
|
545
|
+
@objc(connectByName:)
|
|
546
|
+
func connect(name: String) {
|
|
547
|
+
Bridge.log("NEX-CONN: 🔗 connect(name:) called with \(name)")
|
|
548
|
+
if _isScanning {
|
|
549
|
+
stopScan()
|
|
550
|
+
}
|
|
551
|
+
peripheralToConnectName = name
|
|
552
|
+
startScan()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private func connectByUUID() -> Bool {
|
|
556
|
+
guard let uuid = peripheralUUID else {
|
|
557
|
+
Bridge.log("NEX-CONN: 🔵 No stored UUID to connect by.")
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
guard let centralManager else {
|
|
562
|
+
Bridge.log("NEX-CONN: ❌ Central Manager is nil, cannot connect by UUID.")
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
Bridge.log(
|
|
567
|
+
"NEX-CONN: 🔵 Attempting to retrieve peripheral with stored UUID: \(uuid.uuidString)"
|
|
568
|
+
)
|
|
569
|
+
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid])
|
|
570
|
+
|
|
571
|
+
if let peripheralToConnect = peripherals.first {
|
|
572
|
+
Bridge.log(
|
|
573
|
+
"NEX-CONN: 🔵 Found peripheral by UUID: \(peripheralToConnect.name ?? "Unknown"). Initiating connection."
|
|
574
|
+
)
|
|
575
|
+
peripheral = peripheralToConnect
|
|
576
|
+
centralManager.connect(peripheralToConnect, options: nil)
|
|
577
|
+
return true
|
|
578
|
+
} else {
|
|
579
|
+
Bridge.log(
|
|
580
|
+
"NEX-CONN: 🔵 Could not find peripheral for stored UUID. Will proceed to scan."
|
|
581
|
+
)
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private func startReconnectionTimer() {
|
|
587
|
+
Bridge.log("NEX-CONN: 🔄 Starting reconnection timer...")
|
|
588
|
+
stopReconnectionTimer() // Ensure no existing timer is running
|
|
589
|
+
reconnectionAttempts = 0
|
|
590
|
+
|
|
591
|
+
DispatchQueue.main.async {
|
|
592
|
+
self.reconnectionTimer = Timer.scheduledTimer(
|
|
593
|
+
timeInterval: self.reconnectionInterval,
|
|
594
|
+
target: self,
|
|
595
|
+
selector: #selector(self.attemptReconnection),
|
|
596
|
+
userInfo: nil,
|
|
597
|
+
repeats: true
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private func stopReconnectionTimer() {
|
|
603
|
+
if reconnectionTimer != nil {
|
|
604
|
+
Bridge.log("NEX-CONN: 🛑 Stopping reconnection timer.")
|
|
605
|
+
reconnectionTimer?.invalidate()
|
|
606
|
+
reconnectionTimer = nil
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
@objc private func attemptReconnection() {
|
|
611
|
+
if nexReady {
|
|
612
|
+
Bridge.log("NEX-CONN: ✅ Already connected, stopping reconnection attempts.")
|
|
613
|
+
stopReconnectionTimer()
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if maxReconnectionAttempts != -1, reconnectionAttempts >= maxReconnectionAttempts {
|
|
618
|
+
Bridge.log("NEX-CONN: ❌ Max reconnection attempts reached.")
|
|
619
|
+
stopReconnectionTimer()
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
reconnectionAttempts += 1
|
|
624
|
+
Bridge.log("NEX-CONN: 🔄 Attempting reconnection (\(reconnectionAttempts))...")
|
|
625
|
+
startScan()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// MARK: - Public Methods
|
|
629
|
+
|
|
630
|
+
private func startScan() {
|
|
631
|
+
Bridge.log("NEX-CONN: 🔍 startScan called")
|
|
632
|
+
|
|
633
|
+
isDisconnecting = false // Reset intentional disconnect flag
|
|
634
|
+
|
|
635
|
+
guard let centralManager else {
|
|
636
|
+
Bridge.log("NEX-CONN: ❌ Central Manager is nil!")
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
guard centralManager.state == .poweredOn else {
|
|
641
|
+
Bridge.log(
|
|
642
|
+
"NEX-CONN: ❌ Bluetooth not powered on. State: \(centralManager.state.rawValue)"
|
|
643
|
+
)
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// First, try to reconnect using stored UUID (faster and works in background)
|
|
648
|
+
if connectByUUID() {
|
|
649
|
+
Bridge.log("NEX-CONN: 🔄 Attempting connection with stored UUID. Halting scan.")
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// If that fails, check for already-connected system devices
|
|
654
|
+
let connectedPeripherals = centralManager.retrieveConnectedPeripherals(withServices: [
|
|
655
|
+
MAIN_SERVICE_UUID,
|
|
656
|
+
])
|
|
657
|
+
if let targetName = peripheralToConnectName,
|
|
658
|
+
let existingPeripheral = connectedPeripherals.first(where: {
|
|
659
|
+
$0.name?.contains(targetName) == true
|
|
660
|
+
})
|
|
661
|
+
{
|
|
662
|
+
Bridge.log(
|
|
663
|
+
"NEX-CONN: 📱 Found already connected peripheral that matches target: \(existingPeripheral.name ?? "Unknown")"
|
|
664
|
+
)
|
|
665
|
+
if peripheral == nil {
|
|
666
|
+
peripheral = existingPeripheral
|
|
667
|
+
centralManager.connect(existingPeripheral, options: nil)
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Check if we have a saved device name to reconnect to (like MentraLive)
|
|
673
|
+
if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
|
|
674
|
+
!savedDeviceName.isEmpty
|
|
675
|
+
{
|
|
676
|
+
Bridge.log("NEX-CONN: 🔄 Looking for saved device: \(savedDeviceName)")
|
|
677
|
+
// This will be handled in didDiscover when the device is found
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
Bridge.log("NEX-CONN: ✅ Bluetooth is powered on, starting scan...")
|
|
681
|
+
_isScanning = true
|
|
682
|
+
|
|
683
|
+
// Scan for ALL devices, not just those with specific services
|
|
684
|
+
// Use same options as G1 scanner for consistency
|
|
685
|
+
let scanOptions: [String: Any] = [
|
|
686
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false, // Don't allow duplicate advertisements
|
|
687
|
+
]
|
|
688
|
+
centralManager.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
689
|
+
|
|
690
|
+
Bridge.log("NEX-CONN: 🚀 Scan started successfully")
|
|
691
|
+
|
|
692
|
+
// Re-emit already discovered peripherals (like MentraLive)
|
|
693
|
+
for (_, peripheral) in discoveredPeripherals {
|
|
694
|
+
Bridge.log(
|
|
695
|
+
"NEX-CONN: 📡 (Re-emitting from cache) peripheral: \(peripheral.name ?? "Unknown")"
|
|
696
|
+
)
|
|
697
|
+
if let name = peripheral.name {
|
|
698
|
+
emitDiscoveredDevice(name)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// No auto-stop timer (like G1) - manual control
|
|
703
|
+
Bridge.log("NEX-CONN: 💡 To stop scanning manually, call: MentraNexSGC.shared.stopScan()")
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
@objc func stopScan() {
|
|
707
|
+
centralManager?.stopScan()
|
|
708
|
+
_isScanning = false
|
|
709
|
+
Bridge.log("NEX-CONN: 🛑 Stopped scanning.")
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
@objc func isScanning() -> Bool {
|
|
713
|
+
_isScanning
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
@objc func isConnected() -> Bool {
|
|
717
|
+
nexReady && connectionState == ConnTypes.CONNECTED
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@objc func getConnectionState() -> String {
|
|
721
|
+
switch connectionState {
|
|
722
|
+
case ConnTypes.DISCONNECTED:
|
|
723
|
+
return "disconnected"
|
|
724
|
+
case ConnTypes.CONNECTING:
|
|
725
|
+
return "connecting"
|
|
726
|
+
case ConnTypes.CONNECTED:
|
|
727
|
+
return "connected"
|
|
728
|
+
default:
|
|
729
|
+
return "disconnected"
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// MARK: - MTU Information Access
|
|
734
|
+
|
|
735
|
+
@objc func getCurrentMTU() -> Int {
|
|
736
|
+
currentMTU
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
@objc func getMaxChunkSize() -> Int {
|
|
740
|
+
maxChunkSize
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
@objc func getDeviceMaxMTU() -> Int {
|
|
744
|
+
deviceMaxMTU
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
@objc func getMTUInfo() -> [String: Any] {
|
|
748
|
+
[
|
|
749
|
+
"current_mtu": currentMTU,
|
|
750
|
+
"device_max_mtu": deviceMaxMTU,
|
|
751
|
+
"max_chunk_size": maxChunkSize,
|
|
752
|
+
"bmp_chunk_size": bmpChunkSize,
|
|
753
|
+
"mtu_negotiated": nexReady,
|
|
754
|
+
]
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@objc func findCompatibleDevices() {
|
|
758
|
+
Bridge.log("NEX-DISCOVERY: Finding compatible devices. Clearing connection targets.")
|
|
759
|
+
|
|
760
|
+
// Clear any specific device targets to ensure we are only discovering
|
|
761
|
+
peripheralToConnectName = nil
|
|
762
|
+
clearSavedDeviceInfo() // This clears UserDefaults and our in-memory cache of saved/preferred devices.
|
|
763
|
+
|
|
764
|
+
Task {
|
|
765
|
+
if centralManager == nil {
|
|
766
|
+
centralManager = CBCentralManager(
|
|
767
|
+
delegate: self, queue: bluetoothQueue,
|
|
768
|
+
options: ["CBCentralManagerOptionShowPowerAlertKey": 0]
|
|
769
|
+
)
|
|
770
|
+
// wait for the central manager to be fully initialized before we start scanning:
|
|
771
|
+
try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if centralManager?.state == .poweredOn {
|
|
775
|
+
startScan()
|
|
776
|
+
} else {
|
|
777
|
+
Bridge.log("NEX-DISCOVERY: Bluetooth not ready, will scan on power on.")
|
|
778
|
+
scanOnPowerOn = true
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
func sendTextWall(_ text: String) {
|
|
784
|
+
guard nexReady else {
|
|
785
|
+
Bridge.log("NEX: Not ready to display text. Device not initialized.")
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
Bridge.log("NEX: Displaying text wall: '\(text)'")
|
|
790
|
+
|
|
791
|
+
let displayText = Mentraos_Ble_DisplayText.with {
|
|
792
|
+
$0.text = text
|
|
793
|
+
$0.size = 48
|
|
794
|
+
$0.x = 20
|
|
795
|
+
$0.y = 260
|
|
796
|
+
$0.color = 10000
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
800
|
+
$0.displayText = displayText
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
804
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
@objc func displayTextLine(_ text: String) {
|
|
808
|
+
sendTextWall(text)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
@objc func displayDoubleTextWall(_ textTop: String, textBottom: String) {
|
|
812
|
+
let combinedText = "\(textTop)\n\n\(textBottom)"
|
|
813
|
+
sendTextWall(combinedText)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
@objc func displayReferenceCardSimple(_ title: String, body: String) {
|
|
817
|
+
let combinedText = "\(title)\n\n\(body)"
|
|
818
|
+
sendTextWall(combinedText)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
@objc func displayRowsCard(_ rowStrings: [String]) {
|
|
822
|
+
let combinedText = rowStrings.joined(separator: "\n")
|
|
823
|
+
sendTextWall(combinedText)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
@objc func displayBulletList(_ title: String, bullets: [String]) {
|
|
827
|
+
var text = title
|
|
828
|
+
if !title.isEmpty {
|
|
829
|
+
text += "\n"
|
|
830
|
+
}
|
|
831
|
+
text += bullets.map { "• \($0)" }.joined(separator: "\n")
|
|
832
|
+
sendTextWall(text)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
@objc func displayScrollingText(_ text: String) {
|
|
836
|
+
guard nexReady else {
|
|
837
|
+
Bridge.log("NEX: Not ready to display scrolling text. Device not initialized.")
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
Bridge.log("NEX: Displaying scrolling text: '\(text)'")
|
|
842
|
+
|
|
843
|
+
let displayScrollingText = Mentraos_Ble_DisplayScrollingText.with {
|
|
844
|
+
$0.text = text
|
|
845
|
+
$0.size = 48
|
|
846
|
+
$0.x = 20
|
|
847
|
+
$0.y = 50
|
|
848
|
+
$0.width = 200
|
|
849
|
+
$0.height = 100
|
|
850
|
+
$0.speed = 50
|
|
851
|
+
$0.pauseMs = 10
|
|
852
|
+
$0.loop = true
|
|
853
|
+
$0.align = .center
|
|
854
|
+
$0.lineSpacing = 2
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
858
|
+
$0.displayScrollingText = displayScrollingText
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
862
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// MARK: - Display Image Commands
|
|
866
|
+
|
|
867
|
+
@objc func displayBitmap(_ bitmap: UIImage) {
|
|
868
|
+
guard nexReady else {
|
|
869
|
+
Bridge.log("NEX: Not ready to display bitmap. Device not initialized.")
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
Bridge.log("NEX: Displaying bitmap image")
|
|
874
|
+
|
|
875
|
+
// Convert UIImage to raw bitmap data
|
|
876
|
+
guard let bmpData = convertUIImageToBmpData(bitmap) else {
|
|
877
|
+
Bridge.log("NEX: Failed to convert UIImage to BMP data")
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
displayBitmapData(bmpData, width: Int(bitmap.size.width), height: Int(bitmap.size.height))
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
@objc func displayBitmapFromData(_ bmpData: Data, width: Int, height: Int) {
|
|
885
|
+
displayBitmapData(bmpData, width: width, height: height)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private func displayBitmapData(_ bmpData: Data, width: Int, height: Int) {
|
|
889
|
+
guard nexReady else {
|
|
890
|
+
Bridge.log("NEX: Not ready to display bitmap data. Device not initialized.")
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
Bridge.log("NEX: Displaying bitmap data (\(bmpData.count) bytes, \(width)x\(height))")
|
|
895
|
+
|
|
896
|
+
// Generate stream ID for image transfer
|
|
897
|
+
let streamId = String(format: "%04X", Int.random(in: 0 ... 0xFFFF))
|
|
898
|
+
let totalChunks = Int(ceil(Double(bmpData.count) / Double(bmpChunkSize)))
|
|
899
|
+
|
|
900
|
+
// Send display image command first
|
|
901
|
+
let displayImage = Mentraos_Ble_DisplayImage.with {
|
|
902
|
+
$0.streamID = streamId
|
|
903
|
+
$0.totalChunks = UInt32(totalChunks)
|
|
904
|
+
$0.x = 0
|
|
905
|
+
$0.y = 0
|
|
906
|
+
$0.width = UInt32(width)
|
|
907
|
+
$0.height = UInt32(height)
|
|
908
|
+
$0.encoding = "raw"
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
912
|
+
$0.msgID = "img_start_1"
|
|
913
|
+
$0.displayImage = displayImage
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
917
|
+
queueDataWithOptimalChunking(
|
|
918
|
+
protobufData, packetType: PACKET_TYPE_PROTOBUF, waitTimeMs: 100
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
// Send image chunks
|
|
922
|
+
sendImageChunks(streamId: streamId, imageData: bmpData)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private func sendImageChunks(streamId: String, imageData: Data) {
|
|
926
|
+
let streamIdInt = Int(streamId, radix: 16) ?? 0
|
|
927
|
+
let totalChunks = Int(ceil(Double(imageData.count) / Double(bmpChunkSize)))
|
|
928
|
+
|
|
929
|
+
var chunks: [[UInt8]] = []
|
|
930
|
+
|
|
931
|
+
for i in 0 ..< totalChunks {
|
|
932
|
+
let start = i * bmpChunkSize
|
|
933
|
+
let end = min(start + bmpChunkSize, imageData.count)
|
|
934
|
+
let chunkData = imageData.subdata(in: start ..< end)
|
|
935
|
+
|
|
936
|
+
var header: [UInt8] = [
|
|
937
|
+
PACKET_TYPE_IMAGE, // 0xB0
|
|
938
|
+
UInt8((streamIdInt >> 8) & 0xFF), // Stream ID high byte
|
|
939
|
+
UInt8(streamIdInt & 0xFF), // Stream ID low byte
|
|
940
|
+
UInt8(i & 0xFF), // Chunk index
|
|
941
|
+
]
|
|
942
|
+
header.append(contentsOf: chunkData)
|
|
943
|
+
chunks.append(header)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
Bridge.log("NEX: Sending \(chunks.count) image chunks")
|
|
947
|
+
queueChunks(chunks, waitTimeMs: 50)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
private func convertUIImageToBmpData(_ image: UIImage) -> Data? {
|
|
951
|
+
// This is a simplified conversion - in production you'd want proper BMP encoding
|
|
952
|
+
guard let cgImage = image.cgImage else { return nil }
|
|
953
|
+
|
|
954
|
+
let width = cgImage.width
|
|
955
|
+
let height = cgImage.height
|
|
956
|
+
let bytesPerPixel = 4
|
|
957
|
+
let bytesPerRow = width * bytesPerPixel
|
|
958
|
+
let bitsPerComponent = 8
|
|
959
|
+
|
|
960
|
+
var pixelData = Data(count: width * height * bytesPerPixel)
|
|
961
|
+
|
|
962
|
+
pixelData.withUnsafeMutableBytes { bytes in
|
|
963
|
+
guard
|
|
964
|
+
let context = CGContext(
|
|
965
|
+
data: bytes.bindMemory(to: UInt8.self).baseAddress,
|
|
966
|
+
width: width,
|
|
967
|
+
height: height,
|
|
968
|
+
bitsPerComponent: bitsPerComponent,
|
|
969
|
+
bytesPerRow: bytesPerRow,
|
|
970
|
+
space: CGColorSpaceCreateDeviceRGB(),
|
|
971
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
972
|
+
)
|
|
973
|
+
else { return }
|
|
974
|
+
|
|
975
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return pixelData
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// MARK: - Display Control Commands
|
|
982
|
+
|
|
983
|
+
@objc func clearDisplay() {
|
|
984
|
+
guard nexReady else {
|
|
985
|
+
Bridge.log("NEX: Not ready to clear display. Device not initialized.")
|
|
986
|
+
return
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
Bridge.log("NEX: Clearing display")
|
|
990
|
+
|
|
991
|
+
let clearDisplay = Mentraos_Ble_ClearDisplay()
|
|
992
|
+
|
|
993
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
994
|
+
$0.msgID = "clear_disp_001"
|
|
995
|
+
$0.clearDisplay_p = clearDisplay
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
999
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
@objc func blankScreen() {
|
|
1003
|
+
clearDisplay()
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
@objc func showHomeScreen() {
|
|
1007
|
+
Bridge.log("NEX: Showing home screen")
|
|
1008
|
+
clearDisplay()
|
|
1009
|
+
|
|
1010
|
+
// Send a simple home screen text
|
|
1011
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
1012
|
+
self.sendTextWall("MentraOS Ready")
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
@objc func exitAllFunctions() {
|
|
1017
|
+
guard nexReady else {
|
|
1018
|
+
Bridge.log("NEX: Not ready to exit functions. Device not initialized.")
|
|
1019
|
+
return
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
Bridge.log("NEX: Exiting all functions")
|
|
1023
|
+
|
|
1024
|
+
// Send exit command (0x18 from Android implementation)
|
|
1025
|
+
let exitCommand: [UInt8] = [0x18]
|
|
1026
|
+
queueChunks([exitCommand], waitTimeMs: 100)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// MARK: - Configuration Commands
|
|
1030
|
+
|
|
1031
|
+
@objc func updateGlassesBrightness(_ brightness: Int) {
|
|
1032
|
+
guard nexReady else {
|
|
1033
|
+
Bridge.log("NEX: Not ready to update brightness. Device not initialized.")
|
|
1034
|
+
return
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Validate brightness range (0-100)
|
|
1038
|
+
let validBrightness = max(0, min(100, brightness))
|
|
1039
|
+
Bridge.log("NEX: Setting brightness to \(validBrightness)%")
|
|
1040
|
+
|
|
1041
|
+
let brightnessConfig = Mentraos_Ble_BrightnessConfig.with {
|
|
1042
|
+
$0.value = UInt32(validBrightness)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1046
|
+
$0.brightness = brightnessConfig
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1050
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
@objc func updateGlassesAutoBrightness(_ enabled: Bool) {
|
|
1054
|
+
guard nexReady else {
|
|
1055
|
+
Bridge.log("NEX: Not ready to update auto brightness. Device not initialized.")
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
Bridge.log("NEX: Setting auto brightness to \(enabled)")
|
|
1060
|
+
|
|
1061
|
+
let autoBrightnessConfig = Mentraos_Ble_AutoBrightnessConfig.with {
|
|
1062
|
+
$0.enabled = enabled
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1066
|
+
$0.autoBrightness = autoBrightnessConfig
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1070
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
@objc func updateGlassesHeadUpAngle(_ angle: Int) {
|
|
1074
|
+
guard nexReady else {
|
|
1075
|
+
Bridge.log("NEX: Not ready to update head-up angle. Device not initialized.")
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Validate angle range (0-60 degrees)
|
|
1080
|
+
let validAngle = max(0, min(60, angle))
|
|
1081
|
+
Bridge.log("NEX: Setting head-up angle to \(validAngle) degrees")
|
|
1082
|
+
|
|
1083
|
+
let headUpAngleConfig = Mentraos_Ble_HeadUpAngleConfig.with {
|
|
1084
|
+
$0.angle = UInt32(validAngle)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1088
|
+
$0.headUpAngle = headUpAngleConfig
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1092
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
@objc func updateGlassesDisplayHeight(_ height: Int) {
|
|
1096
|
+
guard nexReady else {
|
|
1097
|
+
Bridge.log("NEX: Not ready to update display height. Device not initialized.")
|
|
1098
|
+
return
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Validate height range (0-8)
|
|
1102
|
+
let validHeight = max(0, min(8, height))
|
|
1103
|
+
Bridge.log("NEX: Setting display height to \(validHeight)")
|
|
1104
|
+
|
|
1105
|
+
let displayHeightConfig = Mentraos_Ble_DisplayHeightConfig.with {
|
|
1106
|
+
$0.height = UInt32(validHeight)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1110
|
+
$0.displayHeight = displayHeightConfig
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1114
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
private func updateGlassesDisplayDistance(depth: Int) {
|
|
1118
|
+
guard nexReady else {
|
|
1119
|
+
Bridge.log("NEX: Not ready to update display distance. Device not initialized.")
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
let tier = NexDashboardDisplayWire.depthToWireTier(depth)
|
|
1124
|
+
Bridge.log("NEX: Setting display distance tier \(tier) in distance_cm field (dashboard depth \(depth))")
|
|
1125
|
+
|
|
1126
|
+
let displayDistanceConfig = Mentraos_Ble_DisplayDistanceConfig.with {
|
|
1127
|
+
$0.distanceCm = tier
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1131
|
+
$0.displayDistance = displayDistanceConfig
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1135
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
@objc func setMicrophoneEnabled(_ enabled: Bool) {
|
|
1139
|
+
guard nexReady else {
|
|
1140
|
+
Bridge.log("NEX: Not ready to set microphone state. Device not initialized.")
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
Bridge.log("NEX: Setting microphone enabled: \(enabled)")
|
|
1145
|
+
|
|
1146
|
+
let micStateConfig = Mentraos_Ble_MicStateConfig.with {
|
|
1147
|
+
$0.enabled = enabled
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1151
|
+
$0.micState = micStateConfig
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1155
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1156
|
+
|
|
1157
|
+
// Update aiListening state when microphone state changes (G1-compatible)
|
|
1158
|
+
if enabled, !vadActive {
|
|
1159
|
+
// Only set aiListening if VAD isn't already controlling it
|
|
1160
|
+
aiListening = enabled
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/// G1-compatible alias for microphone control
|
|
1165
|
+
func setMicEnabled(enabled: Bool) async -> Bool {
|
|
1166
|
+
setMicrophoneEnabled(enabled)
|
|
1167
|
+
return true
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
func sortMicRanking(list: [String]) -> [String] {
|
|
1171
|
+
return list
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// MARK: - Status Query Commands
|
|
1175
|
+
|
|
1176
|
+
@objc func queryBatteryStatus() {
|
|
1177
|
+
guard nexReady else {
|
|
1178
|
+
Bridge.log("NEX: Not ready to query battery status. Device not initialized.")
|
|
1179
|
+
return
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
Bridge.log("NEX: Querying battery status")
|
|
1183
|
+
|
|
1184
|
+
let batteryStateRequest = Mentraos_Ble_BatteryStateRequest()
|
|
1185
|
+
|
|
1186
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1187
|
+
$0.batteryState = batteryStateRequest
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1191
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
@objc func queryGlassesInfo() {
|
|
1195
|
+
guard nexReady else {
|
|
1196
|
+
Bridge.log("NEX: Not ready to query glasses info. Device not initialized.")
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
Bridge.log("NEX: Querying glasses information")
|
|
1201
|
+
|
|
1202
|
+
let glassesInfoRequest = Mentraos_Ble_GlassesInfoRequest()
|
|
1203
|
+
|
|
1204
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1205
|
+
$0.glassesInfo = glassesInfoRequest
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1209
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// MARK: - Utility Methods
|
|
1213
|
+
|
|
1214
|
+
@objc func sendPongResponse() {
|
|
1215
|
+
guard nexReady else {
|
|
1216
|
+
Bridge.log("NEX: Not ready to send pong. Device not initialized.")
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1221
|
+
Bridge.log("NEX: Sending pong response (Time: \(timestamp))")
|
|
1222
|
+
|
|
1223
|
+
let pongResponse = Mentraos_Ble_PongResponse()
|
|
1224
|
+
|
|
1225
|
+
let phoneToGlasses = Mentraos_Ble_PhoneToGlasses.with {
|
|
1226
|
+
$0.pong = pongResponse
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
let protobufData = try! phoneToGlasses.serializedData()
|
|
1230
|
+
queueDataWithOptimalChunking(protobufData, packetType: PACKET_TYPE_PROTOBUF)
|
|
1231
|
+
|
|
1232
|
+
// Notify about heartbeat sent (pong response)
|
|
1233
|
+
notifyHeartbeatSent(timestamp)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
@objc func isDeviceReady() -> Bool {
|
|
1237
|
+
nexReady && connectionState == ConnTypes.CONNECTED
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@objc func getDeviceInfo() -> [String: Any] {
|
|
1241
|
+
[
|
|
1242
|
+
"device_ready": nexReady,
|
|
1243
|
+
"connection_state": getConnectionState(),
|
|
1244
|
+
"current_mtu": currentMTU,
|
|
1245
|
+
"device_max_mtu": deviceMaxMTU,
|
|
1246
|
+
"max_chunk_size": maxChunkSize,
|
|
1247
|
+
"bmp_chunk_size": bmpChunkSize,
|
|
1248
|
+
"device_name": peripheral?.name ?? "Unknown",
|
|
1249
|
+
"device_id": peripheral?.identifier.uuidString ?? "Unknown",
|
|
1250
|
+
]
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// MARK: - Advanced Display Methods
|
|
1254
|
+
|
|
1255
|
+
@objc func displayCustomContent(_ content: String) {
|
|
1256
|
+
// For now, treat custom content as regular text
|
|
1257
|
+
sendTextWall(content)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
@objc func setUpdatingScreen(_ updating: Bool) {
|
|
1261
|
+
Bridge.log("NEX: Set updating screen: \(updating)")
|
|
1262
|
+
// This could be used to prevent display updates during certain operations
|
|
1263
|
+
// Implementation depends on specific requirements
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// MARK: - Data Processing and Event Listeners
|
|
1267
|
+
|
|
1268
|
+
private func processReceivedData(_ data: Data) {
|
|
1269
|
+
guard data.count > 0 else { return }
|
|
1270
|
+
|
|
1271
|
+
let packetType = data[0]
|
|
1272
|
+
Bridge.log("NEX: Processing packet type: 0x\(String(format: "%02X", packetType))")
|
|
1273
|
+
|
|
1274
|
+
switch packetType {
|
|
1275
|
+
case PACKET_TYPE_JSON:
|
|
1276
|
+
if data.count > 1 {
|
|
1277
|
+
let jsonData = data.subdata(in: 1 ..< data.count)
|
|
1278
|
+
processJsonData(jsonData)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
case PACKET_TYPE_PROTOBUF:
|
|
1282
|
+
if data.count > 1 {
|
|
1283
|
+
let protobufData = data.subdata(in: 1 ..< data.count)
|
|
1284
|
+
processProtobufData(protobufData)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
case PACKET_TYPE_AUDIO:
|
|
1288
|
+
if data.count > 2 {
|
|
1289
|
+
let sequenceNumber = data[1]
|
|
1290
|
+
let audioData = data.subdata(in: 2 ..< data.count)
|
|
1291
|
+
processAudioData(audioData, sequenceNumber: sequenceNumber)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
case PACKET_TYPE_IMAGE:
|
|
1295
|
+
processImageData(data)
|
|
1296
|
+
|
|
1297
|
+
default:
|
|
1298
|
+
Bridge.log("NEX: Unknown packet type: 0x\(String(format: "%02X", packetType))")
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private func processJsonData(_ jsonData: Data) {
|
|
1303
|
+
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
|
|
1304
|
+
Bridge.log("NEX: Failed to decode JSON data")
|
|
1305
|
+
return
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
Bridge.log("NEX: Processing JSON: \(jsonString)")
|
|
1309
|
+
|
|
1310
|
+
do {
|
|
1311
|
+
guard let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
|
|
1312
|
+
let type = json["type"] as? String
|
|
1313
|
+
else {
|
|
1314
|
+
return
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
switch type {
|
|
1318
|
+
case "battery_status":
|
|
1319
|
+
handleBatteryStatusJson(json)
|
|
1320
|
+
case "device_info":
|
|
1321
|
+
handleDeviceInfoJson(json)
|
|
1322
|
+
case "button_event":
|
|
1323
|
+
handleButtonEventJson(json)
|
|
1324
|
+
case "ping":
|
|
1325
|
+
handlePingJson(json)
|
|
1326
|
+
case "vad_event":
|
|
1327
|
+
handleVadEventJson(json)
|
|
1328
|
+
case "imu_data":
|
|
1329
|
+
handleImuDataJson(json)
|
|
1330
|
+
case "head_gesture":
|
|
1331
|
+
handleHeadGestureJson(json)
|
|
1332
|
+
default:
|
|
1333
|
+
Bridge.log("NEX: Unhandled JSON type: \(type)")
|
|
1334
|
+
}
|
|
1335
|
+
} catch {
|
|
1336
|
+
Bridge.log("NEX: Error parsing JSON: \(error)")
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private func processProtobufData(_ protobufData: Data) {
|
|
1341
|
+
do {
|
|
1342
|
+
let glassesToPhone = try Mentraos_Ble_GlassesToPhone(serializedData: protobufData)
|
|
1343
|
+
Bridge.log("NEX: Processing protobuf payload case: \(glassesToPhone.payload)")
|
|
1344
|
+
|
|
1345
|
+
switch glassesToPhone.payload {
|
|
1346
|
+
case let .batteryStatus(batteryStatus):
|
|
1347
|
+
handleBatteryStatusProtobuf(batteryStatus)
|
|
1348
|
+
|
|
1349
|
+
case let .chargingState(chargingState):
|
|
1350
|
+
handleChargingStateProtobuf(chargingState)
|
|
1351
|
+
|
|
1352
|
+
case let .deviceInfo(deviceInfo):
|
|
1353
|
+
handleDeviceInfoProtobuf(deviceInfo)
|
|
1354
|
+
|
|
1355
|
+
case let .headPosition(headPosition):
|
|
1356
|
+
handleHeadPositionProtobuf(headPosition)
|
|
1357
|
+
|
|
1358
|
+
case let .headUpAngleSet(headUpAngleResponse):
|
|
1359
|
+
handleHeadUpAngleResponseProtobuf(headUpAngleResponse)
|
|
1360
|
+
|
|
1361
|
+
case let .ping(pingRequest):
|
|
1362
|
+
handlePingProtobuf(pingRequest)
|
|
1363
|
+
|
|
1364
|
+
case let .vadEvent(vadEvent):
|
|
1365
|
+
handleVadEventProtobuf(vadEvent)
|
|
1366
|
+
|
|
1367
|
+
case let .imageTransferComplete(transferComplete):
|
|
1368
|
+
handleImageTransferCompleteProtobuf(transferComplete)
|
|
1369
|
+
|
|
1370
|
+
case let .imuData(imuData):
|
|
1371
|
+
handleImuDataProtobuf(imuData)
|
|
1372
|
+
|
|
1373
|
+
case let .buttonEvent(buttonEvent):
|
|
1374
|
+
handleButtonEventProtobuf(buttonEvent)
|
|
1375
|
+
|
|
1376
|
+
case let .headGesture(headGesture):
|
|
1377
|
+
handleHeadGestureProtobuf(headGesture)
|
|
1378
|
+
|
|
1379
|
+
// Note: VersionResponse not available in current protobuf structure
|
|
1380
|
+
|
|
1381
|
+
case .none:
|
|
1382
|
+
Bridge.log("NEX: Protobuf payload not set")
|
|
1383
|
+
|
|
1384
|
+
default:
|
|
1385
|
+
Bridge.log("NEX: Unhandled protobuf payload type")
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
} catch {
|
|
1389
|
+
Bridge.log("NEX: Error parsing protobuf data: \(error)")
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private func processAudioData(_ audioData: Data, sequenceNumber: UInt8) {
|
|
1394
|
+
Bridge.log(
|
|
1395
|
+
"NEX: Received audio data - sequence: \(sequenceNumber), size: \(audioData.count) bytes"
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
// Update @Published property (G1-compatible approach)
|
|
1399
|
+
// Create packet with sequence number prefix like G1 expects
|
|
1400
|
+
var packetData = Data()
|
|
1401
|
+
packetData.append(sequenceNumber)
|
|
1402
|
+
packetData.append(audioData)
|
|
1403
|
+
|
|
1404
|
+
compressedVoiceData = packetData
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
private func processImageData(_ imageData: Data) {
|
|
1408
|
+
Bridge.log("NEX: Received image data: \(imageData.count) bytes")
|
|
1409
|
+
// Image data processing can be implemented based on specific requirements
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// MARK: - Protobuf Event Handlers
|
|
1413
|
+
|
|
1414
|
+
private func handleBatteryStatusProtobuf(_ batteryStatus: Mentraos_Ble_BatteryStatus) {
|
|
1415
|
+
let level = Int(batteryStatus.level)
|
|
1416
|
+
let isCharging = batteryStatus.charging
|
|
1417
|
+
|
|
1418
|
+
Bridge.log("NEX: 🔋 Battery Status - Level: \(level)%, Charging: \(isCharging)")
|
|
1419
|
+
|
|
1420
|
+
// Update @Published properties (G1-compatible approach)
|
|
1421
|
+
DeviceStore.shared.apply("glasses", "batteryLevel", level)
|
|
1422
|
+
DeviceStore.shared.apply("glasses", "charging", isCharging)
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
private func handleChargingStateProtobuf(_ chargingState: Mentraos_Ble_ChargingState) {
|
|
1426
|
+
let chargingState = chargingState.state == .charging
|
|
1427
|
+
|
|
1428
|
+
Bridge.log("NEX: 🔌 Charging State: \(chargingState ? "CHARGING" : "NOT_CHARGING")")
|
|
1429
|
+
|
|
1430
|
+
// Update @Published property (G1-compatible approach)
|
|
1431
|
+
DeviceStore.shared.apply("glasses", "charging", chargingState)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
private func handleDeviceInfoProtobuf(_ deviceInfo: Mentraos_Ble_DeviceInfo) {
|
|
1435
|
+
Bridge.log("NEX: 📱 Device Info: \(deviceInfo)")
|
|
1436
|
+
|
|
1437
|
+
// Update @Published properties (G1-compatible approach)
|
|
1438
|
+
DeviceStore.shared.apply("glasses", "deviceFirmwareVersion", deviceInfo.fwVersion)
|
|
1439
|
+
DeviceStore.shared.apply("glasses", "deviceHardwareModel", deviceInfo.hwModel)
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private func handleHeadPositionProtobuf(_ headPosition: Mentraos_Ble_HeadPosition) {
|
|
1443
|
+
let angle = Int(headPosition.angle)
|
|
1444
|
+
|
|
1445
|
+
Bridge.log("NEX: 📐 Head Position - Angle: \(angle)°")
|
|
1446
|
+
|
|
1447
|
+
// Update @Published property (G1-compatible approach)
|
|
1448
|
+
headUpAngle = angle
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private func handleHeadUpAngleResponseProtobuf(_ response: Mentraos_Ble_HeadUpAngleResponse) {
|
|
1452
|
+
let success = response.success
|
|
1453
|
+
|
|
1454
|
+
Bridge.log("NEX: 📐 Head Up Angle Set Response - Success: \(success)")
|
|
1455
|
+
|
|
1456
|
+
// Emit response event
|
|
1457
|
+
let eventBody: [String: Any] = [
|
|
1458
|
+
"head_up_angle_set_result": success,
|
|
1459
|
+
"device_model": "Mentra Display",
|
|
1460
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1461
|
+
]
|
|
1462
|
+
|
|
1463
|
+
emitEvent("HeadUpAngleResponseEvent", body: eventBody)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private func handlePingProtobuf(_: Mentraos_Ble_PingRequest) {
|
|
1467
|
+
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1468
|
+
|
|
1469
|
+
Bridge.log("NEX: 💓 Received PING from glasses (Time: \(timestamp))")
|
|
1470
|
+
|
|
1471
|
+
// Automatically send pong response
|
|
1472
|
+
sendPongResponse()
|
|
1473
|
+
|
|
1474
|
+
// Emit heartbeat received event
|
|
1475
|
+
let eventBody: [String: Any] = [
|
|
1476
|
+
"heartbeat_received": [
|
|
1477
|
+
"timestamp": timestamp,
|
|
1478
|
+
"device_model": "Mentra Display",
|
|
1479
|
+
],
|
|
1480
|
+
]
|
|
1481
|
+
|
|
1482
|
+
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1483
|
+
|
|
1484
|
+
// Query battery status periodically (every 10 pings like Java implementation)
|
|
1485
|
+
heartbeatCount += 1
|
|
1486
|
+
if heartbeatCount % 10 == 0 {
|
|
1487
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
1488
|
+
self.queryBatteryStatus()
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
private func handleVadEventProtobuf(_ vadEvent: Mentraos_Ble_VadEvent) {
|
|
1494
|
+
let vadActiveState = vadEvent.state == .active
|
|
1495
|
+
|
|
1496
|
+
Bridge.log("NEX: 🎤 VAD Event - Voice Activity: \(vadActiveState)")
|
|
1497
|
+
|
|
1498
|
+
// Update @Published properties (G1-compatible approach)
|
|
1499
|
+
vadActive = vadActiveState
|
|
1500
|
+
aiListening = vadActiveState // Mirror G1's aiListening behavior
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
private func handleImageTransferCompleteProtobuf(
|
|
1504
|
+
_ transferComplete: Mentraos_Ble_ImageTransferComplete
|
|
1505
|
+
) {
|
|
1506
|
+
let status = transferComplete.status
|
|
1507
|
+
let missingChunks = transferComplete.missingChunks
|
|
1508
|
+
|
|
1509
|
+
Bridge.log("NEX: 🖼️ Image Transfer Complete - Status: \(status)")
|
|
1510
|
+
|
|
1511
|
+
switch status {
|
|
1512
|
+
case .ok:
|
|
1513
|
+
Bridge.log("NEX: Image transfer completed successfully")
|
|
1514
|
+
// Clear any pending image chunks
|
|
1515
|
+
|
|
1516
|
+
case .incomplete:
|
|
1517
|
+
Bridge.log("NEX: Image transfer incomplete - Missing chunks: \(missingChunks)")
|
|
1518
|
+
// Could implement chunk retransmission here
|
|
1519
|
+
|
|
1520
|
+
default:
|
|
1521
|
+
Bridge.log("NEX: Unknown image transfer status")
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Emit image transfer complete event
|
|
1525
|
+
let eventBody: [String: Any] = [
|
|
1526
|
+
"image_transfer_complete": [
|
|
1527
|
+
"status": status == .ok ? "success" : "incomplete",
|
|
1528
|
+
"missing_chunks": missingChunks,
|
|
1529
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1530
|
+
],
|
|
1531
|
+
]
|
|
1532
|
+
|
|
1533
|
+
emitEvent("ImageTransferCompleteEvent", body: eventBody)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
private func handleImuDataProtobuf(_ imuData: Mentraos_Ble_ImuData) {
|
|
1537
|
+
Bridge.log("NEX: 📊 IMU Data: \(imuData)")
|
|
1538
|
+
|
|
1539
|
+
// Update @Published properties (G1-compatible approach)
|
|
1540
|
+
accelerometer = [imuData.accel.x, imuData.accel.y, imuData.accel.z]
|
|
1541
|
+
gyroscope = [imuData.gyro.x, imuData.gyro.y, imuData.gyro.z]
|
|
1542
|
+
magnetometer = [imuData.mag.x, imuData.mag.y, imuData.mag.z]
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
private func handleButtonEventProtobuf(_ buttonEvent: Mentraos_Ble_ButtonEvent) {
|
|
1546
|
+
let buttonNumber = Int(buttonEvent.button.rawValue)
|
|
1547
|
+
let buttonState = buttonEvent.state
|
|
1548
|
+
|
|
1549
|
+
Bridge.log("NEX: 🔘 Button Event - Button: \(buttonNumber), State: \(buttonState)")
|
|
1550
|
+
|
|
1551
|
+
// Update @Published properties (G1-compatible approach)
|
|
1552
|
+
lastButtonPressed = buttonNumber
|
|
1553
|
+
lastButtonState = "\(buttonState.rawValue)"
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
private func handleHeadGestureProtobuf(_ headGesture: Mentraos_Ble_HeadGesture) {
|
|
1557
|
+
let gestureType = headGesture.gesture
|
|
1558
|
+
|
|
1559
|
+
Bridge.log("NEX: 👤 Head Gesture: \(gestureType)")
|
|
1560
|
+
|
|
1561
|
+
// Update @Published properties (G1-compatible approach)
|
|
1562
|
+
switch gestureType {
|
|
1563
|
+
case .headUp:
|
|
1564
|
+
DeviceStore.shared.apply("glasses", "headUp", true)
|
|
1565
|
+
lastHeadGesture = "headUp"
|
|
1566
|
+
case .nod:
|
|
1567
|
+
lastHeadGesture = "nod"
|
|
1568
|
+
case .shake:
|
|
1569
|
+
lastHeadGesture = "shake"
|
|
1570
|
+
default:
|
|
1571
|
+
Bridge.log("NEX: Unknown head gesture type: \(gestureType)")
|
|
1572
|
+
lastHeadGesture = "unknown"
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// MARK: - JSON Event Handlers
|
|
1577
|
+
|
|
1578
|
+
private func handleBatteryStatusJson(_ json: [String: Any]) {
|
|
1579
|
+
let level = json["level"] as? Int ?? -1
|
|
1580
|
+
let isCharging = json["charging"] as? Bool ?? false
|
|
1581
|
+
|
|
1582
|
+
Bridge.log("NEX: 🔋 JSON Battery Status - Level: \(level)%, Charging: \(charging)")
|
|
1583
|
+
|
|
1584
|
+
// Update @Published properties (G1-compatible approach)
|
|
1585
|
+
DeviceStore.shared.apply("glasses", "batteryLevel", level)
|
|
1586
|
+
DeviceStore.shared.apply("glasses", "charging", isCharging)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private func handleDeviceInfoJson(_ json: [String: Any]) {
|
|
1590
|
+
Bridge.log("NEX: 📱 JSON Device Info: \(json)")
|
|
1591
|
+
|
|
1592
|
+
let eventBody: [String: Any] = [
|
|
1593
|
+
"device_info": json,
|
|
1594
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1595
|
+
]
|
|
1596
|
+
|
|
1597
|
+
emitEvent("DeviceInfoEvent", body: eventBody)
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
private func handleButtonEventJson(_ json: [String: Any]) {
|
|
1601
|
+
let buttonId = json["button_id"] as? String ?? "unknown"
|
|
1602
|
+
let pressType = json["press_type"] as? String ?? "short"
|
|
1603
|
+
|
|
1604
|
+
Bridge.log("NEX: 🔘 JSON Button Event - Button: \(buttonId), Type: \(pressType)")
|
|
1605
|
+
|
|
1606
|
+
let eventBody: [String: Any] = [
|
|
1607
|
+
"button_press": [
|
|
1608
|
+
"device_model": "Mentra Display",
|
|
1609
|
+
"button_id": buttonId,
|
|
1610
|
+
"press_type": pressType,
|
|
1611
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1612
|
+
],
|
|
1613
|
+
]
|
|
1614
|
+
|
|
1615
|
+
emitEvent("ButtonPressEvent", body: eventBody)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
private func handlePingJson(_: [String: Any]) {
|
|
1619
|
+
let timestamp = Date().timeIntervalSince1970 * 1000
|
|
1620
|
+
|
|
1621
|
+
Bridge.log("NEX: 💓 JSON PING received (Time: \(timestamp))")
|
|
1622
|
+
|
|
1623
|
+
// Send pong response
|
|
1624
|
+
sendPongResponse()
|
|
1625
|
+
|
|
1626
|
+
// Emit heartbeat received event
|
|
1627
|
+
let eventBody: [String: Any] = [
|
|
1628
|
+
"heartbeat_received": [
|
|
1629
|
+
"timestamp": timestamp,
|
|
1630
|
+
"device_model": "Mentra Display",
|
|
1631
|
+
],
|
|
1632
|
+
]
|
|
1633
|
+
|
|
1634
|
+
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
private func handleVadEventJson(_ json: [String: Any]) {
|
|
1638
|
+
let vadActiveState = json["vad"] as? Bool ?? false
|
|
1639
|
+
|
|
1640
|
+
Bridge.log("NEX: 🎤 JSON VAD Event - Voice Activity: \(vadActiveState)")
|
|
1641
|
+
|
|
1642
|
+
// Update @Published properties (G1-compatible approach)
|
|
1643
|
+
vadActive = vadActiveState
|
|
1644
|
+
aiListening = vadActiveState // Mirror G1's aiListening behavior
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
private func handleImuDataJson(_ json: [String: Any]) {
|
|
1648
|
+
Bridge.log("NEX: 📊 JSON IMU Data: \(json)")
|
|
1649
|
+
|
|
1650
|
+
let eventBody: [String: Any] = [
|
|
1651
|
+
"imu_data": json,
|
|
1652
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1653
|
+
]
|
|
1654
|
+
|
|
1655
|
+
emitEvent("ImuDataEvent", body: eventBody)
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private func handleHeadGestureJson(_ json: [String: Any]) {
|
|
1659
|
+
let gesture = json["gesture"] as? String ?? "unknown"
|
|
1660
|
+
|
|
1661
|
+
Bridge.log("NEX: 👤 JSON Head Gesture: \(gesture)")
|
|
1662
|
+
|
|
1663
|
+
let eventBody: [String: Any] = [
|
|
1664
|
+
"head_gesture": [
|
|
1665
|
+
"gesture": gesture,
|
|
1666
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
1667
|
+
],
|
|
1668
|
+
]
|
|
1669
|
+
|
|
1670
|
+
emitEvent("HeadGestureEvent", body: eventBody)
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// MARK: - Event Emission Helper
|
|
1674
|
+
|
|
1675
|
+
private func emitEvent(_ eventName: String, body: [String: Any]) {
|
|
1676
|
+
// Use the standardized Bridge.sendTypedMessage helper for consistent type field handling
|
|
1677
|
+
Bridge.sendTypedMessage(eventName, body: body)
|
|
1678
|
+
Bridge.log("NEX: 📡 Emitted \(eventName) via Bridge.sendTypedMessage")
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// MARK: - Heartbeat Management
|
|
1682
|
+
|
|
1683
|
+
private func notifyHeartbeatSent(_ timestamp: TimeInterval) {
|
|
1684
|
+
lastHeartbeatSentTime = timestamp
|
|
1685
|
+
|
|
1686
|
+
let eventBody: [String: Any] = [
|
|
1687
|
+
"heartbeat_sent": [
|
|
1688
|
+
"timestamp": timestamp,
|
|
1689
|
+
"device_model": "Mentra Display",
|
|
1690
|
+
],
|
|
1691
|
+
]
|
|
1692
|
+
|
|
1693
|
+
emitEvent("HeartbeatSentEvent", body: eventBody)
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private func notifyHeartbeatReceived(_ timestamp: TimeInterval) {
|
|
1697
|
+
lastHeartbeatReceivedTime = timestamp
|
|
1698
|
+
|
|
1699
|
+
let eventBody: [String: Any] = [
|
|
1700
|
+
"heartbeat_received": [
|
|
1701
|
+
"timestamp": timestamp,
|
|
1702
|
+
"device_model": "Mentra Display",
|
|
1703
|
+
],
|
|
1704
|
+
]
|
|
1705
|
+
|
|
1706
|
+
emitEvent("HeartbeatReceivedEvent", body: eventBody)
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
@objc func getLastHeartbeatSentTime() -> TimeInterval {
|
|
1710
|
+
lastHeartbeatSentTime
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
@objc func getLastHeartbeatReceivedTime() -> TimeInterval {
|
|
1714
|
+
lastHeartbeatReceivedTime
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// MARK: - Java-Compatible Initialization Methods
|
|
1718
|
+
|
|
1719
|
+
private func startMicBeat() {
|
|
1720
|
+
Bridge.log("NEX: Starting micbeat (30 min interval)")
|
|
1721
|
+
|
|
1722
|
+
if micBeatCount > 0 {
|
|
1723
|
+
stopMicBeat()
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Set mic enabled first (like Java line 1751)
|
|
1727
|
+
setMicrophoneEnabled(true)
|
|
1728
|
+
micBeatCount += 1
|
|
1729
|
+
|
|
1730
|
+
// Schedule periodic mic beat (like Java lines 1753-1762)
|
|
1731
|
+
micBeatTimer = Timer.scheduledTimer(withTimeInterval: MICBEAT_INTERVAL_MS, repeats: true) {
|
|
1732
|
+
[weak self] _ in
|
|
1733
|
+
guard let self else { return }
|
|
1734
|
+
Bridge.log("NEX: SENDING MIC BEAT")
|
|
1735
|
+
self.setMicrophoneEnabled(self.shouldUseGlassesMic)
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private func stopMicBeat() {
|
|
1740
|
+
setMicrophoneEnabled(false)
|
|
1741
|
+
micBeatTimer?.invalidate()
|
|
1742
|
+
micBeatTimer = nil
|
|
1743
|
+
micBeatCount = 0
|
|
1744
|
+
Bridge.log("NEX: Stopped mic beat")
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
private func sendWhiteListCommand() {
|
|
1748
|
+
guard !whiteListedAlready else {
|
|
1749
|
+
Bridge.log("NEX: Whitelist already sent, skipping")
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
whiteListedAlready = true
|
|
1753
|
+
|
|
1754
|
+
Bridge.log("NEX: Sending whitelist command")
|
|
1755
|
+
|
|
1756
|
+
// Create whitelist JSON exactly like Java (lines 2642-2680)
|
|
1757
|
+
let whitelistJson = createWhitelistJson()
|
|
1758
|
+
let chunks = createWhitelistChunks(json: whitelistJson)
|
|
1759
|
+
|
|
1760
|
+
// Send chunks with delay like Java
|
|
1761
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { // 10ms delay
|
|
1762
|
+
self.queueChunks(chunks)
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
private func createWhitelistJson() -> String {
|
|
1767
|
+
// Exact JSON structure from Java implementation (lines 2653-2680)
|
|
1768
|
+
let whitelistDict: [String: Any] = [
|
|
1769
|
+
"calendar_enable": false,
|
|
1770
|
+
"call_enable": false,
|
|
1771
|
+
"msg_enable": false,
|
|
1772
|
+
"ios_mail_enable": false,
|
|
1773
|
+
"app": [
|
|
1774
|
+
"list": [
|
|
1775
|
+
["id": "com.augment.os", "name": "AugmentOS"],
|
|
1776
|
+
],
|
|
1777
|
+
"enable": true,
|
|
1778
|
+
],
|
|
1779
|
+
]
|
|
1780
|
+
|
|
1781
|
+
do {
|
|
1782
|
+
let jsonData = try JSONSerialization.data(withJSONObject: whitelistDict)
|
|
1783
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
1784
|
+
Bridge.log("NEX: Created whitelist JSON: \(jsonString)")
|
|
1785
|
+
return jsonString
|
|
1786
|
+
}
|
|
1787
|
+
} catch {
|
|
1788
|
+
Bridge.log("NEX: Error creating whitelist JSON: \(error)")
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
return "{}"
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
private func createWhitelistChunks(json: String) -> [[UInt8]] {
|
|
1795
|
+
// Exact chunking logic from Java (lines 2703-2728)
|
|
1796
|
+
guard let jsonData = json.data(using: .utf8) else { return [] }
|
|
1797
|
+
|
|
1798
|
+
let totalChunks = Int(ceil(Double(jsonData.count) / Double(maxChunkSize)))
|
|
1799
|
+
var chunks: [[UInt8]] = []
|
|
1800
|
+
|
|
1801
|
+
for i in 0 ..< totalChunks {
|
|
1802
|
+
let start = i * maxChunkSize
|
|
1803
|
+
let end = min(start + maxChunkSize, jsonData.count)
|
|
1804
|
+
let payloadChunk = jsonData.subdata(in: start ..< end)
|
|
1805
|
+
|
|
1806
|
+
// Create header: [WHITELIST_CMD, total_chunks, chunk_index] (Java lines 2714-2717)
|
|
1807
|
+
var header: [UInt8] = [
|
|
1808
|
+
WHITELIST_CMD, // Command ID (0x04)
|
|
1809
|
+
UInt8(totalChunks), // Total number of chunks
|
|
1810
|
+
UInt8(i), // Current chunk index
|
|
1811
|
+
]
|
|
1812
|
+
|
|
1813
|
+
// Combine header and payload (Java lines 2720-2725)
|
|
1814
|
+
header.append(contentsOf: payloadChunk)
|
|
1815
|
+
chunks.append(header)
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
Bridge.log("NEX: Created \(chunks.count) whitelist chunks")
|
|
1819
|
+
return chunks
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
private func postProtobufSchemaVersionInfo() {
|
|
1823
|
+
guard !protobufVersionPosted else {
|
|
1824
|
+
Bridge.log("NEX: Protobuf version already posted, skipping")
|
|
1825
|
+
return
|
|
1826
|
+
}
|
|
1827
|
+
protobufVersionPosted = true
|
|
1828
|
+
|
|
1829
|
+
Bridge.log("NEX: 📋 Posting protobuf schema version info")
|
|
1830
|
+
|
|
1831
|
+
// Emit protobuf schema version event like Java (lines 3709-3728)
|
|
1832
|
+
let eventBody: [String: Any] = [
|
|
1833
|
+
"protobuf_schema_version": [
|
|
1834
|
+
"schema_version": 1, // Default version
|
|
1835
|
+
"build_info": "Schema v1 | mentraos_ble.proto",
|
|
1836
|
+
"device_model": "Mentra Display",
|
|
1837
|
+
],
|
|
1838
|
+
]
|
|
1839
|
+
|
|
1840
|
+
// emitEvent("ProtobufSchemaVersionEvent", body: eventBody)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/// Save microphone state before disconnection (like Java implementation)
|
|
1844
|
+
private func saveMicrophoneStateBeforeDisconnection() {
|
|
1845
|
+
UserDefaults.standard.set(shouldUseGlassesMic, forKey: "microphoneStateBeforeDisconnection")
|
|
1846
|
+
microphoneStateBeforeDisconnection = shouldUseGlassesMic
|
|
1847
|
+
Bridge.log("NEX: Saved microphone state before disconnection: \(shouldUseGlassesMic)")
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
@objc func disconnect() {
|
|
1851
|
+
Bridge.log("NEX: 🔌 User-initiated disconnect")
|
|
1852
|
+
if let peripheral {
|
|
1853
|
+
// Save microphone state before disconnection (like Java implementation)
|
|
1854
|
+
saveMicrophoneStateBeforeDisconnection()
|
|
1855
|
+
|
|
1856
|
+
// Stop mic beat system
|
|
1857
|
+
stopMicBeat()
|
|
1858
|
+
|
|
1859
|
+
isDisconnecting = true
|
|
1860
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
1861
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1862
|
+
}
|
|
1863
|
+
stopReconnectionTimer()
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// MARK: - Lifecycle Management (ported from Java)
|
|
1867
|
+
|
|
1868
|
+
@objc func destroy() {
|
|
1869
|
+
Bridge.log("NEX: 💥 Destroying MentraNexSGC instance")
|
|
1870
|
+
|
|
1871
|
+
isKilled = true
|
|
1872
|
+
isDisconnecting = true
|
|
1873
|
+
|
|
1874
|
+
// Stop all timers
|
|
1875
|
+
// Save microphone state before destruction (like Java implementation)
|
|
1876
|
+
saveMicrophoneStateBeforeDisconnection()
|
|
1877
|
+
|
|
1878
|
+
// Stop mic beat system (like Java implementation)
|
|
1879
|
+
stopMicBeat()
|
|
1880
|
+
|
|
1881
|
+
stopReconnectionTimer()
|
|
1882
|
+
|
|
1883
|
+
// Disconnect from peripheral
|
|
1884
|
+
if let peripheral {
|
|
1885
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Stop scanning
|
|
1889
|
+
if _isScanning {
|
|
1890
|
+
stopScan()
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// Clear all references
|
|
1894
|
+
peripheral = nil
|
|
1895
|
+
writeCharacteristic = nil
|
|
1896
|
+
notifyCharacteristic = nil
|
|
1897
|
+
centralManager?.delegate = nil
|
|
1898
|
+
centralManager = nil
|
|
1899
|
+
|
|
1900
|
+
// Clear discovery cache
|
|
1901
|
+
discoveredPeripherals.removeAll()
|
|
1902
|
+
|
|
1903
|
+
Bridge.log("NEX: ✅ MentraNexSGC destroyed successfully")
|
|
1904
|
+
// Reset initialization flags
|
|
1905
|
+
whiteListedAlready = false
|
|
1906
|
+
protobufVersionPosted = false
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
@objc func reset() {
|
|
1910
|
+
Bridge.log("NEX: 🔄 Resetting MentraNexSGC to fresh state")
|
|
1911
|
+
|
|
1912
|
+
// Disconnect current connection
|
|
1913
|
+
disconnect()
|
|
1914
|
+
|
|
1915
|
+
// Clear all saved device information
|
|
1916
|
+
clearSavedDeviceInfo()
|
|
1917
|
+
|
|
1918
|
+
// Clear discovery cache
|
|
1919
|
+
discoveredPeripherals.removeAll()
|
|
1920
|
+
|
|
1921
|
+
// Reset internal state
|
|
1922
|
+
isKilled = false
|
|
1923
|
+
isDisconnecting = false
|
|
1924
|
+
nexReady = false
|
|
1925
|
+
reconnectionAttempts = 0
|
|
1926
|
+
peripheralToConnectName = nil
|
|
1927
|
+
|
|
1928
|
+
Bridge.log("NEX: ✅ Reset complete - ready for fresh pairing")
|
|
1929
|
+
// Reset initialization flags (like Java implementation)
|
|
1930
|
+
whiteListedAlready = false
|
|
1931
|
+
protobufVersionPosted = false
|
|
1932
|
+
heartbeatCount = 0
|
|
1933
|
+
micBeatCount = 0
|
|
1934
|
+
shouldUseGlassesMic = true
|
|
1935
|
+
microphoneStateBeforeDisconnection = false
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// MARK: - Helper Methods (like G1)
|
|
1939
|
+
|
|
1940
|
+
private func getConnectedDevices() -> [CBPeripheral] {
|
|
1941
|
+
guard let centralManager else { return [] }
|
|
1942
|
+
// Retrieve peripherals already connected that expose our main service
|
|
1943
|
+
return centralManager.retrieveConnectedPeripherals(withServices: [])
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
private func emitDiscoveredDevice(_ name: String) {
|
|
1947
|
+
// Emit device discovery event using standardized typed message function
|
|
1948
|
+
Bridge.log("NEX: 📡 Emitting discovered device: \(name)")
|
|
1949
|
+
Bridge.sendDiscoveredDevice(DeviceTypes.NEX, name)
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
@objc func checkBluetoothState() {
|
|
1953
|
+
Bridge.log("NEX: 🔍 Checking Bluetooth State...")
|
|
1954
|
+
if let centralManager {
|
|
1955
|
+
Bridge.log("NEX: 📱 Central Manager exists: YES")
|
|
1956
|
+
Bridge.log("NEX: 📱 Current Bluetooth State: \(centralManager.state.rawValue)")
|
|
1957
|
+
|
|
1958
|
+
switch centralManager.state {
|
|
1959
|
+
case .poweredOn:
|
|
1960
|
+
Bridge.log("NEX: ✅ Bluetooth is ready for scanning")
|
|
1961
|
+
|
|
1962
|
+
if let savedDeviceName = UserDefaults.standard.string(forKey: PREFS_DEVICE_NAME),
|
|
1963
|
+
!savedDeviceName.isEmpty
|
|
1964
|
+
{
|
|
1965
|
+
Bridge.log("NEX: 🔄 Looking for saved device: \(savedDeviceName)")
|
|
1966
|
+
// This will be handled in didDiscover when the device is found
|
|
1967
|
+
startScan()
|
|
1968
|
+
}
|
|
1969
|
+
case .poweredOff:
|
|
1970
|
+
Bridge.log("NEX: ❌ Bluetooth is turned off")
|
|
1971
|
+
case .resetting:
|
|
1972
|
+
Bridge.log("NEX: 🔄 Bluetooth is resetting")
|
|
1973
|
+
case .unauthorized:
|
|
1974
|
+
Bridge.log("NEX: ❌ Bluetooth permission denied")
|
|
1975
|
+
case .unsupported:
|
|
1976
|
+
Bridge.log("NEX: ❌ Bluetooth not supported")
|
|
1977
|
+
case .unknown:
|
|
1978
|
+
Bridge.log("NEX: ❓ Bluetooth state unknown")
|
|
1979
|
+
@unknown default:
|
|
1980
|
+
Bridge.log("NEX: ❓ Unknown Bluetooth state: \(centralManager.state.rawValue)")
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
Bridge.log("NEX: ❌ Central Manager is nil!")
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// MARK: - CBCentralManagerDelegate
|
|
1988
|
+
|
|
1989
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
1990
|
+
Bridge.log("NEX: 🔄 Bluetooth state changed to: \(central.state.rawValue)")
|
|
1991
|
+
|
|
1992
|
+
switch central.state {
|
|
1993
|
+
case .poweredOn:
|
|
1994
|
+
Bridge.log("NEX: ✅ Bluetooth is On and ready for scanning")
|
|
1995
|
+
if scanOnPowerOn {
|
|
1996
|
+
Bridge.log("NEX: 🚀 Triggering scan after power on.")
|
|
1997
|
+
scanOnPowerOn = false
|
|
1998
|
+
startScan()
|
|
1999
|
+
}
|
|
2000
|
+
case .poweredOff:
|
|
2001
|
+
Bridge.log("NEX: ❌ Bluetooth is Off - user needs to enable Bluetooth")
|
|
2002
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2003
|
+
case .resetting:
|
|
2004
|
+
Bridge.log("NEX: 🔄 Bluetooth is resetting - wait for completion")
|
|
2005
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2006
|
+
case .unauthorized:
|
|
2007
|
+
Bridge.log("NEX: ❌ Bluetooth is unauthorized - check app permissions")
|
|
2008
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2009
|
+
case .unsupported:
|
|
2010
|
+
Bridge.log("NEX: ❌ Bluetooth is unsupported on this device")
|
|
2011
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2012
|
+
case .unknown:
|
|
2013
|
+
Bridge.log("NEX: ❓ Bluetooth state is unknown - may be initializing")
|
|
2014
|
+
@unknown default:
|
|
2015
|
+
Bridge.log("NEX: ❓ A new Bluetooth state was introduced: \(central.state.rawValue)")
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
func centralManager(
|
|
2020
|
+
_: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
2021
|
+
advertisementData _: [String: Any], rssi RSSI: NSNumber
|
|
2022
|
+
) {
|
|
2023
|
+
guard let deviceName = peripheral.name else {
|
|
2024
|
+
// Bridge.log("NEX-CONN: 🚫 Ignoring device with no name")
|
|
2025
|
+
return
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// guard isCompatibleNexDevice(deviceName) else {
|
|
2029
|
+
// // Bridge.log("NEX-CONN: 🚫 Ignoring incompatible device: \(deviceName)")
|
|
2030
|
+
// return
|
|
2031
|
+
// }
|
|
2032
|
+
|
|
2033
|
+
Bridge.log("NEX-CONN: 🎯 === Compatible Nex Device Found ===")
|
|
2034
|
+
Bridge.log("NEX-CONN: 📱 Device Name: \(deviceName)")
|
|
2035
|
+
Bridge.log("NEX-CONN: 📶 RSSI: \(RSSI) dBm")
|
|
2036
|
+
|
|
2037
|
+
// Store the peripheral in cache (like MentraLive)
|
|
2038
|
+
discoveredPeripherals[deviceName] = peripheral
|
|
2039
|
+
|
|
2040
|
+
// Always emit the discovered device for the UI list
|
|
2041
|
+
emitDiscoveredDevice(deviceName)
|
|
2042
|
+
|
|
2043
|
+
// Auto-connect logic based on target or saved device (from Java MentraNexSGC)
|
|
2044
|
+
var shouldConnect = false
|
|
2045
|
+
var connectionReason = ""
|
|
2046
|
+
|
|
2047
|
+
// Check if this matches our target device name for connection
|
|
2048
|
+
if let targetName = peripheralToConnectName, deviceName.contains(targetName) {
|
|
2049
|
+
shouldConnect = true
|
|
2050
|
+
connectionReason = "Target device name match: \(targetName)"
|
|
2051
|
+
}
|
|
2052
|
+
// Check if this matches our saved device for reconnection
|
|
2053
|
+
else if let savedName = savedDeviceName, deviceName == savedName {
|
|
2054
|
+
shouldConnect = true
|
|
2055
|
+
connectionReason = "Saved device reconnection: \(savedName)"
|
|
2056
|
+
}
|
|
2057
|
+
// Check if this matches preferred device ID
|
|
2058
|
+
else if let preferredId = preferredDeviceId {
|
|
2059
|
+
if let extractedId = extractDeviceId(from: deviceName), extractedId == preferredId {
|
|
2060
|
+
shouldConnect = true
|
|
2061
|
+
connectionReason = "Preferred device ID match: \(preferredId)"
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if shouldConnect {
|
|
2066
|
+
connectToFoundDevice(peripheral, reason: connectionReason)
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// MARK: - Enhanced Connection Helper
|
|
2071
|
+
|
|
2072
|
+
private func connectToFoundDevice(_ peripheral: CBPeripheral, reason: String) {
|
|
2073
|
+
guard self.peripheral == nil else {
|
|
2074
|
+
Bridge.log(
|
|
2075
|
+
"NEX-CONN: ⚠️ Already connected/connecting to a device, ignoring new connect request for '\(peripheral.name ?? "Unknown")'"
|
|
2076
|
+
)
|
|
2077
|
+
return
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
Bridge.log(
|
|
2081
|
+
"NEX-CONN: 🔗 Connecting to device '\(peripheral.name ?? "Unknown")' - Reason: \(reason)"
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
// Stop scanning since we found our target
|
|
2085
|
+
if _isScanning {
|
|
2086
|
+
stopScan()
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Store the peripheral and initiate connection
|
|
2090
|
+
self.peripheral = peripheral
|
|
2091
|
+
isConnecting = true
|
|
2092
|
+
connectionState = ConnTypes.CONNECTING
|
|
2093
|
+
|
|
2094
|
+
// Use connection options for better reliability (from Java implementation)
|
|
2095
|
+
let connectionOptions: [String: Any] = [
|
|
2096
|
+
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
2097
|
+
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
2098
|
+
CBConnectPeripheralOptionNotifyOnNotificationKey: true,
|
|
2099
|
+
]
|
|
2100
|
+
|
|
2101
|
+
centralManager?.connect(peripheral, options: connectionOptions)
|
|
2102
|
+
|
|
2103
|
+
Bridge.log("NEX-CONN: 🚀 Connection initiated with enhanced options")
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
2107
|
+
Bridge.log("NEX-CONN: ✅ Successfully connected to \(peripheral.name ?? "unknown device").")
|
|
2108
|
+
isConnecting = false
|
|
2109
|
+
peripheralUUID = peripheral.identifier // Persist UUID
|
|
2110
|
+
stopReconnectionTimer() // Successfully connected, stop trying to reconnect.
|
|
2111
|
+
|
|
2112
|
+
// Enhanced device info saving (from Java implementation)
|
|
2113
|
+
let deviceName = peripheral.name
|
|
2114
|
+
let deviceAddress = peripheral.identifier.uuidString
|
|
2115
|
+
|
|
2116
|
+
// Save all device information for future reconnection
|
|
2117
|
+
savePairedDeviceInfo(name: deviceName, address: deviceAddress)
|
|
2118
|
+
|
|
2119
|
+
// Extract and save device ID if possible
|
|
2120
|
+
if let deviceName, let deviceId = extractDeviceId(from: deviceName) {
|
|
2121
|
+
savePreferredDeviceId(deviceId)
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
Bridge.log("NEX-CONN: 💾 Device information saved for reliable reconnection")
|
|
2125
|
+
peripheral.delegate = self
|
|
2126
|
+
Bridge.log("NEX-CONN: 🔍 Discovering services...")
|
|
2127
|
+
peripheral.discoverServices([MAIN_SERVICE_UUID])
|
|
2128
|
+
|
|
2129
|
+
// Reset any failed connection attempt counters
|
|
2130
|
+
reconnectionAttempts = 0
|
|
2131
|
+
Bridge.log("NEX-CONN: 🔄 Reset reconnection attempts counter")
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
func centralManager(
|
|
2135
|
+
_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?
|
|
2136
|
+
) {
|
|
2137
|
+
Bridge.log(
|
|
2138
|
+
"NEX-CONN: ❌ Failed to connect to peripheral \(peripheral.name ?? "Unknown"). Error: \(error?.localizedDescription ?? "unknown")"
|
|
2139
|
+
)
|
|
2140
|
+
isConnecting = false
|
|
2141
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2142
|
+
self.peripheral = nil // Reset peripheral on failure to allow reconnection
|
|
2143
|
+
// Optionally, start reconnection attempts here
|
|
2144
|
+
if !isDisconnecting, !isKilled {
|
|
2145
|
+
startReconnectionTimer()
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
func centralManager(
|
|
2150
|
+
_: CBCentralManager, didDisconnectPeripheral disconnectedPeripheral: CBPeripheral,
|
|
2151
|
+
error: Error?
|
|
2152
|
+
) {
|
|
2153
|
+
Bridge.log(
|
|
2154
|
+
"NEX-CONN: 🔌 Disconnected from peripheral: \(disconnectedPeripheral.name ?? "Unknown")"
|
|
2155
|
+
)
|
|
2156
|
+
|
|
2157
|
+
if let error {
|
|
2158
|
+
Bridge.log("NEX-CONN: ⚠️ Disconnect error: \(error.localizedDescription)")
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// Reset connection state
|
|
2162
|
+
// Save microphone state before disconnection (like Java implementation)
|
|
2163
|
+
saveMicrophoneStateBeforeDisconnection()
|
|
2164
|
+
|
|
2165
|
+
// Reset protobuf version posted flag for next connection (like Java implementation)
|
|
2166
|
+
protobufVersionPosted = false
|
|
2167
|
+
|
|
2168
|
+
// Stop mic beat system (like Java implementation)
|
|
2169
|
+
stopMicBeat()
|
|
2170
|
+
|
|
2171
|
+
nexReady = false
|
|
2172
|
+
deviceReady = false
|
|
2173
|
+
// batteryLevel = -1
|
|
2174
|
+
// charging = false
|
|
2175
|
+
DeviceStore.shared.apply("glasses", "batteryLevel", -1)
|
|
2176
|
+
DeviceStore.shared.apply("glasses", "charging", false)
|
|
2177
|
+
vadActive = false
|
|
2178
|
+
compressedVoiceData = .init()
|
|
2179
|
+
aiListening = false
|
|
2180
|
+
deviceFirmwareVersion = ""
|
|
2181
|
+
deviceHardwareModel = ""
|
|
2182
|
+
accelerometer = [0.0, 0.0, 0.0]
|
|
2183
|
+
gyroscope = [0.0, 0.0, 0.0]
|
|
2184
|
+
magnetometer = [0.0, 0.0, 0.0]
|
|
2185
|
+
lastButtonPressed = -1
|
|
2186
|
+
lastButtonState = ""
|
|
2187
|
+
lastHeadGesture = ""
|
|
2188
|
+
headUpAngle = 0
|
|
2189
|
+
|
|
2190
|
+
peripheral = nil
|
|
2191
|
+
writeCharacteristic = nil
|
|
2192
|
+
notifyCharacteristic = nil
|
|
2193
|
+
connectionState = ConnTypes.DISCONNECTED
|
|
2194
|
+
|
|
2195
|
+
// Clear command queue if needed
|
|
2196
|
+
if isQueueWorkerRunning {
|
|
2197
|
+
Bridge.log("NEX-CONN: 🧹 Clearing command queue due to disconnection")
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if !isDisconnecting, !isKilled {
|
|
2201
|
+
Bridge.log("NEX-CONN: 🔄 Unintentional disconnect detected. Attempting reconnection...")
|
|
2202
|
+
|
|
2203
|
+
// Enhanced reconnection strategy from Java implementation
|
|
2204
|
+
if let savedName = savedDeviceName {
|
|
2205
|
+
Bridge.log("NEX-CONN: 🎯 Will attempt to reconnect to saved device: \(savedName)")
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
startReconnectionTimer()
|
|
2209
|
+
} else {
|
|
2210
|
+
Bridge.log(
|
|
2211
|
+
"NEX-CONN: ✅ Intentional disconnect (isDisconnecting: \(isDisconnecting), isKilled: \(isKilled))"
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
if isDisconnecting {
|
|
2215
|
+
// Don't clear device info on intentional disconnect - user might reconnect later
|
|
2216
|
+
Bridge.log("NEX-CONN: 💾 Keeping device info for potential future reconnection")
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// MARK: - MTU Negotiation (iOS-specific implementation)
|
|
2222
|
+
|
|
2223
|
+
private func requestOptimalMTU(for peripheral: CBPeripheral) {
|
|
2224
|
+
Bridge.log("NEX-CONN: negotiating MTU")
|
|
2225
|
+
Bridge.log("NEX: 🔍 iOS MTU Discovery (Platform Limitation: max \(MTU_MAX_IOS) bytes)")
|
|
2226
|
+
Bridge.log("NEX: 🎯 iOS maximum: \(MTU_MAX_IOS) bytes, default: \(MTU_DEFAULT) bytes")
|
|
2227
|
+
|
|
2228
|
+
// iOS MTU is automatically negotiated - we can only discover the current value
|
|
2229
|
+
// No manual MTU request available on iOS (platform limitation)
|
|
2230
|
+
|
|
2231
|
+
// Get current MTU capability (iOS-specific approach)
|
|
2232
|
+
let maxWriteLength = peripheral.maximumWriteValueLength(for: .withResponse)
|
|
2233
|
+
let actualMTU = maxWriteLength + 3 // Add L2CAP header size
|
|
2234
|
+
|
|
2235
|
+
Bridge.log("NEX: 📊 iOS MTU Discovery Results:")
|
|
2236
|
+
Bridge.log("NEX: 📏 Max write length: \(maxWriteLength) bytes")
|
|
2237
|
+
Bridge.log("NEX: 📡 Effective MTU: \(actualMTU) bytes")
|
|
2238
|
+
|
|
2239
|
+
// Validate against iOS limitations
|
|
2240
|
+
let validatedMTU = min(actualMTU, MTU_MAX_IOS)
|
|
2241
|
+
if actualMTU > MTU_MAX_IOS {
|
|
2242
|
+
Bridge.log("NEX: 🔧 Clamping MTU from \(actualMTU) to iOS maximum: \(MTU_MAX_IOS)")
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Process MTU result immediately (iOS doesn't have callback like Android)
|
|
2246
|
+
onMTUNegotiated(mtu: validatedMTU, success: true)
|
|
2247
|
+
|
|
2248
|
+
// After MTU is set, start device initialization sequence (from Java implementation)
|
|
2249
|
+
initializeNexDevice()
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
private func onMTUNegotiated(mtu: Int, success: Bool) {
|
|
2253
|
+
Bridge.log("NEX-CONN: 🔄 MTU Negotiation Result: Success=\(success), Device MTU=\(mtu)")
|
|
2254
|
+
|
|
2255
|
+
if success, mtu > MTU_DEFAULT {
|
|
2256
|
+
// Store device capability and calculate actual negotiated MTU
|
|
2257
|
+
deviceMaxMTU = mtu
|
|
2258
|
+
// iOS limitation: Use actual MTU but cap at iOS maximum
|
|
2259
|
+
currentMTU = min(MTU_MAX_IOS, mtu)
|
|
2260
|
+
|
|
2261
|
+
Bridge.log("NEX: 🎯 iOS MTU Configuration Complete:")
|
|
2262
|
+
Bridge.log("NEX: 🍎 iOS Platform Max: \(MTU_MAX_IOS) bytes")
|
|
2263
|
+
Bridge.log("NEX: 📡 Device Supports: \(deviceMaxMTU) bytes")
|
|
2264
|
+
Bridge.log("NEX: 🤝 Final MTU: \(currentMTU) bytes")
|
|
2265
|
+
|
|
2266
|
+
// Calculate optimal chunk sizes based on iOS MTU constraints
|
|
2267
|
+
maxChunkSize = currentMTU - 10 // Reserve 10 bytes for headers
|
|
2268
|
+
bmpChunkSize = currentMTU - 6 // Reserve 6 bytes for image headers
|
|
2269
|
+
|
|
2270
|
+
Bridge.log("NEX: 📦 Optimized Chunk Sizes:")
|
|
2271
|
+
Bridge.log("NEX: 📄 Data Chunk Size: \(maxChunkSize) bytes")
|
|
2272
|
+
Bridge.log("NEX: 🖼️ Image Chunk Size: \(bmpChunkSize) bytes")
|
|
2273
|
+
|
|
2274
|
+
} else {
|
|
2275
|
+
Bridge.log("NEX: ⚠️ MTU negotiation failed or using minimum, applying iOS defaults")
|
|
2276
|
+
currentMTU = MTU_DEFAULT
|
|
2277
|
+
deviceMaxMTU = MTU_DEFAULT
|
|
2278
|
+
maxChunkSize = 20 // Very conservative for 23-byte MTU
|
|
2279
|
+
bmpChunkSize = 20 // Very conservative for 23-byte MTU
|
|
2280
|
+
|
|
2281
|
+
Bridge.log("NEX: 📋 iOS Fallback Configuration:")
|
|
2282
|
+
Bridge.log("NEX: 📊 Default MTU: \(MTU_DEFAULT) bytes")
|
|
2283
|
+
Bridge.log("NEX: 📦 Data Chunk Size: \(maxChunkSize) bytes")
|
|
2284
|
+
Bridge.log("NEX: 🖼️ Image Chunk Size: \(bmpChunkSize) bytes")
|
|
2285
|
+
Bridge.log("NEX: ⚠️ Using minimal chunks due to MTU limitation")
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// Device is now ready for communication
|
|
2289
|
+
Bridge.log("NEX-CONN: ✅ Device initialization complete - ready for communication")
|
|
2290
|
+
nexReady = true
|
|
2291
|
+
connectionState = ConnTypes.CONNECTED
|
|
2292
|
+
|
|
2293
|
+
// Update @Published property for device ready state
|
|
2294
|
+
deviceReady = true
|
|
2295
|
+
|
|
2296
|
+
// Initialize command queue worker to process queued commands
|
|
2297
|
+
setupCommandQueue()
|
|
2298
|
+
|
|
2299
|
+
// Emit device ready event to React Native
|
|
2300
|
+
// emitDeviceReady()
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// MARK: - Device Initialization (ported from Java MentraNexSGC)
|
|
2304
|
+
|
|
2305
|
+
private func initializeNexDevice() {
|
|
2306
|
+
Bridge.log("NEX-CONN: 🚀 Starting Nex device initialization (matching Java sequence)")
|
|
2307
|
+
|
|
2308
|
+
// Exact Java initialization sequence from lines 648-691:
|
|
2309
|
+
|
|
2310
|
+
// 1. Do first battery status query (Java line 650)
|
|
2311
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { // 10ms delay like Java
|
|
2312
|
+
Bridge.log("NEX: 🔋 Sending first battery status query")
|
|
2313
|
+
self.queryBatteryStatus()
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// 2. Restore previous microphone state (Java lines 657-665)
|
|
2317
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { // 20ms delay
|
|
2318
|
+
let shouldRestoreMic = UserDefaults.standard.bool(
|
|
2319
|
+
forKey: "microphoneStateBeforeDisconnection"
|
|
2320
|
+
)
|
|
2321
|
+
Bridge.log("NEX: 🎤 Restoring microphone state to: \(shouldRestoreMic)")
|
|
2322
|
+
|
|
2323
|
+
if shouldRestoreMic {
|
|
2324
|
+
self.startMicBeat()
|
|
2325
|
+
} else {
|
|
2326
|
+
self.stopMicBeat()
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// 3. Enable AugmentOS notification key (Java line 668)
|
|
2331
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { // 30ms delay
|
|
2332
|
+
self.sendWhiteListCommand()
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// 4. Show home screen to turn on the NexGlasses display (Java line 673)
|
|
2336
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // 50ms delay
|
|
2337
|
+
self.showHomeScreen()
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// 5. Post protobuf schema version information (Java lines 684-687)
|
|
2341
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // 100ms delay
|
|
2342
|
+
self.postProtobufSchemaVersionInfo()
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// 6. Query glasses protobuf version from firmware (Java line 690)
|
|
2346
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { // 150ms delay
|
|
2347
|
+
self.queryGlassesInfo()
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
Bridge.log("NEX-CONN: ✅ Java-compatible initialization sequence started")
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
private func emitDeviceReady() {
|
|
2354
|
+
let eventBody: [String: Any] = [
|
|
2355
|
+
"device_ready": [
|
|
2356
|
+
"model_name": "Mentra Display",
|
|
2357
|
+
"mtu_negotiated": currentMTU,
|
|
2358
|
+
"max_chunk_size": maxChunkSize,
|
|
2359
|
+
"connection_state": "ready",
|
|
2360
|
+
],
|
|
2361
|
+
]
|
|
2362
|
+
|
|
2363
|
+
// Use the standardized Bridge.sendTypedMessage helper for consistent type field handling
|
|
2364
|
+
Bridge.sendTypedMessage("device_ready", body: eventBody)
|
|
2365
|
+
Bridge.log("NEX: 📡 Emitted device ready event with MTU: \(currentMTU)")
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// MARK: - CBPeripheralDelegate
|
|
2369
|
+
|
|
2370
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
2371
|
+
if let error {
|
|
2372
|
+
Bridge.log("NEX-CONN: ❌ Error discovering services: \(error.localizedDescription)")
|
|
2373
|
+
return
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
guard let services = peripheral.services else {
|
|
2377
|
+
Bridge.log("NEX-CONN: ⚠️ No services found for peripheral.")
|
|
2378
|
+
return
|
|
2379
|
+
}
|
|
2380
|
+
for service in services {
|
|
2381
|
+
if service.uuid == MAIN_SERVICE_UUID {
|
|
2382
|
+
Bridge.log("NEX-CONN: ✅ Found main service. Discovering characteristics...")
|
|
2383
|
+
peripheral.discoverCharacteristics(
|
|
2384
|
+
[WRITE_CHAR_UUID, NOTIFY_CHAR_UUID], for: service
|
|
2385
|
+
)
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
func peripheral(
|
|
2391
|
+
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?
|
|
2392
|
+
) {
|
|
2393
|
+
if let error {
|
|
2394
|
+
Bridge.log(
|
|
2395
|
+
"NEX-CONN: ❌ Error discovering characteristics: \(error.localizedDescription)"
|
|
2396
|
+
)
|
|
2397
|
+
return
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
guard let characteristics = service.characteristics else {
|
|
2401
|
+
Bridge.log("NEX-CONN: ⚠️ No characteristics found for service \(service.uuid).")
|
|
2402
|
+
return
|
|
2403
|
+
}
|
|
2404
|
+
for characteristic in characteristics {
|
|
2405
|
+
if characteristic.uuid == WRITE_CHAR_UUID {
|
|
2406
|
+
Bridge.log("NEX-CONN: ✅ Found write characteristic.")
|
|
2407
|
+
writeCharacteristic = characteristic
|
|
2408
|
+
} else if characteristic.uuid == NOTIFY_CHAR_UUID {
|
|
2409
|
+
Bridge.log(
|
|
2410
|
+
"NEX-CONN: ✅ Found notify characteristic. Subscribing for notifications."
|
|
2411
|
+
)
|
|
2412
|
+
notifyCharacteristic = characteristic
|
|
2413
|
+
peripheral.setNotifyValue(true, for: characteristic)
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if writeCharacteristic != nil, notifyCharacteristic != nil {
|
|
2418
|
+
Bridge.log(
|
|
2419
|
+
"NEX-CONN: ✅ All required characteristics discovered. Proceeding to MTU negotiation."
|
|
2420
|
+
)
|
|
2421
|
+
|
|
2422
|
+
// Start MTU negotiation like Java implementation
|
|
2423
|
+
requestOptimalMTU(for: peripheral)
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
func peripheral(
|
|
2428
|
+
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?
|
|
2429
|
+
) {
|
|
2430
|
+
if let error {
|
|
2431
|
+
Bridge.log("NEX-CONN: ❌ Error on updating value: \(error.localizedDescription)")
|
|
2432
|
+
return
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
guard let data = characteristic.value else {
|
|
2436
|
+
Bridge.log("NEX-CONN: ⚠️ Received notification with no data.")
|
|
2437
|
+
return
|
|
2438
|
+
}
|
|
2439
|
+
Bridge.log("NEX-CONN: 📥 Received data (\(data.count) bytes): \(data.toHexString())")
|
|
2440
|
+
|
|
2441
|
+
// Process the received data based on packet type
|
|
2442
|
+
processReceivedData(data)
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
func peripheral(
|
|
2446
|
+
_: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
|
|
2447
|
+
) {
|
|
2448
|
+
if let error {
|
|
2449
|
+
Bridge.log(
|
|
2450
|
+
"NEX-CONN: ❌ Error writing value to \(characteristic.uuid): \(error.localizedDescription)"
|
|
2451
|
+
)
|
|
2452
|
+
return
|
|
2453
|
+
}
|
|
2454
|
+
// This log can be very noisy, so it's commented out.
|
|
2455
|
+
// Bridge.log("NEX-CONN: 📤 Successfully wrote value to \(characteristic.uuid).")
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
func peripheral(
|
|
2459
|
+
_: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
2460
|
+
error: Error?
|
|
2461
|
+
) {
|
|
2462
|
+
if let error {
|
|
2463
|
+
Bridge.log(
|
|
2464
|
+
"NEX-CONN: ❌ Error changing notification state for \(characteristic.uuid): \(error.localizedDescription)"
|
|
2465
|
+
)
|
|
2466
|
+
return
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
if characteristic.isNotifying {
|
|
2470
|
+
Bridge.log(
|
|
2471
|
+
"NEX-CONN: ✅ Successfully subscribed to notifications for characteristic \(characteristic.uuid.uuidString)."
|
|
2472
|
+
)
|
|
2473
|
+
} else {
|
|
2474
|
+
Bridge.log(
|
|
2475
|
+
"NEX-CONN: unsubscribed from notifications for characteristic \(characteristic.uuid.uuidString)."
|
|
2476
|
+
)
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|