@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,1478 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DeviceManager.swift
|
|
3
|
+
// MentraOS_Manager
|
|
4
|
+
//
|
|
5
|
+
// Created by Matthew Fosse on 3/5/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Combine
|
|
10
|
+
import CoreBluetooth
|
|
11
|
+
import Foundation
|
|
12
|
+
import UIKit
|
|
13
|
+
|
|
14
|
+
struct ViewState {
|
|
15
|
+
var topText: String
|
|
16
|
+
var bottomText: String
|
|
17
|
+
var title: String
|
|
18
|
+
var layoutType: String
|
|
19
|
+
var text: String
|
|
20
|
+
var data: String?
|
|
21
|
+
var animationData: [String: Any]?
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@MainActor
|
|
25
|
+
@objc(DeviceManager) class DeviceManager: NSObject {
|
|
26
|
+
static let shared = DeviceManager()
|
|
27
|
+
|
|
28
|
+
@objc static func getInstance() -> DeviceManager {
|
|
29
|
+
return DeviceManager.shared
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// MARK: - Unique (iOS)
|
|
33
|
+
|
|
34
|
+
private var cancellables = Set<AnyCancellable>()
|
|
35
|
+
var sendStateWorkItem: DispatchWorkItem?
|
|
36
|
+
let sendStateQueue = DispatchQueue(label: "sendStateQueue", qos: .userInitiated)
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Setup Bluetooth audio pairing after BLE connection is established
|
|
40
|
+
* Attempts to automatically activate Mentra Live as the system audio device
|
|
41
|
+
* If not paired yet, prompts user to pair in Settings
|
|
42
|
+
*/
|
|
43
|
+
func setupAudioPairing(deviceName _: String) {
|
|
44
|
+
// Don't configure audio session - PhoneMic.swift handles that
|
|
45
|
+
// Just check if audio session supports Bluetooth (informational only)
|
|
46
|
+
if !AudioSessionMonitor.isAudioSessionConfigured() {
|
|
47
|
+
Bridge.log(
|
|
48
|
+
"Audio: Audio session not configured for Bluetooth yet - mic system will configure it when recording"
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract device ID pattern to match the specific device
|
|
53
|
+
// BLE name: "MENTRA_LIVE_BLE_ABC123"
|
|
54
|
+
// BT Classic could be: "MENTRA_LIVE_BLE_ABC123" or "MENTRA_LIVE_BT_ABC123"
|
|
55
|
+
// We need to match on the unique device ID part (e.g., "ABC123")
|
|
56
|
+
let audioDevicePattern = getAudioDevicePattern()
|
|
57
|
+
|
|
58
|
+
if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
|
|
59
|
+
Bridge.log("Audio: Device pattern is empty or simulated, returning")
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if device is paired (don't activate to preserve A2DP music playback)
|
|
64
|
+
let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
65
|
+
|
|
66
|
+
if isPaired {
|
|
67
|
+
// Device is paired! Don't activate it - let PhoneMic.swift activate when recording starts
|
|
68
|
+
Bridge.log("Audio: ✅ Mentra Live is paired (preserving A2DP for music)")
|
|
69
|
+
glassesBtcConnected = true
|
|
70
|
+
} else {
|
|
71
|
+
glassesBtcConnected = false
|
|
72
|
+
// Not found in availableInputs - not paired yet
|
|
73
|
+
|
|
74
|
+
// Start monitoring for when user pairs manually
|
|
75
|
+
AudioSessionMonitor.startMonitoring(devicePattern: audioDevicePattern) {
|
|
76
|
+
[weak self] (connected: Bool, _: String?) in
|
|
77
|
+
guard let self = self else { return }
|
|
78
|
+
|
|
79
|
+
if connected {
|
|
80
|
+
Bridge.log("Audio: ✅ Device paired and connected")
|
|
81
|
+
// Don't activate - let PhoneMic.swift handle that when recording starts
|
|
82
|
+
self.glassesBtcConnected = true
|
|
83
|
+
} else {
|
|
84
|
+
Bridge.log("Audio: Device disconnected")
|
|
85
|
+
self.glassesBtcConnected = false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// MARK: - End Unique
|
|
92
|
+
|
|
93
|
+
// MARK: - Properties
|
|
94
|
+
|
|
95
|
+
var coreToken: String = ""
|
|
96
|
+
var coreTokenOwner: String = ""
|
|
97
|
+
var userEmail: String = ""
|
|
98
|
+
var sgc: SGCManager?
|
|
99
|
+
var controller: ControllerManager?
|
|
100
|
+
|
|
101
|
+
// state
|
|
102
|
+
// var lastStatusObj: [String: Any] = [:]
|
|
103
|
+
|
|
104
|
+
/// settings:
|
|
105
|
+
private var defaultWearable: String {
|
|
106
|
+
get { DeviceStore.shared.get("bluetooth", "default_wearable") as? String ?? "" }
|
|
107
|
+
set { DeviceStore.shared.apply("bluetooth", "default_wearable", newValue) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private var pendingWearable: String {
|
|
111
|
+
get { DeviceStore.shared.get("bluetooth", "pending_wearable") as? String ?? "" }
|
|
112
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_wearable", newValue) }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private var deviceName: String {
|
|
116
|
+
get { DeviceStore.shared.get("bluetooth", "device_name") as? String ?? "" }
|
|
117
|
+
set { DeviceStore.shared.apply("bluetooth", "device_name", newValue) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private var deviceAddress: String {
|
|
121
|
+
get { DeviceStore.shared.get("bluetooth", "device_address") as? String ?? "" }
|
|
122
|
+
set { DeviceStore.shared.apply("bluetooth", "device_address", newValue) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private var defaultController: String {
|
|
126
|
+
get { DeviceStore.shared.get("bluetooth", "default_controller") as? String ?? "" }
|
|
127
|
+
set { DeviceStore.shared.apply("bluetooth", "default_controller", newValue) }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private var pendingController: String {
|
|
131
|
+
get { DeviceStore.shared.get("bluetooth", "pending_controller") as? String ?? "" }
|
|
132
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_controller", newValue) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private var controllerDeviceName: String {
|
|
136
|
+
get { DeviceStore.shared.get("bluetooth", "controller_device_name") as? String ?? "" }
|
|
137
|
+
set { DeviceStore.shared.apply("bluetooth", "controller_device_name", newValue) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private var screenDisabled: Bool {
|
|
141
|
+
get { DeviceStore.shared.get("bluetooth", "screen_disabled") as? Bool ?? false }
|
|
142
|
+
set { DeviceStore.shared.apply("bluetooth", "screen_disabled", newValue) }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private var preferredMic: String {
|
|
146
|
+
get { DeviceStore.shared.get("bluetooth", "preferred_mic") as? String ?? "auto" }
|
|
147
|
+
set { DeviceStore.shared.apply("bluetooth", "preferred_mic", newValue) }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private var autoBrightness: Bool {
|
|
151
|
+
get { DeviceStore.shared.get("bluetooth", "auto_brightness") as? Bool ?? true }
|
|
152
|
+
set { DeviceStore.shared.apply("bluetooth", "auto_brightness", newValue) }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private var brightness: Int {
|
|
156
|
+
get { DeviceStore.shared.get("bluetooth", "brightness") as? Int ?? 50 }
|
|
157
|
+
set { DeviceStore.shared.apply("bluetooth", "brightness", newValue) }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private var headUpAngle: Int {
|
|
161
|
+
get { DeviceStore.shared.get("bluetooth", "head_up_angle") as? Int ?? 30 }
|
|
162
|
+
set { DeviceStore.shared.apply("bluetooth", "head_up_angle", newValue) }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private var sensingEnabled: Bool {
|
|
166
|
+
get { DeviceStore.shared.get("bluetooth", "sensing_enabled") as? Bool ?? true }
|
|
167
|
+
set { DeviceStore.shared.apply("bluetooth", "sensing_enabled", newValue) }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private var bypassVad: Bool {
|
|
171
|
+
get { DeviceStore.shared.get("bluetooth", "bypass_vad") as? Bool ?? true }
|
|
172
|
+
set { DeviceStore.shared.apply("bluetooth", "bypass_vad", newValue) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private var offlineCaptionsRunning: Bool {
|
|
176
|
+
get { DeviceStore.shared.get("bluetooth", "offline_captions_running") as? Bool ?? false }
|
|
177
|
+
set { DeviceStore.shared.apply("bluetooth", "offline_captions_running", newValue) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private var localSttFallbackActive: Bool {
|
|
181
|
+
get { DeviceStore.shared.get("bluetooth", "local_stt_fallback_active") as? Bool ?? false }
|
|
182
|
+
set { DeviceStore.shared.apply("bluetooth", "local_stt_fallback_active", newValue) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private var shouldSendPcm: Bool {
|
|
186
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_pcm") as? Bool ?? false }
|
|
187
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_pcm", newValue) }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private var shouldSendLc3: Bool {
|
|
191
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_lc3") as? Bool ?? false }
|
|
192
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_lc3", newValue) }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private var shouldSendTranscript: Bool {
|
|
196
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_transcript") as? Bool ?? false }
|
|
197
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_transcript", newValue) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private var contextualDashboard: Bool {
|
|
201
|
+
get { DeviceStore.shared.get("bluetooth", "contextual_dashboard") as? Bool ?? true }
|
|
202
|
+
set { DeviceStore.shared.apply("bluetooth", "contextual_dashboard", newValue) }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// state:
|
|
206
|
+
|
|
207
|
+
private var searching: Bool {
|
|
208
|
+
get { DeviceStore.shared.get("bluetooth", "searching") as? Bool ?? false }
|
|
209
|
+
set { DeviceStore.shared.apply("bluetooth", "searching", newValue) }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private var searchingController: Bool {
|
|
213
|
+
get { DeviceStore.shared.get("bluetooth", "searchingController") as? Bool ?? false }
|
|
214
|
+
set { DeviceStore.shared.apply("bluetooth", "searchingController", newValue) }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private var glassesBtcConnected: Bool {
|
|
218
|
+
get { DeviceStore.shared.get("glasses", "btcConnected") as? Bool ?? false }
|
|
219
|
+
set { DeviceStore.shared.apply("glasses", "btcConnected", newValue) }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private var micRanking: [String] {
|
|
223
|
+
get {
|
|
224
|
+
DeviceStore.shared.get("bluetooth", "micRanking") as? [String] ?? MicMap.map["auto"]!
|
|
225
|
+
}
|
|
226
|
+
set { DeviceStore.shared.apply("bluetooth", "micRanking", newValue) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private var shouldSendBootingMessage: Bool {
|
|
230
|
+
get { DeviceStore.shared.get("bluetooth", "shouldSendBootingMessage") as? Bool ?? true }
|
|
231
|
+
set { DeviceStore.shared.apply("bluetooth", "shouldSendBootingMessage", newValue) }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private var systemMicUnavailable: Bool {
|
|
235
|
+
get { DeviceStore.shared.get("bluetooth", "systemMicUnavailable") as? Bool ?? false }
|
|
236
|
+
set { DeviceStore.shared.apply("bluetooth", "systemMicUnavailable", newValue) }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private var headUp: Bool {
|
|
240
|
+
get { DeviceStore.shared.get("glasses", "headUp") as? Bool ?? false }
|
|
241
|
+
set { DeviceStore.shared.apply("glasses", "headUp", newValue) }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private var micEnabled: Bool {
|
|
245
|
+
get { DeviceStore.shared.get("bluetooth", "micEnabled") as? Bool ?? false }
|
|
246
|
+
set { DeviceStore.shared.apply("bluetooth", "micEnabled", newValue) }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private var currentMic: String {
|
|
250
|
+
get { DeviceStore.shared.get("bluetooth", "currentMic") as? String ?? "" }
|
|
251
|
+
set { DeviceStore.shared.apply("bluetooth", "currentMic", newValue) }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private var searchResults: [[String: Any]] {
|
|
255
|
+
get { DeviceStore.shared.get("bluetooth", "searchResults") as? [[String: Any]] ?? [] }
|
|
256
|
+
set { DeviceStore.shared.apply("bluetooth", "searchResults", newValue) }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private var wifiScanResults: [[String: Any]] {
|
|
260
|
+
get { DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? [] }
|
|
261
|
+
set { DeviceStore.shared.apply("bluetooth", "wifiScanResults", newValue) }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private var lastLog: [String] {
|
|
265
|
+
get { DeviceStore.shared.get("bluetooth", "lastLog") as? [String] ?? [] }
|
|
266
|
+
set { DeviceStore.shared.apply("bluetooth", "lastLog", newValue) }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private var otherBtConnected: Bool {
|
|
270
|
+
get { DeviceStore.shared.get("bluetooth", "otherBtConnected") as? Bool ?? false }
|
|
271
|
+
set { DeviceStore.shared.apply("bluetooth", "otherBtConnected", newValue) }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// LC3 Audio Encoding
|
|
275
|
+
/// Audio output format enum
|
|
276
|
+
enum AudioOutputFormat { case lc3, pcm }
|
|
277
|
+
/// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
|
|
278
|
+
/// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
|
|
279
|
+
/// Persistent LC3 converter for encoding/decoding
|
|
280
|
+
var lc3Converter: PcmConverter?
|
|
281
|
+
/// Audio output format - defaults to LC3 for bandwidth savings
|
|
282
|
+
private var audioOutputFormat: AudioOutputFormat = .lc3
|
|
283
|
+
/// Last time we received an LC3 frame from the glasses (used by the mic
|
|
284
|
+
/// inactivity watchdog).
|
|
285
|
+
private var lastLc3Event: Date?
|
|
286
|
+
private var micReinitTimer: Timer?
|
|
287
|
+
|
|
288
|
+
// VAD:
|
|
289
|
+
private var vad: SileroVADStrategy?
|
|
290
|
+
private var vadBuffer = [Data]()
|
|
291
|
+
private var isSpeaking = false
|
|
292
|
+
|
|
293
|
+
/// STT:
|
|
294
|
+
private var transcriber: SherpaOnnxTranscriber?
|
|
295
|
+
|
|
296
|
+
var viewStates: [ViewState] = [
|
|
297
|
+
ViewState(
|
|
298
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: ""
|
|
299
|
+
),
|
|
300
|
+
ViewState(
|
|
301
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
|
|
302
|
+
text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
|
|
303
|
+
),
|
|
304
|
+
ViewState(
|
|
305
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: "",
|
|
306
|
+
data: nil, animationData: nil
|
|
307
|
+
),
|
|
308
|
+
ViewState(
|
|
309
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
|
|
310
|
+
text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$", data: nil,
|
|
311
|
+
animationData: nil
|
|
312
|
+
),
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
override init() {
|
|
316
|
+
Bridge.log("MAN: init()")
|
|
317
|
+
vad = SileroVADStrategy()
|
|
318
|
+
super.init()
|
|
319
|
+
|
|
320
|
+
// Start memory monitoring (logs every 30s to help detect leaks)
|
|
321
|
+
// MemoryMonitor.start()
|
|
322
|
+
|
|
323
|
+
// Initialize SherpaOnnx Transcriber
|
|
324
|
+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
325
|
+
let window = windowScene.windows.first,
|
|
326
|
+
let rootViewController = window.rootViewController
|
|
327
|
+
{
|
|
328
|
+
transcriber = SherpaOnnxTranscriber(context: rootViewController)
|
|
329
|
+
} else {
|
|
330
|
+
Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Initialize the transcriber
|
|
334
|
+
if let transcriber = transcriber {
|
|
335
|
+
transcriber.initialize()
|
|
336
|
+
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
Task {
|
|
340
|
+
self.vad?.setup(
|
|
341
|
+
sampleRate: .rate_16k,
|
|
342
|
+
frameSize: .size_1024,
|
|
343
|
+
quality: .normal,
|
|
344
|
+
silenceTriggerDurationMs: 4000,
|
|
345
|
+
speechTriggerDurationMs: 50
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Initialize persistent LC3 converter for unified audio encoding
|
|
350
|
+
lc3Converter = PcmConverter()
|
|
351
|
+
Bridge.log("LC3 converter initialized for unified audio encoding")
|
|
352
|
+
|
|
353
|
+
DispatchQueue.main.async { [weak self] in
|
|
354
|
+
guard let self = self else { return }
|
|
355
|
+
self.micReinitTimer = Timer.scheduledTimer(
|
|
356
|
+
withTimeInterval: 10.0, repeats: true
|
|
357
|
+
) { [weak self] _ in
|
|
358
|
+
self?.checkAndReinitGlassesMic()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// MARK: - AUX Voice Data Handling
|
|
364
|
+
|
|
365
|
+
private func checkSetVadStatus(speaking: Bool) {
|
|
366
|
+
if speaking != isSpeaking {
|
|
367
|
+
isSpeaking = speaking
|
|
368
|
+
Bridge.sendVadEvent(isSpeaking)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private func convertAndSendMicLc3(_ pcmData: Data) {
|
|
373
|
+
guard let lc3Converter = lc3Converter else {
|
|
374
|
+
Bridge.log("MAN: ERROR - LC3 converter not initialized but format is LC3")
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
let frameSize = DeviceStore.shared.get("bluetooth", "lc3_frame_size") as! Int
|
|
378
|
+
let lc3Data = lc3Converter.encode(pcmData, frameSize: frameSize) as Data
|
|
379
|
+
guard lc3Data.count > 0 else {
|
|
380
|
+
Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
Bridge.sendMicLc3(lc3Data)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private func handleSendingPcm(_ pcmData: Data) {
|
|
387
|
+
// Bridge.log("MAN: handleSendingPcm() shouldSendPcm: \(shouldSendPcm) shouldSendLc3: \(shouldSendLc3)")
|
|
388
|
+
if shouldSendPcm {
|
|
389
|
+
Bridge.sendMicPcm(pcmData)
|
|
390
|
+
}
|
|
391
|
+
if shouldSendLc3 {
|
|
392
|
+
convertAndSendMicLc3(pcmData)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private func emptyVadBuffer() {
|
|
397
|
+
// go through the buffer, popping from the first element in the array (FIFO):
|
|
398
|
+
while !vadBuffer.isEmpty {
|
|
399
|
+
let chunk = vadBuffer.removeFirst()
|
|
400
|
+
handleSendingPcm(chunk)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private func addToVadBuffer(_ chunk: Data) {
|
|
405
|
+
let MAX_BUFFER_SIZE = 20
|
|
406
|
+
vadBuffer.append(chunk)
|
|
407
|
+
while vadBuffer.count > MAX_BUFFER_SIZE {
|
|
408
|
+
// pop from the front of the array:
|
|
409
|
+
vadBuffer.removeFirst()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Handle raw LC3 audio data from glasses.
|
|
415
|
+
* Decodes the glasses LC3 to PCM, then forwards to handlePcm for processing.
|
|
416
|
+
* This matches Android behavior - glasses forward raw LC3, DeviceManager handles encoding.
|
|
417
|
+
*/
|
|
418
|
+
func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20) {
|
|
419
|
+
lastLc3Event = Date()
|
|
420
|
+
guard let lc3Converter = lc3Converter else {
|
|
421
|
+
Bridge.log("MAN: LC3 converter not initialized")
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
guard lc3Data.count > 2 else {
|
|
426
|
+
Bridge.log("MAN: Received invalid LC3 data size: \(lc3Data.count)")
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let pcmData = lc3Converter.decode(lc3Data, frameSize: frameSize) as Data
|
|
431
|
+
guard pcmData.count > 0 else {
|
|
432
|
+
Bridge.log("MAN: Failed to decode glasses LC3 audio")
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
// Forward to handlePcm which handles VAD and encoding
|
|
436
|
+
handlePcm(pcmData)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func handlePcm(_ pcmData: Data) {
|
|
440
|
+
// handle incoming PCM data from the microphone manager and feed to the VAD:
|
|
441
|
+
if bypassVad {
|
|
442
|
+
handleSendingPcm(pcmData)
|
|
443
|
+
|
|
444
|
+
// Send PCM to local transcriber (always needs raw PCM)
|
|
445
|
+
if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
|
|
446
|
+
transcriber?.acceptAudio(pcm16le: pcmData)
|
|
447
|
+
}
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// feed PCM to the VAD:
|
|
452
|
+
guard let vad = vad else {
|
|
453
|
+
Bridge.log("VAD not initialized")
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// convert audioData to Int16 array for VAD:
|
|
458
|
+
let pcmDataArray = pcmData.withUnsafeBytes { pointer -> [Int16] in
|
|
459
|
+
Array(
|
|
460
|
+
UnsafeBufferPointer(
|
|
461
|
+
start: pointer.bindMemory(to: Int16.self).baseAddress,
|
|
462
|
+
count: pointer.count / MemoryLayout<Int16>.stride
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
vad.checkVAD(pcm: pcmDataArray) { [weak self] state in
|
|
468
|
+
guard let self = self else { return }
|
|
469
|
+
Bridge.log("VAD State: \(state)")
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let vadState = vad.currentState()
|
|
473
|
+
if vadState == .speeching {
|
|
474
|
+
checkSetVadStatus(speaking: true)
|
|
475
|
+
// first send out whatever's in the vadBuffer (if there is anything):
|
|
476
|
+
emptyVadBuffer()
|
|
477
|
+
|
|
478
|
+
handleSendingPcm(pcmData)
|
|
479
|
+
|
|
480
|
+
// Send PCM to local transcriber (always needs raw PCM)
|
|
481
|
+
if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
|
|
482
|
+
transcriber?.acceptAudio(pcm16le: pcmData)
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
checkSetVadStatus(speaking: false)
|
|
486
|
+
// add to the vadBuffer (stores PCM for potential later sending):
|
|
487
|
+
addToVadBuffer(pcmData)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
func updateMicState() {
|
|
492
|
+
// go through the micRanking and find the first mic that is available:
|
|
493
|
+
var micUsed = ""
|
|
494
|
+
|
|
495
|
+
// allow the sgc to make changes to the micRanking:
|
|
496
|
+
micRanking = sgc?.sortMicRanking(list: micRanking) ?? micRanking
|
|
497
|
+
// Bridge.log("MAN: updateMicState() micRanking: \(micRanking)")
|
|
498
|
+
|
|
499
|
+
var phoneMicUnavailable = systemMicUnavailable
|
|
500
|
+
|
|
501
|
+
let appState = UIApplication.shared.applicationState
|
|
502
|
+
if appState == .background {
|
|
503
|
+
// Bridge.log("App is in background - onboard mic unavailable to start!")
|
|
504
|
+
phoneMicUnavailable = true
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if micEnabled {
|
|
508
|
+
for micMode in micRanking {
|
|
509
|
+
if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BT_CLASSIC
|
|
510
|
+
|| micMode == MicTypes.BT
|
|
511
|
+
{
|
|
512
|
+
if PhoneMic.shared.isRecordingWithMode(micMode) {
|
|
513
|
+
micUsed = micMode
|
|
514
|
+
break
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if phoneMicUnavailable {
|
|
518
|
+
continue
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// if the phone mic is not recording, start recording:
|
|
522
|
+
let success = PhoneMic.shared.startMode(micMode)
|
|
523
|
+
Bridge.log("MAN: starting mic mode: \(micMode) -> \(success)")
|
|
524
|
+
if success {
|
|
525
|
+
micUsed = micMode
|
|
526
|
+
break
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if micMode == MicTypes.GLASSES_CUSTOM {
|
|
531
|
+
// Bridge.log(
|
|
532
|
+
// "MAN: glasses custom mic found - hasMic: \(sgc?.hasMic ?? false), micEnabled: \(sgc?.micEnabled ?? false)"
|
|
533
|
+
// )
|
|
534
|
+
// if the glasses has a mic that's already on, mark it as used and break:
|
|
535
|
+
if sgc?.hasMic ?? false {
|
|
536
|
+
// enable the mic if it's not already on:
|
|
537
|
+
if sgc?.micEnabled == false {
|
|
538
|
+
sgc?.setMicEnabled(true)
|
|
539
|
+
micUsed = micMode
|
|
540
|
+
break
|
|
541
|
+
} else {
|
|
542
|
+
// the mic is already on, mark it as used and break:
|
|
543
|
+
micUsed = micMode
|
|
544
|
+
break
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// if the glasses doesn't have a mic, continue to the next mic:
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
currentMic = micUsed
|
|
554
|
+
|
|
555
|
+
// log if no mic was found:
|
|
556
|
+
if micUsed == "" && micEnabled {
|
|
557
|
+
Bridge.log("MAN: No available mic found!")
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// go through and disable all mics after the first used one:
|
|
562
|
+
var allMics = micRanking
|
|
563
|
+
// add any missing mics to the list:
|
|
564
|
+
for micMode in MicMap.map["auto"]! {
|
|
565
|
+
if !allMics.contains(micMode) {
|
|
566
|
+
allMics.append(micMode)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
for micMode in allMics {
|
|
571
|
+
if micMode == micUsed {
|
|
572
|
+
continue
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BT_CLASSIC
|
|
576
|
+
|| micMode == MicTypes.BT
|
|
577
|
+
{
|
|
578
|
+
PhoneMic.shared.stopMode(micMode)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true {
|
|
582
|
+
sgc?.setMicEnabled(false)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func setOnboardMicEnabled(_ isEnabled: Bool) {
|
|
588
|
+
Task {
|
|
589
|
+
if isEnabled {
|
|
590
|
+
// Just check permissions - we no longer request them directly from Swift
|
|
591
|
+
// Permissions should already be granted via React Native UI flow
|
|
592
|
+
if !(PhoneMic.shared.checkPermissions()) {
|
|
593
|
+
Bridge.log("Microphone permissions not granted. Cannot enable microphone.")
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let success = PhoneMic.shared.startRecording()
|
|
598
|
+
if !success {
|
|
599
|
+
// fallback to glasses mic if possible:
|
|
600
|
+
if sgc?.hasMic ?? false {
|
|
601
|
+
await sgc?.setMicEnabled(true)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
PhoneMic.shared.stopRecording()
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// MARK: - Glasses Commands
|
|
611
|
+
|
|
612
|
+
private func playStartupSequence() {
|
|
613
|
+
Bridge.log("MAN: playStartupSequence()")
|
|
614
|
+
// Arrow frames for the animation
|
|
615
|
+
let arrowFrames = ["↑", "↗", "↑", "↖"]
|
|
616
|
+
|
|
617
|
+
let delay = 0.25 // Frame delay in seconds
|
|
618
|
+
let totalCycles = 2 // Number of animation cycles
|
|
619
|
+
|
|
620
|
+
// Variables to track animation state
|
|
621
|
+
var frameIndex = 0
|
|
622
|
+
var cycles = 0
|
|
623
|
+
|
|
624
|
+
// Create a dispatch queue for the animation
|
|
625
|
+
let animationQueue = DispatchQueue.global(qos: .userInteractive)
|
|
626
|
+
|
|
627
|
+
/// Function to display the current animation frame
|
|
628
|
+
func displayFrame() {
|
|
629
|
+
// Check if we've completed all cycles
|
|
630
|
+
if cycles >= totalCycles {
|
|
631
|
+
// End animation with final message
|
|
632
|
+
sgc?.sendTextWall(" /// MentraOS Connected \\\\\\")
|
|
633
|
+
animationQueue.asyncAfter(deadline: .now() + 1.0) {
|
|
634
|
+
self.sgc?.clearDisplay()
|
|
635
|
+
}
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Display current animation frame
|
|
640
|
+
let frameText =
|
|
641
|
+
" \(arrowFrames[frameIndex]) MentraOS Booting \(arrowFrames[frameIndex])"
|
|
642
|
+
sgc?.sendTextWall(frameText)
|
|
643
|
+
|
|
644
|
+
// Move to next frame
|
|
645
|
+
frameIndex = (frameIndex + 1) % arrowFrames.count
|
|
646
|
+
|
|
647
|
+
// Count completed cycles
|
|
648
|
+
if frameIndex == 0 {
|
|
649
|
+
cycles += 1
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Schedule next frame
|
|
653
|
+
animationQueue.asyncAfter(deadline: .now() + delay) {
|
|
654
|
+
displayFrame()
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Start the animation after a short initial delay
|
|
659
|
+
animationQueue.asyncAfter(deadline: .now() + 0.35) {
|
|
660
|
+
displayFrame()
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// MARK: - Auxiliary Commands
|
|
665
|
+
|
|
666
|
+
func initSGC(_ wearable: String) {
|
|
667
|
+
Bridge.log("Initializing manager for wearable: \(wearable)")
|
|
668
|
+
if sgc != nil && sgc?.type != wearable {
|
|
669
|
+
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
670
|
+
sgc?.cleanup()
|
|
671
|
+
sgc = nil
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if sgc != nil {
|
|
675
|
+
Bridge.log("MAN: SGC already initialized")
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
if wearable.contains(DeviceTypes.SIMULATED) {
|
|
679
|
+
sgc = Simulated()
|
|
680
|
+
} else if wearable.contains(DeviceTypes.G1) {
|
|
681
|
+
sgc = G1()
|
|
682
|
+
} else if wearable.contains(DeviceTypes.G2) {
|
|
683
|
+
sgc = G2()
|
|
684
|
+
} else if wearable.contains(DeviceTypes.LIVE) {
|
|
685
|
+
sgc = MentraLive()
|
|
686
|
+
} else if wearable.contains(DeviceTypes.MACH1) {
|
|
687
|
+
sgc = Mach1()
|
|
688
|
+
} else if wearable.contains(DeviceTypes.Z100) {
|
|
689
|
+
sgc = Mach1() // Z100 uses same hardware/SDK as Mach1
|
|
690
|
+
sgc?.type = DeviceTypes.Z100 // Override type to Z100
|
|
691
|
+
} else if wearable.contains(DeviceTypes.FRAME) {
|
|
692
|
+
// sgc = FrameManager()
|
|
693
|
+
}
|
|
694
|
+
// update device model:
|
|
695
|
+
DeviceStore.shared.apply("glasses", "deviceModel", sgc?.type ?? "")
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
func initController(_ controllerModel: String) {
|
|
699
|
+
Bridge.log("MAN: Initializing controller: \(controllerModel)")
|
|
700
|
+
if controller != nil && controller?.type != controllerModel {
|
|
701
|
+
Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
|
|
702
|
+
controller?.cleanup()
|
|
703
|
+
controller = nil
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if controller != nil {
|
|
707
|
+
Bridge.log("MAN: Controller already initialized")
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if controllerModel == ControllerTypes.R1 {
|
|
712
|
+
controller = R1()
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
func sendCurrentState() {
|
|
717
|
+
if screenDisabled {
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
Task {
|
|
722
|
+
var currentViewState: ViewState!
|
|
723
|
+
if headUp {
|
|
724
|
+
currentViewState = self.viewStates[1]
|
|
725
|
+
} else {
|
|
726
|
+
currentViewState = self.viewStates[0]
|
|
727
|
+
}
|
|
728
|
+
if headUp && !self.contextualDashboard {
|
|
729
|
+
currentViewState = self.viewStates[0]
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true {
|
|
733
|
+
// dont send the event to glasses that aren't there:
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
var fullyBooted = sgc?.fullyBooted ?? false
|
|
738
|
+
if !fullyBooted {
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// cancel any pending clear display work item:
|
|
743
|
+
sendStateWorkItem?.cancel()
|
|
744
|
+
|
|
745
|
+
let layoutType = currentViewState.layoutType
|
|
746
|
+
switch layoutType {
|
|
747
|
+
case "text_wall":
|
|
748
|
+
let text = currentViewState.text
|
|
749
|
+
sgc?.sendTextWall(text)
|
|
750
|
+
case "double_text_wall":
|
|
751
|
+
let topText = currentViewState.topText
|
|
752
|
+
let bottomText = currentViewState.bottomText
|
|
753
|
+
sgc?.sendDoubleTextWall(topText, bottomText)
|
|
754
|
+
case "reference_card":
|
|
755
|
+
sgc?.sendTextWall(currentViewState.title + "\n\n" + currentViewState.text)
|
|
756
|
+
case "bitmap_view":
|
|
757
|
+
Bridge.log("MAN: Processing bitmap_view layout")
|
|
758
|
+
guard let data = currentViewState.data else {
|
|
759
|
+
Bridge.log("MAN: ERROR: bitmap_view missing data field")
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
Bridge.log("MAN: Processing bitmap_view with base64 data, length: \(data.count)")
|
|
763
|
+
await sgc?.displayBitmap(base64ImageData: data)
|
|
764
|
+
case "clear_view":
|
|
765
|
+
sgc?.clearDisplay()
|
|
766
|
+
default:
|
|
767
|
+
Bridge.log("UNHANDLED LAYOUT_TYPE \(layoutType)")
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
func parsePlaceholders(_ text: String) -> String {
|
|
773
|
+
let dateFormatter = DateFormatter()
|
|
774
|
+
dateFormatter.dateFormat = "M/dd, h:mm"
|
|
775
|
+
let formattedDate = dateFormatter.string(from: Date())
|
|
776
|
+
|
|
777
|
+
// 12-hour time format (with leading zeros for hours)
|
|
778
|
+
let time12Format = DateFormatter()
|
|
779
|
+
time12Format.dateFormat = "hh:mm"
|
|
780
|
+
let time12 = time12Format.string(from: Date())
|
|
781
|
+
|
|
782
|
+
// 24-hour time format
|
|
783
|
+
let time24Format = DateFormatter()
|
|
784
|
+
time24Format.dateFormat = "HH:mm"
|
|
785
|
+
let time24 = time24Format.string(from: Date())
|
|
786
|
+
|
|
787
|
+
// Current date with format MM/dd
|
|
788
|
+
let dateFormat = DateFormatter()
|
|
789
|
+
dateFormat.dateFormat = "MM/dd"
|
|
790
|
+
let currentDate = dateFormat.string(from: Date())
|
|
791
|
+
|
|
792
|
+
var placeholders: [String: String] = [:]
|
|
793
|
+
placeholders["$no_datetime$"] = formattedDate
|
|
794
|
+
placeholders["$DATE$"] = currentDate
|
|
795
|
+
placeholders["$TIME12$"] = time12
|
|
796
|
+
placeholders["$TIME24$"] = time24
|
|
797
|
+
|
|
798
|
+
if (sgc?.batteryLevel ?? -1) == -1 {
|
|
799
|
+
placeholders["$GBATT$"] = ""
|
|
800
|
+
} else {
|
|
801
|
+
placeholders["$GBATT$"] = "\(sgc!.batteryLevel)%"
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// placeholders["$CONNECTION_STATUS$"] =
|
|
805
|
+
// WebSocketManager.shared.isConnected() ? "Connected" : "Disconnected"
|
|
806
|
+
// TODO: config:
|
|
807
|
+
placeholders["$CONNECTION_STATUS$"] = "Connected"
|
|
808
|
+
|
|
809
|
+
var result = text
|
|
810
|
+
for (key, value) in placeholders {
|
|
811
|
+
result = result.replacingOccurrences(of: key, with: value)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return result
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private func checkAndReinitGlassesMic() {
|
|
818
|
+
// if the glasses mic is marked as enabled (and the glasses are connected), but our last known lc3 event is from > 5 seconds ago, reinitialize the mic:
|
|
819
|
+
let glassesMicEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
|
|
820
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
821
|
+
if !glassesMicEnabled || !glassesConnected {
|
|
822
|
+
return
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
|
|
826
|
+
if timeSinceLastLc3Event > 5 {
|
|
827
|
+
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
|
|
828
|
+
sgc?.setMicEnabled(true)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
func getAudioDevicePattern() -> String {
|
|
833
|
+
let audioDevicePattern: String
|
|
834
|
+
if let idRange = deviceName.range(of: "_BLE_", options: .caseInsensitive) {
|
|
835
|
+
// Extract the ID after "_BLE_" (e.g., "ABC123")
|
|
836
|
+
audioDevicePattern = String(deviceName[idRange.upperBound...])
|
|
837
|
+
} else if let idRange = deviceName.range(of: "_BT_", options: .caseInsensitive) {
|
|
838
|
+
// Extract the ID after "_BT_"
|
|
839
|
+
audioDevicePattern = String(deviceName[idRange.upperBound...])
|
|
840
|
+
} else {
|
|
841
|
+
// Fallback: use the full device name
|
|
842
|
+
audioDevicePattern = deviceName
|
|
843
|
+
}
|
|
844
|
+
return audioDevicePattern
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
func checkCurrentAudioDevice() {
|
|
848
|
+
let audioDevicePattern = getAudioDevicePattern()
|
|
849
|
+
Bridge.log("MAN: checkCurrentAudioDevice: audioDevicePattern: \(audioDevicePattern)")
|
|
850
|
+
|
|
851
|
+
if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
|
|
852
|
+
glassesBtcConnected = false
|
|
853
|
+
Bridge.log("MAN: Audio device pattern is empty or simulated, returning")
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// check if the device disconnected:
|
|
858
|
+
let isConnected = AudioSessionMonitor.isAudioDeviceConnected(
|
|
859
|
+
devicePattern: audioDevicePattern
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
if !isConnected {
|
|
863
|
+
Bridge.log("MAN: Device '\(deviceName)' disconnected")
|
|
864
|
+
glassesBtcConnected = false
|
|
865
|
+
|
|
866
|
+
let isOtherDeviceConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(
|
|
867
|
+
devicePattern: audioDevicePattern
|
|
868
|
+
)
|
|
869
|
+
if isOtherDeviceConnected {
|
|
870
|
+
Bridge.log("MAN: Other device connected, returning")
|
|
871
|
+
otherBtConnected = true
|
|
872
|
+
}
|
|
873
|
+
return
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
877
|
+
if isPaired {
|
|
878
|
+
let session = AVAudioSession.sharedInstance()
|
|
879
|
+
let deviceName = session.availableInputs?.first(where: {
|
|
880
|
+
$0.portName.localizedCaseInsensitiveContains(audioDevicePattern)
|
|
881
|
+
})?.portName
|
|
882
|
+
Bridge.log("MAN: ✅ Successfully detected newly paired device '\(deviceName)'")
|
|
883
|
+
glassesBtcConnected = true
|
|
884
|
+
} else {
|
|
885
|
+
glassesBtcConnected = false
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
func onRouteChange(
|
|
890
|
+
reason: AVAudioSession.RouteChangeReason, availableInputs: [AVAudioSessionPortDescription]
|
|
891
|
+
) {
|
|
892
|
+
Bridge.log("MAN: onRouteChange: reason: \(reason)")
|
|
893
|
+
Bridge.log("MAN: onRouteChange: inputs: \(availableInputs)")
|
|
894
|
+
|
|
895
|
+
// check if our deviceName is connected:
|
|
896
|
+
// (return if deviceName is empty):
|
|
897
|
+
if deviceName.isEmpty {
|
|
898
|
+
Bridge.log("MAN: Device name is empty, returning")
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Add small delay to let iOS populate availableInputs
|
|
903
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
904
|
+
guard let self = self else { return }
|
|
905
|
+
checkCurrentAudioDevice()
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
updateMicState()
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
func onInterruption(began: Bool) {
|
|
912
|
+
Bridge.log("MAN: Interruption: \(began)")
|
|
913
|
+
systemMicUnavailable = began
|
|
914
|
+
updateMicState()
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
func restartTranscriber() {
|
|
918
|
+
Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
|
|
919
|
+
transcriber?.restart()
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// MARK: - connection state management
|
|
923
|
+
|
|
924
|
+
func handleDeviceReady() {
|
|
925
|
+
guard let sgc else {
|
|
926
|
+
Bridge.log("MAN: SGC is nil, returning")
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
|
|
930
|
+
|
|
931
|
+
pendingWearable = ""
|
|
932
|
+
defaultWearable = sgc.type
|
|
933
|
+
searching = false
|
|
934
|
+
|
|
935
|
+
// Show welcome message on first connect for all display glasses
|
|
936
|
+
if shouldSendBootingMessage {
|
|
937
|
+
Task {
|
|
938
|
+
sgc.sendTextWall("// MentraOS Connected")
|
|
939
|
+
try? await Task.sleep(nanoseconds: 3_000_000_000) // 1 second
|
|
940
|
+
sgc.clearDisplay()
|
|
941
|
+
}
|
|
942
|
+
shouldSendBootingMessage = false
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Call device-specific setup handlers
|
|
946
|
+
if defaultWearable.contains(DeviceTypes.G1) {
|
|
947
|
+
handleG1Ready()
|
|
948
|
+
} else if defaultWearable.contains(DeviceTypes.G2) {
|
|
949
|
+
// handleG2Ready()
|
|
950
|
+
} else if defaultWearable.contains(DeviceTypes.MACH1) {
|
|
951
|
+
handleMach1Ready()
|
|
952
|
+
} else if defaultWearable.contains(DeviceTypes.Z100) {
|
|
953
|
+
handleMach1Ready() // Z100 uses same initialization as Mach1
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// check current audio device:
|
|
957
|
+
checkCurrentAudioDevice()
|
|
958
|
+
|
|
959
|
+
// save the default_wearable now that we're connected:
|
|
960
|
+
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
961
|
+
Bridge.saveSetting("device_name", deviceName)
|
|
962
|
+
Bridge.saveSetting("device_address", deviceAddress)
|
|
963
|
+
|
|
964
|
+
// Re-apply display height after reconnection
|
|
965
|
+
let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
|
|
966
|
+
let d = NexDashboardDisplayWire.clampDepthFromStore(
|
|
967
|
+
DeviceStore.shared.get("bluetooth", "dashboard_depth")
|
|
968
|
+
)
|
|
969
|
+
sgc.setDashboardPosition(h, d)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
func handleControllerReady() {
|
|
973
|
+
guard let controller else {
|
|
974
|
+
Bridge.log("MAN: Controller is nil, returning")
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
Bridge.log("MAN: handleControllerReady(): \(controller.type)")
|
|
978
|
+
|
|
979
|
+
pendingController = ""
|
|
980
|
+
defaultController = controller.type
|
|
981
|
+
searching = false
|
|
982
|
+
|
|
983
|
+
// save the default_controller now that we're connected:
|
|
984
|
+
Bridge.saveSetting("default_controller", defaultController)
|
|
985
|
+
Bridge.saveSetting("controller_device_name", controllerDeviceName)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
func handleControllerDisconnected() {
|
|
989
|
+
Bridge.log("MAN: Controller disconnected")
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private func handleG1Ready() {
|
|
993
|
+
// G1-specific setup and configuration
|
|
994
|
+
Task {
|
|
995
|
+
// give the glasses some extra time to finish booting:
|
|
996
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
997
|
+
await sgc?.setSilentMode(false) // turn off silent mode
|
|
998
|
+
await sgc?.getBatteryStatus()
|
|
999
|
+
|
|
1000
|
+
// send loaded settings to glasses:
|
|
1001
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1002
|
+
sgc?.setHeadUpAngle(headUpAngle)
|
|
1003
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1004
|
+
sgc?.setBrightness(brightness, autoMode: autoBrightness)
|
|
1005
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1006
|
+
// self.g1Manager?.RN_setDashboardPosition(self.dashboardHeight, self.dashboardDepth)
|
|
1007
|
+
// try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1008
|
+
// playStartupSequence()
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private func handleMach1Ready() {}
|
|
1013
|
+
|
|
1014
|
+
func handleDeviceDisconnected() {
|
|
1015
|
+
Bridge.log("MAN: Device disconnected")
|
|
1016
|
+
// setMicState(shouldSendPcData, shouldSendTranscript, false)
|
|
1017
|
+
// shouldSendBootingMessage = true // Reset for next first connect
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// MARK: - Network Command handlers
|
|
1021
|
+
|
|
1022
|
+
func displayText(_ params: [String: Any]) {
|
|
1023
|
+
guard let text = params["text"] as? String else {
|
|
1024
|
+
Bridge.log("MAN: display_text missing text parameter")
|
|
1025
|
+
return
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
Bridge.log("MAN: Displaying text: \(text)")
|
|
1029
|
+
sgc?.sendTextWall(text)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
func displayEvent(_ event: [String: Any]) {
|
|
1033
|
+
guard let view = event["view"] as? String else {
|
|
1034
|
+
Bridge.log("MAN: invalid view")
|
|
1035
|
+
return
|
|
1036
|
+
}
|
|
1037
|
+
let isDashboard = view == "dashboard"
|
|
1038
|
+
|
|
1039
|
+
var stateIndex = 0
|
|
1040
|
+
if isDashboard {
|
|
1041
|
+
stateIndex = 1
|
|
1042
|
+
} else {
|
|
1043
|
+
stateIndex = 0
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
let layout = event["layout"] as! [String: Any]
|
|
1047
|
+
let layoutType = layout["layoutType"] as! String
|
|
1048
|
+
var text = layout["text"] as? String ?? " "
|
|
1049
|
+
var topText = layout["topText"] as? String ?? " "
|
|
1050
|
+
var bottomText = layout["bottomText"] as? String ?? " "
|
|
1051
|
+
var title = layout["title"] as? String ?? " "
|
|
1052
|
+
var data = layout["data"] as? String ?? ""
|
|
1053
|
+
|
|
1054
|
+
text = parsePlaceholders(text)
|
|
1055
|
+
topText = parsePlaceholders(topText)
|
|
1056
|
+
bottomText = parsePlaceholders(bottomText)
|
|
1057
|
+
title = parsePlaceholders(title)
|
|
1058
|
+
|
|
1059
|
+
var newViewState = ViewState(
|
|
1060
|
+
topText: topText, bottomText: bottomText, title: title, layoutType: layoutType,
|
|
1061
|
+
text: text, data: data, animationData: nil
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
if layoutType == "bitmap_animation" {
|
|
1065
|
+
if let frames = layout["frames"] as? [String],
|
|
1066
|
+
let interval = layout["interval"] as? Double
|
|
1067
|
+
{
|
|
1068
|
+
let animationData: [String: Any] = [
|
|
1069
|
+
"frames": frames,
|
|
1070
|
+
"interval": interval,
|
|
1071
|
+
"repeat": layout["repeat"] as? Bool ?? true,
|
|
1072
|
+
]
|
|
1073
|
+
newViewState.animationData = animationData
|
|
1074
|
+
Bridge.log(
|
|
1075
|
+
"MAN: Parsed bitmap_animation with \(frames.count) frames, interval: \(interval)ms"
|
|
1076
|
+
)
|
|
1077
|
+
} else {
|
|
1078
|
+
Bridge.log("MAN: ERROR: bitmap_animation missing frames or interval")
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
let cS = viewStates[stateIndex]
|
|
1083
|
+
let nS = newViewState
|
|
1084
|
+
let currentState =
|
|
1085
|
+
cS.layoutType + cS.text + cS.topText + cS.bottomText + cS.title + (cS.data ?? "")
|
|
1086
|
+
let newState =
|
|
1087
|
+
nS.layoutType + nS.text + nS.topText + nS.bottomText + nS.title + (nS.data ?? "")
|
|
1088
|
+
|
|
1089
|
+
if currentState == newState {
|
|
1090
|
+
// Core.log("MAN: View state is the same, skipping update")
|
|
1091
|
+
return
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Bridge.log("MAN: Updating view state \(stateIndex) with \(layoutType) \(text) \(topText) \(bottomText)")
|
|
1095
|
+
|
|
1096
|
+
viewStates[stateIndex] = newViewState
|
|
1097
|
+
|
|
1098
|
+
let hUp = headUp && contextualDashboard
|
|
1099
|
+
// send the state we just received if the user is currently in that state:
|
|
1100
|
+
if stateIndex == 0 && !hUp {
|
|
1101
|
+
sendCurrentState()
|
|
1102
|
+
} else if stateIndex == 1 && hUp {
|
|
1103
|
+
sendCurrentState()
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
func showDashboard() {
|
|
1108
|
+
sgc?.showDashboard()
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
func ping() {
|
|
1112
|
+
sgc?.ping()
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
func dbg1() {
|
|
1116
|
+
sgc?.disconnectController()
|
|
1117
|
+
connectDefaultController()
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
func dbg2() {}
|
|
1121
|
+
|
|
1122
|
+
func startStream(_ message: [String: Any]) {
|
|
1123
|
+
Bridge.log("MAN: startStream: \(message)")
|
|
1124
|
+
sgc?.startStream(message)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
func stopStream() {
|
|
1128
|
+
Bridge.log("MAN: stopStream")
|
|
1129
|
+
sgc?.stopStream()
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
func keepStreamAlive(_ message: [String: Any]) {
|
|
1133
|
+
Bridge.log("MAN: sendStreamKeepAlive: \(message)")
|
|
1134
|
+
sgc?.sendStreamKeepAlive(message)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
func requestWifiScan() {
|
|
1138
|
+
Bridge.log("MAN: Requesting wifi scan")
|
|
1139
|
+
DeviceStore.shared.apply("bluetooth", "wifiScanResults", [])
|
|
1140
|
+
sgc?.requestWifiScan()
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1144
|
+
Bridge.log("MAN: Sending incidentId to glasses for log upload: \(incidentId)")
|
|
1145
|
+
sgc?.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
func sendWifiCredentials(_ ssid: String, _ password: String) {
|
|
1149
|
+
Bridge.log("MAN: Sending wifi credentials: \(ssid) \(password)")
|
|
1150
|
+
sgc?.sendWifiCredentials(ssid, password)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
func forgetWifiNetwork(_ ssid: String) {
|
|
1154
|
+
Bridge.log("MAN: Forgetting wifi network: \(ssid)")
|
|
1155
|
+
sgc?.forgetWifiNetwork(ssid)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
func setHotspotState(_ enabled: Bool) {
|
|
1159
|
+
Bridge.log("MAN: 🔥 Setting glasses hotspot state: \(enabled)")
|
|
1160
|
+
sgc?.sendHotspotState(enabled)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
func queryGalleryStatus() {
|
|
1164
|
+
Bridge.log("MAN: 📸 Querying gallery status from glasses")
|
|
1165
|
+
sgc?.queryGalleryStatus()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/// Send OTA start command to glasses.
|
|
1169
|
+
/// Called when user approves an update (onboarding or background mode).
|
|
1170
|
+
/// Triggers glasses to begin download and installation.
|
|
1171
|
+
func sendOtaStart() {
|
|
1172
|
+
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1173
|
+
sgc?.sendOtaStart()
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
func sendOtaQueryStatus() {
|
|
1177
|
+
Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
|
|
1178
|
+
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/// Request version info from glasses.
|
|
1182
|
+
/// Glasses will respond with version_info message containing build number, firmware version, etc.
|
|
1183
|
+
func requestVersionInfo() {
|
|
1184
|
+
Bridge.log("MAN: 📱 Requesting version info from glasses")
|
|
1185
|
+
sgc?.requestVersionInfo()
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/// Send shutdown command to glasses.
|
|
1189
|
+
/// This will initiate a graceful shutdown of the device.
|
|
1190
|
+
func sendShutdown() {
|
|
1191
|
+
Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
|
|
1192
|
+
sgc?.sendShutdown()
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/// Send reboot command to glasses.
|
|
1196
|
+
/// This will initiate a reboot of the device.
|
|
1197
|
+
func sendReboot() {
|
|
1198
|
+
Bridge.log("MAN: 🔄 Sending reboot command to glasses")
|
|
1199
|
+
sgc?.sendReboot()
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
func startVideoRecording(_ requestId: String, _ save: Bool, _ flash: Bool, _ sound: Bool) {
|
|
1203
|
+
Bridge.log(
|
|
1204
|
+
"MAN: onStartVideoRecording: requestId=\(requestId), save=\(save), flash=\(flash), sound=\(sound)"
|
|
1205
|
+
)
|
|
1206
|
+
sgc?.startVideoRecording(requestId: requestId, save: save, flash: flash, sound: sound)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
func stopVideoRecording(_ requestId: String) {
|
|
1210
|
+
Bridge.log("MAN: onStopVideoRecording: requestId=\(requestId)")
|
|
1211
|
+
sgc?.stopVideoRecording(requestId: requestId)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
func setMicState() {
|
|
1215
|
+
let willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1216
|
+
let willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
|
|
1217
|
+
micEnabled = willSendPcm || willSendTranscript
|
|
1218
|
+
updateMicState()
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
func rgbLedControl(
|
|
1222
|
+
requestId: String,
|
|
1223
|
+
packageName: String?,
|
|
1224
|
+
action: String,
|
|
1225
|
+
color: String?,
|
|
1226
|
+
ontime: Int,
|
|
1227
|
+
offtime: Int,
|
|
1228
|
+
count: Int
|
|
1229
|
+
) {
|
|
1230
|
+
sgc?.sendRgbLedControl(
|
|
1231
|
+
requestId: requestId,
|
|
1232
|
+
packageName: packageName,
|
|
1233
|
+
action: action,
|
|
1234
|
+
color: color,
|
|
1235
|
+
ontime: ontime,
|
|
1236
|
+
offtime: offtime,
|
|
1237
|
+
count: count
|
|
1238
|
+
)
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/// Mentra Live only: K900 `cs_getvol` / `sr_getvol` (step volume 0–15).
|
|
1242
|
+
func getGlassesMediaVolume() async throws -> [String: Any] {
|
|
1243
|
+
guard let live = sgc as? MentraLive else {
|
|
1244
|
+
throw NSError(
|
|
1245
|
+
domain: "DeviceManager",
|
|
1246
|
+
code: 100,
|
|
1247
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
return try await withCheckedThrowingContinuation {
|
|
1251
|
+
(continuation: CheckedContinuation<[String: Any], Error>) in
|
|
1252
|
+
live.getGlassesMediaVolume { result in
|
|
1253
|
+
continuation.resume(with: result)
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/// Mentra Live only: K900 `cs_vol` / `sr_vol`.
|
|
1259
|
+
func setGlassesMediaVolume(level: Int) async throws -> [String: Any] {
|
|
1260
|
+
guard let live = sgc as? MentraLive else {
|
|
1261
|
+
throw NSError(
|
|
1262
|
+
domain: "DeviceManager",
|
|
1263
|
+
code: 100,
|
|
1264
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
return try await withCheckedThrowingContinuation {
|
|
1268
|
+
(continuation: CheckedContinuation<[String: Any], Error>) in
|
|
1269
|
+
live.setGlassesMediaVolume(level: level) { result in
|
|
1270
|
+
continuation.resume(with: result)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
func photoRequest(
|
|
1276
|
+
_ requestId: String,
|
|
1277
|
+
_ appId: String,
|
|
1278
|
+
_ size: String,
|
|
1279
|
+
_ webhookUrl: String?,
|
|
1280
|
+
_ authToken: String?,
|
|
1281
|
+
_ compress: String?,
|
|
1282
|
+
_ flash: Bool,
|
|
1283
|
+
_ sound: Bool
|
|
1284
|
+
) {
|
|
1285
|
+
Bridge.log(
|
|
1286
|
+
"MAN: onPhotoRequest: \(requestId), \(appId), \(webhookUrl), size=\(size), compress=\(compress ?? "none"), flash=\(flash), sound=\(sound)"
|
|
1287
|
+
)
|
|
1288
|
+
sgc?.requestPhoto(
|
|
1289
|
+
requestId, appId: appId, size: size, webhookUrl: webhookUrl, authToken: authToken,
|
|
1290
|
+
compress: compress, flash: flash, sound: sound
|
|
1291
|
+
)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
func connectDefault() {
|
|
1295
|
+
if defaultWearable.isEmpty {
|
|
1296
|
+
Bridge.log("MAN: No default wearable, returning")
|
|
1297
|
+
return
|
|
1298
|
+
}
|
|
1299
|
+
if deviceName.isEmpty {
|
|
1300
|
+
Bridge.log("MAN: No device name, returning")
|
|
1301
|
+
return
|
|
1302
|
+
}
|
|
1303
|
+
initSGC(defaultWearable)
|
|
1304
|
+
searching = true
|
|
1305
|
+
sgc?.connectById(deviceName)
|
|
1306
|
+
connectDefaultController()
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
func connectDefaultController() {
|
|
1310
|
+
if defaultController.isEmpty {
|
|
1311
|
+
Bridge.log("MAN: No default controller, returning")
|
|
1312
|
+
return
|
|
1313
|
+
}
|
|
1314
|
+
if controllerDeviceName.isEmpty {
|
|
1315
|
+
Bridge.log("MAN: No controller device name, returning")
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
initController(defaultController)
|
|
1319
|
+
searchingController = true
|
|
1320
|
+
controller?.connectById(controllerDeviceName)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
func connectByName(_ dName: String) {
|
|
1324
|
+
Bridge.log("MAN: Connecting to wearable: \(dName)")
|
|
1325
|
+
var name = dName
|
|
1326
|
+
|
|
1327
|
+
// use stored device name if available:
|
|
1328
|
+
if dName.isEmpty && !deviceName.isEmpty {
|
|
1329
|
+
name = deviceName
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if pendingWearable.isEmpty, defaultWearable.isEmpty {
|
|
1333
|
+
Bridge.log("MAN: No pending or default wearable, returning")
|
|
1334
|
+
return
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if pendingWearable.isEmpty, !defaultWearable.isEmpty {
|
|
1338
|
+
Bridge.log("MAN: No pending wearable, using default wearable: \(defaultWearable)")
|
|
1339
|
+
pendingWearable = defaultWearable
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// if the pending wearable is a controller, don't disconnect, use the controller manager to connect
|
|
1343
|
+
if ControllerTypes.ALL.contains(pendingWearable) {
|
|
1344
|
+
controller?.disconnect()
|
|
1345
|
+
controller?.connectById(name)
|
|
1346
|
+
return
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
Task {
|
|
1350
|
+
disconnect()
|
|
1351
|
+
try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
|
|
1352
|
+
self.searching = true
|
|
1353
|
+
self.deviceName = name
|
|
1354
|
+
|
|
1355
|
+
initSGC(self.pendingWearable)
|
|
1356
|
+
sgc?.connectById(self.deviceName)
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
func connectDevice(_ deviceModel: String, _ deviceName: String) {
|
|
1361
|
+
Bridge.log("MAN: Connecting to device: \(deviceModel) \(deviceName)")
|
|
1362
|
+
if DeviceTypes.ALL.contains(deviceModel) {
|
|
1363
|
+
pendingWearable = deviceModel
|
|
1364
|
+
initSGC(pendingWearable)
|
|
1365
|
+
sgc?.connectById(deviceName)
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
1369
|
+
pendingWearable = deviceModel
|
|
1370
|
+
initController(deviceModel)
|
|
1371
|
+
controller?.connectById(deviceName)
|
|
1372
|
+
return
|
|
1373
|
+
}
|
|
1374
|
+
Bridge.log("MAN: No compatible device model, returning")
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
func connectSimulated() {
|
|
1378
|
+
defaultWearable = DeviceTypes.SIMULATED
|
|
1379
|
+
deviceName = DeviceTypes.SIMULATED
|
|
1380
|
+
initSGC(defaultWearable)
|
|
1381
|
+
handleDeviceReady()
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
func disconnect() {
|
|
1385
|
+
sgc?.clearDisplay() // clear the screen
|
|
1386
|
+
sgc?.disconnect()
|
|
1387
|
+
sgc = nil // Clear the SGC reference after disconnect
|
|
1388
|
+
searching = false
|
|
1389
|
+
micEnabled = false
|
|
1390
|
+
updateMicState()
|
|
1391
|
+
shouldSendBootingMessage = true // Reset for next first connect
|
|
1392
|
+
// clear glasses properties:
|
|
1393
|
+
DeviceStore.shared.apply("glasses", "deviceModel", "")
|
|
1394
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
1395
|
+
DeviceStore.shared.apply("glasses", "connected", false)
|
|
1396
|
+
// disconnect the controller as well:
|
|
1397
|
+
searchingController = false
|
|
1398
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
1399
|
+
controller?.disconnect()
|
|
1400
|
+
controller = nil // Clear the controller reference after disconnect
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
func disconnectController() {
|
|
1404
|
+
searchingController = false
|
|
1405
|
+
// disconnect the controller from the glasses if applicable:
|
|
1406
|
+
sgc?.disconnectController()
|
|
1407
|
+
controller?.disconnect()
|
|
1408
|
+
controller = nil // Clear the controller reference after disconnect
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
func forget() {
|
|
1412
|
+
Bridge.log("MAN: Forgetting smart glasses")
|
|
1413
|
+
// Call forget first to stop timers/handlers/reconnect logic
|
|
1414
|
+
sgc?.forget()
|
|
1415
|
+
disconnect()
|
|
1416
|
+
// Clear state
|
|
1417
|
+
defaultWearable = ""
|
|
1418
|
+
deviceName = ""
|
|
1419
|
+
deviceAddress = ""
|
|
1420
|
+
Bridge.saveSetting("default_wearable", "")
|
|
1421
|
+
Bridge.saveSetting("device_name", "")
|
|
1422
|
+
Bridge.saveSetting("device_address", "")
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
func forgetController() {
|
|
1426
|
+
Bridge.log("MAN: Forgetting controller")
|
|
1427
|
+
controller?.forget()
|
|
1428
|
+
disconnectController()
|
|
1429
|
+
// Clear state
|
|
1430
|
+
defaultController = ""
|
|
1431
|
+
controllerDeviceName = ""
|
|
1432
|
+
Bridge.saveSetting("controller_device_name", "")
|
|
1433
|
+
Bridge.saveSetting("default_controller", "")
|
|
1434
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
func findCompatibleDevices(_ deviceModel: String) {
|
|
1438
|
+
Bridge.log("MAN: Searching for compatible device names for: \(deviceModel)")
|
|
1439
|
+
|
|
1440
|
+
// reset the search results:
|
|
1441
|
+
searchResults = []
|
|
1442
|
+
|
|
1443
|
+
if DeviceTypes.ALL.contains(deviceModel) {
|
|
1444
|
+
pendingWearable = deviceModel
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
1448
|
+
pendingWearable = deviceModel
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
1452
|
+
initController(deviceModel)
|
|
1453
|
+
controller?.findCompatibleDevices()
|
|
1454
|
+
return
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
initSGC(pendingWearable)
|
|
1458
|
+
sgc?.findCompatibleDevices()
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
func stopScan() {
|
|
1462
|
+
controller?.stopScan()
|
|
1463
|
+
sgc?.stopScan()
|
|
1464
|
+
DeviceStore.shared.apply("bluetooth", "searching", false)
|
|
1465
|
+
DeviceStore.shared.apply("bluetooth", "searchingController", false)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
func cleanup() {
|
|
1469
|
+
// Clean up transcriber resources
|
|
1470
|
+
transcriber?.shutdown()
|
|
1471
|
+
transcriber = nil
|
|
1472
|
+
|
|
1473
|
+
// Clean up LC3 converter
|
|
1474
|
+
lc3Converter = nil
|
|
1475
|
+
|
|
1476
|
+
cancellables.removeAll()
|
|
1477
|
+
}
|
|
1478
|
+
}
|