@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,827 @@
|
|
|
1
|
+
//
|
|
2
|
+
// R1.swift
|
|
3
|
+
// MentraOS_Manager
|
|
4
|
+
//
|
|
5
|
+
// Even Realities R1 Smart Ring controller
|
|
6
|
+
// Protocol reverse-engineered from BTSnoop captures and firmware analysis
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Combine
|
|
10
|
+
import CoreBluetooth
|
|
11
|
+
import Foundation
|
|
12
|
+
|
|
13
|
+
// MARK: - R1 BLE Constants
|
|
14
|
+
|
|
15
|
+
private enum R1BLE {
|
|
16
|
+
/// Ring custom service
|
|
17
|
+
static let SERVICE_UUID = CBUUID(string: "BAE80001-4F05-4503-8E65-3AF1F7329D1F")
|
|
18
|
+
|
|
19
|
+
// Channel 1
|
|
20
|
+
static let WRITE_CHAR_1 = CBUUID(string: "BAE80010-4F05-4503-8E65-3AF1F7329D1F")
|
|
21
|
+
static let NOTIFY_CHAR_1 = CBUUID(string: "BAE80011-4F05-4503-8E65-3AF1F7329D1F")
|
|
22
|
+
|
|
23
|
+
// Channel 2
|
|
24
|
+
static let WRITE_CHAR_2 = CBUUID(string: "BAE80012-4F05-4503-8E65-3AF1F7329D1F")
|
|
25
|
+
static let NOTIFY_CHAR_2 = CBUUID(string: "BAE80013-4F05-4503-8E65-3AF1F7329D1F")
|
|
26
|
+
|
|
27
|
+
// Standard battery service (ring may expose this)
|
|
28
|
+
static let BATTERY_SERVICE = CBUUID(string: "180F")
|
|
29
|
+
static let BATTERY_LEVEL_CHAR = CBUUID(string: "2A19")
|
|
30
|
+
|
|
31
|
+
/// Name filters for scanning
|
|
32
|
+
static let NAME_FILTERS = ["EVEN R1", "BCL60"]
|
|
33
|
+
|
|
34
|
+
// Init sequence config writes
|
|
35
|
+
static let CONFIG_FC = Data([0xFC])
|
|
36
|
+
static let CONFIG_11 = Data([0x11])
|
|
37
|
+
|
|
38
|
+
// BleRing1 command header (cmd, module, subCmd) for advStart
|
|
39
|
+
// From RE: BleRing1Cmd_system=0, BleRing1Module_system=0, BleRing1SubCmd_advStart=9
|
|
40
|
+
static let CMD_SYSTEM: UInt8 = 0x00
|
|
41
|
+
static let MODULE_SYSTEM: UInt8 = 0x00
|
|
42
|
+
static let SUBCMD_ADV_START: UInt8 = 0x09
|
|
43
|
+
|
|
44
|
+
/// Gesture protocol marker
|
|
45
|
+
static let GESTURE_MARKER: UInt8 = 0xFF
|
|
46
|
+
|
|
47
|
+
static let SCAN_TIMEOUT: TimeInterval = 15.0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - R1 Gesture Types
|
|
51
|
+
|
|
52
|
+
private enum R1Gesture: String {
|
|
53
|
+
case hold
|
|
54
|
+
case singleTap = "single_tap"
|
|
55
|
+
case doubleTap = "double_tap"
|
|
56
|
+
case swipeUp = "swipe_up"
|
|
57
|
+
case swipeDown = "swipe_down"
|
|
58
|
+
|
|
59
|
+
/// Parse gesture from notification data: [0xFF, type, param]
|
|
60
|
+
static func parse(from data: Data) -> R1Gesture? {
|
|
61
|
+
guard data.count >= 3, data[0] == R1BLE.GESTURE_MARKER else { return nil }
|
|
62
|
+
switch data[1] {
|
|
63
|
+
case 0x03:
|
|
64
|
+
return .hold
|
|
65
|
+
case 0x04:
|
|
66
|
+
return data[2] == 0x01 ? .singleTap : data[2] == 0x02 ? .doubleTap : nil
|
|
67
|
+
case 0x05:
|
|
68
|
+
return data[2] < 0x80 ? .swipeUp : .swipeDown
|
|
69
|
+
default:
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - R1 Controller
|
|
76
|
+
|
|
77
|
+
@MainActor
|
|
78
|
+
class R1: NSObject, ControllerManager {
|
|
79
|
+
var type = ControllerTypes.R1
|
|
80
|
+
let hasMic = false // R1 ring has no microphone
|
|
81
|
+
|
|
82
|
+
// Connection state
|
|
83
|
+
private var centralManager: CBCentralManager?
|
|
84
|
+
private var ringPeripheral: CBPeripheral?
|
|
85
|
+
private var isDisconnecting = false
|
|
86
|
+
|
|
87
|
+
// BLE characteristics
|
|
88
|
+
private var writeChar1: CBCharacteristic?
|
|
89
|
+
private var notifyChar1: CBCharacteristic?
|
|
90
|
+
private var writeChar2: CBCharacteristic?
|
|
91
|
+
private var notifyChar2: CBCharacteristic?
|
|
92
|
+
private var batteryLevelChar: CBCharacteristic?
|
|
93
|
+
private var notifySubscriptionCount = 0
|
|
94
|
+
private var initSequenceRun = false
|
|
95
|
+
|
|
96
|
+
/// Device search
|
|
97
|
+
var DEVICE_SEARCH_ID = "NOT_SET"
|
|
98
|
+
|
|
99
|
+
/// persisted state for ease of reconnection / background connection:
|
|
100
|
+
/// we could store these elsewhere to be like other settings / state, but in practice they will only ever be set and used here
|
|
101
|
+
/// Stored UUID for background reconnection
|
|
102
|
+
private var ringUUID: UUID? {
|
|
103
|
+
get { UserDefaults.standard.string(forKey: "r1_ringUUID").flatMap { UUID(uuidString: $0) } }
|
|
104
|
+
set {
|
|
105
|
+
if let v = newValue {
|
|
106
|
+
UserDefaults.standard.set(v.uuidString, forKey: "r1_ringUUID")
|
|
107
|
+
} else {
|
|
108
|
+
UserDefaults.standard.removeObject(forKey: "r1_ringUUID")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// maps peripheral.name to 6-byte ring MAC address:
|
|
114
|
+
private var ringMacAddressMap: [String: Data] {
|
|
115
|
+
get {
|
|
116
|
+
UserDefaults.standard.dictionary(forKey: "r1_ringMacAddressMap") as? [String: Data]
|
|
117
|
+
?? [:]
|
|
118
|
+
}
|
|
119
|
+
set { UserDefaults.standard.set(newValue, forKey: "r1_ringMacAddressMap") }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private var ringMacAddress: String? {
|
|
123
|
+
get { UserDefaults.standard.string(forKey: "r1_ringMacAddress") }
|
|
124
|
+
set { UserDefaults.standard.set(newValue, forKey: "r1_ringMacAddress") }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Reconnection
|
|
128
|
+
private let reconnectionManager = R1ReconnectionManager()
|
|
129
|
+
|
|
130
|
+
/// Battery
|
|
131
|
+
@Published private var _batteryLevel: Int = -1 {
|
|
132
|
+
didSet {
|
|
133
|
+
if _batteryLevel != oldValue && _batteryLevel >= 0 {
|
|
134
|
+
DeviceStore.shared.apply("glasses", "controllerBatteryLevel", _batteryLevel)
|
|
135
|
+
// Bridge.sendBatteryStatus(level: _batteryLevel, charging: isCharging)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private var isCharging = false
|
|
141
|
+
|
|
142
|
+
/// Heartbeat
|
|
143
|
+
private var heartbeatTimer: Timer?
|
|
144
|
+
|
|
145
|
+
static let _bluetoothQueue = DispatchQueue(label: "BluetoothR1", qos: .userInitiated)
|
|
146
|
+
|
|
147
|
+
// MARK: - Init
|
|
148
|
+
|
|
149
|
+
override init() {
|
|
150
|
+
super.init()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
deinit {
|
|
154
|
+
centralManager?.delegate = nil
|
|
155
|
+
ringPeripheral?.delegate = nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// MARK: - BLE Scanning
|
|
159
|
+
|
|
160
|
+
@discardableResult
|
|
161
|
+
private func startScan() -> Bool {
|
|
162
|
+
Bridge.log("R1: startScan()")
|
|
163
|
+
if centralManager == nil {
|
|
164
|
+
centralManager = CBCentralManager(
|
|
165
|
+
delegate: self, queue: R1._bluetoothQueue,
|
|
166
|
+
options: [CBCentralManagerOptionShowPowerAlertKey: 0]
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isDisconnecting = false
|
|
171
|
+
guard centralManager!.state == .poweredOn else {
|
|
172
|
+
Bridge.log("R1: Bluetooth not powered on")
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Try UUID-based reconnection first
|
|
177
|
+
if connectByUUID() {
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
centralManager!.scanForPeripherals(
|
|
182
|
+
withServices: nil,
|
|
183
|
+
options: [
|
|
184
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false,
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func stopScan() {
|
|
191
|
+
centralManager?.stopScan()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func connectByUUID() -> Bool {
|
|
195
|
+
// don't do this if we don't have a search id set:
|
|
196
|
+
if DEVICE_SEARCH_ID == "NOT_SET" || DEVICE_SEARCH_ID.isEmpty {
|
|
197
|
+
Bridge.log("R1: 🔵 No DEVICE_SEARCH_ID set, skipping connect by UUID")
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
guard let uuid = ringUUID else { return false }
|
|
201
|
+
guard let ringMac = ringMacAddress else { return false }
|
|
202
|
+
guard let peripheral = centralManager?.retrievePeripherals(withIdentifiers: [uuid]).first
|
|
203
|
+
else { return false }
|
|
204
|
+
|
|
205
|
+
ringPeripheral = peripheral
|
|
206
|
+
peripheral.delegate = self
|
|
207
|
+
centralManager?.connect(peripheral, options: nil)
|
|
208
|
+
Bridge.log("R1: Reconnecting by UUID to \(peripheral.name ?? "ring")")
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func matchesNameFilter(_ name: String?) -> Bool {
|
|
213
|
+
guard let name = name else { return false }
|
|
214
|
+
return R1BLE.NAME_FILTERS.contains(where: { name.contains($0) })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Extract a device identifier from the ring name (e.g. "EVEN R1_CEC5BA" -> "CEC5BA")
|
|
218
|
+
private func extractRingId(_ name: String) -> String? {
|
|
219
|
+
if let range = name.range(of: "R1_") {
|
|
220
|
+
let id = String(name[range.upperBound...])
|
|
221
|
+
return id.isEmpty ? nil : id
|
|
222
|
+
}
|
|
223
|
+
return nil
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// MARK: - Init Sequence
|
|
227
|
+
|
|
228
|
+
/// Runs after notify subscriptions are confirmed.
|
|
229
|
+
/// Sends 0xFC and 0x11 config writes to initialize the ring.
|
|
230
|
+
private func runInitSequence() {
|
|
231
|
+
guard !initSequenceRun else { return }
|
|
232
|
+
initSequenceRun = true
|
|
233
|
+
|
|
234
|
+
Bridge.log("R1: Running init sequence")
|
|
235
|
+
|
|
236
|
+
// Write init commands to both write characteristics
|
|
237
|
+
let writeChars = [writeChar1, writeChar2].compactMap { $0 }
|
|
238
|
+
guard !writeChars.isEmpty else {
|
|
239
|
+
Bridge.log("R1: No write characteristics found, skipping init")
|
|
240
|
+
markConnected()
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for wc in writeChars {
|
|
245
|
+
ringPeripheral?.writeValue(R1BLE.CONFIG_FC, for: wc, type: .withoutResponse)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
249
|
+
guard let self = self else { return }
|
|
250
|
+
for wc in writeChars {
|
|
251
|
+
self.ringPeripheral?.writeValue(R1BLE.CONFIG_11, for: wc, type: .withoutResponse)
|
|
252
|
+
}
|
|
253
|
+
self.markConnected()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Tells the ring to start advertising / connect to the glasses.
|
|
258
|
+
/// Sends BleRing1 advStart (cmd=0, module=0, subCmd=9) with the 6-byte glasses MAC as payload
|
|
259
|
+
/// to WRITE_CHAR_2 (BAE80012-…). Reverse-engineered from the Even Realities mobile app
|
|
260
|
+
/// (BleRing1CmdProto::advStart -> BleRing1CmdPublicExt.sendCmd).
|
|
261
|
+
private func connectToGlasses() {
|
|
262
|
+
let glassesMac = (DeviceStore.shared.get("glasses", "btMacAddress") as? String)
|
|
263
|
+
?? UserDefaults.standard.string(forKey: "glasses_btMacAddress")
|
|
264
|
+
|
|
265
|
+
guard let glassesMac else {
|
|
266
|
+
Bridge.log("R1: connectToGlasses: no glasses MAC")
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
guard let macBytes = parseMac(glassesMac) else {
|
|
271
|
+
Bridge.log("R1: connectToGlasses: could not parse glasses MAC")
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Cache so we can reconnect even before the glasses are scanned.
|
|
276
|
+
UserDefaults.standard.set(glassesMac, forKey: "glasses_btMacAddress")
|
|
277
|
+
|
|
278
|
+
guard let wc = writeChar2 ?? writeChar1 else {
|
|
279
|
+
Bridge.log("R1: connectToGlasses: no write characteristic")
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
var payload = Data([R1BLE.CMD_SYSTEM, R1BLE.MODULE_SYSTEM, R1BLE.SUBCMD_ADV_START])
|
|
284
|
+
payload.append(macBytes)
|
|
285
|
+
Bridge.log("R1: advStart sent")
|
|
286
|
+
ringPeripheral?.writeValue(payload, for: wc, type: .withoutResponse)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Parse a MAC string like "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 raw bytes.
|
|
290
|
+
private func parseMac(_ s: String) -> Data? {
|
|
291
|
+
let cleaned = s.replacingOccurrences(of: ":", with: "")
|
|
292
|
+
.replacingOccurrences(of: "-", with: "")
|
|
293
|
+
guard cleaned.count == 12 else { return nil }
|
|
294
|
+
var out = Data(); out.reserveCapacity(6)
|
|
295
|
+
var idx = cleaned.startIndex
|
|
296
|
+
for _ in 0 ..< 6 {
|
|
297
|
+
let next = cleaned.index(idx, offsetBy: 2)
|
|
298
|
+
guard let byte = UInt8(cleaned[idx ..< next], radix: 16) else { return nil }
|
|
299
|
+
out.append(byte)
|
|
300
|
+
idx = next
|
|
301
|
+
}
|
|
302
|
+
return out
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private func markConnected() {
|
|
306
|
+
Task { await reconnectionManager.stop() }
|
|
307
|
+
Bridge.log("R1: Ring connected")
|
|
308
|
+
|
|
309
|
+
if let name = ringPeripheral?.name, let id = extractRingId(name) {
|
|
310
|
+
DeviceStore.shared.apply("bluetooth", "controller_device_name", id)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
guard let mac = ringMacAddress else {
|
|
314
|
+
Bridge.log("R1: No ring MAC address found")
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
DeviceStore.shared.apply("glasses", "controllerMacAddress", mac)
|
|
318
|
+
|
|
319
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", true)
|
|
320
|
+
// DeviceStore.shared.apply("glasses", "controllerFullyBooted", true)
|
|
321
|
+
|
|
322
|
+
// tell the ring to connect to the glasses if we have it's mac address:
|
|
323
|
+
connectToGlasses()
|
|
324
|
+
|
|
325
|
+
// after a second, connect the glasses to the controller if needed:
|
|
326
|
+
Task {
|
|
327
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
328
|
+
await DeviceManager.shared.sgc?.connectController()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
startHeartbeat()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// MARK: - Heartbeat
|
|
335
|
+
|
|
336
|
+
private func startHeartbeat() {
|
|
337
|
+
heartbeatTimer?.invalidate()
|
|
338
|
+
// Simple keepalive: re-read battery every 30 seconds
|
|
339
|
+
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) {
|
|
340
|
+
[weak self] _ in
|
|
341
|
+
DispatchQueue.main.async {
|
|
342
|
+
guard let self = self, DeviceStore.shared.get("glasses", "controllerConnected") as? Bool ?? false else { return }
|
|
343
|
+
// Read battery if we have the standard battery char
|
|
344
|
+
if let char = self.batteryLevelChar {
|
|
345
|
+
self.ringPeripheral?.readValue(for: char)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func stopHeartbeat() {
|
|
352
|
+
heartbeatTimer?.invalidate()
|
|
353
|
+
heartbeatTimer = nil
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// MARK: - Incoming Data Handling
|
|
357
|
+
|
|
358
|
+
private func handleNotification(from characteristic: CBCharacteristic, data: Data) {
|
|
359
|
+
let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
360
|
+
Bridge.log("R1: \(String(characteristic.uuid.uuidString.suffix(4))) -> \(hex)")
|
|
361
|
+
|
|
362
|
+
let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
|
|
363
|
+
|
|
364
|
+
// Gesture: [0xFF, type, param]
|
|
365
|
+
if data.count >= 3, data[0] == R1BLE.GESTURE_MARKER {
|
|
366
|
+
if let gesture = R1Gesture.parse(from: data) {
|
|
367
|
+
Bridge.log("R1: Gesture: \(gesture.rawValue)")
|
|
368
|
+
Bridge.sendTouchEvent(
|
|
369
|
+
deviceModel: ControllerTypes.R1,
|
|
370
|
+
gestureName: gesture.rawValue,
|
|
371
|
+
timestamp: timestamp
|
|
372
|
+
)
|
|
373
|
+
} else {
|
|
374
|
+
Bridge.log(
|
|
375
|
+
"R1: Unknown gesture type=0x\(String(format: "%02X", data[1])) param=0x\(String(format: "%02X", data[2]))"
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Battery: 2 bytes, first byte is percentage
|
|
382
|
+
if data.count == 2, data[0] <= 100 {
|
|
383
|
+
_batteryLevel = Int(data[0])
|
|
384
|
+
Bridge.log("R1: Battery: \(data[0])%")
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// State: single byte (0x01=ready, 0x00=menu)
|
|
389
|
+
if data.count == 1 {
|
|
390
|
+
let state = data[0] == 0x01 ? "ready" : data[0] == 0x00 ? "menu" : "unknown(\(data[0]))"
|
|
391
|
+
Bridge.log("R1: State: \(state)")
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Longer data: check for embedded gesture marker
|
|
396
|
+
if data.count > 3, let ffIndex = data.firstIndex(of: R1BLE.GESTURE_MARKER),
|
|
397
|
+
ffIndex + 2 < data.count
|
|
398
|
+
{
|
|
399
|
+
let gestureData = Data(data[ffIndex ... ffIndex + 2])
|
|
400
|
+
if let gesture = R1Gesture.parse(from: gestureData) {
|
|
401
|
+
Bridge.log("R1: Embedded gesture: \(gesture.rawValue)")
|
|
402
|
+
Bridge.sendTouchEvent(
|
|
403
|
+
deviceModel: ControllerTypes.R1,
|
|
404
|
+
gestureName: gesture.rawValue,
|
|
405
|
+
timestamp: timestamp
|
|
406
|
+
)
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// MARK: - Connection State Reset
|
|
413
|
+
|
|
414
|
+
private func resetConnectionState() {
|
|
415
|
+
ringPeripheral = nil
|
|
416
|
+
writeChar1 = nil
|
|
417
|
+
notifyChar1 = nil
|
|
418
|
+
writeChar2 = nil
|
|
419
|
+
notifyChar2 = nil
|
|
420
|
+
batteryLevelChar = nil
|
|
421
|
+
notifySubscriptionCount = 0
|
|
422
|
+
initSequenceRun = false
|
|
423
|
+
ringMacAddress = nil
|
|
424
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
425
|
+
DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// MARK: - Reconnection
|
|
429
|
+
|
|
430
|
+
private func startReconnectionTimer() {
|
|
431
|
+
Task {
|
|
432
|
+
await reconnectionManager.start { [weak self] in
|
|
433
|
+
guard let self else { return false }
|
|
434
|
+
if await MainActor.run(body: { DeviceStore.shared.get("glasses", "controllerConnected") as? Bool ?? false }) {
|
|
435
|
+
return true // already connected
|
|
436
|
+
}
|
|
437
|
+
Bridge.log("R1: Attempting reconnection...")
|
|
438
|
+
await MainActor.run { self.startScan() }
|
|
439
|
+
return false // keep trying
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// MARK: - ControllerManager Protocol
|
|
445
|
+
|
|
446
|
+
func findCompatibleDevices() {
|
|
447
|
+
Bridge.log("R1: findCompatibleDevices()")
|
|
448
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
449
|
+
startScan()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
func connectById(_ id: String) {
|
|
453
|
+
Bridge.log("R1: connectById(\(id))")
|
|
454
|
+
DEVICE_SEARCH_ID = id
|
|
455
|
+
startScan()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func disconnect() {
|
|
459
|
+
Bridge.log("R1: disconnect()")
|
|
460
|
+
isDisconnecting = true
|
|
461
|
+
stopHeartbeat()
|
|
462
|
+
Task { await reconnectionManager.stop() }
|
|
463
|
+
|
|
464
|
+
if let peripheral = ringPeripheral {
|
|
465
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
466
|
+
}
|
|
467
|
+
resetConnectionState()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
func forget() {
|
|
471
|
+
disconnect()
|
|
472
|
+
ringUUID = nil
|
|
473
|
+
ringMacAddress = nil
|
|
474
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
475
|
+
centralManager?.delegate = nil
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
func cleanup() {
|
|
479
|
+
disconnect()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
func getConnectedBluetoothName() -> String? {
|
|
483
|
+
return ringPeripheral?.name
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
func ping() {
|
|
487
|
+
if let char = batteryLevelChar {
|
|
488
|
+
ringPeripheral?.readValue(for: char)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
func getBatteryStatus() {
|
|
493
|
+
if let char = batteryLevelChar {
|
|
494
|
+
ringPeripheral?.readValue(for: char)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// MARK: - No-op implementations (ring has no display/camera/wifi/mic)
|
|
499
|
+
|
|
500
|
+
func sendIncidentId(_: String, apiBaseUrl _: String?) {}
|
|
501
|
+
func setMicEnabled(_: Bool) {}
|
|
502
|
+
func sortMicRanking(list: [String]) -> [String] { return list }
|
|
503
|
+
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
504
|
+
func requestPhoto(
|
|
505
|
+
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
506
|
+
compress _: String?, flash _: Bool, sound _: Bool
|
|
507
|
+
) {}
|
|
508
|
+
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
509
|
+
func stopVideoRecording(requestId _: String) {}
|
|
510
|
+
func startStream(_: [String: Any]) {}
|
|
511
|
+
func stopStream() {}
|
|
512
|
+
func sendStreamKeepAlive(_: [String: Any]) {}
|
|
513
|
+
func sendButtonPhotoSettings() {}
|
|
514
|
+
func sendButtonVideoRecordingSettings() {}
|
|
515
|
+
func sendButtonMaxRecordingTime() {}
|
|
516
|
+
func sendButtonCameraLedSetting() {}
|
|
517
|
+
func setBrightness(_: Int, autoMode _: Bool) {}
|
|
518
|
+
func clearDisplay() {}
|
|
519
|
+
func sendTextWall(_: String) {}
|
|
520
|
+
func sendDoubleTextWall(_: String, _: String) {}
|
|
521
|
+
func displayBitmap(base64ImageData _: String) async -> Bool { return false }
|
|
522
|
+
func showDashboard() {}
|
|
523
|
+
func setDashboardPosition(_: Int, _: Int) {}
|
|
524
|
+
func setHeadUpAngle(_: Int) {}
|
|
525
|
+
func setSilentMode(_: Bool) {}
|
|
526
|
+
func exit() {}
|
|
527
|
+
func sendShutdown() {
|
|
528
|
+
disconnect()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
func sendReboot() {}
|
|
532
|
+
func sendRgbLedControl(
|
|
533
|
+
requestId _: String, packageName _: String?, action _: String, color _: String?, ontime _: Int,
|
|
534
|
+
offtime _: Int, count _: Int
|
|
535
|
+
) {}
|
|
536
|
+
func requestWifiScan() {}
|
|
537
|
+
func sendWifiCredentials(_: String, _: String) {}
|
|
538
|
+
func forgetWifiNetwork(_: String) {}
|
|
539
|
+
func sendHotspotState(_: Bool) {}
|
|
540
|
+
func sendOtaStart() {}
|
|
541
|
+
func sendOtaQueryStatus() {}
|
|
542
|
+
func sendUserEmailToGlasses(_: String) {}
|
|
543
|
+
func queryGalleryStatus() {}
|
|
544
|
+
func sendGalleryMode() {}
|
|
545
|
+
func requestVersionInfo() {}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// MARK: - CBCentralManagerDelegate
|
|
549
|
+
|
|
550
|
+
extension R1: CBCentralManagerDelegate {
|
|
551
|
+
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
552
|
+
let state = central.state
|
|
553
|
+
DispatchQueue.main.async { [weak self] in
|
|
554
|
+
guard let self = self else { return }
|
|
555
|
+
Bridge.log("R1: Bluetooth state: \(state.rawValue)")
|
|
556
|
+
if state == .poweredOn {
|
|
557
|
+
_ = self.startScan()
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
nonisolated func centralManager(
|
|
563
|
+
_ central: CBCentralManager,
|
|
564
|
+
didDiscover peripheral: CBPeripheral,
|
|
565
|
+
advertisementData: [String: Any],
|
|
566
|
+
rssi RSSI: NSNumber
|
|
567
|
+
) {
|
|
568
|
+
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
569
|
+
let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
|
570
|
+
|
|
571
|
+
DispatchQueue.main.async { [weak self] in
|
|
572
|
+
guard let self = self else { return }
|
|
573
|
+
guard self.matchesNameFilter(name) else { return }
|
|
574
|
+
|
|
575
|
+
Bridge.log(
|
|
576
|
+
"R1: Discovered: \(name ?? "?") (RSSI: \(RSSI)) mfgData: \(mfgData?.map { String(format: "%02X", $0) }.joined(separator: " ") ?? "none")"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
// Extract ring MAC from manufacturer data if available and store to a map name:mac
|
|
580
|
+
if let mfgData = mfgData {
|
|
581
|
+
Bridge.log(
|
|
582
|
+
"R1: mfgData: \(mfgData.map { String(format: "%02X", $0) }.joined(separator: " "))"
|
|
583
|
+
)
|
|
584
|
+
if mfgData.count >= 6 {
|
|
585
|
+
self.ringMacAddressMap[name ?? ""] = Data(mfgData.suffix(6))
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Emit discovered device
|
|
590
|
+
if let name = name, let id = self.extractRingId(name) {
|
|
591
|
+
Bridge.sendDiscoveredDevice(ControllerTypes.R1, id)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// If scan-only mode, don't auto-connect
|
|
595
|
+
guard self.DEVICE_SEARCH_ID != "NOT_SET" else { return }
|
|
596
|
+
|
|
597
|
+
// If search ID is specific, check it matches the ring name/id
|
|
598
|
+
if let name = name, let id = self.extractRingId(name),
|
|
599
|
+
self.DEVICE_SEARCH_ID != id && !name.contains(self.DEVICE_SEARCH_ID)
|
|
600
|
+
{
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if self.ringPeripheral == nil {
|
|
605
|
+
self.ringPeripheral = peripheral
|
|
606
|
+
peripheral.delegate = self
|
|
607
|
+
central.connect(peripheral, options: nil)
|
|
608
|
+
self.stopScan()
|
|
609
|
+
Bridge.log("R1: Connecting to \(name ?? "ring")")
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
nonisolated func centralManager(
|
|
615
|
+
_: CBCentralManager, didConnect peripheral: CBPeripheral
|
|
616
|
+
) {
|
|
617
|
+
DispatchQueue.main.async { [weak self] in
|
|
618
|
+
guard let self = self else { return }
|
|
619
|
+
Bridge.log("R1: Connected to \(peripheral.name ?? "ring")")
|
|
620
|
+
|
|
621
|
+
self.ringUUID = peripheral.identifier
|
|
622
|
+
|
|
623
|
+
guard let name = peripheral.name, let mac = self.ringMacAddressMap[name] else {
|
|
624
|
+
Bridge.log("R1: No MAC stored in map found for \(peripheral.name ?? "ring")")
|
|
625
|
+
// stop the scan, disconnect, remove the uuid, and try again as we need the mac address to connect:
|
|
626
|
+
self.disconnect()
|
|
627
|
+
self.ringUUID = nil
|
|
628
|
+
// we are still searching!:
|
|
629
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
630
|
+
DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
|
|
631
|
+
DeviceStore.shared.apply("glasses", "controllerSearching", true)
|
|
632
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
633
|
+
self.startScan()
|
|
634
|
+
}
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
self.ringMacAddress = mac.map { String(format: "%02X", $0) }.joined(separator: ":")
|
|
639
|
+
|
|
640
|
+
// Discover all services
|
|
641
|
+
peripheral.discoverServices(nil)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
nonisolated func centralManager(
|
|
646
|
+
_: CBCentralManager, didFailToConnect _: CBPeripheral, error: Error?
|
|
647
|
+
) {
|
|
648
|
+
DispatchQueue.main.async { [weak self] in
|
|
649
|
+
guard let self = self else { return }
|
|
650
|
+
Bridge.log("R1: Failed to connect: \(error?.localizedDescription ?? "unknown")")
|
|
651
|
+
self.resetConnectionState()
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
nonisolated func centralManager(
|
|
656
|
+
_: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error: Error?
|
|
657
|
+
) {
|
|
658
|
+
DispatchQueue.main.async { [weak self] in
|
|
659
|
+
guard let self = self else { return }
|
|
660
|
+
Bridge.log("R1: Disconnected: \(error?.localizedDescription ?? "clean")")
|
|
661
|
+
|
|
662
|
+
self.stopHeartbeat()
|
|
663
|
+
|
|
664
|
+
if self.isDisconnecting { return }
|
|
665
|
+
|
|
666
|
+
self.resetConnectionState()
|
|
667
|
+
// self.startReconnectionTimer()
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// MARK: - CBPeripheralDelegate
|
|
673
|
+
|
|
674
|
+
extension R1: CBPeripheralDelegate {
|
|
675
|
+
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
|
|
676
|
+
guard let services = peripheral.services else { return }
|
|
677
|
+
for service in services {
|
|
678
|
+
// Discover ALL characteristics on every service
|
|
679
|
+
peripheral.discoverCharacteristics(nil, for: service)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
nonisolated func peripheral(
|
|
684
|
+
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?
|
|
685
|
+
) {
|
|
686
|
+
guard let characteristics = service.characteristics else { return }
|
|
687
|
+
|
|
688
|
+
DispatchQueue.main.async { [weak self] in
|
|
689
|
+
guard let self = self else { return }
|
|
690
|
+
|
|
691
|
+
for char in characteristics {
|
|
692
|
+
Bridge.log("R1: char discovered: \(char.uuid)")
|
|
693
|
+
let props = char.properties
|
|
694
|
+
var propStr: [String] = []
|
|
695
|
+
if props.contains(.read) { propStr.append("read") }
|
|
696
|
+
if props.contains(.write) { propStr.append("write") }
|
|
697
|
+
if props.contains(.writeWithoutResponse) { propStr.append("writeNoResp") }
|
|
698
|
+
if props.contains(.notify) { propStr.append("notify") }
|
|
699
|
+
if props.contains(.indicate) { propStr.append("indicate") }
|
|
700
|
+
Bridge.log("R1: char \(char.uuid) props=[\(propStr.joined(separator: ","))]")
|
|
701
|
+
|
|
702
|
+
// Store known characteristics
|
|
703
|
+
switch char.uuid {
|
|
704
|
+
case R1BLE.WRITE_CHAR_1:
|
|
705
|
+
self.writeChar1 = char
|
|
706
|
+
case R1BLE.NOTIFY_CHAR_1:
|
|
707
|
+
self.notifyChar1 = char
|
|
708
|
+
case R1BLE.WRITE_CHAR_2:
|
|
709
|
+
self.writeChar2 = char
|
|
710
|
+
case R1BLE.NOTIFY_CHAR_2:
|
|
711
|
+
self.notifyChar2 = char
|
|
712
|
+
case R1BLE.BATTERY_LEVEL_CHAR:
|
|
713
|
+
self.batteryLevelChar = char
|
|
714
|
+
default:
|
|
715
|
+
break
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Subscribe to any notify/indicate characteristic
|
|
719
|
+
if props.contains(.notify) || props.contains(.indicate) {
|
|
720
|
+
peripheral.setNotifyValue(true, for: char)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Read any readable characteristic (triggers pairing if encrypted)
|
|
724
|
+
if props.contains(.read) {
|
|
725
|
+
peripheral.readValue(for: char)
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
nonisolated func peripheral(
|
|
732
|
+
_: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
733
|
+
error: Error?
|
|
734
|
+
) {
|
|
735
|
+
Bridge.log("R1: didUpdateNotificationStateFor: \(characteristic.uuid)")
|
|
736
|
+
DispatchQueue.main.async { [weak self] in
|
|
737
|
+
guard let self = self else { return }
|
|
738
|
+
|
|
739
|
+
if let error = error {
|
|
740
|
+
Bridge.log(
|
|
741
|
+
"R1: Notify error on \(characteristic.uuid): \(error.localizedDescription)"
|
|
742
|
+
)
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
Bridge.log("R1: Notify enabled on \(characteristic.uuid)")
|
|
746
|
+
|
|
747
|
+
self.notifySubscriptionCount += 1
|
|
748
|
+
|
|
749
|
+
// Run init after subscribing to at least 2 notify characteristics
|
|
750
|
+
if self.notifySubscriptionCount >= 2 && !self.initSequenceRun {
|
|
751
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
|
752
|
+
self?.runInitSequence()
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
nonisolated func peripheral(
|
|
759
|
+
_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
|
|
760
|
+
error: Error?
|
|
761
|
+
) {
|
|
762
|
+
Bridge.log("R1: didUpdateValueFor1: \(characteristic.uuid)")
|
|
763
|
+
guard let data = characteristic.value, !data.isEmpty, error == nil else { return }
|
|
764
|
+
Bridge.log("R1: didUpdateValueFor: \(characteristic.uuid) data: \(data.toHexString())")
|
|
765
|
+
|
|
766
|
+
DispatchQueue.main.async { [weak self] in
|
|
767
|
+
guard let self = self else { return }
|
|
768
|
+
|
|
769
|
+
// Standard battery service
|
|
770
|
+
if characteristic.uuid == R1BLE.BATTERY_LEVEL_CHAR {
|
|
771
|
+
self._batteryLevel = Int(data[0])
|
|
772
|
+
Bridge.log("R1: Battery (std): \(data[0])%")
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
self.handleNotification(from: characteristic, data: data)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
nonisolated func peripheral(
|
|
781
|
+
_: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
|
|
782
|
+
) {
|
|
783
|
+
Bridge.log("R1: didWriteValueFor: \(characteristic.uuid)")
|
|
784
|
+
if let error = error {
|
|
785
|
+
DispatchQueue.main.async {
|
|
786
|
+
Bridge.log(
|
|
787
|
+
"R1: Write error on \(characteristic.uuid): \(error.localizedDescription)"
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// MARK: - R1 Reconnection Manager
|
|
795
|
+
|
|
796
|
+
actor R1ReconnectionManager {
|
|
797
|
+
private var task: Task<Void, Never>?
|
|
798
|
+
private let intervalSeconds: TimeInterval
|
|
799
|
+
private var attempts = 0
|
|
800
|
+
private let maxAttempts: Int
|
|
801
|
+
|
|
802
|
+
init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
|
|
803
|
+
self.intervalSeconds = intervalSeconds
|
|
804
|
+
self.maxAttempts = maxAttempts
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
func start(onAttempt: @escaping @Sendable () async -> Bool) {
|
|
808
|
+
stop()
|
|
809
|
+
attempts = 0
|
|
810
|
+
task = Task {
|
|
811
|
+
while !Task.isCancelled {
|
|
812
|
+
try? await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
|
|
813
|
+
if Task.isCancelled { break }
|
|
814
|
+
attempts += 1
|
|
815
|
+
if maxAttempts > 0, attempts > maxAttempts { break }
|
|
816
|
+
let done = await onAttempt()
|
|
817
|
+
if done { break }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
func stop() {
|
|
823
|
+
task?.cancel()
|
|
824
|
+
task = nil
|
|
825
|
+
attempts = 0
|
|
826
|
+
}
|
|
827
|
+
}
|