@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,2109 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DeviceManager.swift
|
|
3
|
+
// Glassly_Manager
|
|
4
|
+
//
|
|
5
|
+
// Created by Matthew Fosse on 3/5/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Combine
|
|
10
|
+
import CoreBluetooth
|
|
11
|
+
import Foundation
|
|
12
|
+
import UIKit
|
|
13
|
+
#if SWIFT_PACKAGE
|
|
14
|
+
import BluetoothSDKCoreObjC
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
struct ViewState {
|
|
18
|
+
var topText: String
|
|
19
|
+
var bottomText: String
|
|
20
|
+
var title: String
|
|
21
|
+
var layoutType: String
|
|
22
|
+
var text: String
|
|
23
|
+
var data: String?
|
|
24
|
+
var animationData: [String: Any]?
|
|
25
|
+
// Optional container position/size - used by bitmap_view and positioned_text (G2; ignored by others)
|
|
26
|
+
var bmpX: Int32? = nil
|
|
27
|
+
var bmpY: Int32? = nil
|
|
28
|
+
var bmpWidth: Int32? = nil
|
|
29
|
+
var bmpHeight: Int32? = nil
|
|
30
|
+
// Optional positioned_text border (used by G2; ignored by others)
|
|
31
|
+
var borderWidth: Int32? = nil
|
|
32
|
+
var borderRadius: Int32? = nil
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@MainActor
|
|
36
|
+
@objc(DeviceManager) class DeviceManager: NSObject {
|
|
37
|
+
static let shared = DeviceManager()
|
|
38
|
+
|
|
39
|
+
@objc static func getInstance() -> DeviceManager {
|
|
40
|
+
return DeviceManager.shared
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Unique (iOS)
|
|
44
|
+
|
|
45
|
+
private var cancellables = Set<AnyCancellable>()
|
|
46
|
+
var sendStateWorkItem: DispatchWorkItem?
|
|
47
|
+
let sendStateQueue = DispatchQueue(label: "sendStateQueue", qos: .userInitiated)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Setup Bluetooth audio pairing after BLE connection is established
|
|
51
|
+
* Attempts to automatically activate Mentra Live as the system audio device
|
|
52
|
+
* If not paired yet, prompts user to pair in Settings
|
|
53
|
+
*/
|
|
54
|
+
func setupAudioPairing(deviceName _: String) {
|
|
55
|
+
// Don't configure audio session - PhoneMic.swift handles that
|
|
56
|
+
// Just check if audio session supports Bluetooth (informational only)
|
|
57
|
+
if !AudioSessionMonitor.isAudioSessionConfigured() {
|
|
58
|
+
Bridge.log(
|
|
59
|
+
"Audio: Audio session not configured for Bluetooth yet - mic system will configure it when recording"
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Extract device ID pattern to match the specific device
|
|
64
|
+
// BLE name: "MENTRA_LIVE_BLE_ABC123"
|
|
65
|
+
// BT Classic could be: "MENTRA_LIVE_BLE_ABC123" or "MENTRA_LIVE_BT_ABC123"
|
|
66
|
+
// We need to match on the unique device ID part (e.g., "ABC123")
|
|
67
|
+
let audioDevicePattern = getAudioDevicePattern()
|
|
68
|
+
|
|
69
|
+
if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
|
|
70
|
+
Bridge.log("Audio: Device pattern is empty or simulated, returning")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if device is paired (don't activate to preserve A2DP music playback)
|
|
75
|
+
let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
76
|
+
|
|
77
|
+
if isPaired {
|
|
78
|
+
// Device is paired! Don't activate it - let PhoneMic.swift activate when recording starts
|
|
79
|
+
Bridge.log("Audio: Mentra Live is paired (preserving A2DP for music)")
|
|
80
|
+
glassesBluetoothClassicConnected = true
|
|
81
|
+
} else {
|
|
82
|
+
glassesBluetoothClassicConnected = false
|
|
83
|
+
// Not found in availableInputs - not paired yet
|
|
84
|
+
|
|
85
|
+
// Start monitoring for when user pairs manually
|
|
86
|
+
AudioSessionMonitor.startMonitoring(devicePattern: audioDevicePattern) {
|
|
87
|
+
[weak self] (connected: Bool, _: String?) in
|
|
88
|
+
guard let self = self else { return }
|
|
89
|
+
|
|
90
|
+
if connected {
|
|
91
|
+
Bridge.log("Audio: Device paired and connected")
|
|
92
|
+
// Don't activate - let PhoneMic.swift handle that when recording starts
|
|
93
|
+
self.glassesBluetoothClassicConnected = true
|
|
94
|
+
} else {
|
|
95
|
+
Bridge.log("Audio: Device disconnected")
|
|
96
|
+
self.glassesBluetoothClassicConnected = false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - End Unique
|
|
103
|
+
|
|
104
|
+
// MARK: - Properties
|
|
105
|
+
|
|
106
|
+
var coreToken: String = ""
|
|
107
|
+
var coreTokenOwner: String = ""
|
|
108
|
+
var userEmail: String = ""
|
|
109
|
+
var sgc: SGCManager?
|
|
110
|
+
var controller: ControllerManager?
|
|
111
|
+
|
|
112
|
+
// state
|
|
113
|
+
// var lastStatusObj: [String: Any] = [:]
|
|
114
|
+
|
|
115
|
+
/// settings:
|
|
116
|
+
private var defaultWearable: String {
|
|
117
|
+
get { DeviceStore.shared.get("bluetooth", "default_wearable") as? String ?? "" }
|
|
118
|
+
set { DeviceStore.shared.apply("bluetooth", "default_wearable", newValue) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private var pendingWearable: String {
|
|
122
|
+
get { DeviceStore.shared.get("bluetooth", "pending_wearable") as? String ?? "" }
|
|
123
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_wearable", newValue) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private var pendingDeviceName: String {
|
|
127
|
+
get { DeviceStore.shared.get("bluetooth", "pending_device_name") as? String ?? "" }
|
|
128
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_device_name", newValue) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private var pendingDeviceAddress: String {
|
|
132
|
+
get { DeviceStore.shared.get("bluetooth", "pending_device_address") as? String ?? "" }
|
|
133
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_device_address", newValue) }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private var deviceName: String {
|
|
137
|
+
get { DeviceStore.shared.get("bluetooth", "device_name") as? String ?? "" }
|
|
138
|
+
set { DeviceStore.shared.apply("bluetooth", "device_name", newValue) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private var deviceAddress: String {
|
|
142
|
+
get { DeviceStore.shared.get("bluetooth", "device_address") as? String ?? "" }
|
|
143
|
+
set { DeviceStore.shared.apply("bluetooth", "device_address", newValue) }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private var defaultController: String {
|
|
147
|
+
get { DeviceStore.shared.get("bluetooth", "default_controller") as? String ?? "" }
|
|
148
|
+
set { DeviceStore.shared.apply("bluetooth", "default_controller", newValue) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private var pendingController: String {
|
|
152
|
+
get { DeviceStore.shared.get("bluetooth", "pending_controller") as? String ?? "" }
|
|
153
|
+
set { DeviceStore.shared.apply("bluetooth", "pending_controller", newValue) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private var controllerDeviceName: String {
|
|
157
|
+
get { DeviceStore.shared.get("bluetooth", "controller_device_name") as? String ?? "" }
|
|
158
|
+
set { DeviceStore.shared.apply("bluetooth", "controller_device_name", newValue) }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private var screenDisabled: Bool {
|
|
162
|
+
get { DeviceStore.shared.get("bluetooth", "screen_disabled") as? Bool ?? false }
|
|
163
|
+
set { DeviceStore.shared.apply("bluetooth", "screen_disabled", newValue) }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private var preferredMic: String {
|
|
167
|
+
get { DeviceStore.shared.get("bluetooth", "preferred_mic") as? String ?? "auto" }
|
|
168
|
+
set { DeviceStore.shared.apply("bluetooth", "preferred_mic", newValue) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private var autoBrightness: Bool {
|
|
172
|
+
get { DeviceStore.shared.get("bluetooth", "auto_brightness") as? Bool ?? true }
|
|
173
|
+
set { DeviceStore.shared.apply("bluetooth", "auto_brightness", newValue) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private var brightness: Int {
|
|
177
|
+
get { DeviceStore.shared.get("bluetooth", "brightness") as? Int ?? 50 }
|
|
178
|
+
set { DeviceStore.shared.apply("bluetooth", "brightness", newValue) }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private var headUpAngle: Int {
|
|
182
|
+
get { DeviceStore.shared.get("bluetooth", "head_up_angle") as? Int ?? 30 }
|
|
183
|
+
set { DeviceStore.shared.apply("bluetooth", "head_up_angle", newValue) }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private var sensingEnabled: Bool {
|
|
187
|
+
get { DeviceStore.shared.get("bluetooth", "sensing_enabled") as? Bool ?? true }
|
|
188
|
+
set { DeviceStore.shared.apply("bluetooth", "sensing_enabled", newValue) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Phone-side VAD gating switch. Default is OFF (VAD runs) so that the
|
|
192
|
+
/// coordinator can drive per-utterance offline/online STT switching from
|
|
193
|
+
/// `vad_status` events. Set to `true` only as an emergency kill-switch.
|
|
194
|
+
private var bypassVad: Bool {
|
|
195
|
+
get { DeviceStore.shared.get("bluetooth", "bypass_vad") as? Bool ?? false }
|
|
196
|
+
set { DeviceStore.shared.apply("bluetooth", "bypass_vad", newValue) }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private var localSttFallbackActive: Bool {
|
|
200
|
+
get { DeviceStore.shared.get("bluetooth", "local_stt_fallback_active") as? Bool ?? false }
|
|
201
|
+
set { DeviceStore.shared.apply("bluetooth", "local_stt_fallback_active", newValue) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private var shouldSendPcm: Bool {
|
|
205
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_pcm") as? Bool ?? false }
|
|
206
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_pcm", newValue) }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private var shouldSendLc3: Bool {
|
|
210
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_lc3") as? Bool ?? false }
|
|
211
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_lc3", newValue) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private var shouldSendTranscript: Bool {
|
|
215
|
+
get { DeviceStore.shared.get("bluetooth", "should_send_transcript") as? Bool ?? false }
|
|
216
|
+
set { DeviceStore.shared.apply("bluetooth", "should_send_transcript", newValue) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private var contextualDashboard: Bool {
|
|
220
|
+
get { DeviceStore.shared.get("bluetooth", "contextual_dashboard") as? Bool ?? true }
|
|
221
|
+
set { DeviceStore.shared.apply("bluetooth", "contextual_dashboard", newValue) }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// state:
|
|
225
|
+
|
|
226
|
+
private var searching: Bool {
|
|
227
|
+
get { DeviceStore.shared.get("bluetooth", "searching") as? Bool ?? false }
|
|
228
|
+
set { DeviceStore.shared.apply("bluetooth", "searching", newValue) }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private var searchingController: Bool {
|
|
232
|
+
get { DeviceStore.shared.get("bluetooth", "searchingController") as? Bool ?? false }
|
|
233
|
+
set { DeviceStore.shared.apply("bluetooth", "searchingController", newValue) }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private var glassesBluetoothClassicConnected: Bool {
|
|
237
|
+
get { DeviceStore.shared.get("glasses", "bluetoothClassicConnected") as? Bool ?? false }
|
|
238
|
+
set { DeviceStore.shared.apply("glasses", "bluetoothClassicConnected", newValue) }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private var micRanking: [String] {
|
|
242
|
+
get {
|
|
243
|
+
DeviceStore.shared.get("bluetooth", "micRanking") as? [String] ?? MicMap.map["auto"]!
|
|
244
|
+
}
|
|
245
|
+
set { DeviceStore.shared.apply("bluetooth", "micRanking", newValue) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private var shouldSendBootingMessage: Bool {
|
|
249
|
+
get { DeviceStore.shared.get("bluetooth", "shouldSendBootingMessage") as? Bool ?? true }
|
|
250
|
+
set { DeviceStore.shared.apply("bluetooth", "shouldSendBootingMessage", newValue) }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private var lastSystemTimeSyncConnectionKey = ""
|
|
254
|
+
private var pendingSystemTimeSync: DispatchWorkItem?
|
|
255
|
+
private var systemTimeSyncGeneration = 0
|
|
256
|
+
|
|
257
|
+
private var systemMicUnavailable: Bool {
|
|
258
|
+
get { DeviceStore.shared.get("bluetooth", "systemMicUnavailable") as? Bool ?? false }
|
|
259
|
+
set { DeviceStore.shared.apply("bluetooth", "systemMicUnavailable", newValue) }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private var headUp: Bool {
|
|
263
|
+
get { DeviceStore.shared.get("glasses", "headUp") as? Bool ?? false }
|
|
264
|
+
set { DeviceStore.shared.apply("glasses", "headUp", newValue) }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private var micEnabled: Bool {
|
|
268
|
+
get { DeviceStore.shared.get("bluetooth", "micEnabled") as? Bool ?? false }
|
|
269
|
+
set { DeviceStore.shared.apply("bluetooth", "micEnabled", newValue) }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private var currentMic: String {
|
|
273
|
+
get { DeviceStore.shared.get("bluetooth", "currentMic") as? String ?? "" }
|
|
274
|
+
set { DeviceStore.shared.apply("bluetooth", "currentMic", newValue) }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private var searchResults: [[String: Any]] {
|
|
278
|
+
get { DeviceStore.shared.get("bluetooth", "searchResults") as? [[String: Any]] ?? [] }
|
|
279
|
+
set { DeviceStore.shared.apply("bluetooth", "searchResults", newValue) }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private var wifiScanResults: [[String: Any]] {
|
|
283
|
+
get { DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? [] }
|
|
284
|
+
set { DeviceStore.shared.apply("bluetooth", "wifiScanResults", newValue) }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private var lastLog: [String] {
|
|
288
|
+
get { DeviceStore.shared.get("bluetooth", "lastLog") as? [String] ?? [] }
|
|
289
|
+
set { DeviceStore.shared.apply("bluetooth", "lastLog", newValue) }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private var otherBtConnected: Bool {
|
|
293
|
+
get { DeviceStore.shared.get("bluetooth", "otherBtConnected") as? Bool ?? false }
|
|
294
|
+
set { DeviceStore.shared.apply("bluetooth", "otherBtConnected", newValue) }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// LC3 Audio Encoding
|
|
298
|
+
/// Audio output format enum
|
|
299
|
+
enum AudioOutputFormat { case lc3, pcm }
|
|
300
|
+
/// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
|
|
301
|
+
/// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
|
|
302
|
+
/// Persistent LC3 converter for encoding/decoding
|
|
303
|
+
var lc3Converter: PcmConverter?
|
|
304
|
+
/// Audio output format - defaults to LC3 for bandwidth savings
|
|
305
|
+
private var audioOutputFormat: AudioOutputFormat = .lc3
|
|
306
|
+
/// Last time we received an LC3 frame from the glasses (used by the mic
|
|
307
|
+
/// inactivity watchdog).
|
|
308
|
+
private var lastLc3Event: Date?
|
|
309
|
+
private var sequenceGapEvents: Int64 = 0
|
|
310
|
+
private var decodeFailures: Int64 = 0
|
|
311
|
+
private var lastLc3ReceivedAt: Int64?
|
|
312
|
+
private var lastPcmProducedAt: Int64?
|
|
313
|
+
private var lastLc3Sequence: Int?
|
|
314
|
+
private var micReinitTimer: Timer?
|
|
315
|
+
|
|
316
|
+
// STT:
|
|
317
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
|
|
318
|
+
private var transcriber: SherpaOnnxTranscriber?
|
|
319
|
+
#endif
|
|
320
|
+
|
|
321
|
+
var viewStates: [ViewState] = [
|
|
322
|
+
ViewState(
|
|
323
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: ""
|
|
324
|
+
),
|
|
325
|
+
ViewState(
|
|
326
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
|
|
327
|
+
text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
|
|
328
|
+
),
|
|
329
|
+
ViewState(
|
|
330
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: "",
|
|
331
|
+
data: nil, animationData: nil
|
|
332
|
+
),
|
|
333
|
+
ViewState(
|
|
334
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
|
|
335
|
+
text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$", data: nil,
|
|
336
|
+
animationData: nil
|
|
337
|
+
),
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
/// Scene slots - one whole SceneFrame per view (main/dashboard), parallel to
|
|
341
|
+
/// viewStates. When a slot holds a scene, viewStates carries a "scene"
|
|
342
|
+
/// sentinel so sendCurrentState routes here. Holding the WHOLE frame keeps
|
|
343
|
+
/// native re-dispatch coherent (dashboard exit re-applies a complete scene).
|
|
344
|
+
var sceneStates: [SceneFrame?] = [nil, nil]
|
|
345
|
+
|
|
346
|
+
override init() {
|
|
347
|
+
Bridge.log("MAN: init()")
|
|
348
|
+
super.init()
|
|
349
|
+
|
|
350
|
+
// Start memory monitoring (logs every 30s to help detect leaks)
|
|
351
|
+
// MemoryMonitor.start()
|
|
352
|
+
|
|
353
|
+
// Initialize SherpaOnnx Transcriber
|
|
354
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
|
|
355
|
+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
356
|
+
let window = windowScene.windows.first,
|
|
357
|
+
let rootViewController = window.rootViewController
|
|
358
|
+
{
|
|
359
|
+
transcriber = SherpaOnnxTranscriber(context: rootViewController)
|
|
360
|
+
} else {
|
|
361
|
+
Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Initialize the transcriber
|
|
365
|
+
if let transcriber = transcriber {
|
|
366
|
+
transcriber.initialize()
|
|
367
|
+
Bridge.log("SherpaOnnxTranscriber fully initialized")
|
|
368
|
+
}
|
|
369
|
+
#endif
|
|
370
|
+
|
|
371
|
+
// Initialize persistent LC3 converter for unified audio encoding
|
|
372
|
+
lc3Converter = PcmConverter()
|
|
373
|
+
Bridge.log("LC3 converter initialized for unified audio encoding")
|
|
374
|
+
|
|
375
|
+
DispatchQueue.main.async { [weak self] in
|
|
376
|
+
guard let self = self else { return }
|
|
377
|
+
self.micReinitTimer = Timer.scheduledTimer(
|
|
378
|
+
withTimeInterval: 10.0, repeats: true
|
|
379
|
+
) { [weak self] _ in
|
|
380
|
+
self?.checkAndReinitGlassesMic()
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// MARK: - AUX Voice Data Handling
|
|
386
|
+
|
|
387
|
+
private func convertAndSendMicLc3(_ pcmData: Data) {
|
|
388
|
+
guard let lc3Converter = lc3Converter else {
|
|
389
|
+
Bridge.log("MAN: ERROR - LC3 converter not initialized but format is LC3")
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
// Values set from the JS bridge arrive (and persist) as Double, so a
|
|
393
|
+
// force-cast to Int aborts on the mic tap thread. Coerce via NSNumber
|
|
394
|
+
// like the Android side does with `as Number`.
|
|
395
|
+
let frameSize = (DeviceStore.shared.get("bluetooth", "lc3_frame_size") as? NSNumber)?.intValue ?? 60
|
|
396
|
+
let lc3Data = lc3Converter.encode(pcmData, frameSize: frameSize) as Data
|
|
397
|
+
guard lc3Data.count > 0 else {
|
|
398
|
+
Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
Bridge.sendMicLc3(lc3Data)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private func handleSendingPcm(_ pcmData: Data) {
|
|
405
|
+
// Bridge.log("MAN: handleSendingPcm() shouldSendPcm: \(shouldSendPcm) shouldSendLc3: \(shouldSendLc3)")
|
|
406
|
+
if shouldSendPcm {
|
|
407
|
+
Bridge.sendMicPcm(pcmData)
|
|
408
|
+
}
|
|
409
|
+
if shouldSendLc3 {
|
|
410
|
+
convertAndSendMicLc3(pcmData)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Handle raw LC3 audio data from glasses.
|
|
416
|
+
* Decodes the glasses LC3 to PCM, then forwards to handlePcm for processing.
|
|
417
|
+
* This matches Android behavior - glasses forward raw LC3, DeviceManager handles encoding.
|
|
418
|
+
*/
|
|
419
|
+
func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20, sequenceNumber: Int? = nil) {
|
|
420
|
+
recordLc3Packet(sequenceNumber: sequenceNumber)
|
|
421
|
+
guard let lc3Converter = lc3Converter else {
|
|
422
|
+
Bridge.log("MAN: LC3 converter not initialized")
|
|
423
|
+
recordMicDecodeFailure()
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
guard lc3Data.count > 2 else {
|
|
428
|
+
Bridge.log("MAN: Received invalid LC3 data size: \(lc3Data.count)")
|
|
429
|
+
recordMicDecodeFailure()
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let pcmData = lc3Converter.decode(lc3Data, frameSize: frameSize) as Data
|
|
434
|
+
guard pcmData.count > 0 else {
|
|
435
|
+
Bridge.log("MAN: Failed to decode glasses LC3 audio")
|
|
436
|
+
recordMicDecodeFailure()
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
// Forward to handlePcm which handles SDK audio events and encoding.
|
|
440
|
+
lastPcmProducedAt = nowMs()
|
|
441
|
+
handlePcm(pcmData)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func reportGlassesAudioActivity() {
|
|
445
|
+
lastLc3Event = Date()
|
|
446
|
+
lastPcmProducedAt = nowMs()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func micHealth() -> MicHealth {
|
|
450
|
+
MicHealth(
|
|
451
|
+
sequenceGapEvents: sequenceGapEvents,
|
|
452
|
+
decodeFailures: decodeFailures,
|
|
453
|
+
lastLc3ReceivedAt: lastLc3ReceivedAt,
|
|
454
|
+
lastPcmProducedAt: lastPcmProducedAt
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func resetMicSequenceBaseline() {
|
|
459
|
+
lastLc3Sequence = nil
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private func nowMs() -> Int64 {
|
|
463
|
+
Int64(Date().timeIntervalSince1970 * 1000)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private func resetMicHealth() {
|
|
467
|
+
sequenceGapEvents = 0
|
|
468
|
+
decodeFailures = 0
|
|
469
|
+
lastLc3ReceivedAt = nil
|
|
470
|
+
lastPcmProducedAt = nil
|
|
471
|
+
lastLc3Sequence = nil
|
|
472
|
+
lastLc3Event = nil
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func recordLc3Packet(sequenceNumber: Int?) {
|
|
476
|
+
let receivedAt = nowMs()
|
|
477
|
+
lastLc3ReceivedAt = receivedAt
|
|
478
|
+
lastLc3Event = Date()
|
|
479
|
+
var hasGap = false
|
|
480
|
+
if let sequenceNumber {
|
|
481
|
+
let normalized = sequenceNumber & 0xFF
|
|
482
|
+
if let previous = lastLc3Sequence {
|
|
483
|
+
let expected = (previous + 1) & 0xFF
|
|
484
|
+
if normalized != expected {
|
|
485
|
+
hasGap = true
|
|
486
|
+
sequenceGapEvents += 1
|
|
487
|
+
Bridge.log("MAN: LC3 packet sequence mismatch. Expected: \(expected), Got: \(normalized)")
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
lastLc3Sequence = normalized
|
|
491
|
+
}
|
|
492
|
+
if hasGap {
|
|
493
|
+
Bridge.sendMicHealth(micHealth(), reason: "sequence_gap")
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func recordMicDecodeFailure() {
|
|
498
|
+
decodeFailures += 1
|
|
499
|
+
Bridge.sendMicHealth(micHealth(), reason: "decode_failure")
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
func handlePcm(_ pcmData: Data) {
|
|
503
|
+
handleSendingPcm(pcmData)
|
|
504
|
+
|
|
505
|
+
// Speaker identification / enrollment tap — no-op unless enabled.
|
|
506
|
+
SpeakerIdEngine.shared.acceptAudio(pcm16le: pcmData)
|
|
507
|
+
|
|
508
|
+
// Send PCM to local transcriber.
|
|
509
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
|
|
510
|
+
if shouldSendTranscript || localSttFallbackActive {
|
|
511
|
+
transcriber?.acceptAudio(pcm16le: pcmData)
|
|
512
|
+
}
|
|
513
|
+
#endif
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
func updateMicState() {
|
|
517
|
+
// go through the micRanking and find the first mic that is available:
|
|
518
|
+
var micUsed = ""
|
|
519
|
+
|
|
520
|
+
// allow the sgc to make changes to the micRanking:
|
|
521
|
+
micRanking = sgc?.sortMicRanking(list: micRanking) ?? micRanking
|
|
522
|
+
// Bridge.log("MAN: updateMicState() micRanking: \(micRanking)")
|
|
523
|
+
|
|
524
|
+
var phoneMicUnavailable = systemMicUnavailable
|
|
525
|
+
|
|
526
|
+
let appState = UIApplication.shared.applicationState
|
|
527
|
+
if appState == .background {
|
|
528
|
+
// Bridge.log("App is in background - onboard mic unavailable to start!")
|
|
529
|
+
phoneMicUnavailable = true
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if micEnabled {
|
|
533
|
+
for micMode in micRanking {
|
|
534
|
+
if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BLUETOOTH_CLASSIC
|
|
535
|
+
|| micMode == MicTypes.BLUETOOTH
|
|
536
|
+
{
|
|
537
|
+
if PhoneMic.shared.isRecordingWithMode(micMode) {
|
|
538
|
+
micUsed = micMode
|
|
539
|
+
break
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if phoneMicUnavailable {
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// if the phone mic is not recording, start recording:
|
|
547
|
+
let success = PhoneMic.shared.startMode(micMode)
|
|
548
|
+
Bridge.log("MAN: starting mic mode: \(micMode) -> \(success)")
|
|
549
|
+
if success {
|
|
550
|
+
micUsed = micMode
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if micMode == MicTypes.GLASSES_CUSTOM {
|
|
556
|
+
// Bridge.log(
|
|
557
|
+
// "MAN: glasses custom mic found - hasMic: \(sgc?.hasMic ?? false), micEnabled: \(sgc?.micEnabled ?? false)"
|
|
558
|
+
// )
|
|
559
|
+
// if the glasses has a mic that's already on, mark it as used and break:
|
|
560
|
+
if sgc?.hasMic ?? false {
|
|
561
|
+
// enable the mic if it's not already on:
|
|
562
|
+
if sgc?.micEnabled == false {
|
|
563
|
+
sgc?.setMicEnabled(true)
|
|
564
|
+
micUsed = micMode
|
|
565
|
+
break
|
|
566
|
+
} else {
|
|
567
|
+
// the mic is already on, mark it as used and break:
|
|
568
|
+
micUsed = micMode
|
|
569
|
+
break
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// if the glasses doesn't have a mic, continue to the next mic:
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
currentMic = micUsed
|
|
579
|
+
|
|
580
|
+
// log if no mic was found:
|
|
581
|
+
if micUsed == "" && micEnabled {
|
|
582
|
+
Bridge.log("MAN: No available mic found!")
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// go through and disable all mics after the first used one:
|
|
587
|
+
var allMics = micRanking
|
|
588
|
+
// add any missing mics to the list:
|
|
589
|
+
for micMode in MicMap.map["auto"]! {
|
|
590
|
+
if !allMics.contains(micMode) {
|
|
591
|
+
allMics.append(micMode)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for micMode in allMics {
|
|
596
|
+
if micMode == micUsed {
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BLUETOOTH_CLASSIC
|
|
601
|
+
|| micMode == MicTypes.BLUETOOTH
|
|
602
|
+
{
|
|
603
|
+
PhoneMic.shared.stopMode(micMode)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true {
|
|
607
|
+
sgc?.setMicEnabled(false)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
func setOnboardMicEnabled(_ isEnabled: Bool) {
|
|
613
|
+
Task {
|
|
614
|
+
if isEnabled {
|
|
615
|
+
// Just check permissions - we no longer request them directly from Swift
|
|
616
|
+
// Permissions should already be granted via React Native UI flow
|
|
617
|
+
if !(PhoneMic.shared.checkPermissions()) {
|
|
618
|
+
Bridge.log("Microphone permissions not granted. Cannot enable microphone.")
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let success = PhoneMic.shared.startRecording()
|
|
623
|
+
if !success {
|
|
624
|
+
// fallback to glasses mic if possible:
|
|
625
|
+
if sgc?.hasMic ?? false {
|
|
626
|
+
await sgc?.setMicEnabled(true)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
PhoneMic.shared.stopRecording()
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// MARK: - Glasses Commands
|
|
636
|
+
|
|
637
|
+
private func playStartupSequence() {
|
|
638
|
+
Bridge.log("MAN: playStartupSequence()")
|
|
639
|
+
// Arrow frames for the animation
|
|
640
|
+
let arrowFrames = ["↑", "↗", "↑", "↖"]
|
|
641
|
+
|
|
642
|
+
let delay = 0.25 // Frame delay in seconds
|
|
643
|
+
let totalCycles = 2 // Number of animation cycles
|
|
644
|
+
|
|
645
|
+
// Variables to track animation state
|
|
646
|
+
var frameIndex = 0
|
|
647
|
+
var cycles = 0
|
|
648
|
+
|
|
649
|
+
// Create a dispatch queue for the animation
|
|
650
|
+
let animationQueue = DispatchQueue.global(qos: .userInteractive)
|
|
651
|
+
|
|
652
|
+
/// Function to display the current animation frame
|
|
653
|
+
func displayFrame() {
|
|
654
|
+
// Check if we've completed all cycles
|
|
655
|
+
if cycles >= totalCycles {
|
|
656
|
+
// End animation with final message
|
|
657
|
+
Task { await sgc?.sendTextWall(" /// Glassly connected \\\\\\") }
|
|
658
|
+
animationQueue.asyncAfter(deadline: .now() + 1.0) {
|
|
659
|
+
self.sgc?.clearDisplay()
|
|
660
|
+
}
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Display current animation frame
|
|
665
|
+
let frameText =
|
|
666
|
+
" \(arrowFrames[frameIndex]) Glassly Booting \(arrowFrames[frameIndex])"
|
|
667
|
+
Task { await sgc?.sendTextWall(frameText) }
|
|
668
|
+
|
|
669
|
+
// Move to next frame
|
|
670
|
+
frameIndex = (frameIndex + 1) % arrowFrames.count
|
|
671
|
+
|
|
672
|
+
// Count completed cycles
|
|
673
|
+
if frameIndex == 0 {
|
|
674
|
+
cycles += 1
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Schedule next frame
|
|
678
|
+
animationQueue.asyncAfter(deadline: .now() + delay) {
|
|
679
|
+
displayFrame()
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Start the animation after a short initial delay
|
|
684
|
+
animationQueue.asyncAfter(deadline: .now() + 0.35) {
|
|
685
|
+
displayFrame()
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// MARK: - Auxiliary Commands
|
|
690
|
+
|
|
691
|
+
func initSGC(_ wearable: String) {
|
|
692
|
+
Bridge.log("Initializing manager for wearable: \(wearable)")
|
|
693
|
+
if sgc != nil && sgc?.type != wearable {
|
|
694
|
+
Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
|
|
695
|
+
sgc?.cleanup()
|
|
696
|
+
sgc = nil
|
|
697
|
+
resetSystemTimeSync()
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if sgc != nil {
|
|
701
|
+
Bridge.log("MAN: SGC already initialized")
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
if wearable.contains(DeviceTypes.SIMULATED) {
|
|
705
|
+
sgc = Simulated()
|
|
706
|
+
} else if wearable.contains(DeviceTypes.G1) {
|
|
707
|
+
sgc = G1()
|
|
708
|
+
} else if wearable.contains(DeviceTypes.G2) {
|
|
709
|
+
sgc = G2()
|
|
710
|
+
} else if wearable.contains(DeviceTypes.LIVE) {
|
|
711
|
+
sgc = MentraLive()
|
|
712
|
+
} else if wearable.contains(DeviceTypes.NIMO) {
|
|
713
|
+
sgc = Nimo()
|
|
714
|
+
} else if wearable.contains(DeviceTypes.AR99) {
|
|
715
|
+
sgc = Ar99()
|
|
716
|
+
} else if wearable.contains(DeviceTypes.FRAME) {
|
|
717
|
+
// sgc = FrameManager()
|
|
718
|
+
}
|
|
719
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_NEX
|
|
720
|
+
if sgc == nil && wearable.contains(DeviceTypes.NEX) {
|
|
721
|
+
sgc = MentraNexSGC.getInstance()
|
|
722
|
+
}
|
|
723
|
+
#endif
|
|
724
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_VUZIX
|
|
725
|
+
if sgc == nil {
|
|
726
|
+
if wearable.contains(DeviceTypes.MACH1) {
|
|
727
|
+
sgc = Mach1()
|
|
728
|
+
} else if wearable.contains(DeviceTypes.Z100) {
|
|
729
|
+
sgc = Mach1() // Z100 uses same hardware/SDK as Mach1
|
|
730
|
+
sgc?.type = DeviceTypes.Z100 // Override type to Z100
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
#endif
|
|
734
|
+
// update device model:
|
|
735
|
+
DeviceStore.shared.apply("glasses", "deviceModel", sgc?.type ?? "")
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
func initController(_ controllerModel: String) {
|
|
739
|
+
Bridge.log("MAN: Initializing controller: \(controllerModel)")
|
|
740
|
+
if controller != nil && controller?.type != controllerModel {
|
|
741
|
+
Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
|
|
742
|
+
controller?.cleanup()
|
|
743
|
+
controller = nil
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if controller != nil {
|
|
747
|
+
Bridge.log("MAN: Controller already initialized")
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if controllerModel == ControllerTypes.R1 {
|
|
752
|
+
controller = R1()
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
func sendCurrentState() {
|
|
757
|
+
if screenDisabled {
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
if sgc?.navigationModeActive == true {
|
|
761
|
+
// The glasses' native nav UI owns the display. View/scene state is
|
|
762
|
+
// still retained above; the SGC triggers a repaint when nav exits.
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
Task {
|
|
767
|
+
var currentViewState: ViewState!
|
|
768
|
+
if headUp {
|
|
769
|
+
currentViewState = self.viewStates[1]
|
|
770
|
+
} else {
|
|
771
|
+
currentViewState = self.viewStates[0]
|
|
772
|
+
}
|
|
773
|
+
if headUp && !self.contextualDashboard {
|
|
774
|
+
currentViewState = self.viewStates[0]
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true {
|
|
778
|
+
// dont send the event to glasses that aren't there:
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
var fullyBooted = sgc?.fullyBooted ?? false
|
|
783
|
+
if !fullyBooted {
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// cancel any pending clear display work item:
|
|
788
|
+
sendStateWorkItem?.cancel()
|
|
789
|
+
|
|
790
|
+
let layoutType = currentViewState.layoutType
|
|
791
|
+
switch layoutType {
|
|
792
|
+
case "text_wall":
|
|
793
|
+
let text = parsePlaceholders(currentViewState.text)
|
|
794
|
+
await sgc?.sendTextWall(text)
|
|
795
|
+
case "double_text_wall":
|
|
796
|
+
let topText = parsePlaceholders(currentViewState.topText)
|
|
797
|
+
let bottomText = parsePlaceholders(currentViewState.bottomText)
|
|
798
|
+
await sgc?.sendDoubleTextWall(topText, bottomText)
|
|
799
|
+
case "reference_card":
|
|
800
|
+
let title = parsePlaceholders(currentViewState.title)
|
|
801
|
+
let text = parsePlaceholders(currentViewState.text)
|
|
802
|
+
await sgc?.sendTextWall(title + "\n\n" + text)
|
|
803
|
+
case "bitmap_view":
|
|
804
|
+
// Bridge.log("MAN: Processing bitmap_view layout")
|
|
805
|
+
guard let data = currentViewState.data else {
|
|
806
|
+
Bridge.log("MAN: ERROR: bitmap_view missing data field")
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
// Bridge.log("MAN: Processing bitmap_view with base64 data, length: \(data.count)")
|
|
810
|
+
await sgc?.displayBitmap(
|
|
811
|
+
base64ImageData: data,
|
|
812
|
+
x: currentViewState.bmpX,
|
|
813
|
+
y: currentViewState.bmpY,
|
|
814
|
+
width: currentViewState.bmpWidth,
|
|
815
|
+
height: currentViewState.bmpHeight
|
|
816
|
+
)
|
|
817
|
+
case "positioned_text":
|
|
818
|
+
let text = parsePlaceholders(currentViewState.text)
|
|
819
|
+
Bridge.log(
|
|
820
|
+
"MAN: positioned_text -> text='\(text)' rect=\(currentViewState.bmpX ?? 0),\(currentViewState.bmpY ?? 0) \(currentViewState.bmpWidth ?? 576)x\(currentViewState.bmpHeight ?? 288)"
|
|
821
|
+
)
|
|
822
|
+
await sgc?.sendPositionedText(
|
|
823
|
+
text,
|
|
824
|
+
x: currentViewState.bmpX ?? 0,
|
|
825
|
+
y: currentViewState.bmpY ?? 0,
|
|
826
|
+
width: currentViewState.bmpWidth ?? 576,
|
|
827
|
+
height: currentViewState.bmpHeight ?? 288,
|
|
828
|
+
borderWidth: currentViewState.borderWidth ?? 0,
|
|
829
|
+
borderRadius: currentViewState.borderRadius ?? 0
|
|
830
|
+
)
|
|
831
|
+
case "scene":
|
|
832
|
+
let sceneIndex = (headUp && self.contextualDashboard) ? 1 : 0
|
|
833
|
+
if let frame = self.sceneStates[sceneIndex] {
|
|
834
|
+
await sgc?.applySceneFrame(frame)
|
|
835
|
+
}
|
|
836
|
+
case "clear_view":
|
|
837
|
+
sgc?.clearDisplay()
|
|
838
|
+
default:
|
|
839
|
+
Bridge.log("UNHANDLED LAYOUT_TYPE \(layoutType)")
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// MARK: - Native navigation (glasses-rendered nav UI, e.g. G2 service 0x08)
|
|
845
|
+
|
|
846
|
+
func navigationModeStart() {
|
|
847
|
+
sgc?.enterNavigationMode()
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/// Exit the native nav UI. The SGC repaints the retained view state itself
|
|
851
|
+
/// once the firmware hands the screen back (see G2.exitNavigationMode).
|
|
852
|
+
func navigationModeStop(arrived: Bool) {
|
|
853
|
+
sgc?.exitNavigationMode(arrived: arrived)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
func navigationUpdateInfo(_ params: [String: Any]) {
|
|
857
|
+
sgc?.sendNavigationInfo(
|
|
858
|
+
directionIcon: (params["directionIcon"] as? NSNumber)?.int32Value ?? 0,
|
|
859
|
+
distance: params["distance"] as? String ?? "",
|
|
860
|
+
roadName: params["roadName"] as? String ?? "",
|
|
861
|
+
spendTime: params["spendTime"] as? String ?? "",
|
|
862
|
+
remainDistance: params["remainDistance"] as? String ?? "",
|
|
863
|
+
etaTime: params["etaTime"] as? String ?? "",
|
|
864
|
+
speed: params["speed"] as? String ?? ""
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
func navigationMiniMap(_ params: [String: Any]) {
|
|
869
|
+
guard let data = params["data"] as? String else {
|
|
870
|
+
Bridge.log("MAN: navigationMiniMap missing data field")
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
sgc?.sendNavigationMiniMap(
|
|
874
|
+
base64ImageData: data,
|
|
875
|
+
rotationDeg: (params["rotationDeg"] as? NSNumber)?.int32Value ?? 0,
|
|
876
|
+
borderEnabled: (params["border"] as? Bool) ?? true
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
func navigationOverviewMap(_ params: [String: Any]) {
|
|
881
|
+
guard let data = params["data"] as? String else {
|
|
882
|
+
Bridge.log("MAN: navigationOverviewMap missing data field")
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
sgc?.sendNavigationOverviewMap(base64ImageData: data)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
func parsePlaceholders(_ text: String) -> String {
|
|
889
|
+
guard text.contains("$") else { return text }
|
|
890
|
+
|
|
891
|
+
let dateFormatter = DateFormatter()
|
|
892
|
+
dateFormatter.dateFormat = "M/dd, h:mm"
|
|
893
|
+
let formattedDate = dateFormatter.string(from: Date())
|
|
894
|
+
|
|
895
|
+
// 12-hour time format (with leading zeros for hours)
|
|
896
|
+
let time12Format = DateFormatter()
|
|
897
|
+
time12Format.dateFormat = "hh:mm"
|
|
898
|
+
let time12 = time12Format.string(from: Date())
|
|
899
|
+
|
|
900
|
+
// 24-hour time format
|
|
901
|
+
let time24Format = DateFormatter()
|
|
902
|
+
time24Format.dateFormat = "HH:mm"
|
|
903
|
+
let time24 = time24Format.string(from: Date())
|
|
904
|
+
|
|
905
|
+
// Current date with format MM/dd
|
|
906
|
+
let dateFormat = DateFormatter()
|
|
907
|
+
dateFormat.dateFormat = "MM/dd"
|
|
908
|
+
let currentDate = dateFormat.string(from: Date())
|
|
909
|
+
|
|
910
|
+
var placeholders: [String: String] = [:]
|
|
911
|
+
placeholders["$no_datetime$"] = formattedDate
|
|
912
|
+
placeholders["$DATE$"] = currentDate
|
|
913
|
+
placeholders["$TIME12$"] = time12
|
|
914
|
+
placeholders["$TIME24$"] = time24
|
|
915
|
+
|
|
916
|
+
if (sgc?.batteryLevel ?? -1) == -1 {
|
|
917
|
+
placeholders["$GBATT$"] = ""
|
|
918
|
+
} else {
|
|
919
|
+
placeholders["$GBATT$"] = "\(sgc!.batteryLevel)%"
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// TODO: config: derive real connection status instead of hardcoding.
|
|
923
|
+
placeholders["$CONNECTION_STATUS$"] = "Connected"
|
|
924
|
+
|
|
925
|
+
var result = text
|
|
926
|
+
for (key, value) in placeholders {
|
|
927
|
+
result = result.replacingOccurrences(of: key, with: value)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return result
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private func checkAndReinitGlassesMic() {
|
|
934
|
+
// Re-arm from app INTENT, not glasses STATE: a firmware page kill clears
|
|
935
|
+
// glasses/micEnabled to false while the app still wants the mic, which used to
|
|
936
|
+
// make this watchdog return early and strand the mic. micEnabled + currentMic
|
|
937
|
+
// are DeviceManager-owned intent and survive page kills. If the app wants the
|
|
938
|
+
// glasses mic on and we haven't seen an lc3 event in >5s, reinitialize it.
|
|
939
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
940
|
+
if !micEnabled || !glassesConnected {
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Intent is on but no mic was ever selected — updateMicState() ran at a moment
|
|
945
|
+
// when nothing was available (app backgrounded, glasses mid-reconnect) and set
|
|
946
|
+
// currentMic to "". Retry selection instead of staying stranded.
|
|
947
|
+
if currentMic.isEmpty {
|
|
948
|
+
Bridge.log("MAN: mic intent on but no mic selected — retrying mic selection")
|
|
949
|
+
updateMicState()
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if currentMic != MicTypes.GLASSES_CUSTOM {
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if sgc?.isMicSuspendedForAudio == true {
|
|
958
|
+
Bridge.log("MAN: Glasses mic intentionally suspended for phone audio; skipping mic recovery")
|
|
959
|
+
return
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if PhoneAudioMonitor.getInstance().isOwnAppAudioPlaying() {
|
|
963
|
+
Bridge.log("MAN: Glassly audio is playing; skipping glasses mic recovery")
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// When no frame has ever been received, treat elapsed as "forever" so we
|
|
968
|
+
// actually attempt recovery (was `?? Date()` before, which made elapsed 0 and
|
|
969
|
+
// turned the watchdog into a no-op for a mic that never started at all).
|
|
970
|
+
let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? .distantPast)
|
|
971
|
+
if timeSinceLastLc3Event > 5 {
|
|
972
|
+
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
|
|
973
|
+
sgc?.setMicEnabled(true)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
func getAudioDevicePattern() -> String {
|
|
978
|
+
let audioDevicePattern: String
|
|
979
|
+
if let idRange = deviceName.range(of: "_BLE_", options: .caseInsensitive) {
|
|
980
|
+
// Extract the ID after "_BLE_" (e.g., "ABC123")
|
|
981
|
+
audioDevicePattern = String(deviceName[idRange.upperBound...])
|
|
982
|
+
} else if let idRange = deviceName.range(of: "_BT_", options: .caseInsensitive) {
|
|
983
|
+
// Extract the ID after "_BT_"
|
|
984
|
+
audioDevicePattern = String(deviceName[idRange.upperBound...])
|
|
985
|
+
} else {
|
|
986
|
+
// Fallback: use the full device name
|
|
987
|
+
audioDevicePattern = deviceName
|
|
988
|
+
}
|
|
989
|
+
return audioDevicePattern
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
func checkCurrentAudioDevice() {
|
|
993
|
+
let audioDevicePattern = getAudioDevicePattern()
|
|
994
|
+
Bridge.log("MAN: checkCurrentAudioDevice: audioDevicePattern: \(audioDevicePattern)")
|
|
995
|
+
|
|
996
|
+
if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
|
|
997
|
+
glassesBluetoothClassicConnected = false
|
|
998
|
+
Bridge.log("MAN: Audio device pattern is empty or simulated, returning")
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// check if the device disconnected:
|
|
1003
|
+
let isConnected = AudioSessionMonitor.isAudioDeviceConnected(
|
|
1004
|
+
devicePattern: audioDevicePattern
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
if !isConnected {
|
|
1008
|
+
Bridge.log("MAN: Device '\(deviceName)' disconnected")
|
|
1009
|
+
glassesBluetoothClassicConnected = false
|
|
1010
|
+
|
|
1011
|
+
let isOtherDeviceConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(
|
|
1012
|
+
devicePattern: audioDevicePattern
|
|
1013
|
+
)
|
|
1014
|
+
if isOtherDeviceConnected {
|
|
1015
|
+
Bridge.log("MAN: Other device connected, returning")
|
|
1016
|
+
otherBtConnected = true
|
|
1017
|
+
}
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
|
|
1022
|
+
if isPaired {
|
|
1023
|
+
let session = AVAudioSession.sharedInstance()
|
|
1024
|
+
let deviceName = session.availableInputs?.first(where: {
|
|
1025
|
+
$0.portName.localizedCaseInsensitiveContains(audioDevicePattern)
|
|
1026
|
+
})?.portName
|
|
1027
|
+
Bridge.log("MAN: Successfully detected newly paired device '\(deviceName)'")
|
|
1028
|
+
glassesBluetoothClassicConnected = true
|
|
1029
|
+
} else {
|
|
1030
|
+
glassesBluetoothClassicConnected = false
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
func onRouteChange(
|
|
1035
|
+
reason: AVAudioSession.RouteChangeReason, availableInputs: [AVAudioSessionPortDescription]
|
|
1036
|
+
) {
|
|
1037
|
+
Bridge.log("MAN: onRouteChange: reason: \(reason)")
|
|
1038
|
+
Bridge.log("MAN: onRouteChange: inputs: \(availableInputs)")
|
|
1039
|
+
|
|
1040
|
+
// check if our deviceName is connected:
|
|
1041
|
+
// (return if deviceName is empty):
|
|
1042
|
+
if deviceName.isEmpty {
|
|
1043
|
+
Bridge.log("MAN: Device name is empty, returning")
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Add small delay to let iOS populate availableInputs
|
|
1048
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
1049
|
+
guard let self = self else { return }
|
|
1050
|
+
checkCurrentAudioDevice()
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
updateMicState()
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
func onInterruption(began: Bool) {
|
|
1057
|
+
Bridge.log("MAN: Interruption: \(began)")
|
|
1058
|
+
systemMicUnavailable = began
|
|
1059
|
+
updateMicState()
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
func restartTranscriber() {
|
|
1063
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
|
|
1064
|
+
Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
|
|
1065
|
+
transcriber?.restart()
|
|
1066
|
+
#else
|
|
1067
|
+
Bridge.log("MAN: Local STT is not included in this SwiftPM build")
|
|
1068
|
+
#endif
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// MARK: - connection state management
|
|
1072
|
+
|
|
1073
|
+
func handleDeviceReady() {
|
|
1074
|
+
guard let sgc else {
|
|
1075
|
+
Bridge.log("MAN: SGC is nil, returning")
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
|
|
1079
|
+
resetMicHealth()
|
|
1080
|
+
|
|
1081
|
+
if !pendingDeviceName.isEmpty {
|
|
1082
|
+
deviceName = pendingDeviceName
|
|
1083
|
+
}
|
|
1084
|
+
if !pendingDeviceAddress.isEmpty {
|
|
1085
|
+
deviceAddress = pendingDeviceAddress
|
|
1086
|
+
}
|
|
1087
|
+
clearPendingConnection()
|
|
1088
|
+
defaultWearable = sgc.type
|
|
1089
|
+
searching = false
|
|
1090
|
+
|
|
1091
|
+
let connectionKey = "\(sgc.type):\(deviceName)"
|
|
1092
|
+
syncSystemTimeOnceForConnection(sgc, connectionKey: connectionKey)
|
|
1093
|
+
|
|
1094
|
+
// re-apply display height/depth after reconnection
|
|
1095
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
|
1096
|
+
// Re-read the current sgc rather than capturing the connect-time instance: the user may
|
|
1097
|
+
// have disconnected or switched glasses during the 2s window, and we must not push to a
|
|
1098
|
+
// stale/torn-down connection.
|
|
1099
|
+
guard let sgc = self?.sgc else { return }
|
|
1100
|
+
let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
|
|
1101
|
+
// Fall back to the canonical default (2), matching DeviceStore, not 1.
|
|
1102
|
+
let rawDepth = DeviceStore.shared.get("bluetooth", "dashboard_depth") as? Int ?? 2
|
|
1103
|
+
let d = min(max(rawDepth, 1), 4)
|
|
1104
|
+
sgc.setDashboardPosition(h, d)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Show welcome message on first connect for all display glasses
|
|
1108
|
+
if shouldSendBootingMessage {
|
|
1109
|
+
Task {
|
|
1110
|
+
// Wait until stock-vs-CFW is known: a banner drawn down the stock
|
|
1111
|
+
// path with the clear routed down the CFW path never leaves the
|
|
1112
|
+
// glass (the clear can't reach the stock text container).
|
|
1113
|
+
await sgc.awaitFirmwareClassification()
|
|
1114
|
+
await sgc.sendTextWall("// Glassly connected")
|
|
1115
|
+
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
|
1116
|
+
sgc.clearDisplay()
|
|
1117
|
+
}
|
|
1118
|
+
shouldSendBootingMessage = false
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Call device-specific setup handlers
|
|
1122
|
+
if defaultWearable.contains(DeviceTypes.G1) {
|
|
1123
|
+
handleG1Ready()
|
|
1124
|
+
} else if defaultWearable.contains(DeviceTypes.G2) {
|
|
1125
|
+
// handleG2Ready()
|
|
1126
|
+
} else if defaultWearable.contains(DeviceTypes.MACH1) {
|
|
1127
|
+
handleMach1Ready()
|
|
1128
|
+
} else if defaultWearable.contains(DeviceTypes.Z100) {
|
|
1129
|
+
handleMach1Ready() // Z100 uses same initialization as Mach1
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// check current audio device:
|
|
1133
|
+
checkCurrentAudioDevice()
|
|
1134
|
+
|
|
1135
|
+
// Re-arm the mic from the stored should_send_* flags. disconnect() zeroes the
|
|
1136
|
+
// micEnabled intent, but miniapp subscriptions (should_send_pcm/lc3) survive the
|
|
1137
|
+
// reconnect — without this recompute the mic stays off until a subscription edge.
|
|
1138
|
+
setMicState()
|
|
1139
|
+
|
|
1140
|
+
// save the default_wearable now that we're connected:
|
|
1141
|
+
Bridge.saveSetting("default_wearable", defaultWearable)
|
|
1142
|
+
Bridge.saveSetting("device_name", deviceName)
|
|
1143
|
+
Bridge.saveSetting("device_address", deviceAddress)
|
|
1144
|
+
if defaultWearable.contains(DeviceTypes.AR99) {
|
|
1145
|
+
let projectName = (DeviceStore.shared.get("bluetooth", "project_name") as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
1146
|
+
Bridge.saveSetting("project_name", projectName)
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private func syncSystemTimeOnceForConnection(_ sgc: SGCManager, connectionKey: String) {
|
|
1151
|
+
if sgc.type.contains(DeviceTypes.SIMULATED) {
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
if connectionKey == lastSystemTimeSyncConnectionKey {
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
lastSystemTimeSyncConnectionKey = connectionKey
|
|
1159
|
+
pendingSystemTimeSync?.cancel()
|
|
1160
|
+
systemTimeSyncGeneration += 1
|
|
1161
|
+
let generation = systemTimeSyncGeneration
|
|
1162
|
+
let sync = DispatchWorkItem { [weak self] in
|
|
1163
|
+
guard let self,
|
|
1164
|
+
self.lastSystemTimeSyncConnectionKey == connectionKey,
|
|
1165
|
+
self.systemTimeSyncGeneration == generation
|
|
1166
|
+
else {
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
self.pendingSystemTimeSync = nil
|
|
1170
|
+
let timestampMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1171
|
+
Bridge.log("MAN: Syncing glasses system time once for connection: \(timestampMs)")
|
|
1172
|
+
sgc.sendSetSystemTime(timestampMs)
|
|
1173
|
+
}
|
|
1174
|
+
pendingSystemTimeSync = sync
|
|
1175
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: sync)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private func resetSystemTimeSync() {
|
|
1179
|
+
pendingSystemTimeSync?.cancel()
|
|
1180
|
+
pendingSystemTimeSync = nil
|
|
1181
|
+
systemTimeSyncGeneration += 1
|
|
1182
|
+
lastSystemTimeSyncConnectionKey = ""
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
func handleControllerReady() {
|
|
1186
|
+
guard let controller else {
|
|
1187
|
+
Bridge.log("MAN: Controller is nil, returning")
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
1190
|
+
Bridge.log("MAN: handleControllerReady(): \(controller.type)")
|
|
1191
|
+
|
|
1192
|
+
pendingController = ""
|
|
1193
|
+
defaultController = controller.type
|
|
1194
|
+
searching = false
|
|
1195
|
+
|
|
1196
|
+
// save the default_controller now that we're connected:
|
|
1197
|
+
Bridge.saveSetting("default_controller", defaultController)
|
|
1198
|
+
Bridge.saveSetting("controller_device_name", controllerDeviceName)
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
func handleControllerDisconnected() {
|
|
1202
|
+
Bridge.log("MAN: Controller disconnected")
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private func handleG1Ready() {
|
|
1206
|
+
// G1-specific setup and configuration
|
|
1207
|
+
Task {
|
|
1208
|
+
// give the glasses some extra time to finish booting:
|
|
1209
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
1210
|
+
await sgc?.setSilentMode(false) // turn off silent mode
|
|
1211
|
+
await sgc?.getBatteryStatus()
|
|
1212
|
+
|
|
1213
|
+
// send loaded settings to glasses:
|
|
1214
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1215
|
+
sgc?.setHeadUpAngle(headUpAngle)
|
|
1216
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1217
|
+
sgc?.setBrightness(brightness, autoMode: autoBrightness)
|
|
1218
|
+
try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1219
|
+
// self.g1Manager?.RN_setDashboardPosition(self.dashboardHeight, self.dashboardDepth)
|
|
1220
|
+
// try? await Task.sleep(nanoseconds: 400_000_000)
|
|
1221
|
+
// playStartupSequence()
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private func handleMach1Ready() {}
|
|
1226
|
+
|
|
1227
|
+
func handleDeviceDisconnected() {
|
|
1228
|
+
Bridge.log("MAN: Device disconnected")
|
|
1229
|
+
resetSystemTimeSync()
|
|
1230
|
+
resetMicHealth()
|
|
1231
|
+
DeviceStore.shared.apply("glasses", "headUp", false)
|
|
1232
|
+
DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
1233
|
+
// shouldSendBootingMessage = true // Reset for next first connect
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// MARK: - Network Command handlers
|
|
1237
|
+
|
|
1238
|
+
func displayText(_ params: [String: Any]) {
|
|
1239
|
+
guard let text = params["text"] as? String else {
|
|
1240
|
+
Bridge.log("MAN: display_text missing text parameter")
|
|
1241
|
+
return
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
Bridge.log("MAN: Displaying text: \(text)")
|
|
1245
|
+
Task { await sgc?.sendTextWall(text) }
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
func displayEvent(_ event: [String: Any]) {
|
|
1249
|
+
guard let view = event["view"] as? String else {
|
|
1250
|
+
Bridge.log("MAN: invalid view")
|
|
1251
|
+
return
|
|
1252
|
+
}
|
|
1253
|
+
let isDashboard = view == "dashboard"
|
|
1254
|
+
|
|
1255
|
+
var stateIndex = 0
|
|
1256
|
+
if isDashboard {
|
|
1257
|
+
stateIndex = 1
|
|
1258
|
+
} else {
|
|
1259
|
+
stateIndex = 0
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Scene frames (display.render() pipeline) take their own path: the
|
|
1263
|
+
// whole frame is the unit, not a layout, and the host's per-element
|
|
1264
|
+
// annotations make redundant frames self-deduping (all-"unchanged"
|
|
1265
|
+
// frames no-op in the SGC base handler).
|
|
1266
|
+
if let sceneMap = event["scene"] as? [String: Any] {
|
|
1267
|
+
handleSceneEvent(stateIndex, sceneMap)
|
|
1268
|
+
return
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
guard let layout = event["layout"] as? [String: Any],
|
|
1272
|
+
let layoutType = layout["layoutType"] as? String
|
|
1273
|
+
else {
|
|
1274
|
+
Bridge.log("MAN: displayEvent missing layout")
|
|
1275
|
+
return
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Scene→legacy handoff: a legacy layout is about to draw over a scene
|
|
1279
|
+
// (e.g. a cloud app taking the view from a miniapp). Sweep the scene's
|
|
1280
|
+
// elements first so they don't linger under the new content; clear_view
|
|
1281
|
+
// wipes everything anyway.
|
|
1282
|
+
if let prevFrame = sceneStates[stateIndex] {
|
|
1283
|
+
sceneStates[stateIndex] = nil
|
|
1284
|
+
if layoutType != "clear_view" {
|
|
1285
|
+
let ids = prevFrame.elements.map(\.id)
|
|
1286
|
+
Task { [weak self] in
|
|
1287
|
+
await self?.sgc?.clearSceneElements(ids)
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
var text = layout["text"] as? String ?? " "
|
|
1292
|
+
var topText = layout["topText"] as? String ?? " "
|
|
1293
|
+
var bottomText = layout["bottomText"] as? String ?? " "
|
|
1294
|
+
var title = layout["title"] as? String ?? " "
|
|
1295
|
+
var data = layout["data"] as? String ?? ""
|
|
1296
|
+
|
|
1297
|
+
// Optional bitmap_view container position/size (forwarded to the SGC; used by G2).
|
|
1298
|
+
let bmpX = (layout["x"] as? NSNumber).map { $0.int32Value }
|
|
1299
|
+
let bmpY = (layout["y"] as? NSNumber).map { $0.int32Value }
|
|
1300
|
+
let bmpWidth = (layout["width"] as? NSNumber).map { $0.int32Value }
|
|
1301
|
+
let bmpHeight = (layout["height"] as? NSNumber).map { $0.int32Value }
|
|
1302
|
+
let borderWidth = (layout["borderWidth"] as? NSNumber).map { $0.int32Value }
|
|
1303
|
+
let borderRadius = (layout["borderRadius"] as? NSNumber).map { $0.int32Value }
|
|
1304
|
+
|
|
1305
|
+
text = parsePlaceholders(text)
|
|
1306
|
+
topText = parsePlaceholders(topText)
|
|
1307
|
+
bottomText = parsePlaceholders(bottomText)
|
|
1308
|
+
title = parsePlaceholders(title)
|
|
1309
|
+
|
|
1310
|
+
var newViewState = ViewState(
|
|
1311
|
+
topText: topText, bottomText: bottomText, title: title, layoutType: layoutType,
|
|
1312
|
+
text: text, data: data, animationData: nil,
|
|
1313
|
+
bmpX: bmpX, bmpY: bmpY, bmpWidth: bmpWidth, bmpHeight: bmpHeight,
|
|
1314
|
+
borderWidth: borderWidth, borderRadius: borderRadius
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
if layoutType == "bitmap_animation" {
|
|
1318
|
+
if let frames = layout["frames"] as? [String],
|
|
1319
|
+
let interval = layout["interval"] as? Double
|
|
1320
|
+
{
|
|
1321
|
+
let animationData: [String: Any] = [
|
|
1322
|
+
"frames": frames,
|
|
1323
|
+
"interval": interval,
|
|
1324
|
+
"repeat": layout["repeat"] as? Bool ?? true,
|
|
1325
|
+
]
|
|
1326
|
+
newViewState.animationData = animationData
|
|
1327
|
+
Bridge.log(
|
|
1328
|
+
"MAN: Parsed bitmap_animation with \(frames.count) frames, interval: \(interval)ms"
|
|
1329
|
+
)
|
|
1330
|
+
} else {
|
|
1331
|
+
Bridge.log("MAN: ERROR: bitmap_animation missing frames or interval")
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// NOTE: positioned_text used to bypass the viewState slot here (the
|
|
1336
|
+
// "sticky overlay" hack for the old nav HUD). Scenes made that
|
|
1337
|
+
// obsolete: multi-element frames arrive as ONE scene event, so nothing
|
|
1338
|
+
// clobbers anything. Legacy positioned_text now flows through the slot
|
|
1339
|
+
// like every other layout (matching Android).
|
|
1340
|
+
|
|
1341
|
+
let cS = viewStates[stateIndex]
|
|
1342
|
+
let nS = newViewState
|
|
1343
|
+
let currentState =
|
|
1344
|
+
cS.layoutType + cS.text + cS.topText + cS.bottomText + cS.title + (cS.data ?? "")
|
|
1345
|
+
let newState =
|
|
1346
|
+
nS.layoutType + nS.text + nS.topText + nS.bottomText + nS.title + (nS.data ?? "")
|
|
1347
|
+
|
|
1348
|
+
if currentState == newState {
|
|
1349
|
+
// Core.log("MAN: View state is the same, skipping update")
|
|
1350
|
+
return
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Bridge.log("MAN: Updating view state \(stateIndex) with \(layoutType) \(text) \(topText) \(bottomText)")
|
|
1354
|
+
|
|
1355
|
+
viewStates[stateIndex] = newViewState
|
|
1356
|
+
|
|
1357
|
+
let hUp = headUp && contextualDashboard
|
|
1358
|
+
// send the state we just received if the user is currently in that state:
|
|
1359
|
+
if stateIndex == 0 && !hUp {
|
|
1360
|
+
sendCurrentState()
|
|
1361
|
+
} else if stateIndex == 1 && hUp {
|
|
1362
|
+
sendCurrentState()
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/// Parse + store a scene frame, then dispatch it if its view is visible.
|
|
1367
|
+
private func handleSceneEvent(_ stateIndex: Int, _ sceneMap: [String: Any]) {
|
|
1368
|
+
guard var frame = parseSceneFrame(sceneMap) else { return }
|
|
1369
|
+
let prevFrame = sceneStates[stateIndex]
|
|
1370
|
+
|
|
1371
|
+
if prevFrame == nil {
|
|
1372
|
+
// Legacy→scene handoff: stale legacy content (e.g. a cloud app's
|
|
1373
|
+
// text wall) must not linger under the scene's elements.
|
|
1374
|
+
// clearDisplay is the per-device "wipe what's there" (blank-in-place
|
|
1375
|
+
// on G2 - no page rebuild).
|
|
1376
|
+
let prevLegacyType = viewStates[stateIndex].layoutType
|
|
1377
|
+
if !prevLegacyType.isEmpty, prevLegacyType != "clear_view", prevLegacyType != "scene" {
|
|
1378
|
+
sgc?.clearDisplay()
|
|
1379
|
+
}
|
|
1380
|
+
} else if let prevFrame, prevFrame.appId != frame.appId {
|
|
1381
|
+
// Cross-app switch: the host's diff baseline is per-app, so the new
|
|
1382
|
+
// app's annotations don't know the old app's elements are on the
|
|
1383
|
+
// glasses. Sweep the old app's elements (SGC registries still map
|
|
1384
|
+
// them), then paint the new frame from scratch. The boot message
|
|
1385
|
+
// interposes between apps in practice, so this isn't visible.
|
|
1386
|
+
let ids = prevFrame.elements.map(\.id)
|
|
1387
|
+
Task { [weak self] in
|
|
1388
|
+
await self?.sgc?.clearSceneElements(ids)
|
|
1389
|
+
}
|
|
1390
|
+
frame = frame.asReplay()
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Store the REDISPATCH form: any later sendCurrentState (dashboard
|
|
1394
|
+
// exit, head-up return) must repaint the whole frame - the original
|
|
1395
|
+
// annotations are only valid for the first dispatch right now.
|
|
1396
|
+
sceneStates[stateIndex] = frame.asReplay()
|
|
1397
|
+
viewStates[stateIndex] = ViewState(
|
|
1398
|
+
topText: " ", bottomText: " ", title: " ", layoutType: "scene",
|
|
1399
|
+
text: " ", data: nil, animationData: nil
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
let hUp = headUp && contextualDashboard
|
|
1403
|
+
if (stateIndex == 0 && !hUp) || (stateIndex == 1 && hUp) {
|
|
1404
|
+
dispatchSceneFrame(frame)
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/// Guarded scene dispatch - mirrors sendCurrentState's send conditions.
|
|
1409
|
+
private func dispatchSceneFrame(_ frame: SceneFrame) {
|
|
1410
|
+
if screenDisabled {
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
// Native nav UI owns the display; the retained scene state (stored as
|
|
1414
|
+
// a replay frame above) repaints when navigation mode exits.
|
|
1415
|
+
if sgc?.navigationModeActive == true {
|
|
1416
|
+
return
|
|
1417
|
+
}
|
|
1418
|
+
if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true {
|
|
1419
|
+
return
|
|
1420
|
+
}
|
|
1421
|
+
guard sgc?.fullyBooted == true else {
|
|
1422
|
+
Bridge.log("MAN: dispatchSceneFrame(): sgc not ready")
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1425
|
+
Task { [weak self] in
|
|
1426
|
+
await self?.sgc?.applySceneFrame(frame)
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
private func parseSceneFrame(_ sceneMap: [String: Any]) -> SceneFrame? {
|
|
1431
|
+
guard let elementsRaw = sceneMap["elements"] as? [[String: Any]] else { return nil }
|
|
1432
|
+
let elements: [SceneElement] = elementsRaw.compactMap { el in
|
|
1433
|
+
guard let id = el["id"] as? String,
|
|
1434
|
+
let type = el["type"] as? String,
|
|
1435
|
+
let box = el["box"] as? [String: Any]
|
|
1436
|
+
else { return nil }
|
|
1437
|
+
let style = el["style"] as? [String: Any]
|
|
1438
|
+
return SceneElement(
|
|
1439
|
+
id: id,
|
|
1440
|
+
type: type,
|
|
1441
|
+
x: (box["x"] as? NSNumber)?.int32Value ?? 0,
|
|
1442
|
+
y: (box["y"] as? NSNumber)?.int32Value ?? 0,
|
|
1443
|
+
w: (box["w"] as? NSNumber)?.int32Value ?? 0,
|
|
1444
|
+
h: (box["h"] as? NSNumber)?.int32Value ?? 0,
|
|
1445
|
+
text: (el["text"] as? String).map { parsePlaceholders($0) },
|
|
1446
|
+
data: el["data"] as? String,
|
|
1447
|
+
border: ((style?["border"]) as? NSNumber)?.int32Value ?? 0,
|
|
1448
|
+
radius: ((style?["radius"]) as? NSNumber)?.int32Value ?? 0,
|
|
1449
|
+
change: el["change"] as? String ?? "created",
|
|
1450
|
+
contentHash: el["contentHash"] as? String ?? "",
|
|
1451
|
+
font: (style?["font"] as? String) ?? "default",
|
|
1452
|
+
fontSize: ((style?["fontSize"]) as? NSNumber)?.int32Value ?? 0
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
let canvas = sceneMap["canvas"] as? [String: Any]
|
|
1456
|
+
return SceneFrame(
|
|
1457
|
+
appId: sceneMap["appId"] as? String ?? "",
|
|
1458
|
+
epoch: (sceneMap["sceneEpoch"] as? NSNumber)?.intValue ?? 0,
|
|
1459
|
+
replay: sceneMap["replay"] as? Bool ?? false,
|
|
1460
|
+
elements: elements,
|
|
1461
|
+
removed: (sceneMap["removed"] as? [String]) ?? [],
|
|
1462
|
+
canvasWidth: (canvas?["width"] as? NSNumber)?.int32Value ?? 0,
|
|
1463
|
+
canvasHeight: (canvas?["height"] as? NSNumber)?.int32Value ?? 0
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
func showDashboard() {
|
|
1468
|
+
sgc?.showDashboard()
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/// Flip the head-up state (double-tap fallback policy, driven from the JS engine).
|
|
1472
|
+
func toggleHeadUp() {
|
|
1473
|
+
headUp = !headUp
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
func showNotificationsPanel() {
|
|
1477
|
+
Task { await sgc?.showNotificationsPanel() }
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
func ping() {
|
|
1481
|
+
sgc?.ping()
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
func dbg1() {
|
|
1485
|
+
// sgc?.disconnectController()
|
|
1486
|
+
// connectDefaultController()
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
func dbg2() {}
|
|
1490
|
+
|
|
1491
|
+
func startStream(_ message: [String: Any]) {
|
|
1492
|
+
var message = message
|
|
1493
|
+
Bridge.log("MAN: startStream: \(message)")
|
|
1494
|
+
sgc?.startStream(message)
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
func stopStream() {
|
|
1498
|
+
Bridge.log("MAN: stopStream")
|
|
1499
|
+
sgc?.stopStream()
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
func keepStreamAlive(_ message: [String: Any]) {
|
|
1503
|
+
Bridge.log("MAN: sendStreamKeepAlive: \(message)")
|
|
1504
|
+
sgc?.sendStreamKeepAlive(message)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
func requestWifiScan(scanId: String? = nil) {
|
|
1508
|
+
Bridge.log("MAN: Requesting wifi scan")
|
|
1509
|
+
DeviceStore.shared.apply("bluetooth", "wifiScanResults", [])
|
|
1510
|
+
sgc?.requestWifiScan(scanId: scanId)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
|
|
1514
|
+
Bridge.log("MAN: Sending incidentId to glasses for log upload: \(incidentId)")
|
|
1515
|
+
sgc?.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
func sendWifiCredentials(_ ssid: String, _ password: String) {
|
|
1519
|
+
Bridge.log("MAN: Sending wifi credentials: \(ssid) \(password)")
|
|
1520
|
+
sgc?.sendWifiCredentials(ssid, password)
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
func forgetWifiNetwork(_ ssid: String) {
|
|
1524
|
+
Bridge.log("MAN: Forgetting wifi network: \(ssid)")
|
|
1525
|
+
sgc?.forgetWifiNetwork(ssid)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
func setHotspotState(_ enabled: Bool) {
|
|
1529
|
+
Bridge.log("MAN: 🔥 Setting glasses hotspot state: \(enabled)")
|
|
1530
|
+
sgc?.sendHotspotState(enabled)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
func setWifiAdbState(_ enabled: Bool) {
|
|
1534
|
+
Bridge.log("MAN: 🔧 Setting glasses Wi-Fi ADB state: \(enabled)")
|
|
1535
|
+
sgc?.sendWifiAdbState(enabled)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
func setSystemTime(_ timestampMs: Int64) {
|
|
1539
|
+
Bridge.log("MAN: Setting glasses system time: \(timestampMs)")
|
|
1540
|
+
sgc?.sendSetSystemTime(timestampMs)
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
func queryGalleryStatus() {
|
|
1544
|
+
Bridge.log("MAN: 📸 Querying gallery status from glasses")
|
|
1545
|
+
sgc?.queryGalleryStatus()
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/// Send OTA start command to glasses.
|
|
1549
|
+
/// Called when user approves an update (onboarding or background mode).
|
|
1550
|
+
/// Triggers glasses to begin download and installation.
|
|
1551
|
+
func sendOtaStart(otaVersionUrl: String? = nil) {
|
|
1552
|
+
Bridge.log("MAN: 📱 Sending OTA start command to glasses")
|
|
1553
|
+
sgc?.sendOtaStart(otaVersionUrl: otaVersionUrl)
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
func sendOtaQueryStatus() {
|
|
1557
|
+
Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
|
|
1558
|
+
(sgc as? MentraLive)?.sendOtaQueryStatus()
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
func startAr99OtaFromFile(_ path: String) throws -> Bool {
|
|
1562
|
+
guard let ar99 = sgc as? Ar99 else {
|
|
1563
|
+
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires AR99 glasses.")
|
|
1564
|
+
}
|
|
1565
|
+
return ar99.startOtaFromFile(path)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
func cancelAr99Ota() {
|
|
1569
|
+
(sgc as? Ar99)?.cancelAr99Ota()
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
func sendAr99FactoryReset() throws {
|
|
1573
|
+
guard let ar99 = sgc as? Ar99 else {
|
|
1574
|
+
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires AR99 glasses.")
|
|
1575
|
+
}
|
|
1576
|
+
ar99.sendFactoryReset()
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
private func liveSgc() throws -> MentraLive {
|
|
1580
|
+
guard let live = sgc as? MentraLive else {
|
|
1581
|
+
throw BluetoothSdkError(code: "unsupported_device", message: "This command requires Mentra Live glasses.")
|
|
1582
|
+
}
|
|
1583
|
+
return live
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
func sendGalleryMode(requestId: String, enabled: Bool) throws {
|
|
1587
|
+
try liveSgc().sendGalleryMode(requestId: requestId, active: enabled)
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
func sendButtonPhotoSettings(requestId: String, size: String) throws {
|
|
1591
|
+
try sendButtonPhotoSettings(
|
|
1592
|
+
requestId: requestId,
|
|
1593
|
+
settings: PhotoCaptureDefaults(size: PhotoSize(normalizedRawValue: size))
|
|
1594
|
+
)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
func sendButtonPhotoSettings(requestId: String, settings: PhotoCaptureDefaults) throws {
|
|
1598
|
+
try liveSgc().sendButtonPhotoSettings(requestId: requestId, settings: settings)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
func sendButtonVideoRecordingSettings(requestId: String, width: Int, height: Int, fps: Int) throws {
|
|
1602
|
+
try liveSgc().sendButtonVideoRecordingSettings(requestId: requestId, width: width, height: height, fps: fps)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
func sendButtonMaxRecordingTime(requestId: String, minutes: Int) throws {
|
|
1606
|
+
try liveSgc().sendButtonMaxRecordingTime(requestId: requestId, minutes: minutes)
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
func sendCameraFovSetting(requestId: String, fov: Int, roiPosition: Int) throws {
|
|
1610
|
+
try liveSgc().sendCameraFovSetting(requestId: requestId, fov: fov, roiPosition: roiPosition)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
func sendLegacyCameraFovSetting(fov: Int, roiPosition: Int) throws {
|
|
1614
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
1615
|
+
guard glassesConnected else {
|
|
1616
|
+
throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
|
|
1617
|
+
}
|
|
1618
|
+
try liveSgc().sendCameraFovSetting(requestId: nil, fov: fov, roiPosition: roiPosition)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
func restoreLegacyCameraFovSetting() throws {
|
|
1622
|
+
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
|
|
1623
|
+
guard glassesConnected else {
|
|
1624
|
+
throw BluetoothSdkError(code: "not_connected", message: "Mentra Live glasses are not connected.")
|
|
1625
|
+
}
|
|
1626
|
+
try liveSgc().sendCameraFovSetting()
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
func sendCameraFovOverride(
|
|
1630
|
+
requestId: String,
|
|
1631
|
+
leaseId: String,
|
|
1632
|
+
fov: Int,
|
|
1633
|
+
roiPosition: Int,
|
|
1634
|
+
ttlMs: Int
|
|
1635
|
+
) throws {
|
|
1636
|
+
try liveSgc().sendCameraFovOverride(
|
|
1637
|
+
requestId: requestId,
|
|
1638
|
+
leaseId: leaseId,
|
|
1639
|
+
fov: fov,
|
|
1640
|
+
roiPosition: roiPosition,
|
|
1641
|
+
ttlMs: ttlMs
|
|
1642
|
+
)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
func releaseCameraFovOverride(requestId: String, leaseId: String) throws {
|
|
1646
|
+
try liveSgc().releaseCameraFovOverride(requestId: requestId, leaseId: leaseId)
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
func sendCameraTuningConfig(requestId: String, anrOn: Bool, gainOn: Bool) throws {
|
|
1650
|
+
try liveSgc().sendCameraTuningConfig(requestId: requestId, anrOn: anrOn, gainOn: gainOn)
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
func warmUpCamera(
|
|
1654
|
+
requestId: String,
|
|
1655
|
+
size: PhotoSize,
|
|
1656
|
+
mode: PhotoMode = .photo,
|
|
1657
|
+
exposureTimeNs: Double?,
|
|
1658
|
+
durationMs: Int,
|
|
1659
|
+
zsl: Bool? = nil,
|
|
1660
|
+
mfnr: Bool? = nil
|
|
1661
|
+
) throws {
|
|
1662
|
+
guard let live = sgc as? MentraLive else {
|
|
1663
|
+
// Fail fast like other camera commands so the SDK promise rejects immediately instead
|
|
1664
|
+
// of hanging until the request timeout with no camera_status.
|
|
1665
|
+
throw BluetoothSdkError(
|
|
1666
|
+
code: "unsupported_device", message: "This command requires Mentra Live glasses."
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
live.warmUpCamera(
|
|
1670
|
+
requestId: requestId,
|
|
1671
|
+
size: size,
|
|
1672
|
+
mode: mode,
|
|
1673
|
+
exposureTimeNs: exposureTimeNs,
|
|
1674
|
+
durationMs: durationMs,
|
|
1675
|
+
zsl: zsl,
|
|
1676
|
+
mfnr: mfnr
|
|
1677
|
+
)
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
func stopCameraWarmUp(requestId: String) throws {
|
|
1681
|
+
try liveSgc().stopCameraWarmUp(requestId: requestId)
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/// Request version info from glasses.
|
|
1685
|
+
/// Glasses will respond with version_info message containing build number, firmware version, etc.
|
|
1686
|
+
func requestVersionInfo() {
|
|
1687
|
+
Bridge.log("MAN: 📱 Requesting version info from glasses")
|
|
1688
|
+
sgc?.requestVersionInfo()
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
func availableG2FirmwareArtifacts() throws -> [[String: Any]] {
|
|
1692
|
+
guard let g2 = sgc as? G2 else {
|
|
1693
|
+
throw G2FirmwareFlashError.transport("The connected device is not a G2.")
|
|
1694
|
+
}
|
|
1695
|
+
return g2.availableFirmwareArtifacts()
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
func startG2FirmwareFlash(target: String) async throws -> [String: Any] {
|
|
1699
|
+
guard let g2 = sgc as? G2 else {
|
|
1700
|
+
throw G2FirmwareFlashError.transport("The connected device is not a G2.")
|
|
1701
|
+
}
|
|
1702
|
+
return try await g2.startFirmwareFlash(target: target)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/// Send shutdown command to glasses.
|
|
1706
|
+
/// This will initiate a graceful shutdown of the device.
|
|
1707
|
+
func sendShutdown() {
|
|
1708
|
+
Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
|
|
1709
|
+
sgc?.sendShutdown()
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
/// Send reboot command to glasses.
|
|
1713
|
+
/// This will initiate a reboot of the device.
|
|
1714
|
+
func sendReboot() {
|
|
1715
|
+
Bridge.log("MAN: 🔄 Sending reboot command to glasses")
|
|
1716
|
+
sgc?.sendReboot()
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
func startVideoRecording(
|
|
1720
|
+
_ requestId: String, _ save: Bool, _ sound: Bool, _ width: Int = 0, _ height: Int = 0,
|
|
1721
|
+
_ fps: Int = 0, _ maxRecordingTimeMinutes: Int = 0
|
|
1722
|
+
) {
|
|
1723
|
+
Bridge.log(
|
|
1724
|
+
"MAN: onStartVideoRecording: requestId=\(requestId), save=\(save), sound=\(sound), resolution=\(width)x\(height)@\(fps)fps, maxRecordingTimeMinutes=\(maxRecordingTimeMinutes)"
|
|
1725
|
+
)
|
|
1726
|
+
sgc?.startVideoRecording(
|
|
1727
|
+
requestId: requestId, save: save, sound: sound, width: width, height: height,
|
|
1728
|
+
fps: fps, maxRecordingTimeMinutes: maxRecordingTimeMinutes
|
|
1729
|
+
)
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
func queryVideoRecordingStatus(_ requestId: String) {
|
|
1733
|
+
Bridge.log("MAN: Querying video recording status: requestId=\(requestId)")
|
|
1734
|
+
sgc?.queryVideoRecordingStatus(requestId: requestId)
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
func stopVideoRecording(_ requestId: String, _ webhookUrl: String?, _ authToken: String?) {
|
|
1738
|
+
Bridge.log(
|
|
1739
|
+
"MAN: onStopVideoRecording: requestId=\(requestId), webhook=\((webhookUrl?.isEmpty ?? true) ? "none" : "set")"
|
|
1740
|
+
)
|
|
1741
|
+
sgc?.stopVideoRecording(requestId: requestId, webhookUrl: webhookUrl, authToken: authToken)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
func setMicState() {
|
|
1745
|
+
let willSendPcm = shouldSendPcm || shouldSendLc3
|
|
1746
|
+
let willSendTranscript = shouldSendTranscript || localSttFallbackActive
|
|
1747
|
+
// Speaker enrollment records through whichever mic is currently
|
|
1748
|
+
// ranked active, so it holds the mic open like any other consumer.
|
|
1749
|
+
micEnabled = willSendPcm || willSendTranscript || speakerEnrollmentActive
|
|
1750
|
+
updateMicState()
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// MARK: - Speaker identification
|
|
1754
|
+
|
|
1755
|
+
/// Enrollment holds the mic open (see setMicState). Set by
|
|
1756
|
+
/// startSpeakerEnrollment / cleared when the session ends or is cancelled.
|
|
1757
|
+
private var speakerEnrollmentActive = false
|
|
1758
|
+
|
|
1759
|
+
func setSpeakerIdEnabled(_ enabled: Bool) {
|
|
1760
|
+
SpeakerIdEngine.shared.setIdentifyEnabled(enabled)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
func setSpeakerIdModelPath(_ path: String) {
|
|
1764
|
+
SpeakerIdEngine.setModelPath(path)
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
func setSpeakerProfiles(_ profiles: [[String: Any]]) {
|
|
1768
|
+
let parsed: [SpeakerIdEngine.Profile] = profiles.compactMap { entry in
|
|
1769
|
+
guard let id = entry["id"] as? String,
|
|
1770
|
+
let name = entry["name"] as? String,
|
|
1771
|
+
let base64 = entry["embedding"] as? String,
|
|
1772
|
+
let embedding = SpeakerIdEngine.embedding(fromBase64: base64)
|
|
1773
|
+
else {
|
|
1774
|
+
return nil
|
|
1775
|
+
}
|
|
1776
|
+
return SpeakerIdEngine.Profile(id: id, name: name, embedding: embedding)
|
|
1777
|
+
}
|
|
1778
|
+
SpeakerIdEngine.shared.setProfiles(parsed)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
func startSpeakerEnrollment(targetSpeechSeconds: Double) {
|
|
1782
|
+
let source = currentMic == MicTypes.GLASSES_CUSTOM ? "glasses" : "phone"
|
|
1783
|
+
speakerEnrollmentActive = true
|
|
1784
|
+
setMicState()
|
|
1785
|
+
SpeakerIdEngine.shared.startEnrollment(targetSpeechSeconds: targetSpeechSeconds, source: source) {
|
|
1786
|
+
Task { @MainActor [weak self] in
|
|
1787
|
+
guard let self else { return }
|
|
1788
|
+
self.speakerEnrollmentActive = false
|
|
1789
|
+
self.setMicState()
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
func cancelSpeakerEnrollment() {
|
|
1795
|
+
SpeakerIdEngine.shared.cancelEnrollment()
|
|
1796
|
+
speakerEnrollmentActive = false
|
|
1797
|
+
setMicState()
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
func rgbLedControl(
|
|
1801
|
+
requestId: String,
|
|
1802
|
+
packageName: String?,
|
|
1803
|
+
action: String,
|
|
1804
|
+
color: String?,
|
|
1805
|
+
onDurationMs: Int,
|
|
1806
|
+
offDurationMs: Int,
|
|
1807
|
+
count: Int
|
|
1808
|
+
) {
|
|
1809
|
+
sgc?.sendRgbLedControl(
|
|
1810
|
+
requestId: requestId,
|
|
1811
|
+
packageName: packageName,
|
|
1812
|
+
action: action,
|
|
1813
|
+
color: color,
|
|
1814
|
+
onDurationMs: onDurationMs,
|
|
1815
|
+
offDurationMs: offDurationMs,
|
|
1816
|
+
count: count
|
|
1817
|
+
)
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/// Mentra Live only: K900 `cs_getvol` / `sr_getvol` (step volume 0-5).
|
|
1821
|
+
func getGlassesMediaVolume() async throws -> [String: Any] {
|
|
1822
|
+
guard let live = sgc as? MentraLive else {
|
|
1823
|
+
throw NSError(
|
|
1824
|
+
domain: "DeviceManager",
|
|
1825
|
+
code: 100,
|
|
1826
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
|
|
1827
|
+
)
|
|
1828
|
+
}
|
|
1829
|
+
return try await withCheckedThrowingContinuation {
|
|
1830
|
+
(continuation: CheckedContinuation<[String: Any], Error>) in
|
|
1831
|
+
live.getGlassesMediaVolume { result in
|
|
1832
|
+
continuation.resume(with: result)
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/// Mentra Live only: K900 `cs_vol` / `sr_vol`.
|
|
1838
|
+
func setGlassesMediaVolume(level: Int) async throws -> [String: Any] {
|
|
1839
|
+
guard let live = sgc as? MentraLive else {
|
|
1840
|
+
throw NSError(
|
|
1841
|
+
domain: "DeviceManager",
|
|
1842
|
+
code: 100,
|
|
1843
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
|
|
1844
|
+
)
|
|
1845
|
+
}
|
|
1846
|
+
return try await withCheckedThrowingContinuation {
|
|
1847
|
+
(continuation: CheckedContinuation<[String: Any], Error>) in
|
|
1848
|
+
live.setGlassesMediaVolume(level: level) { result in
|
|
1849
|
+
continuation.resume(with: result)
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
func requestPhoto(_ request: PhotoRequest) {
|
|
1855
|
+
let manualExposureNs = request.exposureTimeNs.flatMap { $0.isFinite && $0 > 0 ? $0 : nil }
|
|
1856
|
+
let manualIso = manualExposureNs != nil ? request.iso.flatMap { $0 > 0 ? $0 : nil } : nil
|
|
1857
|
+
let routed = PhotoRequest(
|
|
1858
|
+
requestId: request.requestId,
|
|
1859
|
+
size: request.size,
|
|
1860
|
+
webhookUrl: request.webhookUrl,
|
|
1861
|
+
authToken: request.authToken,
|
|
1862
|
+
compress: request.compress,
|
|
1863
|
+
save: request.save,
|
|
1864
|
+
sound: request.sound,
|
|
1865
|
+
exposureTimeNs: manualExposureNs,
|
|
1866
|
+
iso: manualIso,
|
|
1867
|
+
aeExposureDivisor: request.aeExposureDivisor,
|
|
1868
|
+
isoCap: request.isoCap,
|
|
1869
|
+
noiseReduction: request.noiseReduction,
|
|
1870
|
+
edgeEnhancement: request.edgeEnhancement,
|
|
1871
|
+
mfnr: request.mfnr,
|
|
1872
|
+
zsl: request.zsl,
|
|
1873
|
+
ispDigitalGain: request.ispDigitalGain,
|
|
1874
|
+
ispAnalogGain: request.ispAnalogGain,
|
|
1875
|
+
mode: request.mode,
|
|
1876
|
+
transferMethod: request.transferMethod
|
|
1877
|
+
)
|
|
1878
|
+
Bridge.log(
|
|
1879
|
+
"MAN: PHOTO PIPELINE [4/6] DeviceManager.requestPhoto requestId=\(routed.requestId) webhookUrl=\(routed.webhookUrl ?? "nil") size=\(routed.size.rawValue) compress=\(routed.compress?.rawValue ?? "none") save=\(routed.save) sound=\(routed.sound) exposureTimeNs=\(manualExposureNs.map { String($0) } ?? "nil") iso=\(manualIso.map { String($0) } ?? "auto") aeDivisor=\(routed.aeExposureDivisor.map { String($0) } ?? "nil") isoCap=\(routed.isoCap.map { String($0) } ?? "nil") sgc=\(sgc != nil ? String(describing: type(of: sgc!)) : "null")"
|
|
1880
|
+
)
|
|
1881
|
+
guard let sgc else {
|
|
1882
|
+
Bridge.log(
|
|
1883
|
+
"MAN: PHOTO PIPELINE - sgc is null (glasses not connected); dropping requestId=\(routed.requestId)"
|
|
1884
|
+
)
|
|
1885
|
+
return
|
|
1886
|
+
}
|
|
1887
|
+
sgc.requestPhoto(routed)
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
func connectDefault() {
|
|
1891
|
+
if defaultWearable.isEmpty {
|
|
1892
|
+
Bridge.log("MAN: No default wearable, returning")
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1895
|
+
let reconnectTarget =
|
|
1896
|
+
if defaultWearable.contains(DeviceTypes.AR99), !deviceAddress.isEmpty {
|
|
1897
|
+
deviceAddress
|
|
1898
|
+
} else {
|
|
1899
|
+
deviceName
|
|
1900
|
+
}
|
|
1901
|
+
if reconnectTarget.isEmpty {
|
|
1902
|
+
Bridge.log("MAN: No reconnect target, returning")
|
|
1903
|
+
return
|
|
1904
|
+
}
|
|
1905
|
+
initSGC(defaultWearable)
|
|
1906
|
+
if let live = sgc as? MentraLive, live.isPairingYieldActive() {
|
|
1907
|
+
Bridge.log("MAN: connectDefault skipped — Mentra Live pairing yield active")
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1910
|
+
searching = true
|
|
1911
|
+
sgc?.connectById(reconnectTarget)
|
|
1912
|
+
connectDefaultController()
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
func connectDefaultController() {
|
|
1916
|
+
if defaultController.isEmpty {
|
|
1917
|
+
Bridge.log("MAN: No default controller, returning")
|
|
1918
|
+
return
|
|
1919
|
+
}
|
|
1920
|
+
if controllerDeviceName.isEmpty {
|
|
1921
|
+
Bridge.log("MAN: No controller device name, returning")
|
|
1922
|
+
return
|
|
1923
|
+
}
|
|
1924
|
+
initController(defaultController)
|
|
1925
|
+
searchingController = true
|
|
1926
|
+
controller?.connectById(controllerDeviceName)
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
func connectByName(_ dName: String) {
|
|
1930
|
+
Bridge.log("MAN: Connecting to wearable: \(dName)")
|
|
1931
|
+
var name = dName
|
|
1932
|
+
|
|
1933
|
+
// use stored device name if available:
|
|
1934
|
+
if dName.isEmpty && !deviceName.isEmpty {
|
|
1935
|
+
name = deviceName
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if pendingWearable.isEmpty, defaultWearable.isEmpty {
|
|
1939
|
+
Bridge.log("MAN: No pending or default wearable, returning")
|
|
1940
|
+
return
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if pendingWearable.isEmpty, !defaultWearable.isEmpty {
|
|
1944
|
+
Bridge.log("MAN: No pending wearable, using default wearable: \(defaultWearable)")
|
|
1945
|
+
pendingWearable = defaultWearable
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// if the pending wearable is a controller, don't disconnect, use the controller manager to connect
|
|
1949
|
+
if ControllerTypes.ALL.contains(pendingWearable) {
|
|
1950
|
+
controller?.disconnect()
|
|
1951
|
+
controller?.connectById(name)
|
|
1952
|
+
return
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
Task {
|
|
1956
|
+
disconnect(clearPendingConnection: false)
|
|
1957
|
+
try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
|
|
1958
|
+
self.searching = true
|
|
1959
|
+
self.pendingDeviceName = name
|
|
1960
|
+
|
|
1961
|
+
initSGC(self.pendingWearable)
|
|
1962
|
+
sgc?.connectById(name)
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
func connectDevice(_ deviceModel: String, _ deviceName: String) {
|
|
1967
|
+
Bridge.log("MAN: Connecting to device: \(deviceModel) \(deviceName)")
|
|
1968
|
+
if DeviceTypes.ALL.contains(deviceModel) {
|
|
1969
|
+
pendingWearable = deviceModel
|
|
1970
|
+
initSGC(pendingWearable)
|
|
1971
|
+
sgc?.connectById(deviceName)
|
|
1972
|
+
return
|
|
1973
|
+
}
|
|
1974
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
1975
|
+
pendingWearable = deviceModel
|
|
1976
|
+
initController(deviceModel)
|
|
1977
|
+
controller?.connectById(deviceName)
|
|
1978
|
+
return
|
|
1979
|
+
}
|
|
1980
|
+
Bridge.log("MAN: No compatible device model, returning")
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
func connectSimulated() {
|
|
1984
|
+
defaultWearable = DeviceTypes.SIMULATED
|
|
1985
|
+
deviceName = DeviceTypes.SIMULATED
|
|
1986
|
+
initSGC(defaultWearable)
|
|
1987
|
+
handleDeviceReady()
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
func disconnect(clearPendingConnection: Bool = true) {
|
|
1991
|
+
sgc?.clearDisplay() // clear the screen
|
|
1992
|
+
sgc?.disconnect()
|
|
1993
|
+
sgc = nil // Clear the SGC reference after disconnect
|
|
1994
|
+
resetSystemTimeSync()
|
|
1995
|
+
resetMicHealth()
|
|
1996
|
+
searching = false
|
|
1997
|
+
micEnabled = false
|
|
1998
|
+
updateMicState()
|
|
1999
|
+
shouldSendBootingMessage = true // Reset for next first connect
|
|
2000
|
+
// clear glasses properties:
|
|
2001
|
+
DeviceStore.shared.apply("glasses", "deviceModel", "")
|
|
2002
|
+
// Device identifiers are session-bound. Clear them on every disconnect so a
|
|
2003
|
+
// previously connected pair can never be reported for the next connection.
|
|
2004
|
+
DeviceStore.shared.apply("glasses", "serialNumber", "")
|
|
2005
|
+
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", "")
|
|
2006
|
+
DeviceStore.shared.apply("glasses", "wifiMacAddress", "")
|
|
2007
|
+
DeviceStore.shared.apply("glasses", "leftMacAddress", "")
|
|
2008
|
+
DeviceStore.shared.apply("glasses", "rightMacAddress", "")
|
|
2009
|
+
DeviceStore.shared.apply("glasses", "macAddress", "")
|
|
2010
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
2011
|
+
DeviceStore.shared.apply("glasses", "connected", false)
|
|
2012
|
+
DeviceStore.shared.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
|
|
2013
|
+
DeviceStore.shared.apply("glasses", "voiceActivityDetectionEnabled", BluetoothSdkDefaults.voiceActivityDetectionEnabled)
|
|
2014
|
+
// disconnect the controller as well:
|
|
2015
|
+
searchingController = false
|
|
2016
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
2017
|
+
controller?.disconnect()
|
|
2018
|
+
controller = nil // Clear the controller reference after disconnect
|
|
2019
|
+
if clearPendingConnection {
|
|
2020
|
+
self.clearPendingConnection()
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
private func clearPendingConnection() {
|
|
2025
|
+
pendingDeviceName = ""
|
|
2026
|
+
pendingDeviceAddress = ""
|
|
2027
|
+
pendingWearable = ""
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
func disconnectController() {
|
|
2031
|
+
searchingController = false
|
|
2032
|
+
// disconnect the controller from the glasses if applicable:
|
|
2033
|
+
sgc?.disconnectController()
|
|
2034
|
+
controller?.disconnect()
|
|
2035
|
+
controller = nil // Clear the controller reference after disconnect
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
func forget() {
|
|
2039
|
+
Bridge.log("MAN: Forgetting smart glasses")
|
|
2040
|
+
// Call forget first to stop timers/handlers/reconnect logic
|
|
2041
|
+
sgc?.forget()
|
|
2042
|
+
disconnect()
|
|
2043
|
+
// Clear state
|
|
2044
|
+
defaultWearable = ""
|
|
2045
|
+
deviceName = ""
|
|
2046
|
+
deviceAddress = ""
|
|
2047
|
+
clearPendingConnection()
|
|
2048
|
+
Bridge.saveSetting("default_wearable", "")
|
|
2049
|
+
Bridge.saveSetting("device_name", "")
|
|
2050
|
+
Bridge.saveSetting("device_address", "")
|
|
2051
|
+
Bridge.saveSetting("project_name", "")
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
func forgetController() {
|
|
2055
|
+
Bridge.log("MAN: Forgetting controller")
|
|
2056
|
+
controller?.forget()
|
|
2057
|
+
disconnectController()
|
|
2058
|
+
// Clear state
|
|
2059
|
+
defaultController = ""
|
|
2060
|
+
controllerDeviceName = ""
|
|
2061
|
+
Bridge.saveSetting("controller_device_name", "")
|
|
2062
|
+
Bridge.saveSetting("default_controller", "")
|
|
2063
|
+
DeviceStore.shared.apply("glasses", "controllerConnected", false)
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
func findCompatibleDevices(_ deviceModel: String) {
|
|
2067
|
+
Bridge.log("MAN: Searching for compatible device names for: \(deviceModel)")
|
|
2068
|
+
|
|
2069
|
+
// reset the search results:
|
|
2070
|
+
searchResults = []
|
|
2071
|
+
|
|
2072
|
+
if DeviceTypes.ALL.contains(deviceModel) {
|
|
2073
|
+
pendingWearable = deviceModel
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
2077
|
+
pendingWearable = deviceModel
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
if ControllerTypes.ALL.contains(deviceModel) {
|
|
2081
|
+
initController(deviceModel)
|
|
2082
|
+
controller?.findCompatibleDevices()
|
|
2083
|
+
return
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
initSGC(pendingWearable)
|
|
2087
|
+
sgc?.findCompatibleDevices()
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
func stopScan() {
|
|
2091
|
+
controller?.stopScan()
|
|
2092
|
+
sgc?.stopScan()
|
|
2093
|
+
DeviceStore.shared.apply("bluetooth", "searching", false)
|
|
2094
|
+
DeviceStore.shared.apply("bluetooth", "searchingController", false)
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
func cleanup() {
|
|
2098
|
+
// Clean up transcriber resources
|
|
2099
|
+
#if !SWIFT_PACKAGE || GLASSLY_FEATURE_LOCAL_STT
|
|
2100
|
+
transcriber?.shutdown()
|
|
2101
|
+
transcriber = nil
|
|
2102
|
+
#endif
|
|
2103
|
+
|
|
2104
|
+
// Clean up LC3 converter
|
|
2105
|
+
lc3Converter = nil
|
|
2106
|
+
|
|
2107
|
+
cancellables.removeAll()
|
|
2108
|
+
}
|
|
2109
|
+
}
|