@glassly/bluetooth-sdk 0.1.0-dev.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +563 -0
- package/android/build.gradle +450 -0
- package/android/gradle.properties.example +9 -0
- package/android/lc3Lib/CMakeLists.txt +6 -0
- package/android/lc3Lib/build.gradle +212 -0
- package/android/lc3Lib/proguard-rules.pro +33 -0
- package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
- package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
- package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
- package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
- package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
- package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
- package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
- package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
- package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
- package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
- package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
- package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
- package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
- package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
- package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
- package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
- package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
- package/android/settings.gradle +4 -0
- package/android/silero/LICENSE.md +17 -0
- package/android/silero/build.gradle +50 -0
- package/android/silero/proguard-rules.pro +24 -0
- package/android/silero/src/androidTest/assets/hello.wav +0 -0
- package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
- package/android/silero/src/main/assets/LICENSE +21 -0
- package/android/silero/src/main/assets/silero_vad.onnx +0 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
- package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
- package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
- package/android/src/main/AndroidManifest.xml +42 -0
- package/android/src/main/assets/G2Firmware/README.md +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
- package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
- package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
- package/android/src/test/resources/avif_with_exif.avif +0 -0
- package/app.plugin.js +3 -0
- package/build/BluetoothSdk.types.d.ts +1354 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -0
- package/build/BluetoothSdk.types.js +41 -0
- package/build/BluetoothSdk.types.js.map +1 -0
- package/build/_internal.d.ts +17 -0
- package/build/_internal.d.ts.map +1 -0
- package/build/_internal.js +16 -0
- package/build/_internal.js.map +1 -0
- package/build/_private/BluetoothSdkModule.d.ts +172 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
- package/build/_private/BluetoothSdkModule.js +300 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -0
- package/build/_private/LocalNetworkModule.d.ts +38 -0
- package/build/_private/LocalNetworkModule.d.ts.map +1 -0
- package/build/_private/LocalNetworkModule.js +3 -0
- package/build/_private/LocalNetworkModule.js.map +1 -0
- package/build/_private/cameraRequestPayload.d.ts +4 -0
- package/build/_private/cameraRequestPayload.d.ts.map +1 -0
- package/build/_private/cameraRequestPayload.js +32 -0
- package/build/_private/cameraRequestPayload.js.map +1 -0
- package/build/_private/photoRequestPayload.d.ts +6 -0
- package/build/_private/photoRequestPayload.d.ts.map +1 -0
- package/build/_private/photoRequestPayload.js +86 -0
- package/build/_private/photoRequestPayload.js.map +1 -0
- package/build/_private/sdkOtaManifest.d.ts +11 -0
- package/build/_private/sdkOtaManifest.d.ts.map +1 -0
- package/build/_private/sdkOtaManifest.js +15 -0
- package/build/_private/sdkOtaManifest.js.map +1 -0
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +46 -0
- package/build/changelogs.js.map +1 -0
- package/build/debug.d.ts +3 -0
- package/build/debug.d.ts.map +1 -0
- package/build/debug.js +8 -0
- package/build/debug.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +9 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +12 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +171 -0
- package/build/index.js.map +1 -0
- package/build/ota-server/OtaServer.types.d.ts +14 -0
- package/build/ota-server/OtaServer.types.d.ts.map +1 -0
- package/build/ota-server/OtaServer.types.js +2 -0
- package/build/ota-server/OtaServer.types.js.map +1 -0
- package/build/ota-server/OtaServerModule.d.ts +21 -0
- package/build/ota-server/OtaServerModule.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.js +3 -0
- package/build/ota-server/OtaServerModule.js.map +1 -0
- package/build/ota-server/OtaServerModule.web.d.ts +10 -0
- package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/OtaServerModule.web.js +12 -0
- package/build/ota-server/OtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
- package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiver.types.js +2 -0
- package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.js +3 -0
- package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
- package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
- package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
- package/build/photo-receiver/index.d.ts +3 -0
- package/build/photo-receiver/index.d.ts.map +1 -0
- package/build/photo-receiver/index.js +3 -0
- package/build/photo-receiver/index.js.map +1 -0
- package/build/react/index.d.ts +5 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +4 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useBluetooth.d.ts +100 -0
- package/build/react/useBluetooth.d.ts.map +1 -0
- package/build/react/useBluetooth.js +167 -0
- package/build/react/useBluetooth.js.map +1 -0
- package/build/react/useBluetoothEvent.d.ts +6 -0
- package/build/react/useBluetoothEvent.d.ts.map +1 -0
- package/build/react/useBluetoothEvent.js +21 -0
- package/build/react/useBluetoothEvent.js.map +1 -0
- package/build/react/useBluetoothScan.d.ts +22 -0
- package/build/react/useBluetoothScan.d.ts.map +1 -0
- package/build/react/useBluetoothScan.js +135 -0
- package/build/react/useBluetoothScan.js.map +1 -0
- package/build/react/useBluetoothStatus.d.ts +16 -0
- package/build/react/useBluetoothStatus.d.ts.map +1 -0
- package/build/react/useBluetoothStatus.js +137 -0
- package/build/react/useBluetoothStatus.js.map +1 -0
- package/build/react/useGlassesConnection.d.ts +29 -0
- package/build/react/useGlassesConnection.d.ts.map +1 -0
- package/build/react/useGlassesConnection.js +153 -0
- package/build/react/useGlassesConnection.js.map +1 -0
- package/build/types/index.d.ts +3 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +2 -0
- package/build/types/index.js.map +1 -0
- package/expo-module.config.json +14 -0
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +1034 -0
- package/ios/GlasslyBluetoothSDK.podspec +106 -0
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/LocalPhotoUploadServer.swift +754 -0
- package/ios/OtaServerModule.swift +147 -0
- package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
- package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
- package/ios/Packages/CoreObjC/attdet.c +92 -0
- package/ios/Packages/CoreObjC/attdet.h +44 -0
- package/ios/Packages/CoreObjC/bits.c +375 -0
- package/ios/Packages/CoreObjC/bits.h +315 -0
- package/ios/Packages/CoreObjC/bwdet.c +129 -0
- package/ios/Packages/CoreObjC/bwdet.h +69 -0
- package/ios/Packages/CoreObjC/common.h +151 -0
- package/ios/Packages/CoreObjC/energy.c +70 -0
- package/ios/Packages/CoreObjC/energy.h +43 -0
- package/ios/Packages/CoreObjC/fastmath.h +158 -0
- package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
- package/ios/Packages/CoreObjC/lc3.c +704 -0
- package/ios/Packages/CoreObjC/lc3.h +313 -0
- package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
- package/ios/Packages/CoreObjC/lc3_private.h +163 -0
- package/ios/Packages/CoreObjC/ltpf.c +905 -0
- package/ios/Packages/CoreObjC/ltpf.h +111 -0
- package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
- package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
- package/ios/Packages/CoreObjC/makefile.mk +35 -0
- package/ios/Packages/CoreObjC/mdct.c +469 -0
- package/ios/Packages/CoreObjC/mdct.h +57 -0
- package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
- package/ios/Packages/CoreObjC/meson.build +61 -0
- package/ios/Packages/CoreObjC/plc.c +61 -0
- package/ios/Packages/CoreObjC/plc.h +57 -0
- package/ios/Packages/CoreObjC/rnnoise.h +114 -0
- package/ios/Packages/CoreObjC/sns.c +880 -0
- package/ios/Packages/CoreObjC/sns.h +103 -0
- package/ios/Packages/CoreObjC/spec.c +907 -0
- package/ios/Packages/CoreObjC/spec.h +119 -0
- package/ios/Packages/CoreObjC/tables.c +3457 -0
- package/ios/Packages/CoreObjC/tables.h +94 -0
- package/ios/Packages/CoreObjC/tns.c +457 -0
- package/ios/Packages/CoreObjC/tns.h +99 -0
- package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
- package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
- package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
- package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
- package/ios/Packages/libbz2/module.modulemap +5 -0
- package/ios/Packages/libbz2/shim.h +1 -0
- package/ios/PhotoReceiverModule.swift +129 -0
- package/ios/Source/BluetoothSDK.swift +2303 -0
- package/ios/Source/BluetoothSDKDebug.swift +12 -0
- package/ios/Source/BluetoothSdkDefaults.swift +29 -0
- package/ios/Source/Bridge.swift +708 -0
- package/ios/Source/Bridging-Header.h +16 -0
- package/ios/Source/DeviceManager.swift +2109 -0
- package/ios/Source/DeviceStore.swift +336 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/ObservableStore.swift +118 -0
- package/ios/Source/OtaManifest.swift +211 -0
- package/ios/Source/PrivacyInfo.xcprivacy +23 -0
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/Resources/G2Firmware/README.md +10 -0
- package/ios/Source/audio/AudioModels.swift +178 -0
- package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
- package/ios/Source/camera/CameraModels.swift +866 -0
- package/ios/Source/connection/ScanSession.swift +27 -0
- package/ios/Source/controllers/ControllerManager.swift +184 -0
- package/ios/Source/controllers/R1.swift +829 -0
- package/ios/Source/errors/BluetoothSdkError.swift +19 -0
- package/ios/Source/events/BluetoothEvents.swift +342 -0
- package/ios/Source/internal/BleTraceLogger.swift +192 -0
- package/ios/Source/internal/BluetoothAvailability.swift +78 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
- package/ios/Source/internal/ValueParsing.swift +99 -0
- package/ios/Source/requests/DisplayRequests.swift +82 -0
- package/ios/Source/services/PhoneMic.swift +662 -0
- package/ios/Source/sgcs/Ar99.swift +2451 -0
- package/ios/Source/sgcs/Frame.swift +649 -0
- package/ios/Source/sgcs/G1.swift +2554 -0
- package/ios/Source/sgcs/G2.swift +6887 -0
- package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
- package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
- package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
- package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
- package/ios/Source/sgcs/G2Flasher.swift +773 -0
- package/ios/Source/sgcs/Mach1.swift +541 -0
- package/ios/Source/sgcs/MentraLive.swift +7011 -0
- package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/MentraNex.swift +3221 -0
- package/ios/Source/sgcs/Nimo.swift +1886 -0
- package/ios/Source/sgcs/SGCManager.swift +608 -0
- package/ios/Source/sgcs/Simulated.swift +274 -0
- package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
- package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
- package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
- package/ios/Source/status/DeviceStatus.swift +981 -0
- package/ios/Source/status/RuntimeState.swift +359 -0
- package/ios/Source/status/WifiHotspotStatus.swift +345 -0
- package/ios/Source/streaming/StreamModels.swift +693 -0
- package/ios/Source/stt/STTTools.swift +191 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
- package/ios/Source/tts/TTSTools.swift +171 -0
- package/ios/Source/types/DeviceModels.swift +177 -0
- package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
- package/ios/Source/utils/BinaryCodec.swift +61 -0
- package/ios/Source/utils/BleJsonCompact.swift +405 -0
- package/ios/Source/utils/BleWireProtocol.swift +92 -0
- package/ios/Source/utils/Constants.swift +78 -0
- package/ios/Source/utils/Enums.swift +95 -0
- package/ios/Source/utils/G1Text.swift +2096 -0
- package/ios/Source/utils/JSCExperiment.swift +241 -0
- package/ios/Source/utils/MemoryMonitor.swift +44 -0
- package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
- package/ios/Source/utils/MessageChunker.swift +294 -0
- package/ios/Source/utils/Models.swift +135 -0
- package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
- package/ios/Source/utils/TarBz2Extractor.swift +257 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
- package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
- package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +129 -0
- package/plugin/build/index.d.ts +10 -0
- package/plugin/build/index.js +12 -0
- package/plugin/build/withAndroid.d.ts +3 -0
- package/plugin/build/withAndroid.js +190 -0
- package/plugin/build/withIos.d.ts +5 -0
- package/plugin/build/withIos.js +108 -0
- package/scripts/inject-ios-sdk-version.mjs +65 -0
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +45 -0
- package/scripts/write-release-metadata.mjs +152 -0
- package/scripts/write-release-metadata.test.mjs +37 -0
- package/src/BluetoothSdk.types.ts +1623 -0
- package/src/_internal.ts +16 -0
- package/src/_private/BluetoothSdkModule.ts +684 -0
- package/src/_private/LocalNetworkModule.ts +50 -0
- package/src/_private/cameraRequestPayload.ts +34 -0
- package/src/_private/photoRequestPayload.ts +90 -0
- package/src/_private/sdkOtaManifest.ts +18 -0
- package/src/changelogs.ts +46 -0
- package/src/debug.ts +9 -0
- package/src/generated/changelogCatalog.ts +13 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +321 -0
- package/src/ota-server/OtaServer.types.ts +10 -0
- package/src/ota-server/OtaServerModule.ts +23 -0
- package/src/ota-server/OtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
- package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
- package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
- package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
- package/src/photo-receiver/index.ts +2 -0
- package/src/react/index.ts +21 -0
- package/src/react/useBluetooth.ts +309 -0
- package/src/react/useBluetoothEvent.ts +35 -0
- package/src/react/useBluetoothScan.ts +173 -0
- package/src/react/useBluetoothStatus.ts +180 -0
- package/src/react/useGlassesConnection.ts +202 -0
- package/src/types/index.ts +12 -0
|
@@ -0,0 +1,2303 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
@MainActor
|
|
5
|
+
private final class ActiveStreamKeepAlive {
|
|
6
|
+
let streamId: String
|
|
7
|
+
let intervalSeconds: Int
|
|
8
|
+
var pendingAckId: String?
|
|
9
|
+
var missedAckCount = 0
|
|
10
|
+
var task: Task<Void, Never>?
|
|
11
|
+
/// Missed-ACK counting only begins once the stream is confirmed live/coming up, so a slow
|
|
12
|
+
/// startup (glasses can't ACK until they reach starting/streaming) can't trip a false
|
|
13
|
+
/// keep-alive timeout before the stream is ever up.
|
|
14
|
+
var armed = false
|
|
15
|
+
|
|
16
|
+
init(streamId: String, intervalSeconds: Int) {
|
|
17
|
+
self.streamId = streamId
|
|
18
|
+
self.intervalSeconds = intervalSeconds
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@MainActor
|
|
23
|
+
private final class ActiveScanSession {
|
|
24
|
+
let model: DeviceModel
|
|
25
|
+
let onResults: ([Device]) -> Void
|
|
26
|
+
let onComplete: ([Device]) -> Void
|
|
27
|
+
var latestResults: [Device] = []
|
|
28
|
+
var timeoutTask: Task<Void, Never>?
|
|
29
|
+
weak var publicSession: ScanSession?
|
|
30
|
+
|
|
31
|
+
init(
|
|
32
|
+
model: DeviceModel,
|
|
33
|
+
onResults: @escaping ([Device]) -> Void,
|
|
34
|
+
onComplete: @escaping ([Device]) -> Void
|
|
35
|
+
) {
|
|
36
|
+
self.model = model
|
|
37
|
+
self.onResults = onResults
|
|
38
|
+
self.onComplete = onComplete
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@MainActor
|
|
43
|
+
private final class PendingWifiScan {
|
|
44
|
+
let pending: PendingResponse<[WifiScanResult]>
|
|
45
|
+
let scanId: String
|
|
46
|
+
var latestResults: [WifiScanResult] = []
|
|
47
|
+
/// Chunks accumulated from scanId-echoing glasses, deduplicated by SSID;
|
|
48
|
+
/// resolved only when the glasses flag the scan complete.
|
|
49
|
+
var accumulated: [WifiScanResult] = []
|
|
50
|
+
|
|
51
|
+
init(pending: PendingResponse<[WifiScanResult]>, scanId: String) {
|
|
52
|
+
self.pending = pending
|
|
53
|
+
self.scanId = scanId
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private enum WifiStatusOperation {
|
|
58
|
+
case connect
|
|
59
|
+
case forget
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@MainActor
|
|
63
|
+
private final class PendingWifiStatusRequest {
|
|
64
|
+
let operation: WifiStatusOperation
|
|
65
|
+
let ssid: String
|
|
66
|
+
let pending: PendingResponse<WifiStatusEvent>
|
|
67
|
+
|
|
68
|
+
init(operation: WifiStatusOperation, ssid: String, pending: PendingResponse<WifiStatusEvent>) {
|
|
69
|
+
self.operation = operation
|
|
70
|
+
self.ssid = ssid
|
|
71
|
+
self.pending = pending
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@MainActor
|
|
76
|
+
private final class PendingHotspotStatusRequest {
|
|
77
|
+
let enabled: Bool
|
|
78
|
+
let pending: PendingResponse<HotspotStatusEvent>
|
|
79
|
+
|
|
80
|
+
init(enabled: Bool, pending: PendingResponse<HotspotStatusEvent>) {
|
|
81
|
+
self.enabled = enabled
|
|
82
|
+
self.pending = pending
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@MainActor
|
|
87
|
+
private final class PendingVideoRecordingRequest {
|
|
88
|
+
let expectedStatus: String
|
|
89
|
+
let pending: PendingResponse<VideoRecordingStatusEvent>
|
|
90
|
+
let waitForUpload: Bool
|
|
91
|
+
var stoppedEvent: VideoRecordingStatusEvent?
|
|
92
|
+
var uploadSucceeded = false
|
|
93
|
+
|
|
94
|
+
init(expectedStatus: String, pending: PendingResponse<VideoRecordingStatusEvent>, waitForUpload: Bool = false) {
|
|
95
|
+
self.expectedStatus = expectedStatus
|
|
96
|
+
self.pending = pending
|
|
97
|
+
self.waitForUpload = waitForUpload
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// seq records send order (assigned and handed to the BLE queue in a single
|
|
102
|
+
/// MainActor turn) so that an id-carrying status for a newer start can
|
|
103
|
+
/// identify which older in-flight starts it preempted.
|
|
104
|
+
@MainActor
|
|
105
|
+
private final class PendingStreamStart {
|
|
106
|
+
let seq: Int
|
|
107
|
+
let pending: PendingResponse<StreamStatusEvent>
|
|
108
|
+
/// Set when an id-carrying error arrives: a fatal publisher error never
|
|
109
|
+
/// reaches the reconnect machinery and winds down with a streamId-less
|
|
110
|
+
/// stopped, so the stash both attributes that stopped to this start and
|
|
111
|
+
/// preserves the real error details for the rejection.
|
|
112
|
+
var lastError: StreamStatusEvent?
|
|
113
|
+
|
|
114
|
+
init(seq: Int, pending: PendingResponse<StreamStatusEvent>) {
|
|
115
|
+
self.seq = seq
|
|
116
|
+
self.pending = pending
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@MainActor
|
|
121
|
+
private final class PendingResponse<T> {
|
|
122
|
+
private let operation: String
|
|
123
|
+
private var continuation: CheckedContinuation<T, Error>?
|
|
124
|
+
private var timeoutTask: Task<Void, Never>?
|
|
125
|
+
private var result: Result<T, Error>?
|
|
126
|
+
private var completed = false
|
|
127
|
+
|
|
128
|
+
init(operation: String) {
|
|
129
|
+
self.operation = operation
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func resolve(_ value: T) {
|
|
133
|
+
guard !completed else { return }
|
|
134
|
+
completed = true
|
|
135
|
+
result = .success(value)
|
|
136
|
+
timeoutTask?.cancel()
|
|
137
|
+
continuation?.resume(returning: value)
|
|
138
|
+
continuation = nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func reject(_ error: Error) {
|
|
142
|
+
guard !completed else { return }
|
|
143
|
+
completed = true
|
|
144
|
+
result = .failure(error)
|
|
145
|
+
timeoutTask?.cancel()
|
|
146
|
+
continuation?.resume(throwing: error)
|
|
147
|
+
continuation = nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func wait(timeoutMs: Int = 15000) async throws -> T {
|
|
151
|
+
if let result {
|
|
152
|
+
return try result.get()
|
|
153
|
+
}
|
|
154
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
155
|
+
self.continuation = continuation
|
|
156
|
+
timeoutTask = Task { @MainActor [weak self] in
|
|
157
|
+
do {
|
|
158
|
+
try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
|
|
159
|
+
} catch {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
self?.reject(
|
|
163
|
+
BluetoothSdkError(
|
|
164
|
+
code: "request_timeout",
|
|
165
|
+
message: "\(self?.operation ?? "Request") timed out waiting for glasses response."
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@MainActor
|
|
174
|
+
public final class BluetoothSDK {
|
|
175
|
+
private static let wifiScanTimeoutMs = 20000
|
|
176
|
+
// A photo response is terminal only after capture, encoding, transport, and upload.
|
|
177
|
+
// Max-quality BLE fallback can legitimately exceed the generic command deadline.
|
|
178
|
+
private static let photoRequestTimeoutMs = 30000
|
|
179
|
+
private static let otaBesVersionWaitMs = 5000
|
|
180
|
+
private static let otaMtkVersionWaitMs = 2000
|
|
181
|
+
private static let otaVersionPollMs = 100
|
|
182
|
+
private static let defaultStreamKeepAliveIntervalSeconds = 5
|
|
183
|
+
|
|
184
|
+
public weak var delegate: BluetoothSDKDelegate?
|
|
185
|
+
|
|
186
|
+
private let configuration: BluetoothSDKConfiguration
|
|
187
|
+
private var discoveredDeviceNames = Set<String>()
|
|
188
|
+
private var bluetoothAvailabilityListenerId: UUID?
|
|
189
|
+
private var shouldRestoreGlassesOnBluetoothRestore = false
|
|
190
|
+
private var shouldRestoreControllerOnBluetoothRestore = false
|
|
191
|
+
private var bridgeEventSinkId: String?
|
|
192
|
+
private var storeListenerId: String?
|
|
193
|
+
private let defaultDeviceKeys: Set<String> = ["default_wearable", "device_name", "device_address", "project_name"]
|
|
194
|
+
private let videoUploadStopTimeoutMs = 10 * 60 * 1000
|
|
195
|
+
private var suppressDefaultDeviceEvents = false
|
|
196
|
+
private var defaultDeviceApplyGeneration = 0
|
|
197
|
+
private var activeScanSessions: [UUID: ActiveScanSession] = [:]
|
|
198
|
+
private var activeStreamKeepAlive: ActiveStreamKeepAlive?
|
|
199
|
+
private let analytics: BluetoothSdkAnalytics
|
|
200
|
+
private var pendingPhotoRequests: [String: PendingResponse<PhotoResponseEvent>] = [:]
|
|
201
|
+
private var pendingCameraStatusRequests: [String: PendingResponse<CameraStatusEvent>] = [:]
|
|
202
|
+
private var pendingVideoRecordingRequests: [String: PendingVideoRecordingRequest] = [:]
|
|
203
|
+
private var pendingRgbLedRequests: [String: PendingResponse<RgbLedControlResponseEvent>] = [:]
|
|
204
|
+
private var pendingSettingsRequests: [String: PendingResponse<SettingsAckEvent>] = [:]
|
|
205
|
+
private var pendingStreamStarts: [String: PendingStreamStart] = [:]
|
|
206
|
+
private var streamStartSeq = 0
|
|
207
|
+
// seq is the stop's slot in the shared streamStartSeq order (drawn in the
|
|
208
|
+
// same await-free MainActor turn that sends the command): every pending
|
|
209
|
+
// start with a lower seq reached the glasses' FIFO before this stop.
|
|
210
|
+
private var pendingStreamStop: (streamId: String?, seq: Int, pending: PendingResponse<StreamStatusEvent>)?
|
|
211
|
+
private var pendingGalleryStatus: PendingResponse<GalleryStatusEvent>?
|
|
212
|
+
private var pendingOtaQuery: PendingResponse<OtaQueryResult>?
|
|
213
|
+
private var pendingOtaStart: PendingResponse<OtaStartAckEvent>?
|
|
214
|
+
private var pendingWifiScan: PendingWifiScan?
|
|
215
|
+
private var wifiScanTask: Task<[WifiScanResult], Error>?
|
|
216
|
+
private var pendingWifiStatus: PendingWifiStatusRequest?
|
|
217
|
+
private var pendingHotspotStatus: PendingHotspotStatusRequest?
|
|
218
|
+
private var pendingVersionInfo: PendingResponse<VersionInfoResult>?
|
|
219
|
+
private var configuredOtaVersionUrl: String?
|
|
220
|
+
|
|
221
|
+
public init(configuration: BluetoothSDKConfiguration = .default) {
|
|
222
|
+
self.configuration = configuration
|
|
223
|
+
analytics = BluetoothSdkAnalytics(configuration: configuration.analytics)
|
|
224
|
+
bluetoothAvailabilityListenerId = BluetoothAvailability.shared.addStateListener { [weak self] state in
|
|
225
|
+
Task { @MainActor [weak self] in
|
|
226
|
+
self?.handleBluetoothAvailability(state)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
bridgeEventSinkId = Bridge.addEventSink { [weak self] eventName, data in
|
|
230
|
+
// Bridge.dispatchEvent always invokes sinks on the main thread, in the
|
|
231
|
+
// FIFO order events were dispatched (correlated WiFi-scan chunks rely
|
|
232
|
+
// on that order). A fresh Task per event can reach the MainActor out
|
|
233
|
+
// of creation order and reorder chunks, so consume synchronously
|
|
234
|
+
// while still on main.
|
|
235
|
+
if Thread.isMainThread {
|
|
236
|
+
MainActor.assumeIsolated {
|
|
237
|
+
self?.dispatchBridgeEvent(eventName, data)
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// Defensive fallback only — Bridge.dispatchEvent should never take
|
|
241
|
+
// this path. A serial main-queue hop still preserves FIFO order,
|
|
242
|
+
// unlike an unstructured Task.
|
|
243
|
+
DispatchQueue.main.async {
|
|
244
|
+
MainActor.assumeIsolated {
|
|
245
|
+
self?.dispatchBridgeEvent(eventName, data)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
storeListenerId = DeviceStore.shared.store.addListener { [weak self] category, changes in
|
|
251
|
+
Task { @MainActor [weak self] in
|
|
252
|
+
self?.dispatchStoreUpdate(category, changes)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
analytics.initializeGlassesStatus(glassesStatus)
|
|
256
|
+
analytics.captureStarted()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public var state: BluetoothState {
|
|
260
|
+
BluetoothState(glassesStatus: glassesStatus, bluetoothStatus: bluetoothStatus)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public var glasses: GlassesRuntimeState {
|
|
264
|
+
state.glasses
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public var sdkState: PhoneSdkRuntimeState {
|
|
268
|
+
state.sdk
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public var scanState: BluetoothScanState {
|
|
272
|
+
state.scan
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var glassesStatus: GlassesStatus {
|
|
276
|
+
GlassesStatus(values: DeviceStore.shared.store.getCategory("glasses"))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
var bluetoothStatus: BluetoothStatus {
|
|
280
|
+
BluetoothStatus(values: DeviceStore.shared.store.getCategory(ObservableStore.bluetoothCategory))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public var defaultDevice: Device? {
|
|
284
|
+
currentDefaultDevice()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private func requireGlassesConnected(operation: String) throws {
|
|
288
|
+
guard glassesStatus.connected else {
|
|
289
|
+
throw BluetoothSdkError(
|
|
290
|
+
code: "glasses_not_connected",
|
|
291
|
+
message: "Cannot \(operation) because glasses are not connected."
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public func getDefaultDevice() -> Device? {
|
|
297
|
+
currentDefaultDevice()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public func setDefaultDevice(_ device: Device?) {
|
|
301
|
+
guard let device else {
|
|
302
|
+
clearDefaultDevice()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
defaultDeviceApplyGeneration += 1
|
|
306
|
+
let generation = defaultDeviceApplyGeneration
|
|
307
|
+
suppressDefaultDeviceEvents = true
|
|
308
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", device.model.deviceType)
|
|
309
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", device.name)
|
|
310
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", device.identifier ?? "")
|
|
311
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", device.projectName ?? "")
|
|
312
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
313
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
314
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
|
|
315
|
+
finishDefaultDeviceApply(generation: generation)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public func clearDefaultDevice() {
|
|
319
|
+
defaultDeviceApplyGeneration += 1
|
|
320
|
+
let generation = defaultDeviceApplyGeneration
|
|
321
|
+
suppressDefaultDeviceEvents = true
|
|
322
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "default_wearable", "")
|
|
323
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_name", "")
|
|
324
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "device_address", "")
|
|
325
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "project_name", "")
|
|
326
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
327
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
328
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
|
|
329
|
+
finishDefaultDeviceApply(generation: generation)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public func startScan(model: DeviceModel) throws {
|
|
333
|
+
if model != .simulated {
|
|
334
|
+
try BluetoothAvailability.shared.requirePoweredOn(operation: "scan for glasses")
|
|
335
|
+
}
|
|
336
|
+
discoveredDeviceNames.removeAll()
|
|
337
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", true)
|
|
338
|
+
DeviceManager.shared.findCompatibleDevices(model.deviceType)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public func stopScan() {
|
|
342
|
+
stopScan(reason: .cancelled)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private func stopScan(reason: ScanStopReason) {
|
|
346
|
+
DeviceManager.shared.stopScan()
|
|
347
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", false)
|
|
348
|
+
delegate?.bluetoothSDK(self, didStopScan: reason)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@discardableResult
|
|
352
|
+
public func scan(
|
|
353
|
+
model: DeviceModel,
|
|
354
|
+
timeout: TimeInterval = 15,
|
|
355
|
+
onResults: @escaping ([Device]) -> Void,
|
|
356
|
+
onComplete: @escaping ([Device]) -> Void = { _ in }
|
|
357
|
+
) throws -> ScanSession {
|
|
358
|
+
let normalizedTimeout = timeout > 0 && timeout.isFinite ? timeout : 15
|
|
359
|
+
let id = UUID()
|
|
360
|
+
let activeSession = ActiveScanSession(
|
|
361
|
+
model: model,
|
|
362
|
+
onResults: onResults,
|
|
363
|
+
onComplete: onComplete
|
|
364
|
+
)
|
|
365
|
+
let publicSession = ScanSession { [weak self] in
|
|
366
|
+
self?.finishScanSession(id, reason: .cancelled, shouldStopScan: true)
|
|
367
|
+
}
|
|
368
|
+
activeSession.publicSession = publicSession
|
|
369
|
+
activeScanSessions[id] = activeSession
|
|
370
|
+
|
|
371
|
+
do {
|
|
372
|
+
emitScanResults([], forSession: id)
|
|
373
|
+
try startScan(model: model)
|
|
374
|
+
emitScanResults(bluetoothStatus.searchResults.filter { $0.model == model }, forSession: id)
|
|
375
|
+
activeSession.timeoutTask = Task { [weak self] in
|
|
376
|
+
let nanoseconds = UInt64(normalizedTimeout * 1_000_000_000)
|
|
377
|
+
try? await Task.sleep(nanoseconds: nanoseconds)
|
|
378
|
+
await self?.finishScanSession(id, reason: .completed, shouldStopScan: true)
|
|
379
|
+
}
|
|
380
|
+
return publicSession
|
|
381
|
+
} catch {
|
|
382
|
+
activeScanSessions[id] = nil
|
|
383
|
+
publicSession.markStopped()
|
|
384
|
+
throw error
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public func connect(to device: Device, options: ConnectOptions = ConnectOptions()) throws {
|
|
389
|
+
clearBluetoothRestoreIntent()
|
|
390
|
+
if device.model != .simulated {
|
|
391
|
+
try BluetoothAvailability.shared.requirePoweredOn(operation: "connect to glasses")
|
|
392
|
+
}
|
|
393
|
+
let isController = ControllerTypes.ALL.contains(device.model.deviceType)
|
|
394
|
+
if options.cancelExistingConnectionAttempt {
|
|
395
|
+
if isController {
|
|
396
|
+
DeviceManager.shared.disconnectController()
|
|
397
|
+
} else {
|
|
398
|
+
cancelConnectionAttempt()
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if options.saveAsDefault && !isController {
|
|
402
|
+
setDefaultDevice(device)
|
|
403
|
+
} else if !isController {
|
|
404
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", device.name)
|
|
405
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", device.identifier ?? "")
|
|
406
|
+
DeviceStore.shared.apply(
|
|
407
|
+
ObservableStore.bluetoothCategory,
|
|
408
|
+
"pending_device_secure_pairing_capable",
|
|
409
|
+
device.securePairingCapable ?? ""
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_wearable", device.model.deviceType)
|
|
413
|
+
DeviceManager.shared.connectByName(device.name)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
public func connectDefault(options: ConnectOptions = ConnectOptions()) throws {
|
|
417
|
+
clearBluetoothRestoreIntent()
|
|
418
|
+
guard let device = currentDefaultDevice() else {
|
|
419
|
+
throw BluetoothSdkError(
|
|
420
|
+
code: "default_device_missing",
|
|
421
|
+
message: "Set a default glasses device before calling connectDefault."
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
if device.model != .simulated {
|
|
425
|
+
try BluetoothAvailability.shared.requirePoweredOn(operation: "connect to glasses")
|
|
426
|
+
}
|
|
427
|
+
if options.cancelExistingConnectionAttempt {
|
|
428
|
+
cancelConnectionAttempt()
|
|
429
|
+
}
|
|
430
|
+
DeviceManager.shared.connectDefault()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public func cancelConnectionAttempt() {
|
|
434
|
+
clearBluetoothRestoreIntent()
|
|
435
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
436
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
437
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
|
|
438
|
+
DeviceManager.shared.disconnect()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
func connectSimulated() {
|
|
442
|
+
clearBluetoothRestoreIntent()
|
|
443
|
+
DeviceManager.shared.connectSimulated()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
public func disconnect() {
|
|
447
|
+
clearBluetoothRestoreIntent()
|
|
448
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_name", "")
|
|
449
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_address", "")
|
|
450
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "pending_device_secure_pairing_capable", "")
|
|
451
|
+
DeviceManager.shared.disconnect()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public func forget() {
|
|
455
|
+
clearBluetoothRestoreIntent()
|
|
456
|
+
DeviceManager.shared.forget()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public func displayText(_ text: String, x: Int = 0, y: Int = 0, size: Int = 24) async throws {
|
|
460
|
+
try await displayText(DisplayTextRequest(text: text, x: x, y: y, size: size))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
public func displayText(_ request: DisplayTextRequest) async throws {
|
|
464
|
+
DeviceManager.shared.displayText(request.dictionary)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
func displayEvent(_ request: DisplayEventRequest) async throws {
|
|
468
|
+
DeviceManager.shared.displayEvent(request.values)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// MARK: - Native navigation (glasses-rendered nav UI)
|
|
472
|
+
|
|
473
|
+
func navigationModeStart() {
|
|
474
|
+
DeviceManager.shared.navigationModeStart()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func navigationModeStop(arrived: Bool) {
|
|
478
|
+
DeviceManager.shared.navigationModeStop(arrived: arrived)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func navigationUpdateInfo(_ params: [String: Any]) {
|
|
482
|
+
DeviceManager.shared.navigationUpdateInfo(params)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func navigationMiniMap(_ params: [String: Any]) {
|
|
486
|
+
DeviceManager.shared.navigationMiniMap(params)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func navigationOverviewMap(_ params: [String: Any]) {
|
|
490
|
+
DeviceManager.shared.navigationOverviewMap(params)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public func clearDisplay() async throws {
|
|
494
|
+
DeviceManager.shared.sgc?.clearDisplay()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
public func showDashboard() {
|
|
498
|
+
DeviceManager.shared.showDashboard()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public func toggleHeadUp() {
|
|
502
|
+
DeviceManager.shared.toggleHeadUp()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public func showNotificationsPanel() {
|
|
506
|
+
DeviceManager.shared.showNotificationsPanel()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
func setBrightness(_ level: Int, autoMode: Bool? = nil) async throws {
|
|
510
|
+
if let autoMode {
|
|
511
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_brightness", autoMode)
|
|
512
|
+
}
|
|
513
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "brightness", level)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
func setAutoBrightness(enabled: Bool) async throws {
|
|
517
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "auto_brightness", enabled)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public func setDashboardPosition(height: Int, depth: Int) async throws {
|
|
521
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "dashboard_height", height)
|
|
522
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "dashboard_depth", depth)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
public func setDashboardPosition(_ request: DashboardPositionRequest) async throws {
|
|
526
|
+
try await setDashboardPosition(height: request.height, depth: request.depth)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
func setDashboardMenu(_ items: [DashboardMenuItem]) async throws {
|
|
530
|
+
DeviceStore.shared.apply(
|
|
531
|
+
ObservableStore.bluetoothCategory,
|
|
532
|
+
"menu_apps",
|
|
533
|
+
items.map(\.dictionary)
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
func setCalendarEvents(_ events: [CalendarEvent]) async throws {
|
|
538
|
+
DeviceStore.shared.apply(
|
|
539
|
+
ObservableStore.bluetoothCategory,
|
|
540
|
+
"calendar_events",
|
|
541
|
+
events.map(\.dictionary)
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
public func setHeadUpAngle(_ angleDegrees: Int) async throws {
|
|
546
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "head_up_angle", angleDegrees)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public func setScreenDisabled(_ disabled: Bool) async throws {
|
|
550
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "screen_disabled", disabled)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
public func setGalleryModeEnabled(_ enabled: Bool) async throws -> SettingsAckEvent {
|
|
554
|
+
try await performSettingsCommand(
|
|
555
|
+
setting: "gallery_mode",
|
|
556
|
+
updateStore: { _ in DeviceStore.shared.set(ObservableStore.bluetoothCategory, "gallery_mode", enabled) },
|
|
557
|
+
send: { requestId in try DeviceManager.shared.sendGalleryMode(requestId: requestId, enabled: enabled) }
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private func performSettingsCommand(
|
|
562
|
+
setting: String,
|
|
563
|
+
updateStore: (SettingsAckEvent) -> Void,
|
|
564
|
+
send: (String) throws -> Void
|
|
565
|
+
) async throws -> SettingsAckEvent {
|
|
566
|
+
let requestId = "settings-\(setting)-\(UUID().uuidString)"
|
|
567
|
+
let pending = PendingResponse<SettingsAckEvent>(operation: "set \(setting)")
|
|
568
|
+
pendingSettingsRequests[requestId] = pending
|
|
569
|
+
do {
|
|
570
|
+
try send(requestId)
|
|
571
|
+
let ack = try await pending.wait()
|
|
572
|
+
updateStore(ack)
|
|
573
|
+
pendingSettingsRequests.removeValue(forKey: requestId)
|
|
574
|
+
return ack
|
|
575
|
+
} catch {
|
|
576
|
+
pendingSettingsRequests.removeValue(forKey: requestId)
|
|
577
|
+
throw error
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
public func setVoiceActivityDetectionEnabled(_ enabled: Bool) async throws {
|
|
582
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "voice_activity_detection_enabled", enabled)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
public func setLoudnessGateEnabled(_ enabled: Bool) async throws {
|
|
586
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "loudness_gate_enabled", enabled)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@available(*, deprecated, message: "Sticky action-button photo presets are deprecated. Prefer per-request requestPhoto(...) options (e.g. mode .text for text sensor size/crop, or explicit per-shot fields). This method still works but will be removed in a future release.")
|
|
590
|
+
public func setPhotoCaptureDefaults(_ settings: PhotoCaptureDefaults) async throws -> SettingsAckEvent {
|
|
591
|
+
try await performSettingsCommand(
|
|
592
|
+
setting: "button_photo",
|
|
593
|
+
updateStore: { _ in
|
|
594
|
+
if settings.resetCaptureTuning == true {
|
|
595
|
+
// Mirror Android: clear all cached scan-tuning keys so reconnect sync
|
|
596
|
+
// does not replay stale values after a reset.
|
|
597
|
+
let cat = ObservableStore.bluetoothCategory
|
|
598
|
+
for key in ["button_photo_zsl_mfnr", "button_photo_mfnr", "button_photo_zsl", "button_photo_noise_reduction",
|
|
599
|
+
"button_photo_edge_enhancement", "button_photo_isp_digital_gain",
|
|
600
|
+
"button_photo_isp_analog_gain", "button_photo_ae_exposure_divisor",
|
|
601
|
+
"button_photo_iso_cap", "button_photo_compress", "button_photo_sound"]
|
|
602
|
+
{
|
|
603
|
+
DeviceStore.shared.remove(cat, key)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if let size = settings.size {
|
|
607
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_size", size.rawValue)
|
|
608
|
+
}
|
|
609
|
+
if let mfnr = settings.mfnr {
|
|
610
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_mfnr", mfnr)
|
|
611
|
+
}
|
|
612
|
+
if let zsl = settings.zsl {
|
|
613
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_zsl", zsl)
|
|
614
|
+
}
|
|
615
|
+
if let noiseReduction = settings.noiseReduction {
|
|
616
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_noise_reduction", noiseReduction)
|
|
617
|
+
}
|
|
618
|
+
if let edgeEnhancement = settings.edgeEnhancement {
|
|
619
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_edge_enhancement", edgeEnhancement)
|
|
620
|
+
}
|
|
621
|
+
if let ispDigitalGain = settings.ispDigitalGain {
|
|
622
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_isp_digital_gain", ispDigitalGain)
|
|
623
|
+
}
|
|
624
|
+
if let ispAnalogGain = settings.ispAnalogGain {
|
|
625
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_isp_analog_gain", ispAnalogGain)
|
|
626
|
+
}
|
|
627
|
+
if let aeExposureDivisor = settings.aeExposureDivisor {
|
|
628
|
+
DeviceStore.shared.set(
|
|
629
|
+
ObservableStore.bluetoothCategory,
|
|
630
|
+
"button_photo_ae_exposure_divisor",
|
|
631
|
+
aeExposureDivisor
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
if let isoCap = settings.isoCap {
|
|
635
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_iso_cap", isoCap)
|
|
636
|
+
}
|
|
637
|
+
if let compress = settings.compress {
|
|
638
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_compress", compress)
|
|
639
|
+
}
|
|
640
|
+
if let sound = settings.sound {
|
|
641
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_photo_sound", sound)
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
send: { requestId in
|
|
645
|
+
try DeviceManager.shared.sendButtonPhotoSettings(requestId: requestId, settings: settings)
|
|
646
|
+
}
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
public func setVideoRecordingDefaults(_ defaults: VideoRecordingDefaults) async throws -> SettingsAckEvent {
|
|
651
|
+
try await performSettingsCommand(
|
|
652
|
+
setting: "button_video_recording",
|
|
653
|
+
updateStore: { _ in
|
|
654
|
+
DeviceStore.shared.set(
|
|
655
|
+
ObservableStore.bluetoothCategory,
|
|
656
|
+
"button_video_settings",
|
|
657
|
+
["width": defaults.width, "height": defaults.height, "fps": defaults.fps]
|
|
658
|
+
)
|
|
659
|
+
// Keep legacy cache keys readable for older internal callers during migration.
|
|
660
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_width", defaults.width)
|
|
661
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_height", defaults.height)
|
|
662
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_video_fps", defaults.fps)
|
|
663
|
+
},
|
|
664
|
+
send: { requestId in
|
|
665
|
+
try DeviceManager.shared.sendButtonVideoRecordingSettings(
|
|
666
|
+
requestId: requestId,
|
|
667
|
+
width: defaults.width,
|
|
668
|
+
height: defaults.height,
|
|
669
|
+
fps: defaults.fps
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
public func setMaxVideoRecordingDuration(minutes: Int) async throws -> SettingsAckEvent {
|
|
676
|
+
try await performSettingsCommand(
|
|
677
|
+
setting: "button_max_recording_time",
|
|
678
|
+
updateStore: { _ in
|
|
679
|
+
DeviceStore.shared.set(ObservableStore.bluetoothCategory, "button_max_recording_time", minutes)
|
|
680
|
+
},
|
|
681
|
+
send: { requestId in
|
|
682
|
+
try DeviceManager.shared.sendButtonMaxRecordingTime(requestId: requestId, minutes: minutes)
|
|
683
|
+
}
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
public func setCameraFov(_ fov: CameraFov) async throws -> CameraFovResult {
|
|
688
|
+
let ack = try await performSettingsCommand(
|
|
689
|
+
setting: "camera_fov",
|
|
690
|
+
updateStore: { _ in },
|
|
691
|
+
send: { requestId in
|
|
692
|
+
try DeviceManager.shared.sendCameraFovSetting(
|
|
693
|
+
requestId: requestId,
|
|
694
|
+
fov: fov.fov,
|
|
695
|
+
roiPosition: fov.roiPosition.rawValue
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
let result = try CameraFovResult.from(ack: ack, fallback: fov)
|
|
700
|
+
DeviceStore.shared.set(
|
|
701
|
+
ObservableStore.bluetoothCategory,
|
|
702
|
+
"camera_fov",
|
|
703
|
+
["fov": result.fov, "roi_position": result.roiPosition.rawValue]
|
|
704
|
+
)
|
|
705
|
+
return result
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/// Compatibility path for ASG clients that support only the older one-way FOV command.
|
|
709
|
+
public func setLegacyCameraFov(_ fov: CameraFov) throws -> CameraFovResult {
|
|
710
|
+
try DeviceManager.shared.sendLegacyCameraFovSetting(
|
|
711
|
+
fov: fov.fov,
|
|
712
|
+
roiPosition: fov.roiPosition.rawValue
|
|
713
|
+
)
|
|
714
|
+
return CameraFovResult(
|
|
715
|
+
requestId: "legacy",
|
|
716
|
+
fov: fov.fov,
|
|
717
|
+
roiPosition: fov.roiPosition,
|
|
718
|
+
timestamp: Int(Date().timeIntervalSince1970 * 1000)
|
|
719
|
+
)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/// Restores the persistent base FOV without requiring a settings acknowledgement.
|
|
723
|
+
public func restoreLegacyCameraFov() throws {
|
|
724
|
+
try DeviceManager.shared.restoreLegacyCameraFovSetting()
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
public func setCameraFovOverride(
|
|
728
|
+
leaseId: String,
|
|
729
|
+
fov: CameraFov,
|
|
730
|
+
ttlMs: Int
|
|
731
|
+
) async throws -> CameraFovResult {
|
|
732
|
+
let ack = try await performSettingsCommand(
|
|
733
|
+
setting: "camera_fov_override",
|
|
734
|
+
updateStore: { _ in },
|
|
735
|
+
send: { requestId in
|
|
736
|
+
try DeviceManager.shared.sendCameraFovOverride(
|
|
737
|
+
requestId: requestId,
|
|
738
|
+
leaseId: leaseId,
|
|
739
|
+
fov: fov.fov,
|
|
740
|
+
roiPosition: fov.roiPosition.rawValue,
|
|
741
|
+
ttlMs: ttlMs
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
return try CameraFovResult.from(ack: ack, fallback: fov)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
public func releaseCameraFovOverride(leaseId: String) async throws -> SettingsAckEvent {
|
|
749
|
+
try await performSettingsCommand(
|
|
750
|
+
setting: "camera_fov_override",
|
|
751
|
+
updateStore: { _ in },
|
|
752
|
+
send: { requestId in
|
|
753
|
+
try DeviceManager.shared.releaseCameraFovOverride(
|
|
754
|
+
requestId: requestId,
|
|
755
|
+
leaseId: leaseId
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
public func setCameraTuningConfig(anrOn: Bool, gainOn: Bool) async throws -> SettingsAckEvent {
|
|
762
|
+
return try await performSettingsCommand(
|
|
763
|
+
setting: "camera_tuning",
|
|
764
|
+
updateStore: { _ in },
|
|
765
|
+
send: { requestId in
|
|
766
|
+
try DeviceManager.shared.sendCameraTuningConfig(
|
|
767
|
+
requestId: requestId,
|
|
768
|
+
anrOn: anrOn,
|
|
769
|
+
gainOn: gainOn
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
public func setMicState(
|
|
776
|
+
enabled: Bool,
|
|
777
|
+
useGlassesMic: Bool = true,
|
|
778
|
+
sendTranscript: Bool = false,
|
|
779
|
+
sendLc3Data: Bool = false
|
|
780
|
+
) {
|
|
781
|
+
if enabled {
|
|
782
|
+
DeviceStore.shared.apply(
|
|
783
|
+
ObservableStore.bluetoothCategory,
|
|
784
|
+
"preferred_mic",
|
|
785
|
+
useGlassesMic ? MicPreference.glasses.rawValue : MicPreference.phone.rawValue
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
applyMicState(
|
|
789
|
+
sendPcmData: enabled,
|
|
790
|
+
sendTranscript: enabled && sendTranscript,
|
|
791
|
+
sendLc3Data: enabled && sendLc3Data
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private func applyMicState(
|
|
796
|
+
sendPcmData: Bool,
|
|
797
|
+
sendTranscript: Bool,
|
|
798
|
+
sendLc3Data: Bool
|
|
799
|
+
) {
|
|
800
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_pcm", sendPcmData)
|
|
801
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_lc3", sendLc3Data)
|
|
802
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "should_send_transcript", sendTranscript)
|
|
803
|
+
DeviceManager.shared.setMicState()
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
public func setPreferredMic(_ preferredMic: MicPreference) {
|
|
807
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "preferred_mic", preferredMic.rawValue)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
public func setOwnAppAudioPlaying(_ playing: Bool) {
|
|
811
|
+
PhoneAudioMonitor.getInstance().setOwnAppAudioPlaying(playing)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
public func getGlassesMediaVolume() async throws -> GlassesMediaVolumeGetResult {
|
|
815
|
+
try GlassesMediaVolumeGetResult(values: await DeviceManager.shared.getGlassesMediaVolume())
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
public func setGlassesMediaVolume(_ level: Int) async throws -> GlassesMediaVolumeSetResult {
|
|
819
|
+
guard (0 ... 15).contains(level) else {
|
|
820
|
+
throw BluetoothSdkError(
|
|
821
|
+
code: "invalid_volume_level",
|
|
822
|
+
message: "Glasses media volume must be between 0 and 15."
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
return try GlassesMediaVolumeSetResult(values: await DeviceManager.shared.setGlassesMediaVolume(level: level))
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
public func requestWifiScan() async throws -> [WifiScanResult] {
|
|
829
|
+
if let existing = wifiScanTask {
|
|
830
|
+
// Join the in-flight scan instead of failing with request_in_flight;
|
|
831
|
+
// the scan screen auto-starts a scan on mount and can be pushed twice.
|
|
832
|
+
return try await existing.value
|
|
833
|
+
}
|
|
834
|
+
let pending = PendingResponse<[WifiScanResult]>(operation: "WiFi scan request")
|
|
835
|
+
let request = PendingWifiScan(pending: pending, scanId: "scan-\(UUID().uuidString)")
|
|
836
|
+
pendingWifiScan = request
|
|
837
|
+
// Claim the scan-results store for this scan before the command goes out,
|
|
838
|
+
// so a delayed chunk from an older, abandoned scan can no longer mutate it.
|
|
839
|
+
Bridge.claimWifiScanResults(scanId: request.scanId)
|
|
840
|
+
DeviceManager.shared.requestWifiScan(scanId: request.scanId)
|
|
841
|
+
let task = Task { @MainActor [weak self] () async throws -> [WifiScanResult] in
|
|
842
|
+
defer {
|
|
843
|
+
if let self {
|
|
844
|
+
if self.pendingWifiScan === request {
|
|
845
|
+
self.pendingWifiScan = nil
|
|
846
|
+
}
|
|
847
|
+
self.wifiScanTask = nil
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
do {
|
|
851
|
+
// The glasses wait up to 15s for scan-results broadcasts before sending
|
|
852
|
+
// scan_complete, so give them longer than that before falling back.
|
|
853
|
+
return try await pending.wait(timeoutMs: BluetoothSDK.wifiScanTimeoutMs)
|
|
854
|
+
} catch {
|
|
855
|
+
if (error as? BluetoothSdkError)?.code == "request_timeout",
|
|
856
|
+
!request.latestResults.isEmpty
|
|
857
|
+
{
|
|
858
|
+
return request.latestResults
|
|
859
|
+
}
|
|
860
|
+
throw error
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
wifiScanTask = task
|
|
864
|
+
return try await task.value
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
public func sendWifiCredentials(ssid: String, password: String) async throws -> WifiStatusEvent {
|
|
868
|
+
guard pendingWifiStatus == nil else {
|
|
869
|
+
throw BluetoothSdkError(
|
|
870
|
+
code: "request_in_flight",
|
|
871
|
+
message: "A WiFi status command is already waiting for a glasses response."
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi connect request")
|
|
875
|
+
pendingWifiStatus = PendingWifiStatusRequest(operation: .connect, ssid: ssid, pending: pending)
|
|
876
|
+
DeviceManager.shared.sendWifiCredentials(ssid, password)
|
|
877
|
+
do {
|
|
878
|
+
let event = try await pending.wait()
|
|
879
|
+
if pendingWifiStatus?.pending === pending {
|
|
880
|
+
pendingWifiStatus = nil
|
|
881
|
+
}
|
|
882
|
+
return event
|
|
883
|
+
} catch {
|
|
884
|
+
if pendingWifiStatus?.pending === pending {
|
|
885
|
+
pendingWifiStatus = nil
|
|
886
|
+
}
|
|
887
|
+
throw error
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
public func forgetWifiNetwork(ssid: String) async throws -> WifiStatusEvent {
|
|
892
|
+
guard pendingWifiStatus == nil else {
|
|
893
|
+
throw BluetoothSdkError(
|
|
894
|
+
code: "request_in_flight",
|
|
895
|
+
message: "A WiFi status command is already waiting for a glasses response."
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
let pending = PendingResponse<WifiStatusEvent>(operation: "WiFi forget request")
|
|
899
|
+
pendingWifiStatus = PendingWifiStatusRequest(operation: .forget, ssid: ssid, pending: pending)
|
|
900
|
+
DeviceManager.shared.forgetWifiNetwork(ssid)
|
|
901
|
+
do {
|
|
902
|
+
let event = try await pending.wait()
|
|
903
|
+
if pendingWifiStatus?.pending === pending {
|
|
904
|
+
pendingWifiStatus = nil
|
|
905
|
+
}
|
|
906
|
+
return event
|
|
907
|
+
} catch {
|
|
908
|
+
if pendingWifiStatus?.pending === pending {
|
|
909
|
+
pendingWifiStatus = nil
|
|
910
|
+
}
|
|
911
|
+
throw error
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
public func setHotspotState(enabled: Bool) async throws -> HotspotStatusEvent {
|
|
916
|
+
guard pendingHotspotStatus == nil else {
|
|
917
|
+
throw BluetoothSdkError(
|
|
918
|
+
code: "request_in_flight",
|
|
919
|
+
message: "A hotspot command is already waiting for a glasses response."
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
let pending = PendingResponse<HotspotStatusEvent>(
|
|
923
|
+
operation: "hotspot \(enabled ? "enable" : "disable") request"
|
|
924
|
+
)
|
|
925
|
+
pendingHotspotStatus = PendingHotspotStatusRequest(enabled: enabled, pending: pending)
|
|
926
|
+
DeviceManager.shared.setHotspotState(enabled)
|
|
927
|
+
do {
|
|
928
|
+
let event = try await pending.wait()
|
|
929
|
+
if pendingHotspotStatus?.pending === pending {
|
|
930
|
+
pendingHotspotStatus = nil
|
|
931
|
+
}
|
|
932
|
+
return event
|
|
933
|
+
} catch {
|
|
934
|
+
if pendingHotspotStatus?.pending === pending {
|
|
935
|
+
pendingHotspotStatus = nil
|
|
936
|
+
}
|
|
937
|
+
throw error
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/// Fire-and-forget Mentra Live Wi-Fi ADB toggle. Glasses do not ack this
|
|
942
|
+
/// command, so there is no pending-response wait (unlike hotspot / Wi-Fi forget).
|
|
943
|
+
public func setWifiAdbState(enabled: Bool) async throws {
|
|
944
|
+
try requireGlassesConnected(operation: "toggle Wi-Fi ADB")
|
|
945
|
+
DeviceManager.shared.setWifiAdbState(enabled)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
func setSystemTime(timestampMs: Int64) {
|
|
949
|
+
DeviceManager.shared.setSystemTime(timestampMs)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
public func requestPhoto(_ request: PhotoRequest) async throws -> PhotoResponseEvent {
|
|
953
|
+
let routedRequest = nonBlankRequestId(request.requestId).map { request.withRequestId($0) }
|
|
954
|
+
?? request.withRequestId(generatedCameraRequestId("photo"))
|
|
955
|
+
Bridge.log(
|
|
956
|
+
"NATIVE: PHOTO PIPELINE [3b/6] BluetoothSdk.requestPhoto requestId=\(routedRequest.requestId)"
|
|
957
|
+
)
|
|
958
|
+
let pending = PendingResponse<PhotoResponseEvent>(operation: "photo request \(routedRequest.requestId)")
|
|
959
|
+
pendingPhotoRequests[routedRequest.requestId] = pending
|
|
960
|
+
DeviceManager.shared.requestPhoto(routedRequest)
|
|
961
|
+
do {
|
|
962
|
+
let event = try await pending.wait(timeoutMs: BluetoothSDK.photoRequestTimeoutMs)
|
|
963
|
+
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
964
|
+
return event
|
|
965
|
+
} catch {
|
|
966
|
+
pendingPhotoRequests.removeValue(forKey: routedRequest.requestId)
|
|
967
|
+
throw error
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
public func warmUpCamera(
|
|
972
|
+
requestId: String? = nil,
|
|
973
|
+
size: PhotoSize,
|
|
974
|
+
mode: PhotoMode = .photo,
|
|
975
|
+
exposureTimeNs: Double?,
|
|
976
|
+
durationMs: Int,
|
|
977
|
+
zsl: Bool? = nil,
|
|
978
|
+
mfnr: Bool? = nil
|
|
979
|
+
) async throws -> CameraStatusEvent {
|
|
980
|
+
let effectiveRequestId = nonBlankRequestId(requestId) ?? generatedCameraRequestId("warm")
|
|
981
|
+
let pending = PendingResponse<CameraStatusEvent>(operation: "camera warm up \(effectiveRequestId)")
|
|
982
|
+
pendingCameraStatusRequests[effectiveRequestId] = pending
|
|
983
|
+
do {
|
|
984
|
+
// Inside the catch so an unsupported-device throw also clears the pending entry.
|
|
985
|
+
try DeviceManager.shared.warmUpCamera(
|
|
986
|
+
requestId: effectiveRequestId,
|
|
987
|
+
size: size,
|
|
988
|
+
mode: mode,
|
|
989
|
+
exposureTimeNs: exposureTimeNs,
|
|
990
|
+
durationMs: durationMs,
|
|
991
|
+
zsl: zsl,
|
|
992
|
+
mfnr: mfnr
|
|
993
|
+
)
|
|
994
|
+
let event = try await pending.wait()
|
|
995
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
996
|
+
return event
|
|
997
|
+
} catch {
|
|
998
|
+
pendingCameraStatusRequests.removeValue(forKey: effectiveRequestId)
|
|
999
|
+
throw error
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
public func stopCameraWarmUp(requestId: String) throws {
|
|
1004
|
+
guard let effectiveRequestId = nonBlankRequestId(requestId) else {
|
|
1005
|
+
throw BluetoothSdkError(
|
|
1006
|
+
code: "invalid_request", message: "A warm-up request ID is required."
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
try DeviceManager.shared.stopCameraWarmUp(requestId: effectiveRequestId)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
public func queryGalleryStatus() async throws -> GalleryStatusEvent {
|
|
1013
|
+
if pendingGalleryStatus != nil {
|
|
1014
|
+
throw BluetoothSdkError(
|
|
1015
|
+
code: "request_in_flight",
|
|
1016
|
+
message: "A gallery status query is already waiting for a glasses response."
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
let pending = PendingResponse<GalleryStatusEvent>(operation: "gallery status query")
|
|
1020
|
+
pendingGalleryStatus = pending
|
|
1021
|
+
DeviceManager.shared.queryGalleryStatus()
|
|
1022
|
+
do {
|
|
1023
|
+
let event = try await pending.wait()
|
|
1024
|
+
if pendingGalleryStatus === pending {
|
|
1025
|
+
pendingGalleryStatus = nil
|
|
1026
|
+
}
|
|
1027
|
+
return event
|
|
1028
|
+
} catch {
|
|
1029
|
+
if pendingGalleryStatus === pending {
|
|
1030
|
+
pendingGalleryStatus = nil
|
|
1031
|
+
}
|
|
1032
|
+
throw error
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
public func startStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1037
|
+
try await startStream(request, startSdkKeepAlive: true)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
func startExternallyManagedStream(_ request: StreamRequest) async throws -> StreamStatusEvent {
|
|
1041
|
+
try await startStream(request, startSdkKeepAlive: false)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private func startStream(_ request: StreamRequest, startSdkKeepAlive: Bool) async throws -> StreamStatusEvent {
|
|
1045
|
+
var values = request.values
|
|
1046
|
+
let streamId = stringValue(values, "streamId").flatMap { $0.isEmpty ? nil : $0 } ?? "sdk-\(UUID().uuidString)"
|
|
1047
|
+
values["streamId"] = streamId
|
|
1048
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "start stream \(streamId)")
|
|
1049
|
+
// seq assignment through the BLE hand-off must stay await-free so this
|
|
1050
|
+
// MainActor turn is one critical section: rejectPreemptedStreamStarts
|
|
1051
|
+
// only works if seq order equals the order the commands reach the
|
|
1052
|
+
// glasses' FIFO.
|
|
1053
|
+
streamStartSeq += 1
|
|
1054
|
+
pendingStreamStarts[streamId] = PendingStreamStart(seq: streamStartSeq, pending: pending)
|
|
1055
|
+
stopStreamKeepAliveMonitor()
|
|
1056
|
+
DeviceManager.shared.startStream(values)
|
|
1057
|
+
do {
|
|
1058
|
+
let event = try await pending.wait(timeoutMs: 30000)
|
|
1059
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1060
|
+
if startSdkKeepAlive {
|
|
1061
|
+
startStreamKeepAliveMonitor(
|
|
1062
|
+
streamId: streamId,
|
|
1063
|
+
intervalSeconds: Self.defaultStreamKeepAliveIntervalSeconds
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
1066
|
+
return event
|
|
1067
|
+
} catch {
|
|
1068
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1069
|
+
throw error
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
func sendExternallyManagedStreamKeepAlive(_ request: StreamKeepAliveRequest) {
|
|
1074
|
+
DeviceManager.shared.keepStreamAlive(request.values)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
public func rgbLedControl(_ request: RgbLedRequest) async throws -> RgbLedControlResponseEvent {
|
|
1078
|
+
let pending = PendingResponse<RgbLedControlResponseEvent>(operation: "RGB LED command \(request.requestId)")
|
|
1079
|
+
pendingRgbLedRequests[request.requestId] = pending
|
|
1080
|
+
DeviceManager.shared.rgbLedControl(
|
|
1081
|
+
requestId: request.requestId,
|
|
1082
|
+
packageName: request.packageName,
|
|
1083
|
+
action: request.action.rawValue,
|
|
1084
|
+
color: request.color?.rawValue,
|
|
1085
|
+
onDurationMs: request.onDurationMs,
|
|
1086
|
+
offDurationMs: request.offDurationMs,
|
|
1087
|
+
count: request.count
|
|
1088
|
+
)
|
|
1089
|
+
do {
|
|
1090
|
+
let event = try await pending.wait()
|
|
1091
|
+
pendingRgbLedRequests.removeValue(forKey: request.requestId)
|
|
1092
|
+
return event
|
|
1093
|
+
} catch {
|
|
1094
|
+
pendingRgbLedRequests.removeValue(forKey: request.requestId)
|
|
1095
|
+
throw error
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
public func stopStream() async throws -> StreamStatusEvent {
|
|
1100
|
+
guard pendingStreamStop == nil else {
|
|
1101
|
+
throw BluetoothSdkError(
|
|
1102
|
+
code: "request_in_flight",
|
|
1103
|
+
message: "A stream stop command is already waiting for a glasses response."
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
let pending = PendingResponse<StreamStatusEvent>(operation: "stop stream")
|
|
1107
|
+
// The seq draw and the BLE hand-off stay in this single await-free
|
|
1108
|
+
// MainActor turn, sharing startStream's ordering critical section:
|
|
1109
|
+
// the stop takes its own slot in the send order, so its ack can
|
|
1110
|
+
// separate the pending starts the glasses consumed before it (lower
|
|
1111
|
+
// seq) from starts sent after it (higher seq).
|
|
1112
|
+
streamStartSeq += 1
|
|
1113
|
+
pendingStreamStop = (streamId: activeStreamKeepAlive?.streamId, seq: streamStartSeq, pending: pending)
|
|
1114
|
+
stopStreamKeepAliveMonitor()
|
|
1115
|
+
DeviceManager.shared.stopStream()
|
|
1116
|
+
do {
|
|
1117
|
+
let event = try await pending.wait(timeoutMs: 15000)
|
|
1118
|
+
if pendingStreamStop?.pending === pending {
|
|
1119
|
+
pendingStreamStop = nil
|
|
1120
|
+
}
|
|
1121
|
+
return event
|
|
1122
|
+
} catch {
|
|
1123
|
+
if pendingStreamStop?.pending === pending {
|
|
1124
|
+
pendingStreamStop = nil
|
|
1125
|
+
}
|
|
1126
|
+
throw error
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
public func startVideoRecording(_ request: VideoRecordingRequest) async throws -> VideoRecordingStatusEvent {
|
|
1131
|
+
guard !request.requestId.isEmpty else {
|
|
1132
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to start video recording.")
|
|
1133
|
+
}
|
|
1134
|
+
try requireGlassesConnected(operation: "start video recording")
|
|
1135
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(
|
|
1136
|
+
operation: "start video recording \(request.requestId)"
|
|
1137
|
+
)
|
|
1138
|
+
guard pendingVideoRecordingRequests[request.requestId] == nil else {
|
|
1139
|
+
throw BluetoothSdkError(
|
|
1140
|
+
code: "request_in_flight",
|
|
1141
|
+
message: "A video recording command is already waiting for requestId \(request.requestId)."
|
|
1142
|
+
)
|
|
1143
|
+
}
|
|
1144
|
+
pendingVideoRecordingRequests[request.requestId] = PendingVideoRecordingRequest(
|
|
1145
|
+
expectedStatus: "recording_started",
|
|
1146
|
+
pending: pending
|
|
1147
|
+
)
|
|
1148
|
+
DeviceManager.shared.startVideoRecording(
|
|
1149
|
+
request.requestId,
|
|
1150
|
+
request.save,
|
|
1151
|
+
request.sound,
|
|
1152
|
+
request.width,
|
|
1153
|
+
request.height,
|
|
1154
|
+
request.fps,
|
|
1155
|
+
request.maxRecordingTimeMinutes
|
|
1156
|
+
)
|
|
1157
|
+
do {
|
|
1158
|
+
let event = try await pending.wait()
|
|
1159
|
+
pendingVideoRecordingRequests.removeValue(forKey: request.requestId)
|
|
1160
|
+
return event
|
|
1161
|
+
} catch {
|
|
1162
|
+
pendingVideoRecordingRequests.removeValue(forKey: request.requestId)
|
|
1163
|
+
throw error
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
public func stopVideoRecording(
|
|
1168
|
+
requestId: String, webhookUrl: String? = nil, authToken: String? = nil
|
|
1169
|
+
) async throws -> VideoRecordingStatusEvent {
|
|
1170
|
+
guard !requestId.isEmpty else {
|
|
1171
|
+
throw BluetoothSdkError(code: "missing_request_id", message: "requestId is required to stop video recording.")
|
|
1172
|
+
}
|
|
1173
|
+
try requireGlassesConnected(operation: "stop video recording")
|
|
1174
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "stop video recording \(requestId)")
|
|
1175
|
+
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1176
|
+
throw BluetoothSdkError(
|
|
1177
|
+
code: "request_in_flight",
|
|
1178
|
+
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
1179
|
+
)
|
|
1180
|
+
}
|
|
1181
|
+
let waitForUpload = !(webhookUrl?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
|
|
1182
|
+
pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
|
|
1183
|
+
expectedStatus: "recording_stopped",
|
|
1184
|
+
pending: pending,
|
|
1185
|
+
waitForUpload: waitForUpload
|
|
1186
|
+
)
|
|
1187
|
+
DeviceManager.shared.stopVideoRecording(requestId, webhookUrl, authToken)
|
|
1188
|
+
do {
|
|
1189
|
+
let timeoutMs = waitForUpload ? videoUploadStopTimeoutMs : 15000
|
|
1190
|
+
let event = try await pending.wait(timeoutMs: timeoutMs)
|
|
1191
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1192
|
+
return event
|
|
1193
|
+
} catch {
|
|
1194
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1195
|
+
throw error
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
public func queryVideoRecordingStatus(requestId: String) async throws -> VideoRecordingStatusEvent {
|
|
1200
|
+
guard !requestId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
1201
|
+
throw BluetoothSdkError(
|
|
1202
|
+
code: "missing_request_id",
|
|
1203
|
+
message: "requestId is required to query video recording status."
|
|
1204
|
+
)
|
|
1205
|
+
}
|
|
1206
|
+
try requireGlassesConnected(operation: "query video recording status")
|
|
1207
|
+
let pending = PendingResponse<VideoRecordingStatusEvent>(operation: "query video recording status \(requestId)")
|
|
1208
|
+
guard pendingVideoRecordingRequests[requestId] == nil else {
|
|
1209
|
+
throw BluetoothSdkError(
|
|
1210
|
+
code: "request_in_flight",
|
|
1211
|
+
message: "A video recording command is already waiting for requestId \(requestId)."
|
|
1212
|
+
)
|
|
1213
|
+
}
|
|
1214
|
+
pendingVideoRecordingRequests[requestId] = PendingVideoRecordingRequest(
|
|
1215
|
+
expectedStatus: "recording_status",
|
|
1216
|
+
pending: pending
|
|
1217
|
+
)
|
|
1218
|
+
DeviceManager.shared.queryVideoRecordingStatus(requestId)
|
|
1219
|
+
do {
|
|
1220
|
+
let event = try await pending.wait()
|
|
1221
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1222
|
+
return event
|
|
1223
|
+
} catch {
|
|
1224
|
+
pendingVideoRecordingRequests.removeValue(forKey: requestId)
|
|
1225
|
+
throw error
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
public func requestVersionInfo() async throws -> VersionInfoResult {
|
|
1230
|
+
guard pendingVersionInfo == nil else {
|
|
1231
|
+
throw BluetoothSdkError(
|
|
1232
|
+
code: "request_in_flight",
|
|
1233
|
+
message: "A version info request is already waiting for a glasses response."
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
let pending = PendingResponse<VersionInfoResult>(operation: "version info request")
|
|
1237
|
+
pendingVersionInfo = pending
|
|
1238
|
+
DeviceManager.shared.requestVersionInfo()
|
|
1239
|
+
do {
|
|
1240
|
+
let status = try await pending.wait()
|
|
1241
|
+
if pendingVersionInfo === pending {
|
|
1242
|
+
pendingVersionInfo = nil
|
|
1243
|
+
}
|
|
1244
|
+
return status
|
|
1245
|
+
} catch {
|
|
1246
|
+
if pendingVersionInfo === pending {
|
|
1247
|
+
pendingVersionInfo = nil
|
|
1248
|
+
}
|
|
1249
|
+
throw error
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
public func availableG2FirmwareArtifacts() throws -> [[String: Any]] {
|
|
1254
|
+
try requireGlassesConnected(operation: "list G2 firmware")
|
|
1255
|
+
return try DeviceManager.shared.availableG2FirmwareArtifacts()
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
public func startG2FirmwareFlash(target: String) async throws -> [String: Any] {
|
|
1259
|
+
try requireGlassesConnected(operation: "flash G2 firmware")
|
|
1260
|
+
return try await DeviceManager.shared.startG2FirmwareFlash(target: target)
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/// Select the HTTP(S) manifest used by subsequent OTA checks and installs.
|
|
1264
|
+
public func setOtaVersionUrl(_ otaVersionUrl: String) throws {
|
|
1265
|
+
configuredOtaVersionUrl = try OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/// Return the configured OTA manifest, or this SDK release's default manifest.
|
|
1269
|
+
public func getOtaVersionUrl() throws -> String {
|
|
1270
|
+
try configuredOtaVersionUrl ?? OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/// Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available.
|
|
1274
|
+
public func checkForOtaUpdate() async throws -> Bool {
|
|
1275
|
+
let status = await getFreshGlassesStatus()
|
|
1276
|
+
guard status.connected else {
|
|
1277
|
+
throw BluetoothSdkError(
|
|
1278
|
+
code: "glasses_not_connected",
|
|
1279
|
+
message: "Cannot check OTA update because glasses are not connected."
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
guard !status.buildNumber.isEmpty else {
|
|
1283
|
+
throw BluetoothSdkError(
|
|
1284
|
+
code: "missing_glasses_version",
|
|
1285
|
+
message: "Cannot check OTA update because glasses build number is unavailable."
|
|
1286
|
+
)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
let manifestUrl = try resolveOtaVersionUrl(status: status)
|
|
1290
|
+
let manifest = try await OtaManifestChecker.fetch(manifestUrl)
|
|
1291
|
+
let otaStatus = try await waitForOtaManifestStatus(status, manifest: manifest)
|
|
1292
|
+
return try OtaManifestChecker.hasUpdate(
|
|
1293
|
+
currentBuildNumber: otaStatus.buildNumber,
|
|
1294
|
+
currentMtkVersion: otaStatus.mtkFirmwareVersion,
|
|
1295
|
+
currentBesVersion: otaStatus.besFirmwareVersion,
|
|
1296
|
+
manifest: manifest
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/// Return bundled release changelogs crossed between two coordinated product versions, newest first.
|
|
1301
|
+
public func getReleaseChangelogs(
|
|
1302
|
+
fromVersion: String? = nil,
|
|
1303
|
+
toVersion: String? = nil
|
|
1304
|
+
) throws -> [ReleaseChangelog] {
|
|
1305
|
+
try ReleaseChangelogCatalog.select(fromVersion: fromVersion, toVersion: toVersion)
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/// Ask connected Mentra Live glasses to report the current OTA install/session status.
|
|
1309
|
+
private func queryOtaStatus() async throws -> OtaQueryResult {
|
|
1310
|
+
try await performOtaQuery(operation: "OTA status query") {
|
|
1311
|
+
DeviceManager.shared.sendOtaQueryStatus()
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
private func performOtaQuery(
|
|
1316
|
+
operation: String,
|
|
1317
|
+
sendRequest: () -> Void
|
|
1318
|
+
) async throws -> OtaQueryResult {
|
|
1319
|
+
if pendingOtaQuery != nil {
|
|
1320
|
+
throw BluetoothSdkError(
|
|
1321
|
+
code: "request_in_flight",
|
|
1322
|
+
message: "An OTA status query is already waiting for a glasses response."
|
|
1323
|
+
)
|
|
1324
|
+
}
|
|
1325
|
+
let pending = PendingResponse<OtaQueryResult>(operation: operation)
|
|
1326
|
+
pendingOtaQuery = pending
|
|
1327
|
+
sendRequest()
|
|
1328
|
+
do {
|
|
1329
|
+
let result = try await pending.wait()
|
|
1330
|
+
if pendingOtaQuery === pending {
|
|
1331
|
+
pendingOtaQuery = nil
|
|
1332
|
+
}
|
|
1333
|
+
return result
|
|
1334
|
+
} catch {
|
|
1335
|
+
if pendingOtaQuery === pending {
|
|
1336
|
+
pendingOtaQuery = nil
|
|
1337
|
+
}
|
|
1338
|
+
throw error
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/// Start the OTA flow after your app has presented the available update to the user.
|
|
1343
|
+
public func startOtaUpdate() async throws -> OtaStartAckEvent {
|
|
1344
|
+
let status = await getFreshGlassesStatus()
|
|
1345
|
+
let otaVersionUrl = try resolveOtaVersionUrl(status: status)
|
|
1346
|
+
return try await startOtaUpdate(otaVersionUrl: otaVersionUrl)
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
private func startOtaCommand(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
1350
|
+
if pendingOtaStart != nil {
|
|
1351
|
+
throw BluetoothSdkError(
|
|
1352
|
+
code: "request_in_flight",
|
|
1353
|
+
message: "An OTA start command is already waiting for a glasses response."
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1356
|
+
let pending = PendingResponse<OtaStartAckEvent>(operation: "OTA start command")
|
|
1357
|
+
pendingOtaStart = pending
|
|
1358
|
+
DeviceManager.shared.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
1359
|
+
do {
|
|
1360
|
+
let event = try await pending.wait()
|
|
1361
|
+
if pendingOtaStart === pending {
|
|
1362
|
+
pendingOtaStart = nil
|
|
1363
|
+
}
|
|
1364
|
+
return event
|
|
1365
|
+
} catch {
|
|
1366
|
+
if pendingOtaStart === pending {
|
|
1367
|
+
pendingOtaStart = nil
|
|
1368
|
+
}
|
|
1369
|
+
throw error
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
func startOtaUpdate(otaVersionUrl: String) async throws -> OtaStartAckEvent {
|
|
1374
|
+
try await startOtaCommand(otaVersionUrl: otaVersionUrl)
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
func sendOtaQueryStatus() async throws -> OtaQueryResult {
|
|
1378
|
+
try await queryOtaStatus()
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1382
|
+
try requireGlassesConnected(operation: "start AR99 OTA")
|
|
1383
|
+
return try DeviceManager.shared.startAr99OtaFromFile(path)
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
func cancelAr99Ota() {
|
|
1387
|
+
DeviceManager.shared.cancelAr99Ota()
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
func sendAr99FactoryReset() throws {
|
|
1391
|
+
try requireGlassesConnected(operation: "factory reset AR99")
|
|
1392
|
+
try DeviceManager.shared.sendAr99FactoryReset()
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private func getFreshGlassesStatus() async -> GlassesStatus {
|
|
1396
|
+
let status = glassesStatus
|
|
1397
|
+
if !status.connected || !status.buildNumber.isEmpty {
|
|
1398
|
+
return status
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
do {
|
|
1402
|
+
let versionInfo = try await requestVersionInfo()
|
|
1403
|
+
let values = status.values.merging(versionInfo.dictionary) { existing, new in
|
|
1404
|
+
if let newString = new as? String, newString.isEmpty {
|
|
1405
|
+
return existing
|
|
1406
|
+
}
|
|
1407
|
+
return new
|
|
1408
|
+
}
|
|
1409
|
+
return GlassesStatus(values: values)
|
|
1410
|
+
} catch {
|
|
1411
|
+
return status
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
private func waitForOtaManifestStatus(_ initialStatus: GlassesStatus, manifest: OtaManifest) async throws -> GlassesStatus {
|
|
1416
|
+
var status = initialStatus
|
|
1417
|
+
if OtaManifestChecker.hasBesFirmware(manifest), status.besFirmwareVersion.isEmpty {
|
|
1418
|
+
status = await waitForGlassesStatus(status, timeoutMs: Self.otaBesVersionWaitMs) {
|
|
1419
|
+
!$0.connected || !$0.besFirmwareVersion.isEmpty
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if OtaManifestChecker.hasMtkPatches(manifest), status.mtkFirmwareVersion.isEmpty {
|
|
1424
|
+
status = await waitForGlassesStatus(status, timeoutMs: Self.otaMtkVersionWaitMs) {
|
|
1425
|
+
!$0.connected || !$0.mtkFirmwareVersion.isEmpty
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
guard status.connected else {
|
|
1430
|
+
throw BluetoothSdkError(
|
|
1431
|
+
code: "glasses_not_connected",
|
|
1432
|
+
message: "Cannot check OTA update because glasses disconnected."
|
|
1433
|
+
)
|
|
1434
|
+
}
|
|
1435
|
+
return status
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private func waitForGlassesStatus(
|
|
1439
|
+
_ initialStatus: GlassesStatus,
|
|
1440
|
+
timeoutMs: Int,
|
|
1441
|
+
isReady: (GlassesStatus) -> Bool
|
|
1442
|
+
) async -> GlassesStatus {
|
|
1443
|
+
let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000)
|
|
1444
|
+
var status = initialStatus
|
|
1445
|
+
while Date() < deadline {
|
|
1446
|
+
status = glassesStatus
|
|
1447
|
+
if isReady(status) {
|
|
1448
|
+
return status
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
let remainingMs = max(0, Int(deadline.timeIntervalSinceNow * 1000))
|
|
1452
|
+
let sleepMs = min(Self.otaVersionPollMs, remainingMs)
|
|
1453
|
+
if sleepMs <= 0 {
|
|
1454
|
+
break
|
|
1455
|
+
}
|
|
1456
|
+
try? await Task.sleep(nanoseconds: UInt64(sleepMs) * 1_000_000)
|
|
1457
|
+
}
|
|
1458
|
+
return glassesStatus
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
private func resolveOtaVersionUrl(status: GlassesStatus) throws -> String {
|
|
1462
|
+
let deviceUrl = status.otaVersionUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1463
|
+
if isLegacyAsgOtaStartBuild(status.buildNumber) {
|
|
1464
|
+
return OtaManifestDefaults.legacyProdOtaVersionUrl
|
|
1465
|
+
}
|
|
1466
|
+
// SDK consumers are pinned to the manifest built for their SDK version.
|
|
1467
|
+
// A future glasses-advertised URL should not silently change that pairing.
|
|
1468
|
+
if let configuredOtaVersionUrl {
|
|
1469
|
+
return configuredOtaVersionUrl
|
|
1470
|
+
}
|
|
1471
|
+
return try OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
private func isLegacyAsgOtaStartBuild(_ buildNumber: String) -> Bool {
|
|
1475
|
+
guard let parsed = Int(buildNumber) else { return false }
|
|
1476
|
+
return parsed < 39
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
func sendShutdown() {
|
|
1480
|
+
DeviceManager.shared.sendShutdown()
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
func sendReboot() {
|
|
1484
|
+
DeviceManager.shared.sendReboot()
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1488
|
+
DeviceManager.shared.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
public func invalidate() {
|
|
1492
|
+
stopStreamKeepAliveMonitor()
|
|
1493
|
+
if let bluetoothAvailabilityListenerId {
|
|
1494
|
+
BluetoothAvailability.shared.removeStateListener(bluetoothAvailabilityListenerId)
|
|
1495
|
+
self.bluetoothAvailabilityListenerId = nil
|
|
1496
|
+
}
|
|
1497
|
+
if let bridgeEventSinkId {
|
|
1498
|
+
Bridge.removeEventSink(bridgeEventSinkId)
|
|
1499
|
+
self.bridgeEventSinkId = nil
|
|
1500
|
+
}
|
|
1501
|
+
if let storeListenerId {
|
|
1502
|
+
DeviceStore.shared.store.removeListener(storeListenerId)
|
|
1503
|
+
self.storeListenerId = nil
|
|
1504
|
+
}
|
|
1505
|
+
delegate = nil
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
private func handleBluetoothAvailability(_ state: CBManagerState) {
|
|
1509
|
+
switch state {
|
|
1510
|
+
case .poweredOff, .resetting, .unauthorized, .unsupported:
|
|
1511
|
+
handleBluetoothUnavailable()
|
|
1512
|
+
case .poweredOn:
|
|
1513
|
+
handleBluetoothRestored()
|
|
1514
|
+
case .unknown:
|
|
1515
|
+
break
|
|
1516
|
+
@unknown default:
|
|
1517
|
+
handleBluetoothUnavailable()
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
private func handleBluetoothUnavailable() {
|
|
1522
|
+
cancelActiveScanSessions(reason: .cancelled)
|
|
1523
|
+
clearBluetoothDiscoveryState()
|
|
1524
|
+
disconnectActiveConnections()
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
private func disconnectActiveConnections() {
|
|
1528
|
+
if glassesStatus.controllerConnected {
|
|
1529
|
+
DeviceManager.shared.disconnectController()
|
|
1530
|
+
shouldRestoreControllerOnBluetoothRestore = true
|
|
1531
|
+
}
|
|
1532
|
+
if glassesStatus.deviceModel == DeviceTypes.SIMULATED
|
|
1533
|
+
|| DeviceManager.shared.sgc?.type.contains(DeviceTypes.SIMULATED) == true
|
|
1534
|
+
{
|
|
1535
|
+
return
|
|
1536
|
+
}
|
|
1537
|
+
if glassesStatus.connected || glassesStatus.connectionState != .disconnected {
|
|
1538
|
+
DeviceManager.shared.disconnect()
|
|
1539
|
+
shouldRestoreGlassesOnBluetoothRestore = true
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/// Reconnect only what `handleBluetoothUnavailable` tore down, never a
|
|
1544
|
+
/// connection the user closed themselves (explicit connect/disconnect
|
|
1545
|
+
/// calls clear the restore intent).
|
|
1546
|
+
private func handleBluetoothRestored() {
|
|
1547
|
+
let restoreGlasses = shouldRestoreGlassesOnBluetoothRestore
|
|
1548
|
+
let restoreController = shouldRestoreControllerOnBluetoothRestore
|
|
1549
|
+
clearBluetoothRestoreIntent()
|
|
1550
|
+
|
|
1551
|
+
if restoreGlasses, !glassesStatus.connected, glassesStatus.connectionState == .disconnected {
|
|
1552
|
+
DeviceManager.shared.connectDefault() // also restores the controller
|
|
1553
|
+
} else if restoreController, !glassesStatus.controllerConnected {
|
|
1554
|
+
DeviceManager.shared.connectDefaultController()
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
private func clearBluetoothRestoreIntent() {
|
|
1559
|
+
shouldRestoreGlassesOnBluetoothRestore = false
|
|
1560
|
+
shouldRestoreControllerOnBluetoothRestore = false
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
private func clearBluetoothDiscoveryState() {
|
|
1564
|
+
discoveredDeviceNames.removeAll()
|
|
1565
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searching", false)
|
|
1566
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searchingController", false)
|
|
1567
|
+
DeviceStore.shared.apply(ObservableStore.bluetoothCategory, "searchResults", [] as [[String: Any]])
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private func cancelActiveScanSessions(reason: ScanStopReason) {
|
|
1571
|
+
let ids = Array(activeScanSessions.keys)
|
|
1572
|
+
guard !ids.isEmpty else {
|
|
1573
|
+
if bluetoothStatus.searching || bluetoothStatus.searchingController {
|
|
1574
|
+
stopScan(reason: reason)
|
|
1575
|
+
}
|
|
1576
|
+
return
|
|
1577
|
+
}
|
|
1578
|
+
for (index, id) in ids.enumerated() {
|
|
1579
|
+
// Stop the underlying scan once (first session); the rest only
|
|
1580
|
+
// complete their callbacks.
|
|
1581
|
+
finishScanSession(id, reason: reason, shouldStopScan: index == 0)
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
private func startStreamKeepAliveMonitor(streamId: String, intervalSeconds requestedIntervalSeconds: Int) {
|
|
1586
|
+
let intervalSeconds = requestedIntervalSeconds > 0 ? requestedIntervalSeconds : Self.defaultStreamKeepAliveIntervalSeconds
|
|
1587
|
+
let tracker = ActiveStreamKeepAlive(streamId: streamId, intervalSeconds: intervalSeconds)
|
|
1588
|
+
activeStreamKeepAlive = tracker
|
|
1589
|
+
sendNextStreamKeepAlive(for: tracker)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
private func stopStreamKeepAliveMonitor() {
|
|
1593
|
+
activeStreamKeepAlive?.task?.cancel()
|
|
1594
|
+
activeStreamKeepAlive = nil
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
private func sendNextStreamKeepAlive(for tracker: ActiveStreamKeepAlive) {
|
|
1598
|
+
guard activeStreamKeepAlive === tracker else { return }
|
|
1599
|
+
|
|
1600
|
+
if tracker.armed, tracker.pendingAckId != nil {
|
|
1601
|
+
tracker.missedAckCount += 1
|
|
1602
|
+
if tracker.missedAckCount >= 3 {
|
|
1603
|
+
activeStreamKeepAlive = nil
|
|
1604
|
+
tracker.task?.cancel()
|
|
1605
|
+
let event = StreamStatusEvent(
|
|
1606
|
+
status: .error(
|
|
1607
|
+
streamId: tracker.streamId,
|
|
1608
|
+
errorDetails: "Stream keep-alive timed out after \(tracker.missedAckCount) missed ACKs",
|
|
1609
|
+
timestamp: Int(Date().timeIntervalSince1970 * 1000),
|
|
1610
|
+
resolvedConfig: nil
|
|
1611
|
+
)
|
|
1612
|
+
)
|
|
1613
|
+
delegate?.bluetoothSDK(self, didReceive: .streamStatus(event))
|
|
1614
|
+
stopStreamKeepAliveMonitor()
|
|
1615
|
+
DeviceManager.shared.stopStream()
|
|
1616
|
+
return
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
let ackId = "ack-\(Int(Date().timeIntervalSince1970 * 1000))"
|
|
1621
|
+
tracker.pendingAckId = ackId
|
|
1622
|
+
DeviceManager.shared.keepStreamAlive(
|
|
1623
|
+
StreamKeepAliveRequest(streamId: tracker.streamId, ackId: ackId).values
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
tracker.task?.cancel()
|
|
1627
|
+
tracker.task = Task { @MainActor [weak self, weak tracker] in
|
|
1628
|
+
guard let tracker else { return }
|
|
1629
|
+
try? await Task.sleep(nanoseconds: UInt64(tracker.intervalSeconds) * 1_000_000_000)
|
|
1630
|
+
self?.sendNextStreamKeepAlive(for: tracker)
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
private func handleStreamKeepAliveAck(_ event: KeepAliveAckEvent) -> Bool {
|
|
1635
|
+
guard let tracker = activeStreamKeepAlive,
|
|
1636
|
+
event.streamId == tracker.streamId,
|
|
1637
|
+
event.ackId == tracker.pendingAckId
|
|
1638
|
+
else {
|
|
1639
|
+
return false
|
|
1640
|
+
}
|
|
1641
|
+
tracker.pendingAckId = nil
|
|
1642
|
+
tracker.missedAckCount = 0
|
|
1643
|
+
return true
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
private func handleStreamStatusForKeepAlive(_ status: StreamStatus) {
|
|
1647
|
+
guard let streamId = status.streamId,
|
|
1648
|
+
activeStreamKeepAlive?.streamId == streamId
|
|
1649
|
+
else {
|
|
1650
|
+
return
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
switch status.state {
|
|
1654
|
+
case .stopped, .stopping, .error, .reconnectFailed:
|
|
1655
|
+
stopStreamKeepAliveMonitor()
|
|
1656
|
+
default:
|
|
1657
|
+
// A non-terminal status means the stream is live or coming up and the glasses can
|
|
1658
|
+
// now ACK; arm the missed-ACK detector from here so a slow startup before the first
|
|
1659
|
+
// ACK can't trip a false keep-alive timeout. On the arming transition, drop any
|
|
1660
|
+
// pre-arm bookkeeping so a stale unacked id can't immediately count as a miss.
|
|
1661
|
+
if let tracker = activeStreamKeepAlive, !tracker.armed {
|
|
1662
|
+
tracker.armed = true
|
|
1663
|
+
tracker.pendingAckId = nil
|
|
1664
|
+
tracker.missedAckCount = 0
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
private func handleStreamStatusForRequests(_ event: StreamStatusEvent) {
|
|
1670
|
+
if let (streamId, start) = matchingStreamStart(for: event) {
|
|
1671
|
+
// Preemption is only proven by statuses the start path emits after
|
|
1672
|
+
// the glasses' stopAllServices has run: initializing, reconnecting,
|
|
1673
|
+
// reconnected, streaming. An id-stamped `error` can be a
|
|
1674
|
+
// command-level preflight rejection (#3488 firmware) emitted
|
|
1675
|
+
// synchronously before stopAllServices, and `stopped` is a
|
|
1676
|
+
// stop-ack for a previous stream — treating either as a winner
|
|
1677
|
+
// could reject an older start that is still healthy.
|
|
1678
|
+
if let eventStreamId = event.streamId, !eventStreamId.isEmpty {
|
|
1679
|
+
switch event.state {
|
|
1680
|
+
case .initializing, .reconnecting, .reconnected, .streaming:
|
|
1681
|
+
rejectPreemptedStreamStarts(winnerSeq: start.seq)
|
|
1682
|
+
default:
|
|
1683
|
+
break
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
switch event.state {
|
|
1687
|
+
// `reconnected` also proves the stream is live: when an async start
|
|
1688
|
+
// failure triggers the glasses' publisher retry, a successful retry
|
|
1689
|
+
// reports `reconnected` instead of `streaming`.
|
|
1690
|
+
case .streaming, .reconnected:
|
|
1691
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1692
|
+
start.pending.resolve(event)
|
|
1693
|
+
case .reconnectFailed, .stopped:
|
|
1694
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1695
|
+
start.pending.reject(streamStatusError(start.lastError ?? event, code: "stream_start_failed"))
|
|
1696
|
+
case .error:
|
|
1697
|
+
if let eventStreamId = event.streamId, !eventStreamId.isEmpty, event.willRetry == true {
|
|
1698
|
+
// The glasses flag errors their publisher will retry with
|
|
1699
|
+
// `willRetry` (emitting side lands in PR #3488), so keep the
|
|
1700
|
+
// start pending for the retry's verdict — `reconnected` or
|
|
1701
|
+
// `streaming` on success, `reconnect_failed` or the
|
|
1702
|
+
// streamId-less `stopped` wind-down (which reports these
|
|
1703
|
+
// stashed details) on failure.
|
|
1704
|
+
start.lastError = event
|
|
1705
|
+
} else {
|
|
1706
|
+
// An id-less error is the glasses' command-level rejection
|
|
1707
|
+
// (missing URL, low battery, no WiFi) emitted before any
|
|
1708
|
+
// publisher starts; an id-carrying error without `willRetry`
|
|
1709
|
+
// is terminal (`camera_busy` emits an error and nothing else).
|
|
1710
|
+
// Old firmware never sends `willRetry`, so its errors always
|
|
1711
|
+
// fail fast here — the shipped pre-#3487 Android behavior.
|
|
1712
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1713
|
+
start.pending.reject(streamStatusError(event, code: "stream_start_failed"))
|
|
1714
|
+
}
|
|
1715
|
+
default:
|
|
1716
|
+
break
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if let stop = pendingStreamStop, streamStatus(event, matches: stop.streamId) {
|
|
1721
|
+
if isAlreadyStoppedStreamStatus(event) {
|
|
1722
|
+
if pendingStreamStop?.pending === stop.pending {
|
|
1723
|
+
pendingStreamStop = nil
|
|
1724
|
+
}
|
|
1725
|
+
rejectStreamStartsSuperseded(byStopSeq: stop.seq)
|
|
1726
|
+
stop.pending.resolve(stoppedStreamEvent(from: event, fallbackStreamId: stop.streamId))
|
|
1727
|
+
} else if event.state == .error || event.state == .reconnectFailed {
|
|
1728
|
+
if pendingStreamStop?.pending === stop.pending {
|
|
1729
|
+
pendingStreamStop = nil
|
|
1730
|
+
}
|
|
1731
|
+
stop.pending.reject(streamStatusError(event, code: "stream_stop_failed"))
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/// The glasses process BLE commands FIFO and every start_stream begins by
|
|
1737
|
+
/// stopping whatever runs, so an id-carrying status proving start X's
|
|
1738
|
+
/// start path ran on the glasses also proves every lower-seq start has
|
|
1739
|
+
/// already been preempted. Their only verdict on current firmware is a
|
|
1740
|
+
/// streamId-less stopped, which the id-less heuristic deliberately
|
|
1741
|
+
/// ignores — without this they would run out the 30s timeout instead of
|
|
1742
|
+
/// failing fast.
|
|
1743
|
+
private func rejectPreemptedStreamStarts(winnerSeq: Int) {
|
|
1744
|
+
for (streamId, start) in pendingStreamStarts where start.seq < winnerSeq {
|
|
1745
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1746
|
+
start.pending.reject(
|
|
1747
|
+
BluetoothSdkError(
|
|
1748
|
+
code: "stream_preempted",
|
|
1749
|
+
message: "start stream \(streamId) was preempted by a newer start_stream; "
|
|
1750
|
+
+ "the glasses run a single stream and the last request wins."
|
|
1751
|
+
)
|
|
1752
|
+
)
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/// The glasses process BLE commands FIFO, so this stop's ack also settles
|
|
1757
|
+
/// every start sent before it: whatever those starts brought up (or would
|
|
1758
|
+
/// have brought up) has been stopped, and no further status will arrive
|
|
1759
|
+
/// for them. Without this they would run out the 30s start timeout.
|
|
1760
|
+
/// Starts sent after the stop keep waiting for their own statuses.
|
|
1761
|
+
private func rejectStreamStartsSuperseded(byStopSeq stopSeq: Int) {
|
|
1762
|
+
for (streamId, start) in pendingStreamStarts where start.seq < stopSeq {
|
|
1763
|
+
pendingStreamStarts.removeValue(forKey: streamId)
|
|
1764
|
+
start.pending.reject(
|
|
1765
|
+
BluetoothSdkError(
|
|
1766
|
+
code: "stream_stopped",
|
|
1767
|
+
message: "start stream \(streamId) was superseded by a stop_stream issued after it."
|
|
1768
|
+
)
|
|
1769
|
+
)
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
private func matchingStreamStart(for event: StreamStatusEvent) -> (String, PendingStreamStart)? {
|
|
1774
|
+
if let streamId = event.streamId, !streamId.isEmpty {
|
|
1775
|
+
guard let start = pendingStreamStarts[streamId] else { return nil }
|
|
1776
|
+
return (streamId, start)
|
|
1777
|
+
}
|
|
1778
|
+
if event.state == .error {
|
|
1779
|
+
// An id-less error is a command-level preflight rejection (missing
|
|
1780
|
+
// URL, low battery, no WiFi) emitted synchronously, while lifecycle
|
|
1781
|
+
// statuses arrive async — so with overlapping starts the wire is
|
|
1782
|
+
// genuinely ambiguous about which start it answers. Attribute it to
|
|
1783
|
+
// the newest pending start: the common case is a later start
|
|
1784
|
+
// failing preflight while an earlier one is already emitting
|
|
1785
|
+
// lifecycle statuses, and at worst this swaps which start carries
|
|
1786
|
+
// the error instead of letting both time out.
|
|
1787
|
+
guard let entry = pendingStreamStarts.max(by: { $0.value.seq < $1.value.seq }) else { return nil }
|
|
1788
|
+
return (entry.key, entry.value)
|
|
1789
|
+
}
|
|
1790
|
+
if pendingStreamStarts.count == 1, let entry = pendingStreamStarts.first {
|
|
1791
|
+
// A streamId-less STOPPED is the glasses' stop-ack for a PREVIOUS
|
|
1792
|
+
// stream (their stop ack carries no streamId), not a verdict on the
|
|
1793
|
+
// pending start — a start_stream that replaces a running publisher
|
|
1794
|
+
// emits exactly this sequence (stopped -> initializing -> streaming).
|
|
1795
|
+
// Attributing it here would reject a start that is about to succeed.
|
|
1796
|
+
// After a stashed `willRetry` error for the pending start, though,
|
|
1797
|
+
// the stopped is the retrying publisher's wind-down (the stop-ack
|
|
1798
|
+
// always precedes any status for the new start), so it is that
|
|
1799
|
+
// start's verdict.
|
|
1800
|
+
if event.state == .stopped, entry.value.lastError == nil {
|
|
1801
|
+
return nil
|
|
1802
|
+
}
|
|
1803
|
+
return (entry.key, entry.value)
|
|
1804
|
+
}
|
|
1805
|
+
return nil
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private func streamStatus(_ event: StreamStatusEvent, matches streamId: String?) -> Bool {
|
|
1809
|
+
guard let streamId, !streamId.isEmpty else { return true }
|
|
1810
|
+
guard let eventStreamId = event.streamId, !eventStreamId.isEmpty else { return true }
|
|
1811
|
+
return eventStreamId == streamId
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
private func isAlreadyStoppedStreamStatus(_ event: StreamStatusEvent) -> Bool {
|
|
1815
|
+
if event.state == .stopped {
|
|
1816
|
+
return true
|
|
1817
|
+
}
|
|
1818
|
+
guard case let .error(_, errorDetails, _, _) = event.status else {
|
|
1819
|
+
return false
|
|
1820
|
+
}
|
|
1821
|
+
return ["not_streaming", "already_stopped", "not streaming"].contains(errorDetails.lowercased())
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
private func stoppedStreamEvent(from event: StreamStatusEvent, fallbackStreamId: String?) -> StreamStatusEvent {
|
|
1825
|
+
StreamStatusEvent(
|
|
1826
|
+
status: .lifecycle(
|
|
1827
|
+
state: .stopped,
|
|
1828
|
+
streamId: event.streamId ?? fallbackStreamId,
|
|
1829
|
+
timestamp: event.status.timestamp ?? Int(Date().timeIntervalSince1970 * 1000),
|
|
1830
|
+
resolvedConfig: event.resolvedConfig
|
|
1831
|
+
)
|
|
1832
|
+
)
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
private func streamStatusError(_ event: StreamStatusEvent, code: String) -> BluetoothSdkError {
|
|
1836
|
+
let message: String
|
|
1837
|
+
if case let .error(_, errorDetails, _, _) = event.status {
|
|
1838
|
+
message = errorDetails
|
|
1839
|
+
} else {
|
|
1840
|
+
message = "Stream status \(event.state.rawValue)"
|
|
1841
|
+
}
|
|
1842
|
+
return BluetoothSdkError(code: code, message: message)
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
private func handlePhotoResponseForRequests(_ event: PhotoResponseEvent) {
|
|
1846
|
+
guard let pending = pendingPhotoRequests[event.requestId] else { return }
|
|
1847
|
+
switch event.response {
|
|
1848
|
+
case .success:
|
|
1849
|
+
pending.resolve(event)
|
|
1850
|
+
case let .error(_, errorCode, errorMessage, _):
|
|
1851
|
+
pending.reject(
|
|
1852
|
+
BluetoothSdkError(
|
|
1853
|
+
code: errorCode ?? "photo_request_failed",
|
|
1854
|
+
message: errorMessage
|
|
1855
|
+
)
|
|
1856
|
+
)
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private func handleCameraStatusForRequests(_ event: CameraStatusEvent) {
|
|
1861
|
+
guard let pending = pendingCameraStatusRequests[event.requestId] else { return }
|
|
1862
|
+
switch event.state.lowercased() {
|
|
1863
|
+
case "ready":
|
|
1864
|
+
pending.resolve(event)
|
|
1865
|
+
case "error":
|
|
1866
|
+
pending.reject(
|
|
1867
|
+
BluetoothSdkError(
|
|
1868
|
+
code: event.errorCode ?? "camera_warm_up_failed",
|
|
1869
|
+
message: event.errorMessage ?? "Camera warm-up failed."
|
|
1870
|
+
)
|
|
1871
|
+
)
|
|
1872
|
+
default:
|
|
1873
|
+
// "warming"/"stopped" are progress updates; leave the pending promise alone.
|
|
1874
|
+
break
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private func handleVideoRecordingStatusForRequests(_ event: VideoRecordingStatusEvent) {
|
|
1879
|
+
guard let request = pendingVideoRecordingRequests[event.requestId] else { return }
|
|
1880
|
+
if event.success {
|
|
1881
|
+
if event.status == request.expectedStatus {
|
|
1882
|
+
if request.waitForUpload {
|
|
1883
|
+
request.stoppedEvent = event
|
|
1884
|
+
if request.uploadSucceeded {
|
|
1885
|
+
request.pending.resolve(event)
|
|
1886
|
+
}
|
|
1887
|
+
} else {
|
|
1888
|
+
request.pending.resolve(event)
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
request.pending.reject(
|
|
1893
|
+
BluetoothSdkError(
|
|
1894
|
+
code: event.status.isEmpty ? "video_recording_failed" : event.status,
|
|
1895
|
+
message: event.details ?? "Video recording command failed."
|
|
1896
|
+
)
|
|
1897
|
+
)
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private func handleMediaUploadForRequests(_ event: MediaUploadEvent) {
|
|
1902
|
+
guard event.isVideo, let request = pendingVideoRecordingRequests[event.requestId], request.waitForUpload else {
|
|
1903
|
+
return
|
|
1904
|
+
}
|
|
1905
|
+
if event.isSuccess {
|
|
1906
|
+
if let stoppedEvent = request.stoppedEvent {
|
|
1907
|
+
request.pending.resolve(stoppedEvent)
|
|
1908
|
+
} else {
|
|
1909
|
+
request.uploadSucceeded = true
|
|
1910
|
+
}
|
|
1911
|
+
} else {
|
|
1912
|
+
request.pending.reject(
|
|
1913
|
+
BluetoothSdkError(
|
|
1914
|
+
code: "video_upload_failed",
|
|
1915
|
+
message: event.errorMessage ?? "Video upload failed."
|
|
1916
|
+
)
|
|
1917
|
+
)
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
private func handleRgbLedResponseForRequests(_ event: RgbLedControlResponseEvent) {
|
|
1922
|
+
guard let pending = pendingRgbLedRequests[event.requestId] else { return }
|
|
1923
|
+
if event.state == "success" {
|
|
1924
|
+
pending.resolve(event)
|
|
1925
|
+
} else {
|
|
1926
|
+
pending.reject(
|
|
1927
|
+
BluetoothSdkError(
|
|
1928
|
+
code: event.errorCode ?? "rgb_led_control_failed",
|
|
1929
|
+
message: event.errorCode ?? "RGB LED command failed."
|
|
1930
|
+
)
|
|
1931
|
+
)
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
private func handleSettingsAckForRequests(_ event: SettingsAckEvent) {
|
|
1936
|
+
guard let pending = pendingSettingsRequests[event.requestId] else { return }
|
|
1937
|
+
if isFailureStatus(event.status) {
|
|
1938
|
+
let fallbackSetting = event.setting.isEmpty ? event.requestId : event.setting
|
|
1939
|
+
pending.reject(
|
|
1940
|
+
BluetoothSdkError(
|
|
1941
|
+
code: event.errorCode ?? "\(event.setting.isEmpty ? "settings" : event.setting)_failed",
|
|
1942
|
+
message: event.errorMessage ?? "Settings command \(fallbackSetting) failed."
|
|
1943
|
+
)
|
|
1944
|
+
)
|
|
1945
|
+
} else {
|
|
1946
|
+
pending.resolve(event)
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
private func isFailureStatus(_ status: String) -> Bool {
|
|
1951
|
+
["error", "failed", "failure", "rejected"].contains(status.lowercased())
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
private func handleWifiScanResultsForRequests(_ results: [WifiScanResult]) {
|
|
1955
|
+
guard let request = pendingWifiScan else { return }
|
|
1956
|
+
if pendingWifiScan === request {
|
|
1957
|
+
pendingWifiScan = nil
|
|
1958
|
+
}
|
|
1959
|
+
request.pending.resolve(results)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
/// scanId-echoing glasses: results for another scan are ignored instead of
|
|
1963
|
+
/// resolving the pending request, and matching chunks accumulate until the
|
|
1964
|
+
/// glasses flag the scan complete.
|
|
1965
|
+
private func handleCorrelatedWifiScanChunk(
|
|
1966
|
+
scanId: String,
|
|
1967
|
+
results: [WifiScanResult],
|
|
1968
|
+
scanComplete: Bool
|
|
1969
|
+
) {
|
|
1970
|
+
guard let request = pendingWifiScan, request.scanId == scanId else { return }
|
|
1971
|
+
for network in results where !request.accumulated.contains(where: { $0.ssid == network.ssid }) {
|
|
1972
|
+
request.accumulated.append(network)
|
|
1973
|
+
}
|
|
1974
|
+
if !request.accumulated.isEmpty {
|
|
1975
|
+
request.latestResults = request.accumulated
|
|
1976
|
+
}
|
|
1977
|
+
guard scanComplete else { return }
|
|
1978
|
+
if pendingWifiScan === request {
|
|
1979
|
+
pendingWifiScan = nil
|
|
1980
|
+
}
|
|
1981
|
+
request.pending.resolve(request.accumulated)
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
private func updateWifiScanLatestResults(_ results: [WifiScanResult]) {
|
|
1985
|
+
guard !results.isEmpty else { return }
|
|
1986
|
+
pendingWifiScan?.latestResults = results
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
private func handleWifiStatusForRequests(_ event: WifiStatusEvent) {
|
|
1990
|
+
guard let request = pendingWifiStatus else { return }
|
|
1991
|
+
// A wifi_status carrying the explicit error field is the glasses' failure
|
|
1992
|
+
// verdict for the in-flight connect: reject now instead of running out the
|
|
1993
|
+
// request timeout. Only the error field counts as failure — the glasses'
|
|
1994
|
+
// connect sequence emits a debounced bare connected=false ~1-2s after
|
|
1995
|
+
// credentials while association is still in progress, and rejecting on that
|
|
1996
|
+
// would kill every connect attempt early.
|
|
1997
|
+
if request.operation == .connect, let error = event.error {
|
|
1998
|
+
if pendingWifiStatus === request {
|
|
1999
|
+
pendingWifiStatus = nil
|
|
2000
|
+
}
|
|
2001
|
+
request.pending.reject(
|
|
2002
|
+
BluetoothSdkError(
|
|
2003
|
+
code: error,
|
|
2004
|
+
message: "Glasses failed to join \"\(request.ssid)\": \(error)"
|
|
2005
|
+
)
|
|
2006
|
+
)
|
|
2007
|
+
return
|
|
2008
|
+
}
|
|
2009
|
+
guard wifiStatusMatches(event.status, request: request) else { return }
|
|
2010
|
+
if pendingWifiStatus === request {
|
|
2011
|
+
pendingWifiStatus = nil
|
|
2012
|
+
}
|
|
2013
|
+
request.pending.resolve(event)
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
private func wifiStatusMatches(_ status: WifiStatus, request: PendingWifiStatusRequest) -> Bool {
|
|
2017
|
+
switch request.operation {
|
|
2018
|
+
case .connect:
|
|
2019
|
+
if case let .connected(ssid, _) = status {
|
|
2020
|
+
return ssid == request.ssid
|
|
2021
|
+
}
|
|
2022
|
+
return false
|
|
2023
|
+
case .forget:
|
|
2024
|
+
switch status {
|
|
2025
|
+
case .disconnected:
|
|
2026
|
+
return true
|
|
2027
|
+
case let .connected(ssid, _):
|
|
2028
|
+
return ssid != request.ssid
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
private func handleHotspotStatusForRequests(_ event: HotspotStatusEvent) {
|
|
2034
|
+
guard let request = pendingHotspotStatus else { return }
|
|
2035
|
+
guard hotspotStatusMatches(event.status, enabled: request.enabled) else { return }
|
|
2036
|
+
if pendingHotspotStatus === request {
|
|
2037
|
+
pendingHotspotStatus = nil
|
|
2038
|
+
}
|
|
2039
|
+
request.pending.resolve(event)
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
private func hotspotStatusMatches(_ status: HotspotStatus, enabled: Bool) -> Bool {
|
|
2043
|
+
if enabled {
|
|
2044
|
+
return status.isEnabled
|
|
2045
|
+
}
|
|
2046
|
+
return status == .disabled
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
private func handleHotspotErrorForRequests(_ event: HotspotErrorEvent) {
|
|
2050
|
+
guard let request = pendingHotspotStatus else { return }
|
|
2051
|
+
if pendingHotspotStatus === request {
|
|
2052
|
+
pendingHotspotStatus = nil
|
|
2053
|
+
}
|
|
2054
|
+
request.pending.reject(
|
|
2055
|
+
BluetoothSdkError(
|
|
2056
|
+
code: "hotspot_command_failed",
|
|
2057
|
+
message: event.message ?? "Hotspot command failed."
|
|
2058
|
+
)
|
|
2059
|
+
)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private func dispatchStoreUpdate(_ category: String, _ changes: [String: Any]) {
|
|
2063
|
+
switch ObservableStore.normalizeCategory(category) {
|
|
2064
|
+
case "glasses":
|
|
2065
|
+
analytics.observeGlassesStatus(glassesStatus)
|
|
2066
|
+
let nextState = state
|
|
2067
|
+
delegate?.bluetoothSDK(self, didUpdate: nextState)
|
|
2068
|
+
delegate?.bluetoothSDK(self, didUpdateGlasses: nextState.glasses)
|
|
2069
|
+
case ObservableStore.bluetoothCategory:
|
|
2070
|
+
let nextState = state
|
|
2071
|
+
delegate?.bluetoothSDK(self, didUpdate: nextState)
|
|
2072
|
+
delegate?.bluetoothSDK(self, didUpdateSdkState: nextState.sdk)
|
|
2073
|
+
delegate?.bluetoothSDK(self, didUpdateScan: nextState.scan)
|
|
2074
|
+
if !suppressDefaultDeviceEvents && changes.keys.contains(where: { defaultDeviceKeys.contains($0) }) {
|
|
2075
|
+
dispatchDefaultDeviceChanged()
|
|
2076
|
+
}
|
|
2077
|
+
dispatchDiscoveredDevices(changes["searchResults"])
|
|
2078
|
+
dispatchScanResults(changes["searchResults"])
|
|
2079
|
+
default:
|
|
2080
|
+
break
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
private func dispatchDefaultDeviceChanged() {
|
|
2085
|
+
delegate?.bluetoothSDK(self, didChangeDefaultDevice: currentDefaultDevice())
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
private func finishDefaultDeviceApply(generation: Int) {
|
|
2089
|
+
Task { @MainActor [weak self] in
|
|
2090
|
+
guard let self, generation == self.defaultDeviceApplyGeneration else { return }
|
|
2091
|
+
self.suppressDefaultDeviceEvents = false
|
|
2092
|
+
self.dispatchDefaultDeviceChanged()
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
private func currentDefaultDevice() -> Device? {
|
|
2097
|
+
let core = DeviceStore.shared.store.getCategory(ObservableStore.bluetoothCategory)
|
|
2098
|
+
guard let model = core["default_wearable"] as? String, !model.isEmpty else { return nil }
|
|
2099
|
+
guard let name = core["device_name"] as? String, !name.isEmpty else { return nil }
|
|
2100
|
+
let identifier = (core["device_address"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2101
|
+
let projectName = (core["project_name"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2102
|
+
return Device(
|
|
2103
|
+
model: DeviceModel.fromDeviceType(model),
|
|
2104
|
+
name: name,
|
|
2105
|
+
identifier: identifier,
|
|
2106
|
+
projectName: projectName
|
|
2107
|
+
)
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
private func dispatchDiscoveredDevices(_ rawSearchResults: Any?) {
|
|
2111
|
+
guard let results = rawSearchResults as? [[String: Any]] else { return }
|
|
2112
|
+
for result in results {
|
|
2113
|
+
guard let name = result["name"] as? String else { continue }
|
|
2114
|
+
guard discoveredDeviceNames.insert(name).inserted else { continue }
|
|
2115
|
+
guard let device = Device(values: result) else { continue }
|
|
2116
|
+
delegate?.bluetoothSDK(self, didDiscover: device)
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
private func dispatchScanResults(_ rawSearchResults: Any?) {
|
|
2121
|
+
guard let results = rawSearchResults as? [[String: Any]] else { return }
|
|
2122
|
+
let devices = results.compactMap(Device.init(values:))
|
|
2123
|
+
for id in Array(activeScanSessions.keys) {
|
|
2124
|
+
guard let activeSession = activeScanSessions[id] else { continue }
|
|
2125
|
+
emitScanResults(devices.filter { $0.model == activeSession.model }, forSession: id)
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
private func emitScanResults(_ devices: [Device], forSession id: UUID) {
|
|
2130
|
+
guard let activeSession = activeScanSessions[id] else { return }
|
|
2131
|
+
activeSession.latestResults = devices
|
|
2132
|
+
activeSession.onResults(devices)
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
private func finishScanSession(_ id: UUID, reason: ScanStopReason, shouldStopScan: Bool) {
|
|
2136
|
+
guard let activeSession = activeScanSessions.removeValue(forKey: id) else { return }
|
|
2137
|
+
activeSession.timeoutTask?.cancel()
|
|
2138
|
+
activeSession.publicSession?.markStopped()
|
|
2139
|
+
if shouldStopScan {
|
|
2140
|
+
stopScan(reason: reason)
|
|
2141
|
+
}
|
|
2142
|
+
activeSession.onComplete(activeSession.latestResults)
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
private func dispatchBridgeEvent(_ eventName: String, _ data: [String: Any]) {
|
|
2146
|
+
switch eventName {
|
|
2147
|
+
case "log":
|
|
2148
|
+
delegate?.bluetoothSDK(self, didLog: data["message"] as? String ?? data.description)
|
|
2149
|
+
case "button_press":
|
|
2150
|
+
let event = ButtonPressEvent(
|
|
2151
|
+
buttonId: data["buttonId"] as? String ?? "",
|
|
2152
|
+
pressType: data["pressType"] as? String ?? "",
|
|
2153
|
+
timestamp: intValue(data["timestamp"])
|
|
2154
|
+
)
|
|
2155
|
+
delegate?.bluetoothSDK(self, didReceive: .buttonPress(event))
|
|
2156
|
+
case "touch_event":
|
|
2157
|
+
delegate?.bluetoothSDK(self, didReceive: .touch(TouchEvent(values: data)))
|
|
2158
|
+
case "mic_pcm":
|
|
2159
|
+
let event = MicPcmEvent(values: data)
|
|
2160
|
+
if !event.pcm.isEmpty {
|
|
2161
|
+
delegate?.bluetoothSDK(self, didReceiveMicPcm: event)
|
|
2162
|
+
}
|
|
2163
|
+
case "mic_lc3":
|
|
2164
|
+
let event = MicLc3Event(values: data)
|
|
2165
|
+
if !event.lc3.isEmpty {
|
|
2166
|
+
delegate?.bluetoothSDK(self, didReceiveMicLc3: event)
|
|
2167
|
+
}
|
|
2168
|
+
case "mic_health":
|
|
2169
|
+
delegate?.bluetoothSDK(self, didReceive: .micHealth(MicHealthEvent(values: data)))
|
|
2170
|
+
case "local_transcription":
|
|
2171
|
+
let event = LocalTranscriptionEvent(
|
|
2172
|
+
text: data["text"] as? String ?? "",
|
|
2173
|
+
isFinal: data["isFinal"] as? Bool ?? false,
|
|
2174
|
+
values: data
|
|
2175
|
+
)
|
|
2176
|
+
delegate?.bluetoothSDK(self, didReceive: .localTranscription(event))
|
|
2177
|
+
case "voice_activity_detection_status":
|
|
2178
|
+
delegate?.bluetoothSDK(
|
|
2179
|
+
self,
|
|
2180
|
+
didReceive: .voiceActivityDetectionStatus(VoiceActivityDetectionStatusEvent(values: data))
|
|
2181
|
+
)
|
|
2182
|
+
case "speaking_status":
|
|
2183
|
+
delegate?.bluetoothSDK(
|
|
2184
|
+
self,
|
|
2185
|
+
didReceive: .speakingStatus(SpeakingStatusEvent(values: data))
|
|
2186
|
+
)
|
|
2187
|
+
case "hotspot_status_change":
|
|
2188
|
+
let event = HotspotStatusEvent(values: data)
|
|
2189
|
+
handleHotspotStatusForRequests(event)
|
|
2190
|
+
delegate?.bluetoothSDK(self, didReceive: .hotspotStatus(event))
|
|
2191
|
+
case "wifi_status_change":
|
|
2192
|
+
let event = WifiStatusEvent(values: data)
|
|
2193
|
+
handleWifiStatusForRequests(event)
|
|
2194
|
+
delegate?.bluetoothSDK(self, didReceive: .wifiStatus(event))
|
|
2195
|
+
case "wifi_scan_result":
|
|
2196
|
+
let networks = (data["networks"] as? [[String: Any]])?.map(WifiScanResult.init(values:)) ?? []
|
|
2197
|
+
let hasCompletionFlag = data.keys.contains("scanComplete") || data.keys.contains("scan_complete")
|
|
2198
|
+
let scanComplete = data["scanComplete"] as? Bool ?? data["scan_complete"] as? Bool ?? false
|
|
2199
|
+
let scanId = (data["scanId"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
2200
|
+
if let scanId {
|
|
2201
|
+
handleCorrelatedWifiScanChunk(scanId: scanId, results: networks, scanComplete: scanComplete)
|
|
2202
|
+
} else {
|
|
2203
|
+
// Old firmware: no correlation id, keep the pre-scanId heuristics.
|
|
2204
|
+
updateWifiScanLatestResults(networks)
|
|
2205
|
+
if scanComplete || !hasCompletionFlag {
|
|
2206
|
+
handleWifiScanResultsForRequests(networks)
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
delegate?.bluetoothSDK(self, didReceive: .raw(name: "wifi_scan_result", values: data))
|
|
2210
|
+
case "hotspot_error":
|
|
2211
|
+
let event = HotspotErrorEvent(values: data)
|
|
2212
|
+
handleHotspotErrorForRequests(event)
|
|
2213
|
+
delegate?.bluetoothSDK(self, didReceive: .hotspotError(event))
|
|
2214
|
+
case "gallery_status":
|
|
2215
|
+
let event = GalleryStatusEvent(values: data)
|
|
2216
|
+
pendingGalleryStatus?.resolve(event)
|
|
2217
|
+
delegate?.bluetoothSDK(self, didReceive: .raw(name: "gallery_status", values: event.values))
|
|
2218
|
+
case "pairing_info":
|
|
2219
|
+
delegate?.bluetoothSDK(self, didReceive: .raw(name: "pairing_info", values: data))
|
|
2220
|
+
case "entering_pairing_mode", "owner_replaced":
|
|
2221
|
+
delegate?.bluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2222
|
+
case "photo_response":
|
|
2223
|
+
let event = PhotoResponseEvent(values: data)
|
|
2224
|
+
handlePhotoResponseForRequests(event)
|
|
2225
|
+
delegate?.bluetoothSDK(self, didReceive: .photoResponse(event))
|
|
2226
|
+
case "photo_status":
|
|
2227
|
+
delegate?.bluetoothSDK(self, didReceive: .photoStatus(PhotoStatusEvent(values: data)))
|
|
2228
|
+
case "camera_status":
|
|
2229
|
+
let event = CameraStatusEvent(values: data)
|
|
2230
|
+
handleCameraStatusForRequests(event)
|
|
2231
|
+
delegate?.bluetoothSDK(self, didReceive: .cameraStatus(event))
|
|
2232
|
+
case "video_recording_status":
|
|
2233
|
+
let event = VideoRecordingStatusEvent(values: data)
|
|
2234
|
+
handleVideoRecordingStatusForRequests(event)
|
|
2235
|
+
delegate?.bluetoothSDK(self, didReceive: .videoRecordingStatus(event))
|
|
2236
|
+
case "media_success", "media_error":
|
|
2237
|
+
let event = MediaUploadEvent(values: data)
|
|
2238
|
+
handleMediaUploadForRequests(event)
|
|
2239
|
+
delegate?.bluetoothSDK(self, didReceive: .mediaUpload(event))
|
|
2240
|
+
case "rgb_led_control_response":
|
|
2241
|
+
let event = RgbLedControlResponseEvent(values: data)
|
|
2242
|
+
handleRgbLedResponseForRequests(event)
|
|
2243
|
+
delegate?.bluetoothSDK(self, didReceive: .rgbLedControlResponse(event))
|
|
2244
|
+
case "stream_status":
|
|
2245
|
+
let event = StreamStatusEvent(values: data)
|
|
2246
|
+
handleStreamStatusForRequests(event)
|
|
2247
|
+
handleStreamStatusForKeepAlive(event.status)
|
|
2248
|
+
delegate?.bluetoothSDK(self, didReceive: .streamStatus(event))
|
|
2249
|
+
case "keep_alive_ack":
|
|
2250
|
+
let event = KeepAliveAckEvent(values: data)
|
|
2251
|
+
if !handleStreamKeepAliveAck(event) {
|
|
2252
|
+
delegate?.bluetoothSDK(self, didReceive: .keepAliveAck(event))
|
|
2253
|
+
}
|
|
2254
|
+
case "ota_start_ack":
|
|
2255
|
+
var values = data
|
|
2256
|
+
values["type"] = "ota_start_ack"
|
|
2257
|
+
let event = OtaStartAckEvent(values: values)
|
|
2258
|
+
pendingOtaStart?.resolve(event)
|
|
2259
|
+
delegate?.bluetoothSDK(self, didReceive: .otaStartAck(event))
|
|
2260
|
+
case "ota_status":
|
|
2261
|
+
var resultValues = data
|
|
2262
|
+
resultValues["type"] = "ota_status"
|
|
2263
|
+
pendingOtaQuery?.resolve(OtaQueryResult(values: resultValues))
|
|
2264
|
+
let event = OtaStatusEvent(values: resultValues)
|
|
2265
|
+
if let errorCode = otaStartRejectionErrorCode(event) {
|
|
2266
|
+
pendingOtaStart?.reject(
|
|
2267
|
+
BluetoothSdkError(code: errorCode, message: "Glasses rejected OTA start: \(errorCode)")
|
|
2268
|
+
)
|
|
2269
|
+
}
|
|
2270
|
+
delegate?.bluetoothSDK(self, didReceive: .otaStatus(event))
|
|
2271
|
+
case "settings_ack":
|
|
2272
|
+
let event = SettingsAckEvent(values: data)
|
|
2273
|
+
handleSettingsAckForRequests(event)
|
|
2274
|
+
delegate?.bluetoothSDK(self, didReceive: .settingsAck(event))
|
|
2275
|
+
case "version_info":
|
|
2276
|
+
let event = VersionInfoResult(values: data)
|
|
2277
|
+
pendingVersionInfo?.resolve(event)
|
|
2278
|
+
delegate?.bluetoothSDK(self, didReceive: .versionInfo(event))
|
|
2279
|
+
case "compatible_glasses_search_stop":
|
|
2280
|
+
delegate?.bluetoothSDK(self, didStopScan: .completed)
|
|
2281
|
+
case "pair_failure":
|
|
2282
|
+
delegate?.bluetoothSDK(
|
|
2283
|
+
self,
|
|
2284
|
+
didFail: BluetoothSdkError(
|
|
2285
|
+
code: "pair_failure",
|
|
2286
|
+
message: data["error"] as? String ?? data.description
|
|
2287
|
+
)
|
|
2288
|
+
)
|
|
2289
|
+
default:
|
|
2290
|
+
delegate?.bluetoothSDK(self, didReceive: .raw(name: eventName, values: data))
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/// OTA status messages are not request-correlated, so only known pre-ack failures settle a start.
|
|
2296
|
+
private func otaStartRejectionErrorCode(_ event: OtaStatusEvent) -> String? {
|
|
2297
|
+
guard event.status.lowercased() == "failed",
|
|
2298
|
+
event.errorMessage?.lowercased() == "battery_low"
|
|
2299
|
+
else {
|
|
2300
|
+
return nil
|
|
2301
|
+
}
|
|
2302
|
+
return "battery_low"
|
|
2303
|
+
}
|