@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,2505 @@
|
|
|
1
|
+
//
|
|
2
|
+
// G1.swift
|
|
3
|
+
// MentraOS_Manager
|
|
4
|
+
//
|
|
5
|
+
// Created by Matthew Fosse on 3/3/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Combine
|
|
9
|
+
import CoreBluetooth
|
|
10
|
+
import Foundation
|
|
11
|
+
import UIKit
|
|
12
|
+
|
|
13
|
+
extension Data {
|
|
14
|
+
func chunked(into size: Int) -> [Data] {
|
|
15
|
+
var chunks = [Data]()
|
|
16
|
+
var index = 0
|
|
17
|
+
while index < count {
|
|
18
|
+
let chunkSize = Swift.min(size, count - index)
|
|
19
|
+
let chunk = subdata(in: index ..< (index + chunkSize))
|
|
20
|
+
chunks.append(chunk)
|
|
21
|
+
index += chunkSize
|
|
22
|
+
}
|
|
23
|
+
return chunks
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func hexEncodedString() -> String {
|
|
27
|
+
return map { String(format: "%02x", $0) }.joined(separator: " ")
|
|
28
|
+
// return map { String(format: "%02x", $0) }.joined(separator: ", ")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Extension for CRC32 calculation
|
|
32
|
+
var crc32: UInt32 {
|
|
33
|
+
return withUnsafeBytes { bytes in
|
|
34
|
+
let buffer = bytes.bindMemory(to: UInt8.self)
|
|
35
|
+
var crc: UInt32 = 0xFFFF_FFFF
|
|
36
|
+
|
|
37
|
+
for byte in buffer {
|
|
38
|
+
crc ^= UInt32(byte)
|
|
39
|
+
for _ in 0 ..< 8 {
|
|
40
|
+
if crc & 1 == 1 {
|
|
41
|
+
crc = (crc >> 1) ^ 0xEDB8_8320
|
|
42
|
+
} else {
|
|
43
|
+
crc >>= 1
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ~crc
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Initialize Data from hex string
|
|
53
|
+
init?(hexString: String) {
|
|
54
|
+
let cleanHex = hexString.replacingOccurrences(of: " ", with: "")
|
|
55
|
+
guard cleanHex.count % 2 == 0 else { return nil }
|
|
56
|
+
|
|
57
|
+
var data = Data()
|
|
58
|
+
var index = cleanHex.startIndex
|
|
59
|
+
|
|
60
|
+
while index < cleanHex.endIndex {
|
|
61
|
+
let nextIndex = cleanHex.index(index, offsetBy: 2)
|
|
62
|
+
let byteString = cleanHex[index ..< nextIndex]
|
|
63
|
+
guard let byte = UInt8(byteString, radix: 16) else { return nil }
|
|
64
|
+
data.append(byte)
|
|
65
|
+
index = nextIndex
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self = data
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public struct QuickNote: Equatable {
|
|
73
|
+
let id: UUID
|
|
74
|
+
let text: String
|
|
75
|
+
let timestamp: Date
|
|
76
|
+
|
|
77
|
+
public static func == (lhs: QuickNote, rhs: QuickNote) -> Bool {
|
|
78
|
+
return lhs.id == rhs.id
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
struct BufferedCommand {
|
|
83
|
+
let chunks: [[UInt8]]
|
|
84
|
+
let sendLeft: Bool
|
|
85
|
+
let sendRight: Bool
|
|
86
|
+
let waitTime: Int
|
|
87
|
+
let ignoreAck: Bool
|
|
88
|
+
let chunkTimeMs: Int
|
|
89
|
+
let lastFrameMs: Int
|
|
90
|
+
|
|
91
|
+
init(
|
|
92
|
+
chunks: [[UInt8]], sendLeft: Bool = true, sendRight: Bool = true, waitTime: Int = -1,
|
|
93
|
+
ignoreAck: Bool = false, chunkTimeMs: Int = 10, lastFrameMs: Int = 0
|
|
94
|
+
) {
|
|
95
|
+
self.chunks = chunks
|
|
96
|
+
self.sendLeft = sendLeft
|
|
97
|
+
self.sendRight = sendRight
|
|
98
|
+
self.waitTime = waitTime
|
|
99
|
+
self.ignoreAck = ignoreAck
|
|
100
|
+
self.chunkTimeMs = chunkTimeMs
|
|
101
|
+
self.lastFrameMs = lastFrameMs
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Simple struct to hold app info
|
|
106
|
+
struct AppInfo {
|
|
107
|
+
let id: String
|
|
108
|
+
let name: String
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
enum GlassesError: Error {
|
|
112
|
+
case missingGlasses(String)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Dedicated actor for timer management
|
|
116
|
+
actor HeartbeatManager {
|
|
117
|
+
private var task: Task<Void, Never>?
|
|
118
|
+
private let intervalSeconds: TimeInterval
|
|
119
|
+
|
|
120
|
+
init(intervalSeconds: TimeInterval = 20) {
|
|
121
|
+
self.intervalSeconds = intervalSeconds
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func start(onTick: @escaping @Sendable () async -> Void) {
|
|
125
|
+
stop()
|
|
126
|
+
|
|
127
|
+
task = Task {
|
|
128
|
+
while !Task.isCancelled {
|
|
129
|
+
do {
|
|
130
|
+
try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
|
|
131
|
+
} catch {
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
guard !Task.isCancelled else { break }
|
|
135
|
+
await onTick()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func stop() {
|
|
141
|
+
task?.cancel()
|
|
142
|
+
task = nil
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Dedicated actor for command queue (you already have this partially)
|
|
147
|
+
actor CommandQueue {
|
|
148
|
+
private var commands: [BufferedCommand] = []
|
|
149
|
+
private var continuation: CheckedContinuation<BufferedCommand, Never>?
|
|
150
|
+
|
|
151
|
+
func enqueue(_ command: BufferedCommand) {
|
|
152
|
+
if let continuation {
|
|
153
|
+
self.continuation = nil
|
|
154
|
+
continuation.resume(returning: command)
|
|
155
|
+
} else {
|
|
156
|
+
commands.append(command)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func dequeue() async -> BufferedCommand {
|
|
161
|
+
if let command = commands.first {
|
|
162
|
+
commands.removeFirst()
|
|
163
|
+
return command
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return await withCheckedContinuation { continuation in
|
|
167
|
+
self.continuation = continuation
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Actor for managing pending ACKs
|
|
173
|
+
actor AckManager {
|
|
174
|
+
private var pending: [String: CheckedContinuation<Bool, Never>] = [:]
|
|
175
|
+
|
|
176
|
+
func waitForAck(
|
|
177
|
+
key: String,
|
|
178
|
+
timeoutMs: Int,
|
|
179
|
+
onRegistered: @escaping @Sendable () -> Void,
|
|
180
|
+
onTimeout: @escaping @Sendable () -> Void = {}
|
|
181
|
+
) async -> Bool {
|
|
182
|
+
return await withCheckedContinuation { continuation in
|
|
183
|
+
pending[key] = continuation
|
|
184
|
+
|
|
185
|
+
// Now it's safe to send — registration is complete
|
|
186
|
+
onRegistered()
|
|
187
|
+
|
|
188
|
+
// Start timeout
|
|
189
|
+
Task { [weak self] in
|
|
190
|
+
try? await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
191
|
+
|
|
192
|
+
guard let self else { return }
|
|
193
|
+
if let timedOut = await self.removeIfPending(key: key) {
|
|
194
|
+
onTimeout()
|
|
195
|
+
timedOut.resume(returning: false)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func receiveAck(key: String) -> Bool {
|
|
202
|
+
if let continuation = pending.removeValue(forKey: key) {
|
|
203
|
+
continuation.resume(returning: true)
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private func removeIfPending(key: String) -> CheckedContinuation<Bool, Never>? {
|
|
210
|
+
return pending.removeValue(forKey: key)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Actor for reconnection logic
|
|
215
|
+
actor ReconnectionManager {
|
|
216
|
+
private var task: Task<Void, Never>?
|
|
217
|
+
private let intervalSeconds: TimeInterval
|
|
218
|
+
private var attempts = 0
|
|
219
|
+
private let maxAttempts: Int // -1 for unlimited
|
|
220
|
+
|
|
221
|
+
init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
|
|
222
|
+
self.intervalSeconds = intervalSeconds
|
|
223
|
+
self.maxAttempts = maxAttempts
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var isRunning: Bool {
|
|
227
|
+
task != nil && task?.isCancelled == false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var attemptCount: Int {
|
|
231
|
+
attempts
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func start(onAttempt: @escaping @Sendable () async -> Bool) {
|
|
235
|
+
stop()
|
|
236
|
+
attempts = 0
|
|
237
|
+
|
|
238
|
+
task = Task {
|
|
239
|
+
while !Task.isCancelled {
|
|
240
|
+
if maxAttempts > 0, attempts >= maxAttempts {
|
|
241
|
+
Bridge.log("G1: Max reconnection attempts (\(maxAttempts)) reached")
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
attempts += 1
|
|
246
|
+
Bridge.log("G1: Reconnection attempt \(attempts)")
|
|
247
|
+
|
|
248
|
+
let shouldStop = await onAttempt()
|
|
249
|
+
|
|
250
|
+
if shouldStop {
|
|
251
|
+
Bridge.log("G1: Reconnection successful, stopping")
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
do {
|
|
256
|
+
try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
|
|
257
|
+
} catch {
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
func stop() {
|
|
265
|
+
task?.cancel()
|
|
266
|
+
task = nil
|
|
267
|
+
attempts = 0
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@MainActor
|
|
272
|
+
class G1: NSObject, SGCManager {
|
|
273
|
+
func sendIncidentId(_: String, apiBaseUrl _: String?) {}
|
|
274
|
+
|
|
275
|
+
func sendGalleryMode() {}
|
|
276
|
+
|
|
277
|
+
func sendButtonMaxRecordingTime(_: Int) {}
|
|
278
|
+
|
|
279
|
+
func requestPhoto(
|
|
280
|
+
_: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
|
|
281
|
+
compress _: String?, flash _: Bool, sound _: Bool
|
|
282
|
+
) {}
|
|
283
|
+
|
|
284
|
+
func startStream(_: [String: Any]) {}
|
|
285
|
+
|
|
286
|
+
func stopStream() {}
|
|
287
|
+
|
|
288
|
+
func sendStreamKeepAlive(_: [String: Any]) {}
|
|
289
|
+
|
|
290
|
+
func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
|
|
291
|
+
|
|
292
|
+
func stopVideoRecording(requestId _: String) {}
|
|
293
|
+
|
|
294
|
+
func sendButtonPhotoSettings() {}
|
|
295
|
+
|
|
296
|
+
func sendButtonVideoRecordingSettings() {}
|
|
297
|
+
|
|
298
|
+
func sendButtonMaxRecordingTime() {}
|
|
299
|
+
|
|
300
|
+
func sendButtonCameraLedSetting() {}
|
|
301
|
+
|
|
302
|
+
func sendCameraFovSetting() {}
|
|
303
|
+
|
|
304
|
+
func showDashboard() {}
|
|
305
|
+
|
|
306
|
+
func setSilentMode(_: Bool) {}
|
|
307
|
+
|
|
308
|
+
func requestWifiScan() {}
|
|
309
|
+
|
|
310
|
+
func sendWifiCredentials(_: String, _: String) {}
|
|
311
|
+
|
|
312
|
+
func forgetWifiNetwork(_: String) {}
|
|
313
|
+
|
|
314
|
+
func sendHotspotState(_: Bool) {}
|
|
315
|
+
|
|
316
|
+
func sendUserEmailToGlasses(_: String) {}
|
|
317
|
+
|
|
318
|
+
func queryGalleryStatus() {}
|
|
319
|
+
|
|
320
|
+
func sendOtaStart() {}
|
|
321
|
+
func sendOtaQueryStatus() {}
|
|
322
|
+
|
|
323
|
+
func ping() {}
|
|
324
|
+
func dbg1() {}
|
|
325
|
+
func dbg2() {}
|
|
326
|
+
func connectController() {}
|
|
327
|
+
func disconnectController() {}
|
|
328
|
+
|
|
329
|
+
func requestVersionInfo() {
|
|
330
|
+
Bridge.log("G1: requestVersionInfo - not supported on G1")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var connectionState: String = ConnTypes.DISCONNECTED
|
|
334
|
+
|
|
335
|
+
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
336
|
+
|
|
337
|
+
var type = DeviceTypes.G1
|
|
338
|
+
var hasMic = true
|
|
339
|
+
|
|
340
|
+
// TODO: we probably don't need this
|
|
341
|
+
@objc static func requiresMainQueueSetup() -> Bool {
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Duplicate BMP prevention with timeout
|
|
346
|
+
private var isDisplayingBMP = false
|
|
347
|
+
private var lastBMPStartTime = Date()
|
|
348
|
+
|
|
349
|
+
// Frame synchronization for animations
|
|
350
|
+
private var lastFrameTime = Date()
|
|
351
|
+
private var frameSequence = 0
|
|
352
|
+
|
|
353
|
+
// Animation Batching (iOS-Controlled Timing)
|
|
354
|
+
private var animationFrames: [String] = []
|
|
355
|
+
private var animationTimer: Timer?
|
|
356
|
+
private var currentFrameIndex: Int = 0
|
|
357
|
+
private var animationInterval: TimeInterval = 1.650 // Default 1650ms
|
|
358
|
+
private var animationRepeat: Bool = false
|
|
359
|
+
private var isAnimationRunning: Bool = false
|
|
360
|
+
|
|
361
|
+
// L/R Synchronization - Track BLE write completions
|
|
362
|
+
private var pendingWriteCompletions: [CBCharacteristic: CheckedContinuation<Bool, Never>] = [:]
|
|
363
|
+
private var pendingAckCompletions: [String: CheckedContinuation<Bool, Never>] = [:]
|
|
364
|
+
private let ackCompletionsQueue = DispatchQueue(
|
|
365
|
+
label: "com.erg1.ackCompletions", attributes: .concurrent
|
|
366
|
+
)
|
|
367
|
+
private var writeCompletionCount = 0
|
|
368
|
+
|
|
369
|
+
private var _fullyBooted: Bool = false
|
|
370
|
+
var fullyBooted: Bool {
|
|
371
|
+
get { DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false }
|
|
372
|
+
set {
|
|
373
|
+
let oldValue = DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
|
|
374
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", newValue)
|
|
375
|
+
if !newValue {
|
|
376
|
+
// Reset battery levels when disconnected
|
|
377
|
+
batteryLevel = -1
|
|
378
|
+
leftBatteryLevel = -1
|
|
379
|
+
rightBatteryLevel = -1
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private var connected: Bool {
|
|
385
|
+
get { DeviceStore.shared.get("glasses", "connected") as? Bool ?? false }
|
|
386
|
+
set { DeviceStore.shared.apply("glasses", "connected", newValue) }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var leftReady: Bool = false
|
|
390
|
+
var rightReady: Bool = false
|
|
391
|
+
|
|
392
|
+
@Published var compressedVoiceData: Data = .init()
|
|
393
|
+
@Published var aiListening: Bool = false
|
|
394
|
+
@Published var quickNotes: [QuickNote] = []
|
|
395
|
+
@Published var leftBatteryLevel: Int = -1
|
|
396
|
+
@Published var rightBatteryLevel: Int = -1
|
|
397
|
+
|
|
398
|
+
private var batteryLevel: Int {
|
|
399
|
+
get { DeviceStore.shared.get("glasses", "batteryLevel") as? Int ?? -1 }
|
|
400
|
+
set { DeviceStore.shared.apply("glasses", "batteryLevel", newValue) }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private var caseCharging: Bool {
|
|
404
|
+
get { DeviceStore.shared.get("glasses", "caseCharging") as? Bool ?? false }
|
|
405
|
+
set { DeviceStore.shared.apply("glasses", "caseCharging", newValue) }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private var caseOpen: Bool {
|
|
409
|
+
get { DeviceStore.shared.get("glasses", "caseOpen") as? Bool ?? true }
|
|
410
|
+
set { DeviceStore.shared.apply("glasses", "caseOpen", newValue) }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private var caseRemoved: Bool {
|
|
414
|
+
get { DeviceStore.shared.get("glasses", "caseRemoved") as? Bool ?? true }
|
|
415
|
+
set { DeviceStore.shared.apply("glasses", "caseRemoved", newValue) }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
var isDisconnecting = false
|
|
419
|
+
|
|
420
|
+
private let heartbeatManager = HeartbeatManager()
|
|
421
|
+
private let commandQueue = CommandQueue()
|
|
422
|
+
private let reconnectionManager = ReconnectionManager()
|
|
423
|
+
private let ackManager = AckManager()
|
|
424
|
+
private var globalCounter: UInt8 = 0
|
|
425
|
+
private var heartbeatCounter: UInt8 = 0
|
|
426
|
+
|
|
427
|
+
enum AiMode: String {
|
|
428
|
+
case AI_REQUESTED
|
|
429
|
+
case AI_MIC_ON
|
|
430
|
+
case AI_IDLE
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let UART_SERVICE_UUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
434
|
+
let UART_TX_CHAR_UUID = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
435
|
+
let UART_RX_CHAR_UUID = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
436
|
+
|
|
437
|
+
// Constants
|
|
438
|
+
var DEVICE_SEARCH_ID = "NOT_SET"
|
|
439
|
+
let DELAY_BETWEEN_CHUNKS_SEND: UInt64 = 16_000_000 // 16ms
|
|
440
|
+
let DELAY_BETWEEN_SENDS_MS: UInt64 = 8_000_000 // 8ms
|
|
441
|
+
let INITIAL_CONNECTION_DELAY_MS: UInt64 = 350_000_000 // 350ms
|
|
442
|
+
var textHelper = G1Text()
|
|
443
|
+
var msgId = 100
|
|
444
|
+
|
|
445
|
+
static let _bluetoothQueue = DispatchQueue(label: "BluetoothG1", qos: .userInitiated)
|
|
446
|
+
|
|
447
|
+
private var aiMode: AiMode = .AI_IDLE {
|
|
448
|
+
didSet {
|
|
449
|
+
if aiMode == .AI_MIC_ON {
|
|
450
|
+
aiListening = true
|
|
451
|
+
} else {
|
|
452
|
+
aiListening = false
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private var centralManager: CBCentralManager?
|
|
458
|
+
|
|
459
|
+
private var leftPeripheral: CBPeripheral?
|
|
460
|
+
private var rightPeripheral: CBPeripheral?
|
|
461
|
+
private var connectedDevices: [String: (CBPeripheral?, CBPeripheral?)] = [:]
|
|
462
|
+
var lastConnectionTimestamp: Date = .distantPast
|
|
463
|
+
private var leftInitialized: Bool = false
|
|
464
|
+
private var rightInitialized: Bool = false
|
|
465
|
+
|
|
466
|
+
private var leftGlassUUID: UUID? {
|
|
467
|
+
get {
|
|
468
|
+
if let uuidString = UserDefaults.standard.string(forKey: "leftGlassUUID") {
|
|
469
|
+
return UUID(uuidString: uuidString)
|
|
470
|
+
}
|
|
471
|
+
return nil
|
|
472
|
+
}
|
|
473
|
+
set {
|
|
474
|
+
if let newValue = newValue {
|
|
475
|
+
UserDefaults.standard.set(newValue.uuidString, forKey: "leftGlassUUID")
|
|
476
|
+
} else {
|
|
477
|
+
UserDefaults.standard.removeObject(forKey: "leftGlassUUID")
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private var rightGlassUUID: UUID? {
|
|
483
|
+
get {
|
|
484
|
+
if let uuidString = UserDefaults.standard.string(forKey: "rightGlassUUID") {
|
|
485
|
+
return UUID(uuidString: uuidString)
|
|
486
|
+
}
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
set {
|
|
490
|
+
if let newValue = newValue {
|
|
491
|
+
UserDefaults.standard.set(newValue.uuidString, forKey: "rightGlassUUID")
|
|
492
|
+
} else {
|
|
493
|
+
UserDefaults.standard.removeObject(forKey: "rightGlassUUID")
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
override init() {
|
|
499
|
+
super.init()
|
|
500
|
+
startHeartbeat()
|
|
501
|
+
startCommandProcessor()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
func forget() {
|
|
505
|
+
Task {
|
|
506
|
+
await heartbeatManager.stop()
|
|
507
|
+
await reconnectionManager.stop()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Disconnect BLE peripherals before clearing references
|
|
511
|
+
if let left = leftPeripheral {
|
|
512
|
+
centralManager?.cancelPeripheralConnection(left)
|
|
513
|
+
}
|
|
514
|
+
if let right = rightPeripheral {
|
|
515
|
+
centralManager?.cancelPeripheralConnection(right)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Clear all references
|
|
519
|
+
leftGlassUUID = nil
|
|
520
|
+
rightGlassUUID = nil
|
|
521
|
+
leftPeripheral = nil
|
|
522
|
+
rightPeripheral = nil
|
|
523
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
524
|
+
|
|
525
|
+
// Clean up central manager delegate
|
|
526
|
+
centralManager?.delegate = nil
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
deinit {
|
|
530
|
+
// Note: Can't call async from deinit, but Tasks will be cancelled
|
|
531
|
+
// when the class is deallocated anyway
|
|
532
|
+
centralManager?.delegate = nil
|
|
533
|
+
leftPeripheral?.delegate = nil
|
|
534
|
+
rightPeripheral?.delegate = nil
|
|
535
|
+
Bridge.log("G1: Manager deinitialized")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
func cleanup() {
|
|
539
|
+
// TODO:
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private func startHeartbeat() {
|
|
543
|
+
Task {
|
|
544
|
+
await heartbeatManager.start { [weak self] in
|
|
545
|
+
await self?.sendHeartbeat()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private func startCommandProcessor() {
|
|
551
|
+
Task {
|
|
552
|
+
while !Task.isCancelled {
|
|
553
|
+
let command = await commandQueue.dequeue()
|
|
554
|
+
await processCommand(command)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// MARK: - Serial Number and Color Detection
|
|
560
|
+
|
|
561
|
+
/// Decodes Even G1 serial number to extract style and color information
|
|
562
|
+
/// - Parameter serialNumber: The full serial number (e.g., "S110LABD020021")
|
|
563
|
+
/// - Returns: Tuple containing (style, color) or ("Unknown", "Unknown") if invalid
|
|
564
|
+
static func decodeEvenG1SerialNumber(_ serialNumber: String) -> (style: String, color: String) {
|
|
565
|
+
guard serialNumber.count >= 6 else {
|
|
566
|
+
return ("Unknown", "Unknown")
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Style mapping: 2nd character (index 1)
|
|
570
|
+
let style: String
|
|
571
|
+
let styleChar = serialNumber[serialNumber.index(serialNumber.startIndex, offsetBy: 2)]
|
|
572
|
+
switch styleChar {
|
|
573
|
+
case "0":
|
|
574
|
+
style = "Round"
|
|
575
|
+
case "1":
|
|
576
|
+
style = "Rectangular"
|
|
577
|
+
default:
|
|
578
|
+
style = "Round"
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Color mapping: 5th character (index 4)
|
|
582
|
+
let color: String
|
|
583
|
+
let colorChar = serialNumber[serialNumber.index(serialNumber.startIndex, offsetBy: 5)]
|
|
584
|
+
switch colorChar {
|
|
585
|
+
case "A":
|
|
586
|
+
color = "Grey"
|
|
587
|
+
case "B":
|
|
588
|
+
color = "Brown"
|
|
589
|
+
case "C":
|
|
590
|
+
color = "Green"
|
|
591
|
+
default:
|
|
592
|
+
color = "Grey"
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return (style, color)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/// Decodes serial number from manufacturer data bytes
|
|
599
|
+
/// - Parameter manufacturerData: The manufacturer data bytes
|
|
600
|
+
/// - Returns: Decoded serial number string or nil if not found
|
|
601
|
+
private func decodeSerialFromManufacturerData(_ manufacturerData: Data) -> String? {
|
|
602
|
+
guard manufacturerData.count >= 10 else {
|
|
603
|
+
return nil
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Convert bytes to ASCII string
|
|
607
|
+
var serialBuilder = ""
|
|
608
|
+
for byte in manufacturerData {
|
|
609
|
+
if byte == 0x00 {
|
|
610
|
+
// Stop at null terminator
|
|
611
|
+
break
|
|
612
|
+
}
|
|
613
|
+
if byte >= 0x20, byte <= 0x7E {
|
|
614
|
+
// Only include CoreCommsService.logable ASCII characters
|
|
615
|
+
serialBuilder.append(Character(UnicodeScalar(byte)))
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let decodedString = serialBuilder.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
620
|
+
|
|
621
|
+
// Check if it looks like a valid Even G1 serial number
|
|
622
|
+
if decodedString.count >= 12,
|
|
623
|
+
decodedString.hasPrefix("S1") || decodedString.hasPrefix("100")
|
|
624
|
+
|| decodedString.hasPrefix("110")
|
|
625
|
+
{
|
|
626
|
+
return decodedString
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return nil
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// Emits serial number information to React Native
|
|
633
|
+
private func emitSerialNumberInfo(serialNumber: String, style: String, color: String) {
|
|
634
|
+
let eventBody: [String: Any] = [
|
|
635
|
+
"serialNumber": serialNumber,
|
|
636
|
+
"style": style,
|
|
637
|
+
"color": color,
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
// Convert to JSON string for CoreMessageEvent
|
|
641
|
+
do {
|
|
642
|
+
let jsonData = try JSONSerialization.data(withJSONObject: eventBody, options: [])
|
|
643
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
644
|
+
// Bridge.sendTypedMessage("glasses_serial_number", body: eventBody)
|
|
645
|
+
|
|
646
|
+
Bridge.log(
|
|
647
|
+
"G1: 📱 Emitted serial number info: \(serialNumber), Style: \(style), Color: \(color)"
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
} catch {
|
|
651
|
+
Bridge.log("G1: Error creating serial number JSON: \(error)")
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// @@@ REACT NATIVE FUNCTIONS @@@
|
|
656
|
+
|
|
657
|
+
/// this scans for glasses to connect to and only connnects if SEARCH_ID is set
|
|
658
|
+
func startScan() -> Bool {
|
|
659
|
+
if centralManager == nil {
|
|
660
|
+
centralManager = CBCentralManager(
|
|
661
|
+
delegate: self, queue: G1._bluetoothQueue,
|
|
662
|
+
options: ["CBCentralManagerOptionShowPowerAlertKey": 0]
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
isDisconnecting = false // reset intentional disconnect flag
|
|
667
|
+
guard centralManager!.state == .poweredOn else {
|
|
668
|
+
Bridge.log("G1: Attempting to scan but bluetooth is not powered on.")
|
|
669
|
+
return false
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// send our already connected devices to RN:
|
|
673
|
+
let devices = getConnectedDevices()
|
|
674
|
+
Bridge.log("G1: connnectedDevices.count: (\(devices.count))")
|
|
675
|
+
for device in devices {
|
|
676
|
+
if let name = device.name {
|
|
677
|
+
Bridge.log("G1: Connected to device: \(name)")
|
|
678
|
+
if name.contains("_L_") && name.contains(DEVICE_SEARCH_ID) {
|
|
679
|
+
leftPeripheral = device
|
|
680
|
+
device.delegate = self
|
|
681
|
+
device.discoverServices([UART_SERVICE_UUID])
|
|
682
|
+
} else if name.contains("_R_") && name.contains(DEVICE_SEARCH_ID) {
|
|
683
|
+
rightPeripheral = device
|
|
684
|
+
device.delegate = self
|
|
685
|
+
device.discoverServices([UART_SERVICE_UUID])
|
|
686
|
+
}
|
|
687
|
+
emitDiscoveredDevice(name)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// First try: Connect by UUID (works in background)
|
|
692
|
+
if connectByUUID() {
|
|
693
|
+
Bridge.log("G1: 🔄 Found and attempting to connect to stored glasses UUIDs")
|
|
694
|
+
// Wait for connection to complete - no need to scan
|
|
695
|
+
return true
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let scanOptions: [String: Any] = [
|
|
699
|
+
CBCentralManagerScanOptionAllowDuplicatesKey: false, // Don't allow duplicate advertisements
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
centralManager!.scanForPeripherals(withServices: nil, options: scanOptions)
|
|
703
|
+
return true
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
func connectById(_ id: String) {
|
|
707
|
+
DEVICE_SEARCH_ID = "_" + id + "_"
|
|
708
|
+
startScan()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
func findCompatibleDevices() {
|
|
712
|
+
DEVICE_SEARCH_ID = "NOT_SET"
|
|
713
|
+
startScan()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/// connect to glasses we've discovered:
|
|
717
|
+
@objc func RN_connectGlasses() -> Bool {
|
|
718
|
+
Bridge.log("RN_connectGlasses()")
|
|
719
|
+
|
|
720
|
+
if let side = leftPeripheral {
|
|
721
|
+
Bridge.log("G1: connecting to left glass: \(side.name ?? "(unknown)")")
|
|
722
|
+
centralManager!.connect(side, options: nil)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if let side = rightPeripheral {
|
|
726
|
+
Bridge.log("G1: connecting to right glass: \(side.name ?? "(unknown)")")
|
|
727
|
+
centralManager!.connect(side, options: nil)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// just return if we don't have both a left and right arm:
|
|
731
|
+
guard leftPeripheral != nil && rightPeripheral != nil else {
|
|
732
|
+
return false
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
Bridge.log(
|
|
736
|
+
"G1: found both glasses \(leftPeripheral!.name ?? "(unknown)"), \(rightPeripheral!.name ?? "(unknown)") stopping scan"
|
|
737
|
+
)
|
|
738
|
+
stopScan()
|
|
739
|
+
|
|
740
|
+
// get battery status:
|
|
741
|
+
getBatteryStatus()
|
|
742
|
+
return true
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
@objc func RN_sendText(_ text: String) {
|
|
746
|
+
Task {
|
|
747
|
+
let displayText = "\(text)"
|
|
748
|
+
guard let textData = displayText.data(using: .utf8) else { return }
|
|
749
|
+
|
|
750
|
+
var command: [UInt8] = [
|
|
751
|
+
0x4E, // SEND_RESULT command
|
|
752
|
+
0x00, // sequence number
|
|
753
|
+
0x01, // total packages
|
|
754
|
+
0x00, // current package
|
|
755
|
+
0x71, // screen status (0x70 Text Show | 0x01 New Content)
|
|
756
|
+
0x00, // char position 0
|
|
757
|
+
0x00, // char position 1
|
|
758
|
+
0x01, // page number
|
|
759
|
+
0x01, // max pages
|
|
760
|
+
]
|
|
761
|
+
command.append(contentsOf: Array(textData))
|
|
762
|
+
self.queueChunks([command])
|
|
763
|
+
|
|
764
|
+
// await sendTextWall(text)
|
|
765
|
+
|
|
766
|
+
// // await createQuickNoteIfNeeded(text)
|
|
767
|
+
// // await sendQuickNotesToGlasses()
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// @@@@@@@@ just for testing:
|
|
771
|
+
// Task {
|
|
772
|
+
// msgId += 1
|
|
773
|
+
// let ncsNotification = NCSNotification(
|
|
774
|
+
// msgId: msgId,
|
|
775
|
+
// appIdentifier: "io.heckel.ntfy",
|
|
776
|
+
// title: "Notification Title",
|
|
777
|
+
// subtitle: "Notification Subtitle",
|
|
778
|
+
// message: text,
|
|
779
|
+
// displayName: "Example App"
|
|
780
|
+
// )
|
|
781
|
+
//
|
|
782
|
+
// let notification = G1Notification(ncsNotification: ncsNotification)
|
|
783
|
+
// let encodedChunks = await notification.constructNotification()
|
|
784
|
+
// CoreCommsService.log("encodedChunks: \(encodedChunks.count)")
|
|
785
|
+
// self.queueChunks(encodedChunks)
|
|
786
|
+
// }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
func sendTextWall(_ text: String) {
|
|
790
|
+
let chunks = textHelper.createTextWallChunks(text)
|
|
791
|
+
queueChunks(chunks, sleepAfterMs: 10)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
func createQuickNoteIfNeeded(_ text: String) async {
|
|
795
|
+
if quickNotes.count == 0 {
|
|
796
|
+
await addQuickNote(text)
|
|
797
|
+
} else {
|
|
798
|
+
await updateQuickNote(id: quickNotes[0].id, newText: text)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) {
|
|
803
|
+
let chunks = textHelper.createDoubleTextWallChunks(textTop: top, textBottom: bottom)
|
|
804
|
+
queueChunks(chunks, sleepAfterMs: 10)
|
|
805
|
+
|
|
806
|
+
// quick note testing:
|
|
807
|
+
// Task {
|
|
808
|
+
// await createQuickNoteIfNeeded(top + "\n" + bottom)
|
|
809
|
+
// await sendQuickNotesToGlasses()
|
|
810
|
+
// }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private func sendQuickNotesToGlasses() async {
|
|
814
|
+
// guard let rightGlass = rightPeripheral,
|
|
815
|
+
// let leftGlass = leftPeripheral,
|
|
816
|
+
// let rightTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: rightGlass),
|
|
817
|
+
// let leftTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: leftGlass) else {
|
|
818
|
+
// return
|
|
819
|
+
// }
|
|
820
|
+
|
|
821
|
+
// if !self.isHeadUp {
|
|
822
|
+
// return
|
|
823
|
+
// }
|
|
824
|
+
|
|
825
|
+
// First, clear all existing notes
|
|
826
|
+
// for noteNumber in 1...2 {
|
|
827
|
+
let noteNumber = 1
|
|
828
|
+
var command = Data()
|
|
829
|
+
command.append(Commands.QUICK_NOTE_ADD.rawValue)
|
|
830
|
+
command.append(0x10) // Fixed length for delete command
|
|
831
|
+
command.append(0x00) // Fixed byte
|
|
832
|
+
command.append(0xE0) // Version byte for delete
|
|
833
|
+
command.append(contentsOf: [0x03, 0x01, 0x00, 0x01, 0x00]) // Fixed bytes
|
|
834
|
+
command.append(UInt8(noteNumber)) // Note number to delete
|
|
835
|
+
command.append(contentsOf: [0x00, 0x01, 0x00, 0x01, 0x00, 0x00]) // Fixed bytes for delete
|
|
836
|
+
|
|
837
|
+
// // Send delete command to both glasses with proper timing
|
|
838
|
+
// rightGlass.writeValue(command, for: rightTxChar, type: .withResponse)
|
|
839
|
+
// try? await Task.sleep(nanoseconds: 50 * 1_000_000)
|
|
840
|
+
// leftGlass.writeValue(command, for: leftTxChar, type: .withResponse)
|
|
841
|
+
// try? await Task.sleep(nanoseconds: 150 * 1_000_000)
|
|
842
|
+
|
|
843
|
+
// convert command to array of UInt8
|
|
844
|
+
let commandArray = command.map { $0 }
|
|
845
|
+
queueChunks([commandArray])
|
|
846
|
+
// }
|
|
847
|
+
|
|
848
|
+
// Then add all current notes
|
|
849
|
+
for (index, note) in quickNotes.prefix(4).enumerated() {
|
|
850
|
+
let slotNumber = index + 1
|
|
851
|
+
|
|
852
|
+
guard let textData = note.text.data(using: .utf8),
|
|
853
|
+
let nameData = "Quick Note2".data(using: .utf8)
|
|
854
|
+
else {
|
|
855
|
+
continue
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Calculate payload length
|
|
859
|
+
let fixedBytes: [UInt8] = [0x03, 0x01, 0x00, 0x01, 0x00]
|
|
860
|
+
let versionByte = UInt8(
|
|
861
|
+
Date().timeIntervalSince1970.truncatingRemainder(dividingBy: 256)
|
|
862
|
+
)
|
|
863
|
+
let payloadLength =
|
|
864
|
+
1 // Fixed byte
|
|
865
|
+
+ 1 // Version byte
|
|
866
|
+
+ fixedBytes.count // Fixed bytes sequence
|
|
867
|
+
+ 1 // Note number
|
|
868
|
+
+ 1 // Fixed byte 2
|
|
869
|
+
+ 1 // Name length
|
|
870
|
+
+ nameData.count // Name bytes
|
|
871
|
+
+ 1 // Text length
|
|
872
|
+
+ 1 // Fixed byte after text length
|
|
873
|
+
+ textData.count // Text bytes
|
|
874
|
+
+ 2 // Final bytes
|
|
875
|
+
|
|
876
|
+
// Build command
|
|
877
|
+
var command = Data()
|
|
878
|
+
command.append(Commands.QUICK_NOTE_ADD.rawValue)
|
|
879
|
+
command.append(UInt8(payloadLength & 0xFF))
|
|
880
|
+
command.append(0x00) // Fixed byte
|
|
881
|
+
command.append(versionByte)
|
|
882
|
+
command.append(contentsOf: fixedBytes)
|
|
883
|
+
command.append(UInt8(slotNumber))
|
|
884
|
+
command.append(0x01) // Fixed byte 2
|
|
885
|
+
command.append(UInt8(nameData.count))
|
|
886
|
+
command.append(nameData)
|
|
887
|
+
command.append(UInt8(textData.count))
|
|
888
|
+
command.append(0x00) // Fixed byte
|
|
889
|
+
command.append(textData)
|
|
890
|
+
|
|
891
|
+
// convert command to array of UInt8
|
|
892
|
+
let commandArray = command.map { $0 }
|
|
893
|
+
queueChunks([commandArray])
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
func addQuickNote(_ text: String) async {
|
|
898
|
+
let note = QuickNote(id: UUID(), text: text, timestamp: Date())
|
|
899
|
+
quickNotes.append(note)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
func updateQuickNote(id: UUID, newText: String) async {
|
|
903
|
+
if let index = quickNotes.firstIndex(where: { $0.id == id }) {
|
|
904
|
+
quickNotes[index] = QuickNote(id: id, text: newText, timestamp: Date())
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
func removeQuickNote(id: UUID) async {
|
|
909
|
+
quickNotes.removeAll { $0.id == id }
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
func clearQuickNotes() async {
|
|
913
|
+
quickNotes.removeAll()
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/// only set to true when we receive init_ack response from the glasses
|
|
917
|
+
func setReadiness(left: Bool?, right: Bool?) {
|
|
918
|
+
let prevLeftReady = leftReady
|
|
919
|
+
let prevRightReady = rightReady
|
|
920
|
+
|
|
921
|
+
if left != nil {
|
|
922
|
+
leftReady = left!
|
|
923
|
+
if !prevLeftReady, leftReady {
|
|
924
|
+
Bridge.log("Left ready!")
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if right != nil {
|
|
928
|
+
rightReady = right!
|
|
929
|
+
if !prevRightReady, rightReady {
|
|
930
|
+
Bridge.log("Right ready!")
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// CoreCommsService.log("g1Ready set to \(leftReady) \(rightReady) \(leftReady && rightReady) left: \(left), right: \(right)")
|
|
935
|
+
fullyBooted = leftReady && rightReady
|
|
936
|
+
connected = leftReady && rightReady
|
|
937
|
+
if fullyBooted {
|
|
938
|
+
stopReconnectionTimer()
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
func stopScan() {
|
|
943
|
+
centralManager!.stopScan()
|
|
944
|
+
Bridge.log("G1: Stopped scanning for devices")
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
func getSerialNumberInfo() -> [String: Any] {
|
|
948
|
+
return [
|
|
949
|
+
"serialNumber": serialNumber ?? "",
|
|
950
|
+
"style": style ?? "",
|
|
951
|
+
"color": color ?? "",
|
|
952
|
+
]
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
func getConnectedBluetoothName() -> String? {
|
|
956
|
+
// Return the name of the first connected peripheral (either left or right)
|
|
957
|
+
if let leftName = leftPeripheral?.name {
|
|
958
|
+
return leftName
|
|
959
|
+
}
|
|
960
|
+
return rightPeripheral?.name
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
func disconnect() {
|
|
964
|
+
isDisconnecting = true
|
|
965
|
+
leftGlassUUID = nil
|
|
966
|
+
rightGlassUUID = nil
|
|
967
|
+
stopReconnectionTimer()
|
|
968
|
+
|
|
969
|
+
if let left = leftPeripheral {
|
|
970
|
+
centralManager!.cancelPeripheralConnection(left)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if let right = rightPeripheral {
|
|
974
|
+
centralManager!.cancelPeripheralConnection(right)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
leftPeripheral = nil
|
|
978
|
+
rightPeripheral = nil
|
|
979
|
+
setReadiness(left: false, right: false)
|
|
980
|
+
Bridge.log("G1: Disconnected from glasses")
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private func attemptSend(cmd: BufferedCommand, side: String) async {
|
|
984
|
+
var maxAttempts = 5
|
|
985
|
+
var attempts = 0
|
|
986
|
+
var success = false
|
|
987
|
+
let chunks = cmd.chunks
|
|
988
|
+
|
|
989
|
+
while attempts < maxAttempts, !success {
|
|
990
|
+
if attempts > 0 {
|
|
991
|
+
Bridge.log("G1: trying again to send to:\(side): \(attempts)")
|
|
992
|
+
}
|
|
993
|
+
let data = Data(chunks[0])
|
|
994
|
+
// CoreCommsService.log("SEND (\(side)) \(data.hexEncodedString())")
|
|
995
|
+
|
|
996
|
+
if isDisconnecting {
|
|
997
|
+
// forget whatever we were doing since we're disconnecting:
|
|
998
|
+
break
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
for i in 0 ..< chunks.count - 1 {
|
|
1002
|
+
let chunk = chunks[i]
|
|
1003
|
+
|
|
1004
|
+
let firstFewBytes = String(Data(chunk).hexEncodedString().prefix(16))
|
|
1005
|
+
// CoreCommsService.log("SEND (\(side)) \(firstFewBytes)")
|
|
1006
|
+
await sendCommandToSideWithoutResponse(chunk, side: side)
|
|
1007
|
+
try? await Task.sleep(nanoseconds: UInt64(cmd.chunkTimeMs) * 1_000_000) // 8ms
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
let lastChunk = chunks.last!
|
|
1011
|
+
|
|
1012
|
+
var sequenceNumber = -1
|
|
1013
|
+
|
|
1014
|
+
// if this is a text chunk, set the sequence to the 2nd byte of the chunk:
|
|
1015
|
+
if lastChunk[0] == Commands.BLE_REQ_EVENAI.rawValue {
|
|
1016
|
+
sequenceNumber = Int(lastChunk[1])
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if lastChunk[0] == Commands.CRC_CHECK.rawValue {
|
|
1020
|
+
sequenceNumber = Int(lastChunk[1])
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if cmd.lastFrameMs > 0 {
|
|
1024
|
+
try? await Task.sleep(nanoseconds: UInt64(cmd.lastFrameMs) * 1_000_000) // 100ms
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let firstFewBytes = String(Data(lastChunk).hexEncodedString().prefix(16)).uppercased()
|
|
1028
|
+
// don't log if it starts with 25 (heartbeat):
|
|
1029
|
+
if lastChunk[0] != Commands.BLE_REQ_HEARTBEAT.rawValue {
|
|
1030
|
+
// Bridge.log("SEND (\(side)) \(firstFewBytes)")
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// if (lastChunk[0] == 0x4E) {
|
|
1034
|
+
// sequenceNumber = Int(lastChunk[1])
|
|
1035
|
+
// }
|
|
1036
|
+
|
|
1037
|
+
// CoreCommsService.log("G1: SENDING with sequenceNumber: \(sequenceNumber)")
|
|
1038
|
+
|
|
1039
|
+
// for heartbeats, don't retry and assume success since the glasses don't respond:
|
|
1040
|
+
if lastChunk[0] == Commands.BLE_REQ_HEARTBEAT.rawValue {
|
|
1041
|
+
success = true
|
|
1042
|
+
await sendCommandToSideWithoutResponse(lastChunk, side: side)
|
|
1043
|
+
} else {
|
|
1044
|
+
success = await sendCommandToSide2(
|
|
1045
|
+
lastChunk, side: side, attemptNumber: attempts, sequenceNumber: sequenceNumber
|
|
1046
|
+
)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// CoreCommsService.log("command success: \(success)")
|
|
1050
|
+
// if (!success) {
|
|
1051
|
+
// CoreCommsService.log("G1: timed out waiting for \(s)")
|
|
1052
|
+
// }
|
|
1053
|
+
// await sendCommandToSideWithoutResponse(lastChunk, side: side)
|
|
1054
|
+
// success = true
|
|
1055
|
+
|
|
1056
|
+
attempts += 1
|
|
1057
|
+
if !success, attempts >= maxAttempts {
|
|
1058
|
+
Bridge.log("G1: ❌ Command timed out!")
|
|
1059
|
+
startReconnectionTimer()
|
|
1060
|
+
break
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if success {
|
|
1064
|
+
stopReconnectionTimer()
|
|
1065
|
+
// setReadiness(left: true, right: true)
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/// Process a single number with timeouts
|
|
1071
|
+
private func processCommand(_ command: BufferedCommand) async {
|
|
1072
|
+
if command.chunks.isEmpty {
|
|
1073
|
+
Bridge.log("G1: @@@ chunks was empty! @@@")
|
|
1074
|
+
return
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Send to both sides in parallel
|
|
1078
|
+
await withTaskGroup(of: Void.self) { group in
|
|
1079
|
+
if command.sendLeft {
|
|
1080
|
+
group.addTask {
|
|
1081
|
+
await self.attemptSend(cmd: command, side: "L")
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if command.sendRight {
|
|
1086
|
+
group.addTask {
|
|
1087
|
+
await self.attemptSend(cmd: command, side: "R")
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Wait for all tasks to complete
|
|
1092
|
+
await group.waitForAll()
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if command.waitTime > 0 {
|
|
1096
|
+
// wait waitTime milliseconds before moving on to the next command:
|
|
1097
|
+
try? await Task.sleep(nanoseconds: UInt64(command.waitTime) * 1_000_000)
|
|
1098
|
+
} else {
|
|
1099
|
+
// sleep for a min amount of time unless otherwise specified
|
|
1100
|
+
try? await Task.sleep(nanoseconds: 8 * 1_000_000) // Xms
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private func waitForSemaphore(semaphore: DispatchSemaphore, timeout: TimeInterval) -> Bool {
|
|
1105
|
+
let result = semaphore.wait(timeout: .now() + timeout)
|
|
1106
|
+
return result == .success
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private func findCharacteristic(uuid: CBUUID, peripheral: CBPeripheral) -> CBCharacteristic? {
|
|
1110
|
+
for service in peripheral.services ?? [] {
|
|
1111
|
+
for characteristic in service.characteristics ?? [] {
|
|
1112
|
+
if characteristic.uuid == uuid {
|
|
1113
|
+
return characteristic
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return nil
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private func getConnectedDevices() -> [CBPeripheral] {
|
|
1121
|
+
return centralManager!.retrieveConnectedPeripherals(withServices: [
|
|
1122
|
+
UART_SERVICE_UUID,
|
|
1123
|
+
])
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
private func handleAck(from peripheral: CBPeripheral, success: Bool, sequenceNumber: Int = -1) {
|
|
1127
|
+
guard success else { return }
|
|
1128
|
+
|
|
1129
|
+
let side = peripheral == leftPeripheral ? "L" : "R"
|
|
1130
|
+
let key = sequenceNumber == -1 ? side : "\(side)-\(sequenceNumber)"
|
|
1131
|
+
|
|
1132
|
+
Task {
|
|
1133
|
+
let wasWaiting = await ackManager.receiveAck(key: key)
|
|
1134
|
+
if wasWaiting {
|
|
1135
|
+
// Bridge.log("G1: ✅ ACK received for \(key)")
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private func handleNotification(from peripheral: CBPeripheral, data: Data) {
|
|
1141
|
+
guard let command = data.first else { return } // ensure the data isn't empty
|
|
1142
|
+
|
|
1143
|
+
let side = peripheral == leftPeripheral ? "L" : "R"
|
|
1144
|
+
let s = peripheral == leftPeripheral ? "L" : "R"
|
|
1145
|
+
// Bridge.log("G1: RECV (\(s)) \(data.hexEncodedString())")
|
|
1146
|
+
|
|
1147
|
+
switch Commands(rawValue: command) {
|
|
1148
|
+
case .BLE_REQ_INIT:
|
|
1149
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1150
|
+
handleInitResponse(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1151
|
+
case .QUICK_NOTE_ADD:
|
|
1152
|
+
handleAck(from: peripheral, success: data[1] == 0x10 || data[1] == 0x43)
|
|
1153
|
+
case .BLE_REQ_MIC_ON:
|
|
1154
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1155
|
+
case .BRIGHTNESS:
|
|
1156
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1157
|
+
case .BLE_EXIT_ALL_FUNCTIONS:
|
|
1158
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1159
|
+
case .WHITELIST:
|
|
1160
|
+
// TODO: ios no idea why the glasses send 0xCB before sending ACK: (CB == continue!)
|
|
1161
|
+
handleAck(
|
|
1162
|
+
from: peripheral,
|
|
1163
|
+
success: data[1] == 0xCB || data[1] == CommandResponse.ACK.rawValue
|
|
1164
|
+
)
|
|
1165
|
+
case .DASHBOARD_LAYOUT_COMMAND:
|
|
1166
|
+
// 0x06 seems arbitrary :/
|
|
1167
|
+
handleAck(from: peripheral, success: data[1] == 0x06)
|
|
1168
|
+
case .DASHBOARD_SHOW:
|
|
1169
|
+
handleAck(
|
|
1170
|
+
from: peripheral, success: data[1] == 0x07 || data[1] == 0x90 || data[1] == 0x0C
|
|
1171
|
+
)
|
|
1172
|
+
case .HEAD_UP_ANGLE:
|
|
1173
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1174
|
+
case .CRC_CHECK:
|
|
1175
|
+
handleAck(from: peripheral, success: true, sequenceNumber: Int(data[1]))
|
|
1176
|
+
case .BMP_END:
|
|
1177
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1178
|
+
case .SILENT_MODE:
|
|
1179
|
+
handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
|
|
1180
|
+
case .BLE_REQ_TRANSFER_MIC_DATA:
|
|
1181
|
+
// compressedVoiceData = data
|
|
1182
|
+
// skip the first 2 bytes:
|
|
1183
|
+
let lc3Data = data.subdata(in: 2 ..< data.count)
|
|
1184
|
+
DeviceManager.shared.handleGlassesMicData(lc3Data)
|
|
1185
|
+
// CoreCommsService.log("G1: Got voice data: " + String(data.count))
|
|
1186
|
+
case .UNK_1:
|
|
1187
|
+
handleAck(from: peripheral, success: true)
|
|
1188
|
+
case .UNK_2:
|
|
1189
|
+
handleAck(from: peripheral, success: true)
|
|
1190
|
+
case .BLE_REQ_HEARTBEAT:
|
|
1191
|
+
Bridge.log("heartbeatCounter: \(heartbeatCounter) data[1]: \(data[1])")
|
|
1192
|
+
handleAck(from: peripheral, success: data[1] == heartbeatCounter - 1)
|
|
1193
|
+
case .BLE_REQ_BATTERY:
|
|
1194
|
+
// TODO: ios handle semaphores correctly here
|
|
1195
|
+
// battery info
|
|
1196
|
+
guard data.count >= 6 && data[1] == 0x66 else {
|
|
1197
|
+
break
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
handleAck(from: peripheral, success: data[1] == 0x66)
|
|
1201
|
+
|
|
1202
|
+
// Response format: 2C 66 [battery%] [flags] [voltage_low] [voltage_high] ...
|
|
1203
|
+
let batteryPercent = Int(data[2])
|
|
1204
|
+
let flags = data[3]
|
|
1205
|
+
let voltageLow = Int(data[4])
|
|
1206
|
+
let voltageHigh = Int(data[5])
|
|
1207
|
+
let rawVoltage = (voltageHigh << 8) | voltageLow
|
|
1208
|
+
let voltage = rawVoltage / 10 // Scale down by 10 to get actual millivolts
|
|
1209
|
+
|
|
1210
|
+
// CoreCommsService.log("G1: Raw battery data - Battery: \(batteryPercent)%, Voltage: \(voltage)mV, Flags: 0x\(String(format: "%02X", flags))")
|
|
1211
|
+
|
|
1212
|
+
// if left, update left battery level, if right, update right battery level
|
|
1213
|
+
if peripheral == leftPeripheral {
|
|
1214
|
+
if leftBatteryLevel != batteryPercent {
|
|
1215
|
+
Bridge.log("G1: Left glass battery: \(batteryPercent)%")
|
|
1216
|
+
leftBatteryLevel = batteryPercent
|
|
1217
|
+
}
|
|
1218
|
+
} else if peripheral == rightPeripheral {
|
|
1219
|
+
if rightBatteryLevel != batteryPercent {
|
|
1220
|
+
Bridge.log("G1: Right glass battery: \(batteryPercent)%")
|
|
1221
|
+
rightBatteryLevel = batteryPercent
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// update the main battery level as the lower of the two
|
|
1226
|
+
let newBatteryLevel = min(leftBatteryLevel, rightBatteryLevel)
|
|
1227
|
+
if batteryLevel != newBatteryLevel {
|
|
1228
|
+
batteryLevel = min(leftBatteryLevel, rightBatteryLevel)
|
|
1229
|
+
}
|
|
1230
|
+
case .BLE_REQ_EVENAI:
|
|
1231
|
+
guard data.count > 1 else { break }
|
|
1232
|
+
handleAck(
|
|
1233
|
+
from: peripheral, success: data[1] == CommandResponse.ACK.rawValue,
|
|
1234
|
+
sequenceNumber: Int(data[2])
|
|
1235
|
+
)
|
|
1236
|
+
case .BLE_REQ_DEVICE_ORDER:
|
|
1237
|
+
let order = data[1]
|
|
1238
|
+
switch DeviceOrders(rawValue: order) {
|
|
1239
|
+
case .HEAD_UP:
|
|
1240
|
+
Bridge.log("G1: HEAD_UP")
|
|
1241
|
+
DeviceStore.shared.apply("glasses", "headUp", true)
|
|
1242
|
+
case .HEAD_UP2:
|
|
1243
|
+
Bridge.log("G1: HEAD_UP2")
|
|
1244
|
+
DeviceStore.shared.apply("glasses", "headUp", true)
|
|
1245
|
+
// case .HEAD_DOWN:
|
|
1246
|
+
// CoreCommsService.log("HEAD_DOWN")
|
|
1247
|
+
// break
|
|
1248
|
+
case .HEAD_DOWN2:
|
|
1249
|
+
Bridge.log("G1: HEAD_DOWN2")
|
|
1250
|
+
DeviceStore.shared.apply("glasses", "headUp", false)
|
|
1251
|
+
case .ACTIVATED:
|
|
1252
|
+
Bridge.log("G1: ACTIVATED")
|
|
1253
|
+
case .SILENCED:
|
|
1254
|
+
Bridge.log("G1: SILENCED")
|
|
1255
|
+
case .DISPLAY_READY:
|
|
1256
|
+
Bridge.log("G1: DISPLAY_READY")
|
|
1257
|
+
// sendInitCommand(to: peripheral)// experimental
|
|
1258
|
+
case .TRIGGER_FOR_AI:
|
|
1259
|
+
Bridge.log("G1: TRIGGER AI")
|
|
1260
|
+
case .TRIGGER_FOR_STOP_RECORDING:
|
|
1261
|
+
Bridge.log("G1: STOP RECORDING")
|
|
1262
|
+
case .TRIGGER_CHANGE_PAGE:
|
|
1263
|
+
Bridge.log("G1: TRIGGER_CHANGE_PAGE")
|
|
1264
|
+
case .CASE_REMOVED:
|
|
1265
|
+
Bridge.log("G1: REMOVED FROM CASE")
|
|
1266
|
+
caseRemoved = true
|
|
1267
|
+
case .CASE_REMOVED2:
|
|
1268
|
+
Bridge.log("G1: REMOVED FROM CASE2")
|
|
1269
|
+
caseRemoved = true
|
|
1270
|
+
case .CASE_OPEN:
|
|
1271
|
+
caseOpen = true
|
|
1272
|
+
caseRemoved = false
|
|
1273
|
+
Bridge.log("G1: CASE OPEN")
|
|
1274
|
+
case .CASE_CLOSED:
|
|
1275
|
+
caseOpen = false
|
|
1276
|
+
caseRemoved = false
|
|
1277
|
+
Bridge.log("G1: CASE CLOSED")
|
|
1278
|
+
case .CASE_CHARGING_STATUS:
|
|
1279
|
+
guard data.count >= 3 else { break }
|
|
1280
|
+
let status = data[2]
|
|
1281
|
+
if status == 0x01 {
|
|
1282
|
+
caseCharging = true
|
|
1283
|
+
Bridge.log("G1: CASE CHARGING")
|
|
1284
|
+
} else {
|
|
1285
|
+
caseCharging = false
|
|
1286
|
+
Bridge.log("G1: CASE NOT CHARGING")
|
|
1287
|
+
}
|
|
1288
|
+
case .CASE_CHARGE_INFO:
|
|
1289
|
+
Bridge.log("G1: CASE CHARGE INFO")
|
|
1290
|
+
guard data.count >= 3 else { break }
|
|
1291
|
+
if Int(data[2]) != -1 {
|
|
1292
|
+
let newCaseBatteryLevel = Int(data[2])
|
|
1293
|
+
DeviceStore.shared.apply("glasses", "caseBatteryLevel", newCaseBatteryLevel)
|
|
1294
|
+
Bridge.log("G1: Case battery level: \(newCaseBatteryLevel)%")
|
|
1295
|
+
} else {
|
|
1296
|
+
Bridge.log("G1: Case battery level was -1")
|
|
1297
|
+
}
|
|
1298
|
+
case .DOUBLE_TAP:
|
|
1299
|
+
Bridge.log("G1: DOUBLE TAP / display turned off")
|
|
1300
|
+
// Task {
|
|
1301
|
+
//// RN_sendText("DOUBLE TAP DETECTED")
|
|
1302
|
+
//// queueChunks([[UInt8(0x00), UInt8(0x01)]])
|
|
1303
|
+
// try? await Task.sleep(nanoseconds: 1500 * 1_000_000) // 2s delay after sending
|
|
1304
|
+
// sendInit()
|
|
1305
|
+
// clearState()
|
|
1306
|
+
// }
|
|
1307
|
+
default:
|
|
1308
|
+
// Core.log("G1: Received device order: \(data.subdata(in: 1..<data.count).hexEncodedString())")
|
|
1309
|
+
break
|
|
1310
|
+
}
|
|
1311
|
+
default:
|
|
1312
|
+
Bridge.log("G1: received from G1(not handled): \(data.hexEncodedString())")
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// MARK: Commands
|
|
1318
|
+
|
|
1319
|
+
extension G1 {
|
|
1320
|
+
/// Handle whitelist functionality
|
|
1321
|
+
func getWhitelistChunks() -> [[UInt8]] {
|
|
1322
|
+
// Define the hardcoded whitelist JSON
|
|
1323
|
+
let apps = [
|
|
1324
|
+
["id": "com.mentra.os", "name": "MentraOS"],
|
|
1325
|
+
["id": "io.heckel.ntfy", "name": "ntfy"],
|
|
1326
|
+
]
|
|
1327
|
+
let whitelistJson = createWhitelistJson(apps: apps)
|
|
1328
|
+
|
|
1329
|
+
Bridge.log("G1: Creating chunks for hardcoded whitelist: \(whitelistJson)")
|
|
1330
|
+
|
|
1331
|
+
// Convert JSON to bytes and split into chunks
|
|
1332
|
+
return createWhitelistChunks(json: whitelistJson)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
private func createWhitelistJson(apps: [[String: String]]) -> String {
|
|
1336
|
+
do {
|
|
1337
|
+
// Create app list array
|
|
1338
|
+
var appList: [[String: Any]] = []
|
|
1339
|
+
for app in apps {
|
|
1340
|
+
let appDict: [String: Any] = [
|
|
1341
|
+
"id": app["id"] ?? "",
|
|
1342
|
+
"name": app["name"] ?? "",
|
|
1343
|
+
]
|
|
1344
|
+
appList.append(appDict)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Create the whitelist dictionary
|
|
1348
|
+
let whitelistDict: [String: Any] = [
|
|
1349
|
+
"calendar_enable": true,
|
|
1350
|
+
"call_enable": true,
|
|
1351
|
+
"msg_enable": true,
|
|
1352
|
+
"ios_mail_enable": true,
|
|
1353
|
+
"app": [
|
|
1354
|
+
"list": appList,
|
|
1355
|
+
"enable": true,
|
|
1356
|
+
],
|
|
1357
|
+
]
|
|
1358
|
+
|
|
1359
|
+
// Convert to JSON string
|
|
1360
|
+
let jsonData = try JSONSerialization.data(withJSONObject: whitelistDict, options: [])
|
|
1361
|
+
if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
1362
|
+
return jsonString
|
|
1363
|
+
} else {
|
|
1364
|
+
return "{}"
|
|
1365
|
+
}
|
|
1366
|
+
} catch {
|
|
1367
|
+
Bridge.log("G1: Error creating whitelist JSON: \(error.localizedDescription)")
|
|
1368
|
+
return "{}"
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/// Helper function to split JSON into chunks
|
|
1373
|
+
private func createWhitelistChunks(json: String) -> [[UInt8]] {
|
|
1374
|
+
let MAX_CHUNK_SIZE = 180 - 4 // Reserve space for the header
|
|
1375
|
+
guard let jsonData = json.data(using: .utf8) else { return [] }
|
|
1376
|
+
|
|
1377
|
+
let totalChunks = Int(ceil(Double(jsonData.count) / Double(MAX_CHUNK_SIZE)))
|
|
1378
|
+
var chunks: [Data] = []
|
|
1379
|
+
|
|
1380
|
+
Bridge.log("G1: jsonData.count = \(jsonData.count), totalChunks = \(totalChunks)")
|
|
1381
|
+
|
|
1382
|
+
for i in 0 ..< totalChunks {
|
|
1383
|
+
let start = i * MAX_CHUNK_SIZE
|
|
1384
|
+
let end = min(start + MAX_CHUNK_SIZE, jsonData.count)
|
|
1385
|
+
let range = start ..< end
|
|
1386
|
+
let payloadChunk = jsonData.subdata(in: range)
|
|
1387
|
+
|
|
1388
|
+
// Create the header: [WHITELIST_CMD, total_chunks, chunk_index]
|
|
1389
|
+
var headerData = Data()
|
|
1390
|
+
headerData.append(Commands.WHITELIST.rawValue)
|
|
1391
|
+
headerData.append(UInt8(totalChunks))
|
|
1392
|
+
headerData.append(UInt8(i))
|
|
1393
|
+
|
|
1394
|
+
// Combine header and payload
|
|
1395
|
+
var chunkData = Data()
|
|
1396
|
+
chunkData.append(headerData)
|
|
1397
|
+
chunkData.append(payloadChunk)
|
|
1398
|
+
|
|
1399
|
+
chunks.append(chunkData)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
var uintChunks: [[UInt8]] = []
|
|
1403
|
+
for chunk in chunks {
|
|
1404
|
+
uintChunks.append(Array(chunk))
|
|
1405
|
+
}
|
|
1406
|
+
return uintChunks
|
|
1407
|
+
// return chunks.flatMap { Array($0) }
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
func exitAllFunctions(to peripheral: CBPeripheral, characteristic: CBCharacteristic) {
|
|
1411
|
+
var data = Data()
|
|
1412
|
+
data.append(Commands.BLE_EXIT_ALL_FUNCTIONS.rawValue)
|
|
1413
|
+
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private func sendInitCommand(to peripheral: CBPeripheral) {
|
|
1417
|
+
let initData = Data([Commands.BLE_REQ_INIT.rawValue, 0x01])
|
|
1418
|
+
let initDataArray = initData.map { UInt8($0) }
|
|
1419
|
+
|
|
1420
|
+
if leftPeripheral == peripheral {
|
|
1421
|
+
queueChunks([initDataArray], sendLeft: true, sendRight: false)
|
|
1422
|
+
} else if rightPeripheral == peripheral {
|
|
1423
|
+
queueChunks([initDataArray], sendLeft: false, sendRight: true)
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private func sendInit() {
|
|
1428
|
+
let initData = Data([Commands.BLE_REQ_INIT.rawValue, 0x01])
|
|
1429
|
+
let initDataArray = initData.map { UInt8($0) }
|
|
1430
|
+
queueChunks([initDataArray])
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
func exit() {
|
|
1434
|
+
let exitData = Data([Commands.BLE_EXIT_ALL_FUNCTIONS.rawValue])
|
|
1435
|
+
let exitDataArray = exitData.map { UInt8($0) }
|
|
1436
|
+
queueChunks([exitDataArray])
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
func sendShutdown() {
|
|
1440
|
+
Bridge.log("sendShutdown - not supported on G1")
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
func sendReboot() {
|
|
1444
|
+
Bridge.log("sendReboot - not supported on G1")
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
func sendRgbLedControl(
|
|
1448
|
+
requestId: String, packageName _: String?, action _: String, color _: String?,
|
|
1449
|
+
ontime _: Int, offtime _: Int, count _: Int
|
|
1450
|
+
) {
|
|
1451
|
+
Bridge.log("sendRgbLedControl - not supported on G1")
|
|
1452
|
+
Bridge.sendRgbLedControlResponse(
|
|
1453
|
+
requestId: requestId, success: false, error: "device_not_supported"
|
|
1454
|
+
)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/// don't call semaphore signals here as it's handled elswhere:
|
|
1458
|
+
private func handleInitResponse(from peripheral: CBPeripheral, success: Bool) {
|
|
1459
|
+
if peripheral == leftPeripheral {
|
|
1460
|
+
leftInitialized = success
|
|
1461
|
+
// CoreCommsService.log("G1: Left arm initialized: \(success)")
|
|
1462
|
+
setReadiness(left: true, right: nil)
|
|
1463
|
+
} else if peripheral == rightPeripheral {
|
|
1464
|
+
rightInitialized = success
|
|
1465
|
+
// CoreCommsService.log("G1: Right arm initialized: \(success)")
|
|
1466
|
+
setReadiness(left: nil, right: true)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Only proceed if both glasses are initialized
|
|
1470
|
+
if leftInitialized, rightInitialized {
|
|
1471
|
+
setReadiness(left: true, right: true)
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
private func sendHeartbeat() {
|
|
1476
|
+
incrementHeartbeatCounter()
|
|
1477
|
+
|
|
1478
|
+
var heartbeatData = Data()
|
|
1479
|
+
// heartbeatData.append(Commands.BLE_REQ_HEARTBEAT.rawValue)
|
|
1480
|
+
// heartbeatData.append(UInt8(0x02 & 0xFF))
|
|
1481
|
+
heartbeatData.append(Commands.BLE_REQ_HEARTBEAT.rawValue)
|
|
1482
|
+
heartbeatData.append(UInt8(heartbeatCounter & 0xFF))
|
|
1483
|
+
|
|
1484
|
+
var heartbeatArray = heartbeatData.map { UInt8($0) }
|
|
1485
|
+
|
|
1486
|
+
if fullyBooted {
|
|
1487
|
+
queueChunks([heartbeatArray])
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Periodically request battery status
|
|
1491
|
+
if leftBatteryLevel == -1 || rightBatteryLevel == -1 || heartbeatCounter % 10 == 0 {
|
|
1492
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
1493
|
+
self?.getBatteryStatus()
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// if let txChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: peripheral) {
|
|
1497
|
+
// let hexString = heartbeatData.map { String(format: "%02X", $0) }.joined()
|
|
1498
|
+
// peripheral.writeValue(heartbeatData, for: txChar, type: .withoutResponse)
|
|
1499
|
+
// }
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
func sendCommandToSide(_ command: [UInt8], side: String) async {
|
|
1503
|
+
// Convert to Data
|
|
1504
|
+
let commandData = Data(command)
|
|
1505
|
+
// CoreCommsService.log("G1: Sending command to glasses: \(paddedCommand.map { String(format: "%02X", $0) }.joined(separator: " "))")
|
|
1506
|
+
// CoreCommsService.log("G1: SEND (\(side)) \(commandData.hexEncodedString())")
|
|
1507
|
+
|
|
1508
|
+
if side == "L" {
|
|
1509
|
+
// send to left
|
|
1510
|
+
if let leftPeripheral = leftPeripheral,
|
|
1511
|
+
let characteristic = leftPeripheral.services?
|
|
1512
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1513
|
+
.characteristics?
|
|
1514
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1515
|
+
{
|
|
1516
|
+
leftPeripheral.writeValue(commandData, for: characteristic, type: .withResponse)
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
// send to right
|
|
1520
|
+
if let rightPeripheral = rightPeripheral,
|
|
1521
|
+
let characteristic = rightPeripheral.services?
|
|
1522
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1523
|
+
.characteristics?
|
|
1524
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1525
|
+
{
|
|
1526
|
+
rightPeripheral.writeValue(commandData, for: characteristic, type: .withResponse)
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
private func sendCommandToSide2(
|
|
1532
|
+
_ command: [UInt8],
|
|
1533
|
+
side: String,
|
|
1534
|
+
attemptNumber: Int = 0,
|
|
1535
|
+
sequenceNumber: Int = -1
|
|
1536
|
+
) async -> Bool {
|
|
1537
|
+
let startTime = Date()
|
|
1538
|
+
let commandData = Data(command)
|
|
1539
|
+
|
|
1540
|
+
let peripheral: CBPeripheral?
|
|
1541
|
+
let characteristic: CBCharacteristic?
|
|
1542
|
+
|
|
1543
|
+
if side == "L" {
|
|
1544
|
+
peripheral = leftPeripheral
|
|
1545
|
+
characteristic = leftPeripheral?.services?
|
|
1546
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1547
|
+
.characteristics?
|
|
1548
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1549
|
+
} else {
|
|
1550
|
+
peripheral = rightPeripheral
|
|
1551
|
+
characteristic = rightPeripheral?.services?
|
|
1552
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1553
|
+
.characteristics?
|
|
1554
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
guard let peripheral, let characteristic else {
|
|
1558
|
+
Bridge.log("G1: ⚠️ peripheral/characteristic not found, resuming immediately")
|
|
1559
|
+
return false
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
let key = sequenceNumber == -1 ? side : "\(side)-\(sequenceNumber)"
|
|
1563
|
+
let waitTimeMs = Int((0.3 + (0.2 * Double(attemptNumber))) * 1000)
|
|
1564
|
+
|
|
1565
|
+
return await ackManager.waitForAck(key: key, timeoutMs: waitTimeMs) {
|
|
1566
|
+
peripheral.writeValue(commandData, for: characteristic, type: .withResponse)
|
|
1567
|
+
} onTimeout: {
|
|
1568
|
+
let elapsed = Date().timeIntervalSince(startTime) * 1000
|
|
1569
|
+
Bridge.log("G1: ⚠️ ACK timeout for \(key) after \(String(format: "%.0f", elapsed))ms")
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/// FAST BLE TRANSMISSION (.withoutResponse)
|
|
1574
|
+
func sendCommandToSideWithoutResponse(_ command: [UInt8], side: String) async {
|
|
1575
|
+
// Convert to Data
|
|
1576
|
+
let commandData = Data(command)
|
|
1577
|
+
|
|
1578
|
+
if side == "L" {
|
|
1579
|
+
// send to left
|
|
1580
|
+
if let leftPeripheral = leftPeripheral,
|
|
1581
|
+
let characteristic = leftPeripheral.services?
|
|
1582
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1583
|
+
.characteristics?
|
|
1584
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1585
|
+
{
|
|
1586
|
+
// Fast approach: .withoutResponse for speed
|
|
1587
|
+
leftPeripheral.writeValue(commandData, for: characteristic, type: .withoutResponse)
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
// send to right
|
|
1591
|
+
if let rightPeripheral = rightPeripheral,
|
|
1592
|
+
let characteristic = rightPeripheral.services?
|
|
1593
|
+
.first(where: { $0.uuid == UART_SERVICE_UUID })?
|
|
1594
|
+
.characteristics?
|
|
1595
|
+
.first(where: { $0.uuid == UART_TX_CHAR_UUID })
|
|
1596
|
+
{
|
|
1597
|
+
// Fast approach: .withoutResponse for speed
|
|
1598
|
+
rightPeripheral.writeValue(commandData, for: characteristic, type: .withoutResponse)
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// No waiting for ACK - fire and forget for speed
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
func queueChunks(
|
|
1606
|
+
_ chunks: [[UInt8]], sendLeft: Bool = true, sendRight: Bool = true, sleepAfterMs: Int = 0,
|
|
1607
|
+
ignoreAck: Bool = false, chunkTimeMs: Int = 8, lastFrameMs: Int = 100
|
|
1608
|
+
) {
|
|
1609
|
+
let bufferedCommand = BufferedCommand(
|
|
1610
|
+
chunks: chunks, sendLeft: sendLeft, sendRight: sendRight, waitTime: sleepAfterMs,
|
|
1611
|
+
ignoreAck: ignoreAck, chunkTimeMs: chunkTimeMs, lastFrameMs: lastFrameMs
|
|
1612
|
+
)
|
|
1613
|
+
Task {
|
|
1614
|
+
await commandQueue.enqueue(bufferedCommand)
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
func sendWhitelist() {
|
|
1619
|
+
Bridge.log("G1: sendWhitelist()")
|
|
1620
|
+
let whitelistChunks = getWhitelistChunks()
|
|
1621
|
+
queueChunks(whitelistChunks, sendLeft: true, sendRight: true, sleepAfterMs: 100)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
func setBrightness(_ level: Int, autoMode: Bool = false) {
|
|
1625
|
+
// Convert from percentage (0-100) to the correct range (0-41)
|
|
1626
|
+
let mappedLevel = min(41, max(0, Int((Double(level) / 100.0) * 41.0)))
|
|
1627
|
+
|
|
1628
|
+
// Create and capture the UInt8 value
|
|
1629
|
+
let brightnessLevel = UInt8(mappedLevel)
|
|
1630
|
+
|
|
1631
|
+
// Call the async function from a non-async context
|
|
1632
|
+
Task {
|
|
1633
|
+
let success = await setBrightnessRaw(brightnessLevel, autoMode: autoMode)
|
|
1634
|
+
if !success {
|
|
1635
|
+
NSLog("Failed to set brightness to level \(level)% (mapped to \(mappedLevel))")
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
func setBrightnessRaw(_ level: UInt8, autoMode: Bool = false) async -> Bool {
|
|
1641
|
+
Bridge.log("G1: setBrightness()")
|
|
1642
|
+
// Ensure level is between 0x00 and 0x29 (0-41)
|
|
1643
|
+
var lvl: UInt8 = level
|
|
1644
|
+
if level > 0x29 {
|
|
1645
|
+
lvl = 0x29
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
let command: [UInt8] = [Commands.BRIGHTNESS.rawValue, lvl, autoMode ? 0x01 : 0x00]
|
|
1649
|
+
queueChunks([command])
|
|
1650
|
+
|
|
1651
|
+
// buried data point testing:
|
|
1652
|
+
// let command: [UInt8] = [0x3E]
|
|
1653
|
+
// queueChunks([command])
|
|
1654
|
+
|
|
1655
|
+
// // Send to both glasses with proper timing
|
|
1656
|
+
// if let rightGlass = rightPeripheral,
|
|
1657
|
+
// let rightTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: rightGlass) {
|
|
1658
|
+
// rightGlass.writeValue(Data(command), for: rightTxChar, type: .withResponse)
|
|
1659
|
+
// try? await Task.sleep(nanoseconds: 50 * 1_000_000) // 50ms delay
|
|
1660
|
+
// }
|
|
1661
|
+
//
|
|
1662
|
+
// if let leftGlass = leftPeripheral,
|
|
1663
|
+
// let leftTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: leftGlass) {
|
|
1664
|
+
// leftGlass.writeValue(Data(command), for: leftTxChar, type: .withResponse)
|
|
1665
|
+
// }
|
|
1666
|
+
|
|
1667
|
+
return true
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
func setHeadUpAngle(_ angle: Int) {
|
|
1671
|
+
var agl: Int = angle
|
|
1672
|
+
if angle < 0 {
|
|
1673
|
+
agl = 0
|
|
1674
|
+
} else if angle > 60 {
|
|
1675
|
+
agl = 60
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Call the async function from a non-async context
|
|
1679
|
+
Task {
|
|
1680
|
+
await setHeadUpAngleRaw(UInt8(agl))
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
func setHeadUpAngleRaw(_ angle: UInt8) async {
|
|
1685
|
+
Bridge.log("G1: setHeadUpAngle()")
|
|
1686
|
+
let command: [UInt8] = [Commands.HEAD_UP_ANGLE.rawValue, angle, 0x01]
|
|
1687
|
+
queueChunks([command])
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
func getBatteryStatus() {
|
|
1691
|
+
Bridge.log("G1: getBatteryStatus()")
|
|
1692
|
+
let command: [UInt8] = [Commands.BLE_REQ_BATTERY.rawValue, 0x01]
|
|
1693
|
+
queueChunks([command])
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
func setSilentMode(_ enabled: Bool) async {
|
|
1697
|
+
let command: [UInt8] = [Commands.SILENT_MODE.rawValue, enabled ? 0x0C : 0x0A, 0x00]
|
|
1698
|
+
queueChunks([command])
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
func setDashboardPosition(_ height: Int, _ depth: Int) {
|
|
1702
|
+
Task {
|
|
1703
|
+
await setDashboardPositionRaw(UInt8(height), UInt8(depth))
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
func incrementGlobalCounter() {
|
|
1708
|
+
if globalCounter < 255 {
|
|
1709
|
+
globalCounter += 1
|
|
1710
|
+
} else {
|
|
1711
|
+
globalCounter = 0
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
func incrementHeartbeatCounter() {
|
|
1716
|
+
if heartbeatCounter < 255 {
|
|
1717
|
+
heartbeatCounter += 1
|
|
1718
|
+
} else {
|
|
1719
|
+
heartbeatCounter = 0
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
func setDashboardPositionRaw(_ height: UInt8, _ depth: UInt8) async -> Bool {
|
|
1724
|
+
let h: UInt8 = min(max(height, 0), 8)
|
|
1725
|
+
let d: UInt8 = min(max(depth, 1), 9)
|
|
1726
|
+
|
|
1727
|
+
incrementGlobalCounter()
|
|
1728
|
+
|
|
1729
|
+
// Build dashboard position command
|
|
1730
|
+
var command = Data()
|
|
1731
|
+
command.append(Commands.DASHBOARD_LAYOUT_COMMAND.rawValue)
|
|
1732
|
+
command.append(0x08) // Length
|
|
1733
|
+
command.append(0x00) // Sequence
|
|
1734
|
+
command.append(globalCounter & 0xFF) // Fixed value
|
|
1735
|
+
command.append(0x02) // Fixed value
|
|
1736
|
+
command.append(0x01) // State ON
|
|
1737
|
+
command.append(h) // height
|
|
1738
|
+
command.append(d) // depth
|
|
1739
|
+
|
|
1740
|
+
// while command.count < 20 {
|
|
1741
|
+
// command.append(0x00)
|
|
1742
|
+
// }
|
|
1743
|
+
|
|
1744
|
+
// convert command to array of UInt8
|
|
1745
|
+
let commandArray = command.map { $0 }
|
|
1746
|
+
queueChunks([commandArray])
|
|
1747
|
+
return true
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
func setMicEnabled(_ enabled: Bool) {
|
|
1751
|
+
Bridge.log("G1: setMicEnabled() \(enabled)")
|
|
1752
|
+
DeviceStore.shared.apply("glasses", "micEnabled", enabled)
|
|
1753
|
+
var micOnData = Data()
|
|
1754
|
+
micOnData.append(Commands.BLE_REQ_MIC_ON.rawValue)
|
|
1755
|
+
if enabled {
|
|
1756
|
+
micOnData.append(0x01)
|
|
1757
|
+
} else {
|
|
1758
|
+
micOnData.append(0x00)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
let micOnDataArray: [UInt8] = micOnData.map { UInt8($0) }
|
|
1762
|
+
|
|
1763
|
+
queueChunks([micOnDataArray], sendLeft: false, sendRight: true)
|
|
1764
|
+
|
|
1765
|
+
// if let txChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: peripheral) {
|
|
1766
|
+
// peripheral.writeValue(micOnData, for: txChar, type: .withResponse)
|
|
1767
|
+
// }
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
func sortMicRanking(list: [String]) -> [String] {
|
|
1771
|
+
return list
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// MARK: - Bitmap Conversion
|
|
1775
|
+
|
|
1776
|
+
/// Convert arbitrary image data (PNG/JPEG) to G1-compatible 1-bit BMP format.
|
|
1777
|
+
/// Mirrors G2.convertToG2Bmp() for local miniapp bitmap display support.
|
|
1778
|
+
/// G1 uses 1-bit monochrome BMP at 640x400 (576x135 active area).
|
|
1779
|
+
func convertToG1Bmp(_ imageData: Data, width: Int = 576, height: Int = 135) -> Data? {
|
|
1780
|
+
guard let image = UIImage(data: imageData), let cgImage = image.cgImage else {
|
|
1781
|
+
Bridge.log("G1: convertToG1Bmp - could not decode image")
|
|
1782
|
+
return nil
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
let srcWidth = cgImage.width
|
|
1786
|
+
let srcHeight = cgImage.height
|
|
1787
|
+
|
|
1788
|
+
// Scale to fit within target dimensions, maintaining aspect ratio
|
|
1789
|
+
let scale = min(Double(width) / Double(srcWidth), Double(height) / Double(srcHeight))
|
|
1790
|
+
let scaledW = Int(Double(srcWidth) * scale)
|
|
1791
|
+
let scaledH = Int(Double(srcHeight) * scale)
|
|
1792
|
+
let offsetX = (width - scaledW) / 2
|
|
1793
|
+
let offsetY = (height - scaledH) / 2
|
|
1794
|
+
|
|
1795
|
+
Bridge.log("G1: convertToG1Bmp - input \(srcWidth)x\(srcHeight) -> scaled \(scaledW)x\(scaledH) in \(width)x\(height)")
|
|
1796
|
+
|
|
1797
|
+
// Render to 8-bit grayscale
|
|
1798
|
+
guard let ctx = CGContext(
|
|
1799
|
+
data: nil,
|
|
1800
|
+
width: width,
|
|
1801
|
+
height: height,
|
|
1802
|
+
bitsPerComponent: 8,
|
|
1803
|
+
bytesPerRow: width,
|
|
1804
|
+
space: CGColorSpaceCreateDeviceGray(),
|
|
1805
|
+
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
1806
|
+
) else {
|
|
1807
|
+
Bridge.log("G1: convertToG1Bmp - failed to create CGContext")
|
|
1808
|
+
return nil
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
ctx.setFillColor(gray: 0, alpha: 1)
|
|
1812
|
+
ctx.fill(CGRect(x: 0, y: 0, width: width, height: height))
|
|
1813
|
+
ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
|
|
1814
|
+
|
|
1815
|
+
guard let renderedImage = ctx.makeImage(),
|
|
1816
|
+
let pixels = renderedImage.dataProvider?.data as Data? else {
|
|
1817
|
+
Bridge.log("G1: convertToG1Bmp - failed to get pixel data")
|
|
1818
|
+
return nil
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Convert grayscale pixels to 1-bit BMP
|
|
1822
|
+
let rowBytes = ((width + 31) / 32) * 4 // BMP rows are DWORD-aligned
|
|
1823
|
+
let pixelDataSize = rowBytes * height
|
|
1824
|
+
let headerSize = 62 // 14 file header + 40 DIB header + 8 color table
|
|
1825
|
+
|
|
1826
|
+
var bmp = Data()
|
|
1827
|
+
|
|
1828
|
+
// BMP File Header (14 bytes)
|
|
1829
|
+
let fileSize = UInt32(headerSize + pixelDataSize)
|
|
1830
|
+
bmp.append(contentsOf: [0x42, 0x4D]) // "BM"
|
|
1831
|
+
bmp.append(contentsOf: withUnsafeBytes(of: fileSize.littleEndian) { Array($0) })
|
|
1832
|
+
bmp.append(contentsOf: [0x00, 0x00, 0x00, 0x00]) // reserved
|
|
1833
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt32(headerSize).littleEndian) { Array($0) })
|
|
1834
|
+
|
|
1835
|
+
// DIB Header (BITMAPINFOHEADER, 40 bytes)
|
|
1836
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt32(40).littleEndian) { Array($0) })
|
|
1837
|
+
bmp.append(contentsOf: withUnsafeBytes(of: Int32(width).littleEndian) { Array($0) })
|
|
1838
|
+
bmp.append(contentsOf: withUnsafeBytes(of: Int32(height).littleEndian) { Array($0) })
|
|
1839
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt16(1).littleEndian) { Array($0) }) // planes
|
|
1840
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt16(1).littleEndian) { Array($0) }) // bpp
|
|
1841
|
+
bmp.append(contentsOf: [UInt8](repeating: 0, count: 4)) // compression = BI_RGB
|
|
1842
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt32(pixelDataSize).littleEndian) { Array($0) })
|
|
1843
|
+
bmp.append(contentsOf: withUnsafeBytes(of: Int32(3780).littleEndian) { Array($0) }) // X ppm
|
|
1844
|
+
bmp.append(contentsOf: withUnsafeBytes(of: Int32(3780).littleEndian) { Array($0) }) // Y ppm
|
|
1845
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt32(2).littleEndian) { Array($0) }) // colors used
|
|
1846
|
+
bmp.append(contentsOf: withUnsafeBytes(of: UInt32(2).littleEndian) { Array($0) }) // important colors
|
|
1847
|
+
|
|
1848
|
+
// Color table: index 0 = black, index 1 = white
|
|
1849
|
+
bmp.append(contentsOf: [0x00, 0x00, 0x00, 0x00]) // black
|
|
1850
|
+
bmp.append(contentsOf: [0xFF, 0xFF, 0xFF, 0x00]) // white
|
|
1851
|
+
|
|
1852
|
+
// Pixel data (bottom-up row order for BMP)
|
|
1853
|
+
for row in (0..<height).reversed() {
|
|
1854
|
+
var rowData = [UInt8](repeating: 0, count: rowBytes)
|
|
1855
|
+
for col in 0..<width {
|
|
1856
|
+
let pixelIndex = row * width + col
|
|
1857
|
+
let gray = pixels[pixelIndex]
|
|
1858
|
+
if gray >= 128 {
|
|
1859
|
+
let byteIndex = col / 8
|
|
1860
|
+
let bitIndex = 7 - (col % 8)
|
|
1861
|
+
rowData[byteIndex] |= UInt8(1 << bitIndex)
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
bmp.append(contentsOf: rowData)
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
Bridge.log("G1: convertToG1Bmp - produced \(bmp.count) byte BMP")
|
|
1868
|
+
return bmp
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// MARK: - Enhanced BMP Display Methods
|
|
1872
|
+
|
|
1873
|
+
func displayBitmap(base64ImageData: String) async -> Bool {
|
|
1874
|
+
guard let bmpData = Data(base64Encoded: base64ImageData) else {
|
|
1875
|
+
Bridge.log("G1: Failed to decode base64 image data")
|
|
1876
|
+
return false
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
Bridge.log("G1: ✅ Successfully decoded base64 image data to \(bmpData.count) bytes")
|
|
1880
|
+
let invertedBmpData = invertBmpPixels(bmpData)
|
|
1881
|
+
let result = await sendBmp(bmpData: invertedBmpData)
|
|
1882
|
+
Bridge.log("G1: 🖼️ Single frame: Transmission \(result ? "SUCCESS" : "FAILED")")
|
|
1883
|
+
return result
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
func clearDisplay() {
|
|
1887
|
+
Bridge.log("G1: clearDisplay() - Using space")
|
|
1888
|
+
sendTextWall(" ")
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/// Create a simple test BMP pattern in hex format
|
|
1892
|
+
private func createTestBMPHex() -> String {
|
|
1893
|
+
// BMP header for 576x135 1-bit monochrome (from our working data)
|
|
1894
|
+
let header =
|
|
1895
|
+
"424d36260000000000003e0000002800000040020000870000000100010000000000f82500c40e0000c40e00000200000002000000000000ffffff00"
|
|
1896
|
+
|
|
1897
|
+
// Create a simple pattern: alternating lines
|
|
1898
|
+
var pixelData = ""
|
|
1899
|
+
let bytesPerRow = 72 // 576 pixels / 8 bits per byte
|
|
1900
|
+
|
|
1901
|
+
for row in 0 ..< 135 {
|
|
1902
|
+
for col in 0 ..< bytesPerRow {
|
|
1903
|
+
// Create a pattern: every other row is different
|
|
1904
|
+
if row % 10 < 5 {
|
|
1905
|
+
pixelData += "ff" // White line
|
|
1906
|
+
} else {
|
|
1907
|
+
pixelData += col % 4 == 0 ? "00" : "ff" // Pattern line
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
return header + pixelData
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
private func invertBmpPixels(_ bmpData: Data) -> Data {
|
|
1916
|
+
guard bmpData.count > 62 else {
|
|
1917
|
+
Bridge.log("G1: BMP data too small to contain pixel data")
|
|
1918
|
+
return bmpData
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// BMP header is 62 bytes for your format (14 byte file header + 40 byte DIB header + 8 byte color table)
|
|
1922
|
+
let headerSize = 62
|
|
1923
|
+
var invertedData = Data(bmpData.prefix(headerSize)) // Keep header unchanged
|
|
1924
|
+
|
|
1925
|
+
// Invert the pixel data (everything after the header)
|
|
1926
|
+
let pixelData = bmpData.dropFirst(headerSize)
|
|
1927
|
+
|
|
1928
|
+
for byte in pixelData {
|
|
1929
|
+
// Invert each byte (flip all bits)
|
|
1930
|
+
let invertedByte = ~byte
|
|
1931
|
+
invertedData.append(invertedByte)
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
Bridge.log("G1: Inverted BMP pixels: \(pixelData.count) bytes processed")
|
|
1935
|
+
return invertedData
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
/// Core MentraOS-compatible BMP display implementation
|
|
1939
|
+
private func sendBmp(bmpData: Data) async -> Bool {
|
|
1940
|
+
// Frame timing validation for animation smoothness
|
|
1941
|
+
let currentTime = Date()
|
|
1942
|
+
let timeSinceLastFrame = currentTime.timeIntervalSince(lastFrameTime)
|
|
1943
|
+
|
|
1944
|
+
// Update frame tracking
|
|
1945
|
+
frameSequence += 1
|
|
1946
|
+
lastFrameTime = currentTime
|
|
1947
|
+
|
|
1948
|
+
Bridge.log(
|
|
1949
|
+
"G1: 🎬 Frame \(frameSequence): \(String(format: "%.0f", timeSinceLastFrame * 1000))ms since last frame"
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
// MentraOS constants - exact match
|
|
1953
|
+
let packLen = 194 // Exact chunk size from MentraOS
|
|
1954
|
+
let iosDelayMs = 8 // iOS delay from MentraOS
|
|
1955
|
+
let addressBytes: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // Address from MentraOS
|
|
1956
|
+
|
|
1957
|
+
// // Debug: Check bmpData integrity before chunking
|
|
1958
|
+
// let pixelDataStart = 62
|
|
1959
|
+
// if bmpData.count > pixelDataStart + 50 {
|
|
1960
|
+
// let beforeChunkSample = Array(bmpData[pixelDataStart..<(pixelDataStart + 20)])
|
|
1961
|
+
// let beforeChunkHex = beforeChunkSample.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
1962
|
+
// CoreCommsService.log("G1: 🔍 Before chunking - pixel data sample (bytes 62-82): \(beforeChunkHex)")
|
|
1963
|
+
// }
|
|
1964
|
+
|
|
1965
|
+
// Create chunks exactly like MentraOS
|
|
1966
|
+
var multiPacks: [Data] = []
|
|
1967
|
+
var index = 0
|
|
1968
|
+
while index < bmpData.count {
|
|
1969
|
+
let end = min(index + packLen, bmpData.count)
|
|
1970
|
+
let singlePack = bmpData.subdata(in: index ..< end)
|
|
1971
|
+
|
|
1972
|
+
// Debug first few chunks to see where corruption happens
|
|
1973
|
+
if index < 600 { // First 3 chunks (194 * 3 = 582)
|
|
1974
|
+
let chunkSample = Array(singlePack.prefix(20))
|
|
1975
|
+
let chunkHex = chunkSample.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
1976
|
+
Bridge.log("G1: 🔍 Chunk creation - index \(index), sample: \(chunkHex)")
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
multiPacks.append(singlePack)
|
|
1980
|
+
index += packLen
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
Bridge.log("G1: Created \(multiPacks.count) packs from BMP data (MentraOS format)")
|
|
1984
|
+
|
|
1985
|
+
var chunks: [[UInt8]] = []
|
|
1986
|
+
|
|
1987
|
+
// add bmp data chunks:
|
|
1988
|
+
for (packIndex, pack) in multiPacks.enumerated() {
|
|
1989
|
+
let packData: Data
|
|
1990
|
+
if packIndex == 0 {
|
|
1991
|
+
// First package includes address: [0x15, index, address...]
|
|
1992
|
+
var firstPacketData = Data([0x15, UInt8(packIndex & 0xFF)])
|
|
1993
|
+
firstPacketData.append(Data(addressBytes))
|
|
1994
|
+
firstPacketData.append(pack)
|
|
1995
|
+
packData = firstPacketData
|
|
1996
|
+
} else {
|
|
1997
|
+
// Subsequent packages: [0x15, index, data...]
|
|
1998
|
+
var packetData = Data([0x15, UInt8(packIndex & 0xFF)])
|
|
1999
|
+
packetData.append(pack)
|
|
2000
|
+
packData = packetData
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
chunks.append(Array(packData))
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// queueChunks(chunks)
|
|
2007
|
+
// chunks.removeAll()
|
|
2008
|
+
|
|
2009
|
+
// add end command:
|
|
2010
|
+
chunks.append([0x20, 0x0D, 0x0E])
|
|
2011
|
+
|
|
2012
|
+
queueChunks(chunks, sleepAfterMs: 25, lastFrameMs: 100)
|
|
2013
|
+
chunks.removeAll()
|
|
2014
|
+
|
|
2015
|
+
// CRC validation like MentraOS - frame 1 should be 0x1914adcf
|
|
2016
|
+
var imageWithAddress = Data(addressBytes)
|
|
2017
|
+
imageWithAddress.append(bmpData)
|
|
2018
|
+
|
|
2019
|
+
// Calculate CRC32-XZ like MentraOS (not standard CRC32)
|
|
2020
|
+
let crc32Value = calculateCRC32XZ(data: imageWithAddress)
|
|
2021
|
+
let crcBytes = Data([
|
|
2022
|
+
UInt8((crc32Value >> 24) & 0xFF),
|
|
2023
|
+
UInt8((crc32Value >> 16) & 0xFF),
|
|
2024
|
+
UInt8((crc32Value >> 8) & 0xFF),
|
|
2025
|
+
UInt8(crc32Value & 0xFF),
|
|
2026
|
+
])
|
|
2027
|
+
|
|
2028
|
+
var crcCommand = Data([0x16])
|
|
2029
|
+
crcCommand.append(crcBytes)
|
|
2030
|
+
|
|
2031
|
+
chunks.append(Array(crcCommand))
|
|
2032
|
+
queueChunks(chunks)
|
|
2033
|
+
return true
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/// Helper function to calculate CRC32-XZ like MentraOS (matches Dart crclib)
|
|
2037
|
+
private func calculateCRC32XZ(data: Data) -> UInt32 {
|
|
2038
|
+
// CRC32-XZ table-based implementation (matches Dart crclib exactly)
|
|
2039
|
+
let polynomial: UInt32 = 0x04C1_1DB7
|
|
2040
|
+
var crc: UInt32 = 0xFFFF_FFFF
|
|
2041
|
+
|
|
2042
|
+
// Build CRC table for efficiency (matches crclib behavior)
|
|
2043
|
+
var table: [UInt32] = Array(repeating: 0, count: 256)
|
|
2044
|
+
for i in 0 ..< 256 {
|
|
2045
|
+
var entry = UInt32(i) << 24
|
|
2046
|
+
for _ in 0 ..< 8 {
|
|
2047
|
+
if (entry & 0x8000_0000) != 0 {
|
|
2048
|
+
entry = (entry << 1) ^ polynomial
|
|
2049
|
+
} else {
|
|
2050
|
+
entry <<= 1
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
table[i] = entry
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Calculate CRC using table lookup (matches MentraOS's crclib)
|
|
2057
|
+
for byte in data {
|
|
2058
|
+
let tableIndex = Int((crc >> 24) ^ UInt32(byte)) & 0xFF
|
|
2059
|
+
crc = (crc << 8) ^ table[tableIndex]
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
return ~crc
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/// Helper function to calculate CRC32 (simple implementation)
|
|
2066
|
+
private func calculateCRC32(data: Data) -> UInt32 {
|
|
2067
|
+
let polynomial: UInt32 = 0xEDB8_8320
|
|
2068
|
+
var crc: UInt32 = 0xFFFF_FFFF
|
|
2069
|
+
|
|
2070
|
+
for byte in data {
|
|
2071
|
+
crc ^= UInt32(byte)
|
|
2072
|
+
for _ in 0 ..< 8 {
|
|
2073
|
+
if (crc & 1) != 0 {
|
|
2074
|
+
crc = (crc >> 1) ^ polynomial
|
|
2075
|
+
} else {
|
|
2076
|
+
crc = crc >> 1
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
return ~crc
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/// Create BMP chunks with MentraOS-compatible headers
|
|
2085
|
+
private func createBmpChunks(from bmpData: Data, chunkSize: Int) -> [[UInt8]] {
|
|
2086
|
+
var chunks: [[UInt8]] = []
|
|
2087
|
+
let glassesAddress: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // MentraOS uses address 0x1c
|
|
2088
|
+
|
|
2089
|
+
let totalChunks = (bmpData.count + chunkSize - 1) / chunkSize
|
|
2090
|
+
|
|
2091
|
+
for i in 0 ..< totalChunks {
|
|
2092
|
+
let start = i * chunkSize
|
|
2093
|
+
let end = min(start + chunkSize, bmpData.count)
|
|
2094
|
+
let chunkData = bmpData.subdata(in: start ..< end)
|
|
2095
|
+
|
|
2096
|
+
var chunk: [UInt8] = []
|
|
2097
|
+
|
|
2098
|
+
// First chunk needs address bytes
|
|
2099
|
+
if i == 0 {
|
|
2100
|
+
chunk.append(0x15) // Command
|
|
2101
|
+
chunk.append(UInt8(i & 0xFF)) // Sequence
|
|
2102
|
+
chunk.append(contentsOf: glassesAddress) // Address
|
|
2103
|
+
chunk.append(contentsOf: chunkData)
|
|
2104
|
+
} else {
|
|
2105
|
+
chunk.append(0x15) // Command
|
|
2106
|
+
chunk.append(UInt8(i & 0xFF)) // Sequence
|
|
2107
|
+
chunk.append(contentsOf: chunkData)
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
chunks.append(chunk)
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
return chunks
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/// Send CRC with retry logic
|
|
2117
|
+
private func sendBmpCrcWithRetry(
|
|
2118
|
+
bmpData: Data,
|
|
2119
|
+
sendLeft: Bool,
|
|
2120
|
+
sendRight: Bool,
|
|
2121
|
+
maxAttempts: Int,
|
|
2122
|
+
timeoutMs: Int
|
|
2123
|
+
) async -> Bool {
|
|
2124
|
+
// Create data with address for CRC calculation (MentraOS pattern)
|
|
2125
|
+
let glassesAddress: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // Same address as in chunks
|
|
2126
|
+
var dataWithAddress = Data(glassesAddress)
|
|
2127
|
+
dataWithAddress.append(bmpData)
|
|
2128
|
+
|
|
2129
|
+
// Calculate CRC32 (simplified - in a real implementation, use proper CRC32-XZ)
|
|
2130
|
+
let crcValue = dataWithAddress.crc32
|
|
2131
|
+
|
|
2132
|
+
// Create CRC command packet
|
|
2133
|
+
var crcCommand: [UInt8] = [0x16] // CRC command
|
|
2134
|
+
crcCommand.append(UInt8((crcValue >> 24) & 0xFF))
|
|
2135
|
+
crcCommand.append(UInt8((crcValue >> 16) & 0xFF))
|
|
2136
|
+
crcCommand.append(UInt8((crcValue >> 8) & 0xFF))
|
|
2137
|
+
crcCommand.append(UInt8(crcValue & 0xFF))
|
|
2138
|
+
|
|
2139
|
+
Bridge.log("G1: Sending CRC command, CRC value: \(String(format: "%08x", crcValue))")
|
|
2140
|
+
|
|
2141
|
+
// Send CRC with retry
|
|
2142
|
+
for _ in 0 ..< maxAttempts {
|
|
2143
|
+
queueChunks([crcCommand], sendLeft: sendLeft, sendRight: sendRight)
|
|
2144
|
+
|
|
2145
|
+
// Wait for CRC command to process
|
|
2146
|
+
try? await Task.sleep(nanoseconds: UInt64(timeoutMs * 1_000_000))
|
|
2147
|
+
|
|
2148
|
+
// For now, assume success (in a real implementation, you'd check for ACK)
|
|
2149
|
+
Bridge.log("G1: CRC command sent successfully")
|
|
2150
|
+
return true
|
|
2151
|
+
}
|
|
2152
|
+
// Bridge.log("G1: CRC command failed, attempt \(attempt + 1)")
|
|
2153
|
+
Bridge.log("G1: Failed to send CRC command after \(maxAttempts) attempts")
|
|
2154
|
+
return false
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// MARK: BLE Stubs
|
|
2159
|
+
|
|
2160
|
+
extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
2161
|
+
func getWriteCharacteristic(for peripheral: CBPeripheral?) -> CBCharacteristic? {
|
|
2162
|
+
guard let peripheral = peripheral else { return nil }
|
|
2163
|
+
for service in peripheral.services ?? [] {
|
|
2164
|
+
if service.uuid == UART_SERVICE_UUID {
|
|
2165
|
+
for characteristic in service.characteristics ?? []
|
|
2166
|
+
where characteristic.uuid == UART_TX_CHAR_UUID
|
|
2167
|
+
{
|
|
2168
|
+
return characteristic
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return nil
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
func extractIdNumber(_ string: String) -> Int? {
|
|
2176
|
+
// Pattern to match "G1_" followed by digits, followed by "_"
|
|
2177
|
+
let pattern = "G1_(\\d+)_"
|
|
2178
|
+
|
|
2179
|
+
// Create a regular expression
|
|
2180
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
|
2181
|
+
return nil
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Look for matches in the input string
|
|
2185
|
+
let range = NSRange(string.startIndex ..< string.endIndex, in: string)
|
|
2186
|
+
guard let match = regex.firstMatch(in: string, options: [], range: range) else {
|
|
2187
|
+
return nil
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Extract the captured group (the digits)
|
|
2191
|
+
if let matchRange = Range(match.range(at: 1), in: string) {
|
|
2192
|
+
let idString = String(string[matchRange])
|
|
2193
|
+
return Int(idString)
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
return nil
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
func emitDiscoveredDevice(_ name: String) {
|
|
2200
|
+
if name.contains("_L_") || name.contains("_R_") {
|
|
2201
|
+
// exampleName = "Even G1_74_L_57863C", "Even G1_3_L_57863C", "Even G1_100_L_57863C"
|
|
2202
|
+
guard let deviceName = extractIdNumber(name) else {
|
|
2203
|
+
Bridge.log("Failed to extract ID number from device name: \(name)")
|
|
2204
|
+
return
|
|
2205
|
+
}
|
|
2206
|
+
let deviceModel = "Even Realities G1"
|
|
2207
|
+
let dName = "\(deviceName)"
|
|
2208
|
+
Bridge.sendDiscoveredDevice(deviceModel, dName)
|
|
2209
|
+
} else {
|
|
2210
|
+
Bridge.log("Unknown device type: \(name)")
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/// On BT discovery, automatically connect to both arms if we have them:
|
|
2215
|
+
func centralManager(
|
|
2216
|
+
_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any],
|
|
2217
|
+
rssi _: NSNumber
|
|
2218
|
+
) {
|
|
2219
|
+
guard let name = peripheral.name else { return }
|
|
2220
|
+
guard name.contains("Even G1") else { return }
|
|
2221
|
+
|
|
2222
|
+
Bridge.log("G1: found peripheral: \(name) - SEARCH_ID: \(DEVICE_SEARCH_ID)")
|
|
2223
|
+
|
|
2224
|
+
// Only process serial number for devices that match our search ID
|
|
2225
|
+
if name.contains(DEVICE_SEARCH_ID) {
|
|
2226
|
+
// Extract manufacturer data to decode serial number
|
|
2227
|
+
if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey]
|
|
2228
|
+
as? Data
|
|
2229
|
+
{
|
|
2230
|
+
Bridge.log("G1: 📱 Found manufacturer data: \(manufacturerData.hexEncodedString())")
|
|
2231
|
+
|
|
2232
|
+
// Try to decode serial number from manufacturer data
|
|
2233
|
+
if let decodedSerial = decodeSerialFromManufacturerData(manufacturerData) {
|
|
2234
|
+
Bridge.log("G1: 📱 Decoded serial number: \(decodedSerial)")
|
|
2235
|
+
|
|
2236
|
+
// Decode style and color from serial number
|
|
2237
|
+
let (decodedStyle, decodedColor) = G1.decodeEvenG1SerialNumber(decodedSerial)
|
|
2238
|
+
Bridge.log("G1: 📱 Style: \(style), Color: \(color)")
|
|
2239
|
+
|
|
2240
|
+
// Store the information
|
|
2241
|
+
DeviceStore.shared.apply("glasses", "serialNumber", decodedSerial)
|
|
2242
|
+
DeviceStore.shared.apply("glasses", "style", decodedStyle)
|
|
2243
|
+
DeviceStore.shared.apply("glasses", "color", decodedColor)
|
|
2244
|
+
} else {
|
|
2245
|
+
Bridge.log("G1: 📱 Could not decode serial number from manufacturer data")
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
Bridge.log("G1: 📱 No manufacturer data found in advertisement")
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
if name.contains("_L_"), name.contains(DEVICE_SEARCH_ID) {
|
|
2253
|
+
Bridge.log("G1: Found left arm: \(name)")
|
|
2254
|
+
leftPeripheral = peripheral
|
|
2255
|
+
} else if name.contains("_R_"), name.contains(DEVICE_SEARCH_ID) {
|
|
2256
|
+
Bridge.log("G1: Found right arm: \(name)")
|
|
2257
|
+
rightPeripheral = peripheral
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
emitDiscoveredDevice(name)
|
|
2261
|
+
|
|
2262
|
+
if leftPeripheral != nil, rightPeripheral != nil {
|
|
2263
|
+
// central.stopScan()
|
|
2264
|
+
RN_connectGlasses()
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
2269
|
+
// BLE operations stay on _bluetoothQueue (where this callback runs)
|
|
2270
|
+
peripheral.delegate = self
|
|
2271
|
+
peripheral.discoverServices([UART_SERVICE_UUID])
|
|
2272
|
+
|
|
2273
|
+
DispatchQueue.main.async { [weak self] in
|
|
2274
|
+
guard let self = self else { return }
|
|
2275
|
+
|
|
2276
|
+
Bridge.log("G1: device connected: \(peripheral.name ?? "Unknown")")
|
|
2277
|
+
|
|
2278
|
+
if peripheral == self.leftPeripheral || (peripheral.name?.contains("_L_") ?? false) {
|
|
2279
|
+
self.leftGlassUUID = peripheral.identifier
|
|
2280
|
+
self.leftPeripheral = peripheral
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if peripheral == self.rightPeripheral || (peripheral.name?.contains("_R_") ?? false) {
|
|
2284
|
+
self.rightGlassUUID = peripheral.identifier
|
|
2285
|
+
self.rightPeripheral = peripheral
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
self.lastConnectionTimestamp = Date()
|
|
2289
|
+
Bridge.log("G1: Connected to peripheral: \(peripheral.name ?? "Unknown")")
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
func centralManager(
|
|
2294
|
+
_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error _: (any Error)?
|
|
2295
|
+
) {
|
|
2296
|
+
let side =
|
|
2297
|
+
peripheral == leftPeripheral
|
|
2298
|
+
? "LEFT" : peripheral == rightPeripheral ? "RIGHT" : "unknown"
|
|
2299
|
+
Bridge.log("G1: @@@@@ \(side) PERIPHERAL DISCONNECTED @@@@@")
|
|
2300
|
+
|
|
2301
|
+
// only reconnect if we're not intentionally disconnecting:
|
|
2302
|
+
if isDisconnecting {
|
|
2303
|
+
return
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if peripheral == leftPeripheral || peripheral == rightPeripheral {
|
|
2307
|
+
// force reconnection to both before considering us ready again:
|
|
2308
|
+
leftPeripheral = nil
|
|
2309
|
+
rightPeripheral = nil
|
|
2310
|
+
setReadiness(left: false, right: false)
|
|
2311
|
+
startReconnectionTimer() // Start periodic reconnection attempts
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
private func startReconnectionTimer() {
|
|
2316
|
+
Task {
|
|
2317
|
+
await reconnectionManager.start { [weak self] in
|
|
2318
|
+
guard let self else { return false }
|
|
2319
|
+
|
|
2320
|
+
// Check if already connected
|
|
2321
|
+
if await MainActor.run(body: { self.fullyBooted }) {
|
|
2322
|
+
Bridge.log("G1: Already connected, stopping reconnection")
|
|
2323
|
+
return true // Returning true stops the reconnection loop
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
Bridge.log("G1: Attempting reconnection...")
|
|
2327
|
+
|
|
2328
|
+
// Attempt to reconnect
|
|
2329
|
+
await MainActor.run {
|
|
2330
|
+
self.startScan()
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Return false to keep trying, true if connected
|
|
2334
|
+
return false
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
private func stopReconnectionTimer() {
|
|
2340
|
+
Task {
|
|
2341
|
+
await reconnectionManager.stop()
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/// Connect by UUID
|
|
2346
|
+
func connectByUUID() -> Bool {
|
|
2347
|
+
// don't do this if we don't have a search id set:
|
|
2348
|
+
if DEVICE_SEARCH_ID == "NOT_SET" || DEVICE_SEARCH_ID.isEmpty {
|
|
2349
|
+
Bridge.log("G1: 🔵 No DEVICE_SEARCH_ID set, skipping connect by UUID")
|
|
2350
|
+
return false
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
Bridge.log("G1: 🔵 Attempting to connect by UUID")
|
|
2354
|
+
var foundAny = false
|
|
2355
|
+
|
|
2356
|
+
if let leftUUID = leftGlassUUID {
|
|
2357
|
+
Bridge.log("G1: 🔵 Found stored left glass UUID: \(leftUUID.uuidString)")
|
|
2358
|
+
let leftDevices = centralManager!.retrievePeripherals(withIdentifiers: [leftUUID])
|
|
2359
|
+
|
|
2360
|
+
if let leftDevice = leftDevices.first {
|
|
2361
|
+
Bridge.log(
|
|
2362
|
+
"G1: 🔵 Successfully retrieved left glass: \(leftDevice.name ?? "Unknown")"
|
|
2363
|
+
)
|
|
2364
|
+
foundAny = true
|
|
2365
|
+
leftPeripheral = leftDevice
|
|
2366
|
+
leftDevice.delegate = self
|
|
2367
|
+
centralManager!.connect(
|
|
2368
|
+
leftDevice,
|
|
2369
|
+
options: [
|
|
2370
|
+
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
2371
|
+
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
2372
|
+
]
|
|
2373
|
+
)
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if let rightUUID = rightGlassUUID {
|
|
2378
|
+
Bridge.log("G1: 🔵 Found stored right glass UUID: \(rightUUID.uuidString)")
|
|
2379
|
+
let rightDevices = centralManager!.retrievePeripherals(withIdentifiers: [rightUUID])
|
|
2380
|
+
|
|
2381
|
+
if let rightDevice = rightDevices.first {
|
|
2382
|
+
Bridge.log(
|
|
2383
|
+
"G1: 🔵 Successfully retrieved right glass: \(rightDevice.name ?? "Unknown")"
|
|
2384
|
+
)
|
|
2385
|
+
foundAny = true
|
|
2386
|
+
rightPeripheral = rightDevice
|
|
2387
|
+
rightDevice.delegate = self
|
|
2388
|
+
centralManager!.connect(
|
|
2389
|
+
rightDevice,
|
|
2390
|
+
options: [
|
|
2391
|
+
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
2392
|
+
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
2393
|
+
]
|
|
2394
|
+
)
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
return foundAny
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
|
|
2402
|
+
if let services = peripheral.services {
|
|
2403
|
+
for service in services where service.uuid == UART_SERVICE_UUID {
|
|
2404
|
+
peripheral.discoverCharacteristics(
|
|
2405
|
+
[UART_TX_CHAR_UUID, UART_RX_CHAR_UUID], for: service
|
|
2406
|
+
)
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/// Update peripheral(_:didDiscoverCharacteristicsFor:error:) to set services waiters
|
|
2412
|
+
func peripheral(
|
|
2413
|
+
_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService,
|
|
2414
|
+
error _: Error?
|
|
2415
|
+
) {
|
|
2416
|
+
guard let characteristics = service.characteristics else { return }
|
|
2417
|
+
|
|
2418
|
+
if service.uuid.isEqual(UART_SERVICE_UUID) {
|
|
2419
|
+
for characteristic in characteristics {
|
|
2420
|
+
if characteristic.uuid == UART_TX_CHAR_UUID {
|
|
2421
|
+
sendInitCommand(to: peripheral)
|
|
2422
|
+
} else if characteristic.uuid == UART_RX_CHAR_UUID {
|
|
2423
|
+
peripheral.setNotifyValue(true, for: characteristic)
|
|
2424
|
+
|
|
2425
|
+
// enable notification (needed for pairing from scracth!)
|
|
2426
|
+
Thread.sleep(forTimeInterval: 0.5) // 500ms delay
|
|
2427
|
+
let CLIENT_CHARACTERISTIC_CONFIG_UUID = CBUUID(
|
|
2428
|
+
string: "00002902-0000-1000-8000-00805f9b34fb"
|
|
2429
|
+
)
|
|
2430
|
+
if let descriptor = characteristic.descriptors?.first(where: {
|
|
2431
|
+
$0.uuid == CLIENT_CHARACTERISTIC_CONFIG_UUID
|
|
2432
|
+
}) {
|
|
2433
|
+
let value = Data([0x01, 0x00]) // ENABLE_NOTIFICATION_VALUE in iOS
|
|
2434
|
+
peripheral.writeValue(value, for: descriptor)
|
|
2435
|
+
} else {
|
|
2436
|
+
Bridge.log("PROC_QUEUE - descriptor not found")
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// // Mark the services as ready
|
|
2442
|
+
// if peripheral == leftPeripheral {
|
|
2443
|
+
// Bridge.log("G1: Left glass services discovered and ready")
|
|
2444
|
+
// setReadiness(left: true, right: nil)
|
|
2445
|
+
// } else if peripheral == rightPeripheral {
|
|
2446
|
+
// Bridge.log("G1: Right glass services discovered and ready")
|
|
2447
|
+
// setReadiness(left: nil, right: true)
|
|
2448
|
+
// }
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/// called whenever bluetooth is initialized / turned on or off:
|
|
2453
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
2454
|
+
DispatchQueue.main.async { [weak self] in
|
|
2455
|
+
guard let self = self else { return }
|
|
2456
|
+
|
|
2457
|
+
if central.state == .poweredOn {
|
|
2458
|
+
Bridge.log("G1: Bluetooth was powered on")
|
|
2459
|
+
self.setReadiness(left: false, right: false)
|
|
2460
|
+
|
|
2461
|
+
if self.DEVICE_SEARCH_ID != "NOT_SET", !self.DEVICE_SEARCH_ID.isEmpty {
|
|
2462
|
+
self.startScan()
|
|
2463
|
+
}
|
|
2464
|
+
} else {
|
|
2465
|
+
Bridge.log("G1: Bluetooth was turned off.")
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/// called when we get data from the glasses:
|
|
2471
|
+
func peripheral(
|
|
2472
|
+
_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
|
|
2473
|
+
error: Error?
|
|
2474
|
+
) {
|
|
2475
|
+
if let error = error {
|
|
2476
|
+
Bridge.log("G1: Error updating value for characteristic: \(error.localizedDescription)")
|
|
2477
|
+
return
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
guard let data = characteristic.value else {
|
|
2481
|
+
Bridge.log("G1: Characteristic value is nil.")
|
|
2482
|
+
return
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// Process the notification data
|
|
2486
|
+
DispatchQueue.main.async { [weak self] in
|
|
2487
|
+
self?.handleNotification(from: peripheral, data: data)
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/// L/R Synchronization - Handle BLE write completions
|
|
2492
|
+
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
|
|
2493
|
+
DispatchQueue.main.async { [weak self] in
|
|
2494
|
+
guard let self = self else { return }
|
|
2495
|
+
|
|
2496
|
+
if let error = error {
|
|
2497
|
+
Bridge.log(
|
|
2498
|
+
"G1: ❌ BLE write error for \(peripheral.name ?? "unknown"): \(error.localizedDescription)"
|
|
2499
|
+
)
|
|
2500
|
+
} else {
|
|
2501
|
+
self.writeCompletionCount += 1
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|