@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,2451 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Ar99.swift
|
|
3
|
+
// BluetoothSDK
|
|
4
|
+
//
|
|
5
|
+
// Native iOS implementation for AR99 glasses.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import CoreBluetooth
|
|
10
|
+
import Foundation
|
|
11
|
+
|
|
12
|
+
private enum Ar99BLE {
|
|
13
|
+
static let ctrlService = CBUUID(string: "A72EC9A8-CF72-8544-AD96-D1481A02CE98")
|
|
14
|
+
static let ctrlHostToSlave = CBUUID(string: "A72EC9A9-CF72-8544-AD96-D1481A02CE98")
|
|
15
|
+
static let ctrlSlaveToHost = CBUUID(string: "A72EC9AA-CF72-8544-AD96-D1481A02CE98")
|
|
16
|
+
static let opusService = CBUUID(string: "F6B26AAD-41A5-93F5-F640-AC3BA3C73597")
|
|
17
|
+
static let opusSlaveToHost = CBUUID(string: "F6B26AAF-41A5-93F5-F640-AC3BA3C73597")
|
|
18
|
+
static let otaService = CBUUID(string: "E49A25F8-F69A-11E8-8EB2-F2801F1B9FD1")
|
|
19
|
+
static let otaWrite = CBUUID(string: "E49A25E0-F69A-11E8-8EB2-F2801F1B9FD1")
|
|
20
|
+
static let otaNotify = CBUUID(string: "E49A28E1-F69A-11E8-8EB2-F2801F1B9FD1")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private enum Ar99Protocol {
|
|
24
|
+
static let magicNumber: UInt32 = 0x1A2B3C4D
|
|
25
|
+
static let messageStart: [UInt8] = [0x0D, 0x4D, 0x3C, 0x2B, 0x1A]
|
|
26
|
+
|
|
27
|
+
static let funcComm = 1
|
|
28
|
+
static let funcTrans = 3
|
|
29
|
+
static let funcCommDisplay = 7
|
|
30
|
+
|
|
31
|
+
static let cmdTransDisplayStart = 100
|
|
32
|
+
static let cmdTransDisplayStop = 101
|
|
33
|
+
static let cmdTransDisplaySetContent = 102
|
|
34
|
+
|
|
35
|
+
static let cmdDisplayGetDeviceInfo = 100
|
|
36
|
+
static let cmdDisplayGetBright = 102
|
|
37
|
+
static let cmdDisplayGetBattery = 104
|
|
38
|
+
static let cmdCommSetTime = 31
|
|
39
|
+
static let cmdDisplaySetLanguage = 131
|
|
40
|
+
static let cmdDisplaySetBright = 132
|
|
41
|
+
static let cmdDisplaySetAutoAdjustLight = 138
|
|
42
|
+
static let cmdDisplayCtrlAudioRecord = 161
|
|
43
|
+
static let cmdDisplayCtrlFactoryReset = 162
|
|
44
|
+
static let cmdDisplayCtrlSysFunction = 164
|
|
45
|
+
static let cmdDisplayCtrlMinimalTextStart = 181
|
|
46
|
+
static let cmdDisplayCtrlMinimalTextSet = 182
|
|
47
|
+
static let cmdDisplayCtrlMinimalTextClear = 183
|
|
48
|
+
static let cmdDisplayCtrlMinimalTextStop = 184
|
|
49
|
+
|
|
50
|
+
static let textStreamStateUpdate = 1
|
|
51
|
+
static let transDisplaySourceLanguage = 0
|
|
52
|
+
static let transDisplayTargetLanguage = 1
|
|
53
|
+
static let transDisplayMicSourcePhone = 0
|
|
54
|
+
static let transDisplayFontSize = 24
|
|
55
|
+
static let displayFunctionTypeUiMode = 2
|
|
56
|
+
static let displayUiModeMinimal = 1
|
|
57
|
+
static let displayLanguageEnglish = 1
|
|
58
|
+
static let factoryResetConfirmCode = 0xAA
|
|
59
|
+
|
|
60
|
+
static let targetMobileApp = 0
|
|
61
|
+
static let targetGlassAndroid = 1
|
|
62
|
+
static let targetGlassMcuDisplay = 3
|
|
63
|
+
|
|
64
|
+
static let ctrlSenderMask = 0x03
|
|
65
|
+
static let ctrlActionResponse = 0x40
|
|
66
|
+
|
|
67
|
+
static let bleWriteChunkSize = 180
|
|
68
|
+
static let bleWriteChunkDelayMs = 10
|
|
69
|
+
static let scanDurationMs = 15_000
|
|
70
|
+
static let initialBatteryRetryDelayMs = 1_000
|
|
71
|
+
static let maxInitialBatteryRetries = 5
|
|
72
|
+
static let batteryPollIntervalMs = 60_000
|
|
73
|
+
static let readyMinimalModeDelayMs = 40
|
|
74
|
+
static let readyLanguageDelayMs = 90
|
|
75
|
+
static let readyDeviceInfoDelayMs = 140
|
|
76
|
+
static let readyBrightnessDelayMs = 280
|
|
77
|
+
static let readyBatteryDelayMs = 420
|
|
78
|
+
static let displaySessionIdleRestartMs = 8_000
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private struct Ar99MessageHeader {
|
|
82
|
+
let function: Int
|
|
83
|
+
let cmd: Int
|
|
84
|
+
let ctrl: Int
|
|
85
|
+
let sequence: Int
|
|
86
|
+
let payload: Data
|
|
87
|
+
let totalBytes: Int
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private struct Ar99CommonResult {
|
|
91
|
+
let success: Bool
|
|
92
|
+
let errorCode: Int
|
|
93
|
+
let errorMessage: String
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private struct Ar99BatteryInfo {
|
|
97
|
+
let battery: Int
|
|
98
|
+
let charging: Bool?
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private struct Ar99DeviceInfo {
|
|
102
|
+
let firmwareVersion: String?
|
|
103
|
+
let deviceName: String?
|
|
104
|
+
let productName: String?
|
|
105
|
+
let btMac: String?
|
|
106
|
+
let serialNumber: String?
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private struct Ar99LanguageResponse {
|
|
110
|
+
let success: Bool
|
|
111
|
+
let language: Int?
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private struct Ar99SetTimeResponse {
|
|
115
|
+
let success: Bool
|
|
116
|
+
let timeValue: Int64?
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private struct Ar99ParsedAdvertisement {
|
|
120
|
+
let bleName: String?
|
|
121
|
+
let projectName: String?
|
|
122
|
+
let bleAddress: String?
|
|
123
|
+
let btAddress: String?
|
|
124
|
+
let serialNumber: String?
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private struct Ar99ProtoReader {
|
|
128
|
+
private let bytes: [UInt8]
|
|
129
|
+
private(set) var offset = 0
|
|
130
|
+
|
|
131
|
+
init(_ data: Data) {
|
|
132
|
+
bytes = Array(data)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
var isAtEnd: Bool {
|
|
136
|
+
offset >= bytes.count
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
mutating func nextField() -> (field: Int, wire: Int)? {
|
|
140
|
+
guard let tag = readVarint(), tag != 0 else { return nil }
|
|
141
|
+
return (Int(tag >> 3), Int(tag & 0x07))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
mutating func readVarint() -> UInt64? {
|
|
145
|
+
var result: UInt64 = 0
|
|
146
|
+
var shift: UInt64 = 0
|
|
147
|
+
while offset < bytes.count, shift < 64 {
|
|
148
|
+
let byte = bytes[offset]
|
|
149
|
+
offset += 1
|
|
150
|
+
result |= UInt64(byte & 0x7F) << shift
|
|
151
|
+
if byte & 0x80 == 0 {
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
shift += 7
|
|
155
|
+
}
|
|
156
|
+
return nil
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
mutating func readFixed32() -> UInt32? {
|
|
160
|
+
guard offset + 4 <= bytes.count else { return nil }
|
|
161
|
+
let value =
|
|
162
|
+
UInt32(bytes[offset])
|
|
163
|
+
| (UInt32(bytes[offset + 1]) << 8)
|
|
164
|
+
| (UInt32(bytes[offset + 2]) << 16)
|
|
165
|
+
| (UInt32(bytes[offset + 3]) << 24)
|
|
166
|
+
offset += 4
|
|
167
|
+
return value
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
mutating func readLengthDelimited() -> Data? {
|
|
171
|
+
guard let rawLength = readVarint() else { return nil }
|
|
172
|
+
let length = Int(rawLength)
|
|
173
|
+
guard length >= 0, offset + length <= bytes.count else { return nil }
|
|
174
|
+
let data = Data(bytes[offset ..< offset + length])
|
|
175
|
+
offset += length
|
|
176
|
+
return data
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
mutating func readString() -> String? {
|
|
180
|
+
guard let data = readLengthDelimited() else { return nil }
|
|
181
|
+
return String(data: data, encoding: .utf8)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
mutating func skipField(wire: Int) -> Bool {
|
|
185
|
+
switch wire {
|
|
186
|
+
case 0:
|
|
187
|
+
return readVarint() != nil
|
|
188
|
+
case 1:
|
|
189
|
+
guard offset + 8 <= bytes.count else { return false }
|
|
190
|
+
offset += 8
|
|
191
|
+
return true
|
|
192
|
+
case 2:
|
|
193
|
+
guard let rawLength = readVarint() else { return false }
|
|
194
|
+
let length = Int(rawLength)
|
|
195
|
+
guard length >= 0, offset + length <= bytes.count else { return false }
|
|
196
|
+
offset += length
|
|
197
|
+
return true
|
|
198
|
+
case 5:
|
|
199
|
+
guard offset + 4 <= bytes.count else { return false }
|
|
200
|
+
offset += 4
|
|
201
|
+
return true
|
|
202
|
+
default:
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private enum Ar99ProtoWriter {
|
|
209
|
+
static func writeTag(field: Int, wire: Int, to data: inout Data) {
|
|
210
|
+
writeVarint(UInt64((field << 3) | wire), to: &data)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static func writeVarint(_ value: UInt64, to data: inout Data) {
|
|
214
|
+
var value = value
|
|
215
|
+
while value >= 0x80 {
|
|
216
|
+
data.append(UInt8(value & 0x7F) | 0x80)
|
|
217
|
+
value >>= 7
|
|
218
|
+
}
|
|
219
|
+
data.append(UInt8(value))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static func writeUInt32(field: Int, value: Int, to data: inout Data) {
|
|
223
|
+
writeTag(field: field, wire: 0, to: &data)
|
|
224
|
+
writeVarint(UInt64(max(0, value)), to: &data)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static func writeInt64(field: Int, value: Int64, to data: inout Data) {
|
|
228
|
+
writeTag(field: field, wire: 0, to: &data)
|
|
229
|
+
writeVarint(UInt64(bitPattern: value), to: &data)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static func writeInt32(field: Int, value: Int, to data: inout Data) {
|
|
233
|
+
writeTag(field: field, wire: 0, to: &data)
|
|
234
|
+
writeVarint(UInt64(bitPattern: Int64(Int32(value))), to: &data)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static func writeBool(field: Int, value: Bool, to data: inout Data) {
|
|
238
|
+
writeTag(field: field, wire: 0, to: &data)
|
|
239
|
+
writeVarint(value ? 1 : 0, to: &data)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
static func writeFixed32(field: Int, value: UInt32, to data: inout Data) {
|
|
243
|
+
writeTag(field: field, wire: 5, to: &data)
|
|
244
|
+
data.append(UInt8(value & 0xFF))
|
|
245
|
+
data.append(UInt8((value >> 8) & 0xFF))
|
|
246
|
+
data.append(UInt8((value >> 16) & 0xFF))
|
|
247
|
+
data.append(UInt8((value >> 24) & 0xFF))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static func writeBytes(field: Int, value: Data, to data: inout Data) {
|
|
251
|
+
writeTag(field: field, wire: 2, to: &data)
|
|
252
|
+
writeVarint(UInt64(value.count), to: &data)
|
|
253
|
+
data.append(value)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
static func writeString(field: Int, value: String, to data: inout Data) {
|
|
257
|
+
writeBytes(field: field, value: Data(value.utf8), to: &data)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private final class Ar99SystemOpusDecoder {
|
|
262
|
+
private let opusFormat: AVAudioFormat
|
|
263
|
+
private let pcmFormat: AVAudioFormat
|
|
264
|
+
private var converter: AVAudioConverter?
|
|
265
|
+
|
|
266
|
+
init?() {
|
|
267
|
+
var desc = AudioStreamBasicDescription(
|
|
268
|
+
mSampleRate: 16_000,
|
|
269
|
+
mFormatID: kAudioFormatOpus,
|
|
270
|
+
mFormatFlags: 0,
|
|
271
|
+
mBytesPerPacket: 0,
|
|
272
|
+
mFramesPerPacket: 320,
|
|
273
|
+
mBytesPerFrame: 0,
|
|
274
|
+
mChannelsPerFrame: 1,
|
|
275
|
+
mBitsPerChannel: 0,
|
|
276
|
+
mReserved: 0
|
|
277
|
+
)
|
|
278
|
+
guard let input = AVAudioFormat(streamDescription: &desc),
|
|
279
|
+
let output = AVAudioFormat(
|
|
280
|
+
commonFormat: .pcmFormatInt16,
|
|
281
|
+
sampleRate: 16_000,
|
|
282
|
+
channels: 1,
|
|
283
|
+
interleaved: true
|
|
284
|
+
)
|
|
285
|
+
else {
|
|
286
|
+
return nil
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
opusFormat = input
|
|
290
|
+
pcmFormat = output
|
|
291
|
+
converter = AVAudioConverter(from: input, to: output)
|
|
292
|
+
if converter == nil {
|
|
293
|
+
return nil
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func decode(_ opusFrame: Data) -> Data? {
|
|
298
|
+
guard let converter, !opusFrame.isEmpty else { return nil }
|
|
299
|
+
|
|
300
|
+
let compressed = AVAudioCompressedBuffer(
|
|
301
|
+
format: opusFormat,
|
|
302
|
+
packetCapacity: 1,
|
|
303
|
+
maximumPacketSize: opusFrame.count
|
|
304
|
+
)
|
|
305
|
+
opusFrame.withUnsafeBytes { raw in
|
|
306
|
+
guard let baseAddress = raw.baseAddress else { return }
|
|
307
|
+
compressed.data.copyMemory(from: baseAddress, byteCount: opusFrame.count)
|
|
308
|
+
}
|
|
309
|
+
compressed.byteLength = UInt32(opusFrame.count)
|
|
310
|
+
compressed.packetCount = 1
|
|
311
|
+
compressed.packetDescriptions?.pointee = AudioStreamPacketDescription(
|
|
312
|
+
mStartOffset: 0,
|
|
313
|
+
mVariableFramesInPacket: 0,
|
|
314
|
+
mDataByteSize: UInt32(opusFrame.count)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: pcmFormat, frameCapacity: 5_760) else {
|
|
318
|
+
return nil
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
var didFeedPacket = false
|
|
322
|
+
var error: NSError?
|
|
323
|
+
let status = converter.convert(to: pcmBuffer, error: &error) { _, outStatus in
|
|
324
|
+
if didFeedPacket {
|
|
325
|
+
outStatus.pointee = .noDataNow
|
|
326
|
+
return nil
|
|
327
|
+
}
|
|
328
|
+
didFeedPacket = true
|
|
329
|
+
outStatus.pointee = .haveData
|
|
330
|
+
return compressed
|
|
331
|
+
}
|
|
332
|
+
if status == .error {
|
|
333
|
+
Bridge.log("AR99: Opus decode error: \(error?.localizedDescription ?? "unknown")")
|
|
334
|
+
return nil
|
|
335
|
+
}
|
|
336
|
+
guard pcmBuffer.frameLength > 0, let channel = pcmBuffer.int16ChannelData else {
|
|
337
|
+
return nil
|
|
338
|
+
}
|
|
339
|
+
return Data(bytes: channel[0], count: Int(pcmBuffer.frameLength) * 2)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private final class Ar99OpusPcmDecoder {
|
|
344
|
+
private static let frameBytes = 48
|
|
345
|
+
private static let headerBytes = 8
|
|
346
|
+
private static let maxBufferBytes = 10_000
|
|
347
|
+
private static let maxDiagFrames: Int64 = 8
|
|
348
|
+
private static let expectedPrefix = Data([0x00, 0x00, 0x00, 0x28])
|
|
349
|
+
|
|
350
|
+
private let queue = DispatchQueue(label: "ar99-opus-decode")
|
|
351
|
+
private let onPcmDecoded: (Data) -> Void
|
|
352
|
+
private var decoder: Ar99SystemOpusDecoder?
|
|
353
|
+
private var buffer = Data()
|
|
354
|
+
private var totalNotifyCount: Int64 = 0
|
|
355
|
+
private var totalFrameCount: Int64 = 0
|
|
356
|
+
private var totalDecodeFailCount: Int64 = 0
|
|
357
|
+
private var totalHeaderMismatchCount: Int64 = 0
|
|
358
|
+
private var totalShortChunkCount: Int64 = 0
|
|
359
|
+
private var totalResyncDropBytes: Int64 = 0
|
|
360
|
+
private var headerLooksStable = true
|
|
361
|
+
|
|
362
|
+
init?(onPcmDecoded: @escaping (Data) -> Void) {
|
|
363
|
+
guard let decoder = Ar99SystemOpusDecoder() else { return nil }
|
|
364
|
+
self.decoder = decoder
|
|
365
|
+
self.onPcmDecoded = onPcmDecoded
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func submitBleNotify(_ data: Data) {
|
|
369
|
+
guard !data.isEmpty else { return }
|
|
370
|
+
queue.async { [weak self] in
|
|
371
|
+
self?.handleChunk(data)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func resetBuffer() {
|
|
376
|
+
queue.async { [weak self] in
|
|
377
|
+
self?.resetDecoderState()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private func resetDecoderState() {
|
|
382
|
+
buffer.removeAll()
|
|
383
|
+
decoder = Ar99SystemOpusDecoder()
|
|
384
|
+
totalNotifyCount = 0
|
|
385
|
+
totalFrameCount = 0
|
|
386
|
+
totalDecodeFailCount = 0
|
|
387
|
+
totalHeaderMismatchCount = 0
|
|
388
|
+
totalShortChunkCount = 0
|
|
389
|
+
totalResyncDropBytes = 0
|
|
390
|
+
headerLooksStable = true
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private func handleChunk(_ data: Data) {
|
|
394
|
+
guard decoder != nil else { return }
|
|
395
|
+
totalNotifyCount += 1
|
|
396
|
+
if data.count < Self.frameBytes {
|
|
397
|
+
totalShortChunkCount += 1
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if buffer.count + data.count > Self.maxBufferBytes {
|
|
401
|
+
Bridge.log("AR99 Opus buffer overflow, clear oldBuf=\(buffer.count), chunk=\(data.count), notify#=\(totalNotifyCount)")
|
|
402
|
+
buffer.removeAll()
|
|
403
|
+
}
|
|
404
|
+
let oldBufferLen = buffer.count
|
|
405
|
+
buffer.append(data)
|
|
406
|
+
|
|
407
|
+
var pcmFrames = 0
|
|
408
|
+
var pcmBytesOut = 0
|
|
409
|
+
var decodeFail = 0
|
|
410
|
+
var skipPayload = 0
|
|
411
|
+
var headerMismatch = 0
|
|
412
|
+
|
|
413
|
+
while buffer.count >= Self.expectedPrefix.count {
|
|
414
|
+
guard let frameStart = findFrameStart(in: buffer, from: 0) else {
|
|
415
|
+
let keep = min(buffer.count, Self.expectedPrefix.count - 1)
|
|
416
|
+
let drop = buffer.count - keep
|
|
417
|
+
if drop > 0 {
|
|
418
|
+
totalResyncDropBytes += Int64(drop)
|
|
419
|
+
headerLooksStable = false
|
|
420
|
+
let tail = buffer.suffix(keep)
|
|
421
|
+
Bridge.log("AR99 Opus resync drop(no header) notify#=\(totalNotifyCount) dropBytes=\(drop) keepTail=\(keep) tailHex=\(hex(Data(tail), maxBytes: keep))")
|
|
422
|
+
buffer = Data(tail)
|
|
423
|
+
}
|
|
424
|
+
break
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if frameStart > 0 {
|
|
428
|
+
let dropped = buffer.prefix(frameStart)
|
|
429
|
+
totalResyncDropBytes += Int64(frameStart)
|
|
430
|
+
headerLooksStable = false
|
|
431
|
+
headerMismatch += 1
|
|
432
|
+
totalHeaderMismatchCount += 1
|
|
433
|
+
Bridge.log("AR99 Opus resync shift notify#=\(totalNotifyCount) dropBytes=\(frameStart) bufLen=\(buffer.count) droppedHex=\(hex(Data(dropped), maxBytes: min(frameStart, 24)))")
|
|
434
|
+
buffer.removeFirst(frameStart)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if buffer.count < Self.frameBytes {
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let frame = buffer.prefix(Self.frameBytes)
|
|
442
|
+
buffer.removeFirst(Self.frameBytes)
|
|
443
|
+
totalFrameCount += 1
|
|
444
|
+
|
|
445
|
+
let prefixOk = Data(frame.prefix(Self.expectedPrefix.count)) == Self.expectedPrefix
|
|
446
|
+
if !prefixOk {
|
|
447
|
+
headerMismatch += 1
|
|
448
|
+
totalHeaderMismatchCount += 1
|
|
449
|
+
headerLooksStable = false
|
|
450
|
+
if totalFrameCount <= Self.maxDiagFrames || !headerLooksStable {
|
|
451
|
+
Bridge.log("AR99 Opus frame header mismatch frame#=\(totalFrameCount) notify#=\(totalNotifyCount) remain=\(buffer.count) frameHex=\(hex(Data(frame), maxBytes: Self.frameBytes))")
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let payload = frame.dropFirst(Self.headerBytes)
|
|
456
|
+
guard !payload.isEmpty, let pcm = decoder?.decode(Data(payload)), !pcm.isEmpty else {
|
|
457
|
+
if payload.isEmpty {
|
|
458
|
+
skipPayload += 1
|
|
459
|
+
} else {
|
|
460
|
+
decodeFail += 1
|
|
461
|
+
totalDecodeFailCount += 1
|
|
462
|
+
if totalFrameCount <= Self.maxDiagFrames || !headerLooksStable {
|
|
463
|
+
Bridge.log("AR99 Opus frame decode failed frame#=\(totalFrameCount) notify#=\(totalNotifyCount) prefixOk=\(prefixOk) frameHex=\(hex(Data(frame), maxBytes: Self.frameBytes)) payloadHex=\(hex(Data(payload), maxBytes: payload.count))")
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
pcmFrames += 1
|
|
469
|
+
pcmBytesOut += pcm.count
|
|
470
|
+
DispatchQueue.main.async { [onPcmDecoded] in
|
|
471
|
+
onPcmDecoded(pcm)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if data.count != Self.frameBytes
|
|
476
|
+
|| oldBufferLen != 0
|
|
477
|
+
|| decodeFail > 0
|
|
478
|
+
|| headerMismatch > 0
|
|
479
|
+
|| skipPayload > 0
|
|
480
|
+
|| pcmFrames == 0
|
|
481
|
+
|| !headerLooksStable
|
|
482
|
+
{
|
|
483
|
+
Bridge.log("AR99 Opus decode summary notify#=\(totalNotifyCount) inLen=\(data.count) pcmFrames=\(pcmFrames) pcmBytesOut=\(pcmBytesOut) decodeFail=\(decodeFail) headerMismatch=\(headerMismatch) skipPayload=\(skipPayload) bufRemain=\(buffer.count) totals={frames=\(totalFrameCount),fail=\(totalDecodeFailCount),headerMismatch=\(totalHeaderMismatchCount),shortChunk=\(totalShortChunkCount),resyncDropBytes=\(totalResyncDropBytes)}")
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private func findFrameStart(in data: Data, from: Int) -> Int? {
|
|
488
|
+
guard data.count >= Self.expectedPrefix.count else { return nil }
|
|
489
|
+
let startIndex = data.index(data.startIndex, offsetBy: max(0, from), limitedBy: data.endIndex) ?? data.startIndex
|
|
490
|
+
guard startIndex < data.endIndex,
|
|
491
|
+
let range = data[startIndex...].range(of: Self.expectedPrefix)
|
|
492
|
+
else {
|
|
493
|
+
return nil
|
|
494
|
+
}
|
|
495
|
+
return data.distance(from: data.startIndex, to: range.lowerBound)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private func hex(_ data: Data, maxBytes: Int) -> String {
|
|
499
|
+
guard !data.isEmpty, maxBytes > 0 else { return "" }
|
|
500
|
+
let prefix = data.prefix(maxBytes)
|
|
501
|
+
var output = prefix.map { String(format: "%02X", $0) }.joined(separator: " ")
|
|
502
|
+
if data.count > prefix.count {
|
|
503
|
+
output += "..."
|
|
504
|
+
}
|
|
505
|
+
return output
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@MainActor
|
|
510
|
+
final class Ar99: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, SGCManager, Ar99OtaGattTransport {
|
|
511
|
+
var type = DeviceTypes.AR99
|
|
512
|
+
let hasMic = true
|
|
513
|
+
|
|
514
|
+
private let supportedProjectNames = ["AR99"]
|
|
515
|
+
private var centralManager: CBCentralManager?
|
|
516
|
+
private var peripheral: CBPeripheral?
|
|
517
|
+
private var controlWriteCharacteristic: CBCharacteristic?
|
|
518
|
+
private var controlNotifyCharacteristic: CBCharacteristic?
|
|
519
|
+
private var opusNotifyCharacteristic: CBCharacteristic?
|
|
520
|
+
private var otaWriteCharacteristic: CBCharacteristic?
|
|
521
|
+
private var otaNotifyCharacteristic: CBCharacteristic?
|
|
522
|
+
private var pendingCharacteristicServices = Set<CBUUID>()
|
|
523
|
+
private var pendingNotifyCharacteristics = Set<CBUUID>()
|
|
524
|
+
private var configuredNotifications = false
|
|
525
|
+
private var isScanning = false
|
|
526
|
+
private var scanForConnection = false
|
|
527
|
+
private var targetIdentifier: String?
|
|
528
|
+
private var currentProjectName: String?
|
|
529
|
+
private var currentBroadcastMacAddress: String?
|
|
530
|
+
private var scanTimeoutItem: DispatchWorkItem?
|
|
531
|
+
private var discoveredNames = Set<String>()
|
|
532
|
+
private var lastConnectedDisplayName: String?
|
|
533
|
+
|
|
534
|
+
private var writeQueue: [Data] = []
|
|
535
|
+
private var otaWriteQueue: [Data] = []
|
|
536
|
+
private var writeInFlight = false
|
|
537
|
+
private var otaWriteInFlight = false
|
|
538
|
+
private var otaNotificationEnabled = false
|
|
539
|
+
private var writeDelayItem: DispatchWorkItem?
|
|
540
|
+
private var otaWriteDelayItem: DispatchWorkItem?
|
|
541
|
+
private var controlNotifyBuffer = Data()
|
|
542
|
+
private var sequence = 1
|
|
543
|
+
|
|
544
|
+
private var minimalModeReady = false
|
|
545
|
+
private var displaySessionStarted = false
|
|
546
|
+
private var displayContentStarted = false
|
|
547
|
+
private var lastDisplayContentAtMs: Int64 = 0
|
|
548
|
+
private var hasPendingDisplayUpdate = false
|
|
549
|
+
private var pendingDisplayText = ""
|
|
550
|
+
private var lastDisplayText = ""
|
|
551
|
+
private var pendingSourceText = ""
|
|
552
|
+
private var pendingTargetText = ""
|
|
553
|
+
private var lastDisplaySourceText = ""
|
|
554
|
+
private var lastDisplayTargetText = ""
|
|
555
|
+
private var awaitingMicDisableAck = false
|
|
556
|
+
|
|
557
|
+
private var hasReceivedBatteryForCurrentConnection = false
|
|
558
|
+
private var pendingBatteryRetries = 0
|
|
559
|
+
private var initialBatteryRetryItem: DispatchWorkItem?
|
|
560
|
+
private var periodicBatteryTimer: Timer?
|
|
561
|
+
private var readyItems: [DispatchWorkItem] = []
|
|
562
|
+
private var opusDecoder: Ar99OpusPcmDecoder?
|
|
563
|
+
private var opusLoggedFirstInSession = false
|
|
564
|
+
|
|
565
|
+
override init() {
|
|
566
|
+
super.init()
|
|
567
|
+
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
568
|
+
Ar99OtaManager.shared.setTransport(self)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
deinit {
|
|
572
|
+
Task { @MainActor in
|
|
573
|
+
Ar99OtaManager.shared.setTransport(nil)
|
|
574
|
+
}
|
|
575
|
+
centralManager?.delegate = nil
|
|
576
|
+
peripheral?.delegate = nil
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// MARK: - Audio Control
|
|
580
|
+
|
|
581
|
+
func setMicEnabled(_ enabled: Bool) {
|
|
582
|
+
if enabled {
|
|
583
|
+
awaitingMicDisableAck = false
|
|
584
|
+
DeviceStore.shared.apply("glasses", "micEnabled", true)
|
|
585
|
+
ensureOpusDecoder()
|
|
586
|
+
} else {
|
|
587
|
+
awaitingMicDisableAck = true
|
|
588
|
+
}
|
|
589
|
+
sendControlDisplayAudioRecord(enabled)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
func sortMicRanking(list: [String]) -> [String] {
|
|
593
|
+
list
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// MARK: - Messaging
|
|
597
|
+
|
|
598
|
+
func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
|
|
599
|
+
|
|
600
|
+
// MARK: - Camera & Media
|
|
601
|
+
|
|
602
|
+
func requestPhoto(_: PhotoRequest) {
|
|
603
|
+
Bridge.log("AR99: requestPhoto not implemented")
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
func startStream(_: [String: Any]) {}
|
|
607
|
+
func stopStream() {}
|
|
608
|
+
func sendStreamKeepAlive(_: [String: Any]) {}
|
|
609
|
+
func startVideoRecording(requestId _: String, save _: Bool, sound _: Bool) {}
|
|
610
|
+
func stopVideoRecording(requestId _: String) {}
|
|
611
|
+
func sendButtonPhotoSettings() {}
|
|
612
|
+
func sendButtonVideoRecordingSettings() {}
|
|
613
|
+
func sendButtonMaxRecordingTime() {}
|
|
614
|
+
func sendCameraFovSetting() {}
|
|
615
|
+
|
|
616
|
+
// MARK: - Display
|
|
617
|
+
|
|
618
|
+
func setBrightness(_ level: Int, autoMode: Bool) {
|
|
619
|
+
let brightness = min(max(level, 0), 100)
|
|
620
|
+
enqueueMessage(
|
|
621
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
622
|
+
cmd: Ar99Protocol.cmdDisplaySetAutoAdjustLight,
|
|
623
|
+
payload: buildProtoBool(field: 1, value: autoMode),
|
|
624
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
625
|
+
)
|
|
626
|
+
enqueueMessage(
|
|
627
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
628
|
+
cmd: Ar99Protocol.cmdDisplaySetBright,
|
|
629
|
+
payload: buildProtoUInt32(field: 1, value: brightness),
|
|
630
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
func clearDisplay() {
|
|
635
|
+
clearMinimalText()
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
func sendText(_ text: String) async {
|
|
639
|
+
await sendTextWall(text)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
func sendTextWall(_ text: String) async {
|
|
643
|
+
sendMinimalText(text)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
func sendDoubleTextWall(_ top: String, _ bottom: String) async {
|
|
647
|
+
sendMinimalText(joinDisplayLines(top, bottom))
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
func displayBitmap(
|
|
651
|
+
base64ImageData _: String,
|
|
652
|
+
x _: Int32? = nil,
|
|
653
|
+
y _: Int32? = nil,
|
|
654
|
+
width _: Int32? = nil,
|
|
655
|
+
height _: Int32? = nil
|
|
656
|
+
) async -> Bool {
|
|
657
|
+
false
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
func showDashboard() {}
|
|
661
|
+
func setDashboardPosition(_: Int, _: Int) {}
|
|
662
|
+
|
|
663
|
+
// MARK: - Device Control
|
|
664
|
+
|
|
665
|
+
func setHeadUpAngle(_: Int) {}
|
|
666
|
+
|
|
667
|
+
func getBatteryStatus() {
|
|
668
|
+
enqueueMessage(
|
|
669
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
670
|
+
cmd: Ar99Protocol.cmdDisplayGetBattery,
|
|
671
|
+
payload: Data(),
|
|
672
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
func setSilentMode(_: Bool) {}
|
|
677
|
+
func exit() {}
|
|
678
|
+
func sendShutdown() {}
|
|
679
|
+
func sendReboot() {}
|
|
680
|
+
func sendFactoryReset() {
|
|
681
|
+
Bridge.log("AR99: sending factory reset")
|
|
682
|
+
enqueueMessage(
|
|
683
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
684
|
+
cmd: Ar99Protocol.cmdDisplayCtrlFactoryReset,
|
|
685
|
+
payload: buildProtoUInt32(field: 1, value: Ar99Protocol.factoryResetConfirmCode),
|
|
686
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
func sendRgbLedControl(
|
|
691
|
+
requestId _: String,
|
|
692
|
+
packageName _: String?,
|
|
693
|
+
action _: String,
|
|
694
|
+
color _: String?,
|
|
695
|
+
onDurationMs _: Int,
|
|
696
|
+
offDurationMs _: Int,
|
|
697
|
+
count _: Int
|
|
698
|
+
) {}
|
|
699
|
+
|
|
700
|
+
// MARK: - Connection
|
|
701
|
+
|
|
702
|
+
func findCompatibleDevices() {
|
|
703
|
+
Bridge.log("AR99: findCompatibleDevices()")
|
|
704
|
+
targetIdentifier = nil
|
|
705
|
+
discoveredNames.removeAll()
|
|
706
|
+
startScan(forConnection: false)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
func connectById(_ id: String) {
|
|
710
|
+
Bridge.log("AR99: connectById(\(id))")
|
|
711
|
+
targetIdentifier = id.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
712
|
+
discoveredNames.removeAll()
|
|
713
|
+
startScan(forConnection: true)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
func stopScan() {
|
|
717
|
+
guard isScanning else { return }
|
|
718
|
+
centralManager?.stopScan()
|
|
719
|
+
scanTimeoutItem?.cancel()
|
|
720
|
+
scanTimeoutItem = nil
|
|
721
|
+
isScanning = false
|
|
722
|
+
DeviceStore.shared.apply("bluetooth", "searching", false)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
func disconnect() {
|
|
726
|
+
stopScan()
|
|
727
|
+
cleanupGatt(cancelPeripheral: true)
|
|
728
|
+
updateConnectionState(ConnTypes.DISCONNECTED)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
func forget() {
|
|
732
|
+
targetIdentifier = nil
|
|
733
|
+
disconnect()
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
func getConnectedBluetoothName() -> String? {
|
|
737
|
+
lastConnectedDisplayName ?? peripheral?.name
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
func connectController() {}
|
|
741
|
+
func disconnectController() {}
|
|
742
|
+
|
|
743
|
+
func cleanup() {
|
|
744
|
+
disconnect()
|
|
745
|
+
opusDecoder = nil
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
func ping() {
|
|
749
|
+
requestDeviceInfo()
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
func dbg1() {}
|
|
753
|
+
func dbg2() {}
|
|
754
|
+
|
|
755
|
+
// MARK: - Network and misc
|
|
756
|
+
|
|
757
|
+
func requestWifiScan(scanId _: String?) {}
|
|
758
|
+
func sendWifiCredentials(_: String, _: String) {}
|
|
759
|
+
func forgetWifiNetwork(_: String) {}
|
|
760
|
+
func sendHotspotState(_: Bool) {}
|
|
761
|
+
func sendOtaStart(otaVersionUrl _: String?) {}
|
|
762
|
+
func sendOtaQueryStatus() {}
|
|
763
|
+
func sendUserEmailToGlasses(_: String) {}
|
|
764
|
+
|
|
765
|
+
func sendIncidentId(_: String, apiBaseUrl _: String?) {
|
|
766
|
+
Bridge.log("AR99: sendIncidentId not implemented")
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
func queryGalleryStatus() {}
|
|
770
|
+
func sendGalleryMode() {}
|
|
771
|
+
|
|
772
|
+
func requestVersionInfo() {
|
|
773
|
+
requestDeviceInfo()
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
func startOtaFromFile(_ path: String) -> Bool {
|
|
777
|
+
if path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
778
|
+
sendAr99OtaStatus(phase: "failed", progress: 0, offset: 0, total: 0, errorMessage: "Firmware path is empty")
|
|
779
|
+
return false
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
let manager = Ar99OtaManager.shared
|
|
783
|
+
manager.setCallback(nil)
|
|
784
|
+
if manager.isOTAInProgress() {
|
|
785
|
+
manager.cancelOTA()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
var lastOffset = 0
|
|
789
|
+
var lastTotal = 0
|
|
790
|
+
manager.setCallback(
|
|
791
|
+
Ar99OtaCallbacks(
|
|
792
|
+
onProgress: { [weak self] offset, total, progress in
|
|
793
|
+
lastOffset = offset
|
|
794
|
+
lastTotal = total
|
|
795
|
+
self?.sendAr99OtaStatus(
|
|
796
|
+
phase: "transferring",
|
|
797
|
+
progress: progress,
|
|
798
|
+
offset: offset,
|
|
799
|
+
total: total,
|
|
800
|
+
errorMessage: nil
|
|
801
|
+
)
|
|
802
|
+
},
|
|
803
|
+
onCompleted: { [weak self, weak manager] _ in
|
|
804
|
+
self?.sendAr99OtaStatus(
|
|
805
|
+
phase: "success",
|
|
806
|
+
progress: 100,
|
|
807
|
+
offset: lastTotal,
|
|
808
|
+
total: lastTotal,
|
|
809
|
+
errorMessage: nil
|
|
810
|
+
)
|
|
811
|
+
manager?.setCallback(nil)
|
|
812
|
+
},
|
|
813
|
+
onError: { [weak self, weak manager] _, message in
|
|
814
|
+
self?.sendAr99OtaStatus(
|
|
815
|
+
phase: "failed",
|
|
816
|
+
progress: manager?.getProgress() ?? 0,
|
|
817
|
+
offset: lastOffset,
|
|
818
|
+
total: lastTotal,
|
|
819
|
+
errorMessage: message
|
|
820
|
+
)
|
|
821
|
+
manager?.setCallback(nil)
|
|
822
|
+
},
|
|
823
|
+
onCancelled: { [weak self, weak manager] in
|
|
824
|
+
self?.sendAr99OtaStatus(
|
|
825
|
+
phase: "cancelled",
|
|
826
|
+
progress: manager?.getProgress() ?? 0,
|
|
827
|
+
offset: lastOffset,
|
|
828
|
+
total: lastTotal,
|
|
829
|
+
errorMessage: nil
|
|
830
|
+
)
|
|
831
|
+
manager?.setCallback(nil)
|
|
832
|
+
},
|
|
833
|
+
onPausedWaitingReconnect: { [weak self, weak manager] in
|
|
834
|
+
self?.sendAr99OtaStatus(
|
|
835
|
+
phase: "paused",
|
|
836
|
+
progress: manager?.getProgress() ?? 0,
|
|
837
|
+
offset: lastOffset,
|
|
838
|
+
total: lastTotal,
|
|
839
|
+
errorMessage: "Waiting for reconnect"
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
sendAr99OtaStatus(phase: "preparing", progress: 0, offset: 0, total: 0, errorMessage: nil)
|
|
846
|
+
let started = manager.startOTA(filePath: path)
|
|
847
|
+
if !started {
|
|
848
|
+
sendAr99OtaStatus(phase: "failed", progress: 0, offset: 0, total: 0, errorMessage: "Unable to start AR99 OTA")
|
|
849
|
+
manager.setCallback(nil)
|
|
850
|
+
}
|
|
851
|
+
return started
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
func cancelAr99Ota() {
|
|
855
|
+
Ar99OtaManager.shared.cancelOTA()
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
func enableOtaNotification() -> Bool {
|
|
859
|
+
guard let otaNotifyCharacteristic else { return false }
|
|
860
|
+
if otaNotificationEnabled || otaNotifyCharacteristic.isNotifying {
|
|
861
|
+
otaNotificationEnabled = true
|
|
862
|
+
Ar99OtaManager.shared.onOtaNotifyEnabled()
|
|
863
|
+
return true
|
|
864
|
+
}
|
|
865
|
+
peripheral?.setNotifyValue(true, for: otaNotifyCharacteristic)
|
|
866
|
+
return peripheral != nil
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
func sendOtaData(_ data: Data) {
|
|
870
|
+
guard !data.isEmpty,
|
|
871
|
+
peripheral != nil,
|
|
872
|
+
otaWriteCharacteristic != nil,
|
|
873
|
+
otaNotificationEnabled
|
|
874
|
+
else {
|
|
875
|
+
return
|
|
876
|
+
}
|
|
877
|
+
otaWriteQueue.append(data)
|
|
878
|
+
processNextOtaWrite()
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
func requestMtu(_: Int) {
|
|
882
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
|
883
|
+
_ = Ar99OtaManager.shared.handleMtuNegotiatedForOta(success: true)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
func isBleConnected() -> Bool {
|
|
888
|
+
connected && peripheral != nil && controlWriteCharacteristic != nil && otaWriteCharacteristic != nil && otaNotifyCharacteristic != nil
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private func sendAr99OtaStatus(phase: String, progress: Int, offset: Int, total: Int, errorMessage: String?) {
|
|
892
|
+
var body: [String: Any] = [
|
|
893
|
+
"type": "ar99_ota_status",
|
|
894
|
+
"phase": phase,
|
|
895
|
+
"progress": min(max(progress, 0), 100),
|
|
896
|
+
"offset": offset,
|
|
897
|
+
"total": total,
|
|
898
|
+
]
|
|
899
|
+
if let errorMessage, !errorMessage.isEmpty {
|
|
900
|
+
body["errorMessage"] = errorMessage
|
|
901
|
+
body["error_message"] = errorMessage
|
|
902
|
+
}
|
|
903
|
+
Bridge.sendTypedMessage("ar99_ota_status", body: body)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// MARK: - CBCentralManagerDelegate
|
|
907
|
+
|
|
908
|
+
nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
909
|
+
DispatchQueue.main.async { [weak self] in
|
|
910
|
+
guard let self else { return }
|
|
911
|
+
if central.state == .poweredOn, self.isScanning {
|
|
912
|
+
self.beginScan()
|
|
913
|
+
} else if central.state != .poweredOn {
|
|
914
|
+
Bridge.log("AR99: Bluetooth unavailable state=\(central.state.rawValue)")
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
nonisolated func centralManager(
|
|
920
|
+
_ central: CBCentralManager,
|
|
921
|
+
didDiscover peripheral: CBPeripheral,
|
|
922
|
+
advertisementData: [String: Any],
|
|
923
|
+
rssi RSSI: NSNumber
|
|
924
|
+
) {
|
|
925
|
+
DispatchQueue.main.async { [weak self] in
|
|
926
|
+
self?.handleScanResult(
|
|
927
|
+
central: central,
|
|
928
|
+
peripheral: peripheral,
|
|
929
|
+
advertisementData: advertisementData,
|
|
930
|
+
rssi: RSSI.intValue
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
nonisolated func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
936
|
+
DispatchQueue.main.async { [weak self] in
|
|
937
|
+
guard let self else { return }
|
|
938
|
+
Bridge.log("AR99: connected to \(peripheral.name ?? "unknown")")
|
|
939
|
+
peripheral.delegate = self
|
|
940
|
+
peripheral.discoverServices([Ar99BLE.ctrlService, Ar99BLE.opusService, Ar99BLE.otaService])
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
nonisolated func centralManager(
|
|
945
|
+
_: CBCentralManager,
|
|
946
|
+
didFailToConnect peripheral: CBPeripheral,
|
|
947
|
+
error: Error?
|
|
948
|
+
) {
|
|
949
|
+
DispatchQueue.main.async { [weak self] in
|
|
950
|
+
Bridge.log("AR99: failed to connect \(peripheral.name ?? "unknown"): \(error?.localizedDescription ?? "unknown")")
|
|
951
|
+
self?.cleanupGatt(cancelPeripheral: false)
|
|
952
|
+
self?.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
nonisolated func centralManager(
|
|
957
|
+
_: CBCentralManager,
|
|
958
|
+
didDisconnectPeripheral peripheral: CBPeripheral,
|
|
959
|
+
error: Error?
|
|
960
|
+
) {
|
|
961
|
+
DispatchQueue.main.async { [weak self] in
|
|
962
|
+
guard let self else { return }
|
|
963
|
+
Bridge.log("AR99: disconnected \(peripheral.name ?? "unknown"): \(error?.localizedDescription ?? "clean")")
|
|
964
|
+
if Ar99OtaManager.shared.isOTAInProgress() {
|
|
965
|
+
Ar99OtaManager.shared.onBleDisconnected()
|
|
966
|
+
}
|
|
967
|
+
self.cleanupGatt(cancelPeripheral: false)
|
|
968
|
+
self.updateConnectionState(ConnTypes.DISCONNECTED)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// MARK: - CBPeripheralDelegate
|
|
973
|
+
|
|
974
|
+
nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
975
|
+
DispatchQueue.main.async { [weak self] in
|
|
976
|
+
guard let self else { return }
|
|
977
|
+
if let error {
|
|
978
|
+
Bridge.log("AR99: service discovery failed: \(error.localizedDescription)")
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
guard let services = peripheral.services else { return }
|
|
982
|
+
self.pendingCharacteristicServices.removeAll()
|
|
983
|
+
for service in services where service.uuid == Ar99BLE.ctrlService || service.uuid == Ar99BLE.opusService || service.uuid == Ar99BLE.otaService {
|
|
984
|
+
self.pendingCharacteristicServices.insert(service.uuid)
|
|
985
|
+
peripheral.discoverCharacteristics(nil, for: service)
|
|
986
|
+
}
|
|
987
|
+
self.checkCharacteristicDiscoveryComplete()
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
nonisolated func peripheral(
|
|
992
|
+
_ peripheral: CBPeripheral,
|
|
993
|
+
didDiscoverCharacteristicsFor service: CBService,
|
|
994
|
+
error: Error?
|
|
995
|
+
) {
|
|
996
|
+
DispatchQueue.main.async { [weak self] in
|
|
997
|
+
guard let self else { return }
|
|
998
|
+
if let error {
|
|
999
|
+
Bridge.log("AR99: characteristic discovery failed: \(error.localizedDescription)")
|
|
1000
|
+
}
|
|
1001
|
+
for characteristic in service.characteristics ?? [] {
|
|
1002
|
+
switch characteristic.uuid {
|
|
1003
|
+
case Ar99BLE.ctrlHostToSlave:
|
|
1004
|
+
self.controlWriteCharacteristic = characteristic
|
|
1005
|
+
case Ar99BLE.ctrlSlaveToHost:
|
|
1006
|
+
self.controlNotifyCharacteristic = characteristic
|
|
1007
|
+
case Ar99BLE.opusSlaveToHost:
|
|
1008
|
+
self.opusNotifyCharacteristic = characteristic
|
|
1009
|
+
case Ar99BLE.otaWrite:
|
|
1010
|
+
self.otaWriteCharacteristic = characteristic
|
|
1011
|
+
case Ar99BLE.otaNotify:
|
|
1012
|
+
self.otaNotifyCharacteristic = characteristic
|
|
1013
|
+
default:
|
|
1014
|
+
break
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
self.pendingCharacteristicServices.remove(service.uuid)
|
|
1018
|
+
self.checkCharacteristicDiscoveryComplete()
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
nonisolated func peripheral(
|
|
1023
|
+
_: CBPeripheral,
|
|
1024
|
+
didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
|
1025
|
+
error: Error?
|
|
1026
|
+
) {
|
|
1027
|
+
DispatchQueue.main.async { [weak self] in
|
|
1028
|
+
guard let self else { return }
|
|
1029
|
+
if let error {
|
|
1030
|
+
Bridge.log("AR99: notify enable failed \(characteristic.uuid): \(error.localizedDescription)")
|
|
1031
|
+
}
|
|
1032
|
+
if characteristic.uuid == Ar99BLE.otaNotify {
|
|
1033
|
+
self.otaNotificationEnabled = error == nil && characteristic.isNotifying
|
|
1034
|
+
if self.otaNotificationEnabled {
|
|
1035
|
+
Ar99OtaManager.shared.onOtaNotifyEnabled()
|
|
1036
|
+
}
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1039
|
+
self.pendingNotifyCharacteristics.remove(characteristic.uuid)
|
|
1040
|
+
if self.pendingNotifyCharacteristics.isEmpty {
|
|
1041
|
+
self.markReady()
|
|
1042
|
+
Ar99OtaManager.shared.tryResumeOtaAfterGattReady()
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
nonisolated func peripheral(
|
|
1048
|
+
_: CBPeripheral,
|
|
1049
|
+
didUpdateValueFor characteristic: CBCharacteristic,
|
|
1050
|
+
error: Error?
|
|
1051
|
+
) {
|
|
1052
|
+
guard error == nil, let data = characteristic.value else { return }
|
|
1053
|
+
DispatchQueue.main.async { [weak self] in
|
|
1054
|
+
guard let self else { return }
|
|
1055
|
+
if characteristic.uuid == Ar99BLE.ctrlSlaveToHost {
|
|
1056
|
+
self.handleControlNotifyChunk(data)
|
|
1057
|
+
} else if characteristic.uuid == Ar99BLE.opusSlaveToHost {
|
|
1058
|
+
self.handleOpusData(data)
|
|
1059
|
+
} else if characteristic.uuid == Ar99BLE.otaNotify {
|
|
1060
|
+
Ar99OtaManager.shared.handleOTAResponse(data)
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
nonisolated func peripheral(
|
|
1066
|
+
_: CBPeripheral,
|
|
1067
|
+
didWriteValueFor characteristic: CBCharacteristic,
|
|
1068
|
+
error: Error?
|
|
1069
|
+
) {
|
|
1070
|
+
DispatchQueue.main.async { [weak self] in
|
|
1071
|
+
guard let self else { return }
|
|
1072
|
+
if let error {
|
|
1073
|
+
Bridge.log("AR99: write failed \(characteristic.uuid): \(error.localizedDescription)")
|
|
1074
|
+
}
|
|
1075
|
+
if characteristic.uuid == Ar99BLE.otaWrite {
|
|
1076
|
+
self.otaWriteInFlight = false
|
|
1077
|
+
self.processNextOtaWrite()
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
self.writeInFlight = false
|
|
1081
|
+
self.processNextWrite()
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
nonisolated func peripheralIsReady(toSendWriteWithoutResponse _: CBPeripheral) {
|
|
1086
|
+
DispatchQueue.main.async { [weak self] in
|
|
1087
|
+
self?.processNextWrite()
|
|
1088
|
+
self?.processNextOtaWrite()
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// MARK: - Scanning
|
|
1093
|
+
|
|
1094
|
+
private func ensureCentralManager() {
|
|
1095
|
+
if centralManager == nil {
|
|
1096
|
+
centralManager = CBCentralManager(
|
|
1097
|
+
delegate: self,
|
|
1098
|
+
queue: nil,
|
|
1099
|
+
options: [CBCentralManagerOptionShowPowerAlertKey: false]
|
|
1100
|
+
)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private func startScan(forConnection: Bool) {
|
|
1105
|
+
stopScan()
|
|
1106
|
+
scanForConnection = forConnection
|
|
1107
|
+
isScanning = true
|
|
1108
|
+
updateConnectionState(ConnTypes.SCANNING)
|
|
1109
|
+
ensureCentralManager()
|
|
1110
|
+
if centralManager?.state == .poweredOn {
|
|
1111
|
+
beginScan()
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private func beginScan() {
|
|
1116
|
+
centralManager?.scanForPeripherals(
|
|
1117
|
+
withServices: nil,
|
|
1118
|
+
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
1119
|
+
)
|
|
1120
|
+
scanTimeoutItem?.cancel()
|
|
1121
|
+
let item = DispatchWorkItem { [weak self] in
|
|
1122
|
+
self?.stopScan()
|
|
1123
|
+
}
|
|
1124
|
+
scanTimeoutItem = item
|
|
1125
|
+
DispatchQueue.main.asyncAfter(
|
|
1126
|
+
deadline: .now() + .milliseconds(Ar99Protocol.scanDurationMs),
|
|
1127
|
+
execute: item
|
|
1128
|
+
)
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private func handleScanResult(
|
|
1132
|
+
central: CBCentralManager,
|
|
1133
|
+
peripheral: CBPeripheral,
|
|
1134
|
+
advertisementData: [String: Any],
|
|
1135
|
+
rssi: Int
|
|
1136
|
+
) {
|
|
1137
|
+
let localName =
|
|
1138
|
+
advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
|
1139
|
+
?? peripheral.name
|
|
1140
|
+
let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
|
1141
|
+
let advertisement = parseAdvertisement(localName: localName, manufacturerData: manufacturerData)
|
|
1142
|
+
let name = localName ?? advertisement?.bleName ?? advertisement?.projectName
|
|
1143
|
+
guard matchesAr99(name: name, projectName: advertisement?.projectName) else { return }
|
|
1144
|
+
|
|
1145
|
+
let displayName = buildDisplayName(
|
|
1146
|
+
fallbackName: name ?? "AR99",
|
|
1147
|
+
advertisement: advertisement,
|
|
1148
|
+
deviceAddress: advertisement?.bleAddress ?? advertisement?.btAddress ?? peripheral.identifier.uuidString
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
if !scanForConnection {
|
|
1152
|
+
if discoveredNames.insert(displayName).inserted {
|
|
1153
|
+
Bridge.sendDiscoveredDevice(
|
|
1154
|
+
type,
|
|
1155
|
+
displayName,
|
|
1156
|
+
deviceAddress: advertisement?.bleAddress?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "",
|
|
1157
|
+
rssi: rssi,
|
|
1158
|
+
projectName: advertisement?.projectName
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
return
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if let projectName = advertisement?.projectName?.trimmingCharacters(in: .whitespacesAndNewlines), !projectName.isEmpty {
|
|
1165
|
+
currentProjectName = projectName
|
|
1166
|
+
if let bleAddress = advertisement?.bleAddress?.trimmingCharacters(in: .whitespacesAndNewlines), !bleAddress.isEmpty {
|
|
1167
|
+
currentBroadcastMacAddress = bleAddress
|
|
1168
|
+
} else {
|
|
1169
|
+
currentBroadcastMacAddress = nil
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
guard matchesTarget(
|
|
1174
|
+
targetIdentifier ?? "",
|
|
1175
|
+
name: name,
|
|
1176
|
+
displayName: displayName,
|
|
1177
|
+
peripheralId: peripheral.identifier.uuidString,
|
|
1178
|
+
advertisement: advertisement
|
|
1179
|
+
) else {
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
stopScan()
|
|
1184
|
+
connectPeripheral(central: central, peripheral: peripheral, displayName: displayName)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private func connectPeripheral(
|
|
1188
|
+
central: CBCentralManager,
|
|
1189
|
+
peripheral: CBPeripheral,
|
|
1190
|
+
displayName: String
|
|
1191
|
+
) {
|
|
1192
|
+
cleanupGatt(cancelPeripheral: true)
|
|
1193
|
+
lastConnectedDisplayName = displayName
|
|
1194
|
+
self.peripheral = peripheral
|
|
1195
|
+
peripheral.delegate = self
|
|
1196
|
+
DeviceStore.shared.apply("bluetooth", "device_name", displayName)
|
|
1197
|
+
DeviceStore.shared.apply("bluetooth", "device_address", peripheral.identifier.uuidString)
|
|
1198
|
+
DeviceStore.shared.apply("glasses", "bluetoothName", peripheral.name ?? displayName)
|
|
1199
|
+
updateConnectionState(ConnTypes.CONNECTING)
|
|
1200
|
+
central.connect(peripheral, options: nil)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private func matchesTarget(
|
|
1204
|
+
_ rawTarget: String,
|
|
1205
|
+
name: String?,
|
|
1206
|
+
displayName: String,
|
|
1207
|
+
peripheralId: String,
|
|
1208
|
+
advertisement: Ar99ParsedAdvertisement?
|
|
1209
|
+
) -> Bool {
|
|
1210
|
+
let target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1211
|
+
if target.isEmpty { return true }
|
|
1212
|
+
let normalizedTarget = normalizeDisplayIdentifier(target)
|
|
1213
|
+
let targetSerial = extractTargetSerial(target)
|
|
1214
|
+
let candidates = [
|
|
1215
|
+
name,
|
|
1216
|
+
displayName,
|
|
1217
|
+
peripheralId,
|
|
1218
|
+
advertisement?.bleAddress,
|
|
1219
|
+
advertisement?.btAddress,
|
|
1220
|
+
advertisement?.serialNumber,
|
|
1221
|
+
].compactMap { $0 }
|
|
1222
|
+
|
|
1223
|
+
for candidate in candidates {
|
|
1224
|
+
if candidate.caseInsensitiveCompare(target) == .orderedSame
|
|
1225
|
+
|| candidate.caseInsensitiveCompare(normalizedTarget) == .orderedSame
|
|
1226
|
+
{
|
|
1227
|
+
return true
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if let serial = advertisement?.serialNumber, !targetSerial.isEmpty {
|
|
1231
|
+
return serial.caseInsensitiveCompare(targetSerial) == .orderedSame
|
|
1232
|
+
}
|
|
1233
|
+
return false
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// MARK: - GATT setup
|
|
1237
|
+
|
|
1238
|
+
private func checkCharacteristicDiscoveryComplete() {
|
|
1239
|
+
guard pendingCharacteristicServices.isEmpty, !configuredNotifications else { return }
|
|
1240
|
+
configuredNotifications = true
|
|
1241
|
+
guard controlWriteCharacteristic != nil, controlNotifyCharacteristic != nil else {
|
|
1242
|
+
Bridge.log("AR99: missing required control characteristics")
|
|
1243
|
+
return
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
pendingNotifyCharacteristics.removeAll()
|
|
1247
|
+
if let controlNotifyCharacteristic {
|
|
1248
|
+
pendingNotifyCharacteristics.insert(controlNotifyCharacteristic.uuid)
|
|
1249
|
+
peripheral?.setNotifyValue(true, for: controlNotifyCharacteristic)
|
|
1250
|
+
}
|
|
1251
|
+
if let opusNotifyCharacteristic {
|
|
1252
|
+
pendingNotifyCharacteristics.insert(opusNotifyCharacteristic.uuid)
|
|
1253
|
+
peripheral?.setNotifyValue(true, for: opusNotifyCharacteristic)
|
|
1254
|
+
}
|
|
1255
|
+
if pendingNotifyCharacteristics.isEmpty {
|
|
1256
|
+
markReady()
|
|
1257
|
+
Ar99OtaManager.shared.tryResumeOtaAfterGattReady()
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private func markReady() {
|
|
1262
|
+
guard controlWriteCharacteristic != nil, controlNotifyCharacteristic != nil else { return }
|
|
1263
|
+
if fullyBooted, connected { return }
|
|
1264
|
+
|
|
1265
|
+
minimalModeReady = false
|
|
1266
|
+
hasPendingDisplayUpdate = false
|
|
1267
|
+
pendingDisplayText = ""
|
|
1268
|
+
pendingSourceText = ""
|
|
1269
|
+
pendingTargetText = ""
|
|
1270
|
+
resetMinimalTextSession()
|
|
1271
|
+
hasReceivedBatteryForCurrentConnection = false
|
|
1272
|
+
pendingBatteryRetries = 0
|
|
1273
|
+
|
|
1274
|
+
if let projectName = currentProjectName?.trimmingCharacters(in: .whitespacesAndNewlines), !projectName.isEmpty {
|
|
1275
|
+
DeviceStore.shared.apply("bluetooth", "project_name", projectName)
|
|
1276
|
+
}
|
|
1277
|
+
DeviceStore.shared.apply("glasses", "connected", true)
|
|
1278
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", true)
|
|
1279
|
+
updateConnectionState(ConnTypes.CONNECTED)
|
|
1280
|
+
|
|
1281
|
+
scheduleReadyCommands()
|
|
1282
|
+
scheduleBatteryQueries()
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private func scheduleReadyCommands() {
|
|
1286
|
+
readyItems.forEach { $0.cancel() }
|
|
1287
|
+
readyItems.removeAll()
|
|
1288
|
+
runAfter(ms: Ar99Protocol.readyMinimalModeDelayMs) { [weak self] in self?.setMinimalDisplayMode() }
|
|
1289
|
+
runAfter(ms: Ar99Protocol.readyLanguageDelayMs) { [weak self] in self?.setDisplayLanguage(Ar99Protocol.displayLanguageEnglish) }
|
|
1290
|
+
runAfter(ms: Ar99Protocol.readyDeviceInfoDelayMs) { [weak self] in self?.requestDeviceInfo() }
|
|
1291
|
+
runAfter(ms: Ar99Protocol.readyBrightnessDelayMs) { [weak self] in self?.requestBrightness() }
|
|
1292
|
+
runAfter(ms: Ar99Protocol.readyBatteryDelayMs) { [weak self] in self?.getBatteryStatus() }
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private func runAfter(ms: Int, _ block: @escaping () -> Void) {
|
|
1296
|
+
let item = DispatchWorkItem(block: block)
|
|
1297
|
+
readyItems.append(item)
|
|
1298
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(ms), execute: item)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
private func scheduleBatteryQueries() {
|
|
1302
|
+
initialBatteryRetryItem?.cancel()
|
|
1303
|
+
periodicBatteryTimer?.invalidate()
|
|
1304
|
+
pendingBatteryRetries = 0
|
|
1305
|
+
scheduleInitialBatteryRetry()
|
|
1306
|
+
periodicBatteryTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(Ar99Protocol.batteryPollIntervalMs) / 1000, repeats: true) {
|
|
1307
|
+
[weak self] _ in
|
|
1308
|
+
guard let self, self.connected else { return }
|
|
1309
|
+
self.getBatteryStatus()
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
private func scheduleInitialBatteryRetry() {
|
|
1314
|
+
initialBatteryRetryItem?.cancel()
|
|
1315
|
+
let item = DispatchWorkItem { [weak self] in
|
|
1316
|
+
guard let self, self.connected else { return }
|
|
1317
|
+
guard !self.hasReceivedBatteryForCurrentConnection,
|
|
1318
|
+
self.pendingBatteryRetries < Ar99Protocol.maxInitialBatteryRetries
|
|
1319
|
+
else {
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1322
|
+
self.pendingBatteryRetries += 1
|
|
1323
|
+
self.getBatteryStatus()
|
|
1324
|
+
self.scheduleInitialBatteryRetry()
|
|
1325
|
+
}
|
|
1326
|
+
initialBatteryRetryItem = item
|
|
1327
|
+
DispatchQueue.main.asyncAfter(
|
|
1328
|
+
deadline: .now() + .milliseconds(Ar99Protocol.initialBatteryRetryDelayMs),
|
|
1329
|
+
execute: item
|
|
1330
|
+
)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private func cleanupGatt(cancelPeripheral: Bool) {
|
|
1334
|
+
writeQueue.removeAll()
|
|
1335
|
+
otaWriteQueue.removeAll()
|
|
1336
|
+
writeDelayItem?.cancel()
|
|
1337
|
+
writeDelayItem = nil
|
|
1338
|
+
otaWriteDelayItem?.cancel()
|
|
1339
|
+
otaWriteDelayItem = nil
|
|
1340
|
+
writeInFlight = false
|
|
1341
|
+
otaWriteInFlight = false
|
|
1342
|
+
otaNotificationEnabled = false
|
|
1343
|
+
pendingCharacteristicServices.removeAll()
|
|
1344
|
+
pendingNotifyCharacteristics.removeAll()
|
|
1345
|
+
configuredNotifications = false
|
|
1346
|
+
controlNotifyBuffer.removeAll()
|
|
1347
|
+
controlWriteCharacteristic = nil
|
|
1348
|
+
controlNotifyCharacteristic = nil
|
|
1349
|
+
opusNotifyCharacteristic = nil
|
|
1350
|
+
otaWriteCharacteristic = nil
|
|
1351
|
+
otaNotifyCharacteristic = nil
|
|
1352
|
+
minimalModeReady = false
|
|
1353
|
+
resetMinimalTextSession()
|
|
1354
|
+
hasPendingDisplayUpdate = false
|
|
1355
|
+
pendingDisplayText = ""
|
|
1356
|
+
pendingSourceText = ""
|
|
1357
|
+
pendingTargetText = ""
|
|
1358
|
+
hasReceivedBatteryForCurrentConnection = false
|
|
1359
|
+
pendingBatteryRetries = 0
|
|
1360
|
+
readyItems.forEach { $0.cancel() }
|
|
1361
|
+
readyItems.removeAll()
|
|
1362
|
+
initialBatteryRetryItem?.cancel()
|
|
1363
|
+
initialBatteryRetryItem = nil
|
|
1364
|
+
periodicBatteryTimer?.invalidate()
|
|
1365
|
+
periodicBatteryTimer = nil
|
|
1366
|
+
opusLoggedFirstInSession = false
|
|
1367
|
+
opusDecoder?.resetBuffer()
|
|
1368
|
+
awaitingMicDisableAck = false
|
|
1369
|
+
|
|
1370
|
+
if cancelPeripheral, let peripheral {
|
|
1371
|
+
centralManager?.cancelPeripheralConnection(peripheral)
|
|
1372
|
+
}
|
|
1373
|
+
peripheral?.delegate = nil
|
|
1374
|
+
peripheral = nil
|
|
1375
|
+
|
|
1376
|
+
DeviceStore.shared.apply("glasses", "connected", false)
|
|
1377
|
+
DeviceStore.shared.apply("glasses", "fullyBooted", false)
|
|
1378
|
+
DeviceStore.shared.apply("glasses", "micEnabled", false)
|
|
1379
|
+
DeviceStore.shared.apply("glasses", "batteryLevel", -1)
|
|
1380
|
+
DeviceStore.shared.apply("glasses", "charging", false)
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private func updateConnectionState(_ state: String) {
|
|
1384
|
+
DeviceStore.shared.apply("glasses", "connectionState", state)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// MARK: - Writes
|
|
1388
|
+
|
|
1389
|
+
private func enqueueMessage(function: Int, cmd: Int, payload: Data, receiver: Int) {
|
|
1390
|
+
let packet = buildHeader(
|
|
1391
|
+
function: function,
|
|
1392
|
+
cmd: cmd,
|
|
1393
|
+
payload: payload,
|
|
1394
|
+
sender: Ar99Protocol.targetMobileApp,
|
|
1395
|
+
receiver: receiver,
|
|
1396
|
+
isResponse: false
|
|
1397
|
+
)
|
|
1398
|
+
var offset = 0
|
|
1399
|
+
while offset < packet.count {
|
|
1400
|
+
let end = min(offset + Ar99Protocol.bleWriteChunkSize, packet.count)
|
|
1401
|
+
writeQueue.append(packet.subdata(in: offset ..< end))
|
|
1402
|
+
offset = end
|
|
1403
|
+
}
|
|
1404
|
+
processNextWrite()
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
private func processNextWrite() {
|
|
1408
|
+
guard !writeInFlight,
|
|
1409
|
+
let peripheral,
|
|
1410
|
+
let characteristic = controlWriteCharacteristic,
|
|
1411
|
+
!writeQueue.isEmpty
|
|
1412
|
+
else {
|
|
1413
|
+
return
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
let data = writeQueue.removeFirst()
|
|
1417
|
+
let writeType: CBCharacteristicWriteType =
|
|
1418
|
+
characteristic.properties.contains(.writeWithoutResponse) ? .withoutResponse : .withResponse
|
|
1419
|
+
|
|
1420
|
+
writeInFlight = true
|
|
1421
|
+
peripheral.writeValue(data, for: characteristic, type: writeType)
|
|
1422
|
+
if writeType == .withoutResponse {
|
|
1423
|
+
writeDelayItem?.cancel()
|
|
1424
|
+
let item = DispatchWorkItem { [weak self] in
|
|
1425
|
+
self?.writeInFlight = false
|
|
1426
|
+
self?.processNextWrite()
|
|
1427
|
+
}
|
|
1428
|
+
writeDelayItem = item
|
|
1429
|
+
DispatchQueue.main.asyncAfter(
|
|
1430
|
+
deadline: .now() + .milliseconds(Ar99Protocol.bleWriteChunkDelayMs),
|
|
1431
|
+
execute: item
|
|
1432
|
+
)
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
private func processNextOtaWrite() {
|
|
1437
|
+
guard !otaWriteInFlight,
|
|
1438
|
+
let peripheral,
|
|
1439
|
+
let characteristic = otaWriteCharacteristic,
|
|
1440
|
+
!otaWriteQueue.isEmpty
|
|
1441
|
+
else {
|
|
1442
|
+
return
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
var writeType: CBCharacteristicWriteType = .withResponse
|
|
1446
|
+
if characteristic.properties.contains(.writeWithoutResponse) {
|
|
1447
|
+
let maxWithoutResponse = peripheral.maximumWriteValueLength(for: .withoutResponse)
|
|
1448
|
+
if otaWriteQueue[0].count <= maxWithoutResponse {
|
|
1449
|
+
writeType = .withoutResponse
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
let maxLength = peripheral.maximumWriteValueLength(for: writeType)
|
|
1454
|
+
guard otaWriteQueue[0].count <= maxLength else {
|
|
1455
|
+
let dropped = otaWriteQueue.removeFirst()
|
|
1456
|
+
sendAr99OtaStatus(
|
|
1457
|
+
phase: "failed",
|
|
1458
|
+
progress: Ar99OtaManager.shared.getProgress(),
|
|
1459
|
+
offset: 0,
|
|
1460
|
+
total: 0,
|
|
1461
|
+
errorMessage: "OTA packet too large for current BLE write length: \(dropped.count) > \(maxLength)"
|
|
1462
|
+
)
|
|
1463
|
+
return
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if writeType == .withoutResponse, !peripheral.canSendWriteWithoutResponse {
|
|
1467
|
+
return
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
let data = otaWriteQueue.removeFirst()
|
|
1471
|
+
otaWriteInFlight = true
|
|
1472
|
+
peripheral.writeValue(data, for: characteristic, type: writeType)
|
|
1473
|
+
if writeType == .withoutResponse {
|
|
1474
|
+
otaWriteInFlight = false
|
|
1475
|
+
otaWriteDelayItem?.cancel()
|
|
1476
|
+
let item = DispatchWorkItem { [weak self] in
|
|
1477
|
+
self?.processNextOtaWrite()
|
|
1478
|
+
}
|
|
1479
|
+
otaWriteDelayItem = item
|
|
1480
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1), execute: item)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private func buildHeader(
|
|
1485
|
+
function: Int,
|
|
1486
|
+
cmd: Int,
|
|
1487
|
+
payload: Data,
|
|
1488
|
+
sender: Int,
|
|
1489
|
+
receiver: Int,
|
|
1490
|
+
isResponse: Bool
|
|
1491
|
+
) -> Data {
|
|
1492
|
+
var data = Data()
|
|
1493
|
+
let ctrl =
|
|
1494
|
+
(sender & Ar99Protocol.ctrlSenderMask)
|
|
1495
|
+
| ((receiver & Ar99Protocol.ctrlSenderMask) << 2)
|
|
1496
|
+
| (isResponse ? Ar99Protocol.ctrlActionResponse : 0)
|
|
1497
|
+
Ar99ProtoWriter.writeFixed32(field: 1, value: Ar99Protocol.magicNumber, to: &data)
|
|
1498
|
+
Ar99ProtoWriter.writeUInt32(field: 2, value: function, to: &data)
|
|
1499
|
+
Ar99ProtoWriter.writeUInt32(field: 3, value: cmd, to: &data)
|
|
1500
|
+
if !payload.isEmpty {
|
|
1501
|
+
Ar99ProtoWriter.writeUInt32(field: 4, value: payload.count, to: &data)
|
|
1502
|
+
}
|
|
1503
|
+
Ar99ProtoWriter.writeUInt32(field: 5, value: ctrl, to: &data)
|
|
1504
|
+
Ar99ProtoWriter.writeUInt32(field: 6, value: nextSequence(), to: &data)
|
|
1505
|
+
if !payload.isEmpty {
|
|
1506
|
+
Ar99ProtoWriter.writeUInt32(field: 7, value: checksum16(payload), to: &data)
|
|
1507
|
+
Ar99ProtoWriter.writeBytes(field: 8, value: payload, to: &data)
|
|
1508
|
+
}
|
|
1509
|
+
return data
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private func nextSequence() -> Int {
|
|
1513
|
+
while true {
|
|
1514
|
+
let seq = sequence & 0x7F
|
|
1515
|
+
sequence += 1
|
|
1516
|
+
if seq != 0 { return seq }
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
private func checksum16(_ data: Data) -> Int {
|
|
1521
|
+
data.reduce(0) { ($0 + Int($1)) & 0xFFFF }
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// MARK: - Payload builders
|
|
1525
|
+
|
|
1526
|
+
private func buildProtoUInt32(field: Int, value: Int) -> Data {
|
|
1527
|
+
var data = Data()
|
|
1528
|
+
Ar99ProtoWriter.writeUInt32(field: field, value: value, to: &data)
|
|
1529
|
+
return data
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private func buildProtoBool(field: Int, value: Bool) -> Data {
|
|
1533
|
+
var data = Data()
|
|
1534
|
+
Ar99ProtoWriter.writeBool(field: field, value: value, to: &data)
|
|
1535
|
+
return data
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
private func buildSetTimePayload(timestampMs: Int64) -> Data {
|
|
1539
|
+
var data = Data()
|
|
1540
|
+
let timeZone = TimeZone.current
|
|
1541
|
+
let timezoneOffsetMinutes = timeZone.secondsFromGMT(for: Date(timeIntervalSince1970: TimeInterval(timestampMs) / 1000)) / 60
|
|
1542
|
+
let hourFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current) ?? ""
|
|
1543
|
+
let is24Hour = !hourFormat.contains("a")
|
|
1544
|
+
|
|
1545
|
+
Ar99ProtoWriter.writeInt64(field: 1, value: timestampMs, to: &data)
|
|
1546
|
+
Ar99ProtoWriter.writeUInt32(field: 2, value: 1, to: &data)
|
|
1547
|
+
Ar99ProtoWriter.writeBool(field: 3, value: is24Hour, to: &data)
|
|
1548
|
+
Ar99ProtoWriter.writeInt32(field: 4, value: timezoneOffsetMinutes, to: &data)
|
|
1549
|
+
Ar99ProtoWriter.writeString(field: 5, value: timeZone.identifier, to: &data)
|
|
1550
|
+
return data
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private func buildTranslationDisplayStartPayload(
|
|
1554
|
+
sourceLanguage: Int,
|
|
1555
|
+
targetLanguage: Int,
|
|
1556
|
+
micSource: Int
|
|
1557
|
+
) -> Data {
|
|
1558
|
+
var data = Data()
|
|
1559
|
+
Ar99ProtoWriter.writeUInt32(field: 1, value: sourceLanguage, to: &data)
|
|
1560
|
+
Ar99ProtoWriter.writeUInt32(field: 2, value: targetLanguage, to: &data)
|
|
1561
|
+
Ar99ProtoWriter.writeUInt32(field: 3, value: micSource, to: &data)
|
|
1562
|
+
return data
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
private func buildTranslationDisplaySetContentPayload(
|
|
1566
|
+
state: Int,
|
|
1567
|
+
fontSize: Int,
|
|
1568
|
+
sourceText: String,
|
|
1569
|
+
targetText: String
|
|
1570
|
+
) -> Data {
|
|
1571
|
+
var data = Data()
|
|
1572
|
+
Ar99ProtoWriter.writeUInt32(field: 1, value: state, to: &data)
|
|
1573
|
+
Ar99ProtoWriter.writeUInt32(field: 2, value: fontSize, to: &data)
|
|
1574
|
+
Ar99ProtoWriter.writeUInt32(field: 3, value: Ar99Protocol.transDisplaySourceLanguage, to: &data)
|
|
1575
|
+
Ar99ProtoWriter.writeUInt32(field: 4, value: Ar99Protocol.transDisplayTargetLanguage, to: &data)
|
|
1576
|
+
Ar99ProtoWriter.writeString(field: 5, value: sourceText, to: &data)
|
|
1577
|
+
Ar99ProtoWriter.writeString(field: 6, value: targetText, to: &data)
|
|
1578
|
+
return data
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private func buildDisplaySystemFunctionPayload(functionType: Int, action: Int) -> Data {
|
|
1582
|
+
var data = Data()
|
|
1583
|
+
Ar99ProtoWriter.writeUInt32(field: 1, value: functionType, to: &data)
|
|
1584
|
+
Ar99ProtoWriter.writeUInt32(field: 2, value: action, to: &data)
|
|
1585
|
+
return data
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
private func buildControlDisplayAudioRecordPayload(enabled: Bool) -> Data {
|
|
1589
|
+
var data = Data()
|
|
1590
|
+
Ar99ProtoWriter.writeBool(field: 1, value: !enabled, to: &data)
|
|
1591
|
+
return data
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
private func buildMinimalTextSetPayload(_ text: String) -> Data {
|
|
1595
|
+
var data = Data()
|
|
1596
|
+
Ar99ProtoWriter.writeString(field: 1, value: text, to: &data)
|
|
1597
|
+
return data
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// MARK: - High-level AR99 commands
|
|
1601
|
+
|
|
1602
|
+
private func sendMinimalText(_ text: String) {
|
|
1603
|
+
let normalizedText = text
|
|
1604
|
+
if !minimalModeReady {
|
|
1605
|
+
pendingDisplayText = normalizedText
|
|
1606
|
+
hasPendingDisplayUpdate = true
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1611
|
+
if displaySessionStarted,
|
|
1612
|
+
lastDisplayContentAtMs > 0,
|
|
1613
|
+
now - lastDisplayContentAtMs >= Ar99Protocol.displaySessionIdleRestartMs
|
|
1614
|
+
{
|
|
1615
|
+
displaySessionStarted = false
|
|
1616
|
+
lastDisplayText = ""
|
|
1617
|
+
}
|
|
1618
|
+
sendMinimalTextNow(normalizedText)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
private func sendMinimalTextNow(_ text: String) {
|
|
1622
|
+
let normalizedText = text
|
|
1623
|
+
if normalizedText == lastDisplayText {
|
|
1624
|
+
return
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
if normalizedText.isEmpty {
|
|
1628
|
+
clearMinimalTextNow()
|
|
1629
|
+
return
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
startMinimalTextIfNeeded()
|
|
1633
|
+
enqueueMessage(
|
|
1634
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1635
|
+
cmd: Ar99Protocol.cmdDisplayCtrlMinimalTextSet,
|
|
1636
|
+
payload: buildMinimalTextSetPayload(normalizedText),
|
|
1637
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
lastDisplayText = normalizedText
|
|
1641
|
+
lastDisplayContentAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
private func flushPendingDisplayUpdateIfNeeded() {
|
|
1645
|
+
guard minimalModeReady, hasPendingDisplayUpdate else { return }
|
|
1646
|
+
let text = pendingDisplayText
|
|
1647
|
+
hasPendingDisplayUpdate = false
|
|
1648
|
+
pendingDisplayText = ""
|
|
1649
|
+
sendMinimalTextNow(text)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private func startMinimalTextIfNeeded() {
|
|
1653
|
+
guard !displaySessionStarted else { return }
|
|
1654
|
+
enqueueMessage(
|
|
1655
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1656
|
+
cmd: Ar99Protocol.cmdDisplayCtrlMinimalTextStart,
|
|
1657
|
+
payload: Data(),
|
|
1658
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1659
|
+
)
|
|
1660
|
+
displaySessionStarted = true
|
|
1661
|
+
lastDisplayContentAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
private func clearMinimalText() {
|
|
1665
|
+
if !minimalModeReady {
|
|
1666
|
+
pendingDisplayText = ""
|
|
1667
|
+
hasPendingDisplayUpdate = false
|
|
1668
|
+
lastDisplayText = ""
|
|
1669
|
+
return
|
|
1670
|
+
}
|
|
1671
|
+
clearMinimalTextNow()
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
private func clearMinimalTextNow() {
|
|
1675
|
+
enqueueMessage(
|
|
1676
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1677
|
+
cmd: Ar99Protocol.cmdDisplayCtrlMinimalTextClear,
|
|
1678
|
+
payload: Data(),
|
|
1679
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1680
|
+
)
|
|
1681
|
+
resetMinimalTextSession()
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
private func resetMinimalTextSession() {
|
|
1685
|
+
displaySessionStarted = false
|
|
1686
|
+
displayContentStarted = false
|
|
1687
|
+
lastDisplayContentAtMs = 0
|
|
1688
|
+
lastDisplayText = ""
|
|
1689
|
+
lastDisplaySourceText = ""
|
|
1690
|
+
lastDisplayTargetText = ""
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private func joinDisplayLines(_ top: String, _ bottom: String) -> String {
|
|
1694
|
+
let first = top.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1695
|
+
let second = bottom.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1696
|
+
if first.isEmpty { return second }
|
|
1697
|
+
if second.isEmpty { return first }
|
|
1698
|
+
return "\(first)\n\(second)"
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private func sendControlDisplayAudioRecord(_ enabled: Bool) {
|
|
1702
|
+
enqueueMessage(
|
|
1703
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1704
|
+
cmd: Ar99Protocol.cmdDisplayCtrlAudioRecord,
|
|
1705
|
+
payload: buildControlDisplayAudioRecordPayload(enabled: enabled),
|
|
1706
|
+
receiver: Ar99Protocol.targetGlassAndroid
|
|
1707
|
+
)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
private func setMinimalDisplayMode() {
|
|
1711
|
+
enqueueMessage(
|
|
1712
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1713
|
+
cmd: Ar99Protocol.cmdDisplayCtrlSysFunction,
|
|
1714
|
+
payload: buildDisplaySystemFunctionPayload(
|
|
1715
|
+
functionType: Ar99Protocol.displayFunctionTypeUiMode,
|
|
1716
|
+
action: Ar99Protocol.displayUiModeMinimal
|
|
1717
|
+
),
|
|
1718
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1719
|
+
)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private func setDisplayLanguage(_ language: Int) {
|
|
1723
|
+
enqueueMessage(
|
|
1724
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1725
|
+
cmd: Ar99Protocol.cmdDisplaySetLanguage,
|
|
1726
|
+
payload: buildProtoUInt32(field: 1, value: language),
|
|
1727
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1728
|
+
)
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
private func requestDeviceInfo() {
|
|
1732
|
+
enqueueMessage(
|
|
1733
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1734
|
+
cmd: Ar99Protocol.cmdDisplayGetDeviceInfo,
|
|
1735
|
+
payload: Data(),
|
|
1736
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1737
|
+
)
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private func requestBrightness() {
|
|
1741
|
+
enqueueMessage(
|
|
1742
|
+
function: Ar99Protocol.funcCommDisplay,
|
|
1743
|
+
cmd: Ar99Protocol.cmdDisplayGetBright,
|
|
1744
|
+
payload: Data(),
|
|
1745
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1746
|
+
)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
func sendSetSystemTime(_ timestampMs: Int64) {
|
|
1750
|
+
Bridge.log("AR99: sending set time timestampMs=\(timestampMs)")
|
|
1751
|
+
enqueueMessage(
|
|
1752
|
+
function: Ar99Protocol.funcComm,
|
|
1753
|
+
cmd: Ar99Protocol.cmdCommSetTime,
|
|
1754
|
+
payload: buildSetTimePayload(timestampMs: timestampMs),
|
|
1755
|
+
receiver: Ar99Protocol.targetGlassMcuDisplay
|
|
1756
|
+
)
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// MARK: - Notifications
|
|
1760
|
+
|
|
1761
|
+
private func handleControlNotifyChunk(_ data: Data) {
|
|
1762
|
+
guard !data.isEmpty else { return }
|
|
1763
|
+
controlNotifyBuffer.append(data)
|
|
1764
|
+
|
|
1765
|
+
while true {
|
|
1766
|
+
guard let start = findMessageStart(in: controlNotifyBuffer, from: 0) else {
|
|
1767
|
+
trimControlNotifyBuffer()
|
|
1768
|
+
return
|
|
1769
|
+
}
|
|
1770
|
+
if start > 0 {
|
|
1771
|
+
controlNotifyBuffer.removeFirst(start)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
let nextStart = findMessageStart(in: controlNotifyBuffer, from: 1)
|
|
1775
|
+
let candidate =
|
|
1776
|
+
nextStart.map { controlNotifyBuffer.prefix($0) }
|
|
1777
|
+
?? controlNotifyBuffer[controlNotifyBuffer.startIndex ..< controlNotifyBuffer.endIndex]
|
|
1778
|
+
guard let header = parseHeader(Data(candidate)) else {
|
|
1779
|
+
if let nextStart, nextStart > 0 {
|
|
1780
|
+
controlNotifyBuffer.removeFirst(nextStart)
|
|
1781
|
+
continue
|
|
1782
|
+
}
|
|
1783
|
+
trimControlNotifyBuffer()
|
|
1784
|
+
return
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
guard controlNotifyBuffer.count >= header.totalBytes else {
|
|
1788
|
+
trimControlNotifyBuffer()
|
|
1789
|
+
return
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
let packet = controlNotifyBuffer.prefix(header.totalBytes)
|
|
1793
|
+
controlNotifyBuffer.removeFirst(header.totalBytes)
|
|
1794
|
+
handleControlPacket(Data(packet))
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
private func trimControlNotifyBuffer() {
|
|
1799
|
+
guard controlNotifyBuffer.count > 2_048 else { return }
|
|
1800
|
+
if let start = findMessageStart(in: controlNotifyBuffer, from: 0), start > 0 {
|
|
1801
|
+
controlNotifyBuffer.removeFirst(start)
|
|
1802
|
+
} else {
|
|
1803
|
+
controlNotifyBuffer.removeAll()
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
private func findMessageStart(in data: Data, from: Int) -> Int? {
|
|
1808
|
+
let bytes = Array(data)
|
|
1809
|
+
guard bytes.count >= Ar99Protocol.messageStart.count else { return nil }
|
|
1810
|
+
let start = max(0, from)
|
|
1811
|
+
let maxIndex = bytes.count - Ar99Protocol.messageStart.count
|
|
1812
|
+
guard start <= maxIndex else { return nil }
|
|
1813
|
+
for index in start ... maxIndex {
|
|
1814
|
+
var matched = true
|
|
1815
|
+
for j in 0 ..< Ar99Protocol.messageStart.count where bytes[index + j] != Ar99Protocol.messageStart[j] {
|
|
1816
|
+
matched = false
|
|
1817
|
+
break
|
|
1818
|
+
}
|
|
1819
|
+
if matched { return index }
|
|
1820
|
+
}
|
|
1821
|
+
return nil
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
private func handleControlPacket(_ data: Data) {
|
|
1825
|
+
guard let header = parseHeader(data) else { return }
|
|
1826
|
+
let isResponse = (header.ctrl & Ar99Protocol.ctrlActionResponse) != 0
|
|
1827
|
+
if header.function == Ar99Protocol.funcComm {
|
|
1828
|
+
handleCommMessage(cmd: header.cmd, isResponse: isResponse, payload: header.payload)
|
|
1829
|
+
} else if header.function == Ar99Protocol.funcCommDisplay {
|
|
1830
|
+
if header.cmd == Ar99Protocol.cmdDisplayCtrlAudioRecord {
|
|
1831
|
+
handleLegacyAudioRecordMessage(isResponse: isResponse, payload: header.payload)
|
|
1832
|
+
} else {
|
|
1833
|
+
handleDisplayMessage(cmd: header.cmd, isResponse: isResponse, payload: header.payload)
|
|
1834
|
+
}
|
|
1835
|
+
} else if header.function == Ar99Protocol.funcTrans {
|
|
1836
|
+
handleTransMessage(cmd: header.cmd, isResponse: isResponse, payload: header.payload)
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private func handleOpusData(_ data: Data) {
|
|
1841
|
+
guard !data.isEmpty else { return }
|
|
1842
|
+
if !opusLoggedFirstInSession {
|
|
1843
|
+
opusLoggedFirstInSession = true
|
|
1844
|
+
Bridge.log("AR99: first Opus notify len=\(data.count)")
|
|
1845
|
+
}
|
|
1846
|
+
ensureOpusDecoder()
|
|
1847
|
+
opusDecoder?.submitBleNotify(data)
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private func ensureOpusDecoder() {
|
|
1851
|
+
guard opusDecoder == nil else { return }
|
|
1852
|
+
opusDecoder = Ar99OpusPcmDecoder { pcm in
|
|
1853
|
+
DeviceManager.shared.reportGlassesAudioActivity()
|
|
1854
|
+
DeviceManager.shared.handlePcm(pcm)
|
|
1855
|
+
}
|
|
1856
|
+
if opusDecoder == nil {
|
|
1857
|
+
Bridge.log("AR99: system Opus decoder unavailable")
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// MARK: - Response handling
|
|
1862
|
+
|
|
1863
|
+
private func handleTransMessage(cmd: Int, isResponse: Bool, payload: Data) {
|
|
1864
|
+
if !isResponse, cmd == Ar99Protocol.cmdTransDisplayStop {
|
|
1865
|
+
setMicEnabled(false)
|
|
1866
|
+
resetMinimalTextSession()
|
|
1867
|
+
return
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
if isResponse, cmd == Ar99Protocol.cmdTransDisplayStop {
|
|
1871
|
+
if parseSuccessResponse(payload) == true {
|
|
1872
|
+
resetMinimalTextSession()
|
|
1873
|
+
}
|
|
1874
|
+
return
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private func handleDisplayMessage(cmd: Int, isResponse: Bool, payload: Data) {
|
|
1879
|
+
switch cmd {
|
|
1880
|
+
case Ar99Protocol.cmdDisplayGetBattery:
|
|
1881
|
+
guard let info = parseBatteryResponse(payload) else { return }
|
|
1882
|
+
guard info.battery >= 0, info.battery <= 100 else { return }
|
|
1883
|
+
let charging = info.charging ?? (DeviceStore.shared.get("glasses", "charging") as? Bool ?? false)
|
|
1884
|
+
DeviceStore.shared.apply("glasses", "batteryLevel", info.battery)
|
|
1885
|
+
if info.charging != nil {
|
|
1886
|
+
DeviceStore.shared.apply("glasses", "charging", charging)
|
|
1887
|
+
}
|
|
1888
|
+
Bridge.sendBatteryStatus(level: info.battery, charging: charging)
|
|
1889
|
+
hasReceivedBatteryForCurrentConnection = true
|
|
1890
|
+
pendingBatteryRetries = 0
|
|
1891
|
+
initialBatteryRetryItem?.cancel()
|
|
1892
|
+
|
|
1893
|
+
case Ar99Protocol.cmdDisplayGetDeviceInfo:
|
|
1894
|
+
guard let info = parseDeviceInfoResponse(payload) else { return }
|
|
1895
|
+
if let firmwareVersion = info.firmwareVersion {
|
|
1896
|
+
DeviceStore.shared.apply("glasses", "firmwareVersion", firmwareVersion)
|
|
1897
|
+
}
|
|
1898
|
+
if let productName = info.productName {
|
|
1899
|
+
DeviceStore.shared.apply("glasses", "deviceModel", productName)
|
|
1900
|
+
}
|
|
1901
|
+
let preferredBtMac = currentBroadcastMacAddress?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? currentBroadcastMacAddress : info.btMac
|
|
1902
|
+
if let btMac = preferredBtMac, !btMac.isEmpty {
|
|
1903
|
+
DeviceStore.shared.apply("glasses", "bluetoothMacAddress", btMac)
|
|
1904
|
+
}
|
|
1905
|
+
if let serialNumber = info.serialNumber {
|
|
1906
|
+
DeviceStore.shared.apply("glasses", "serialNumber", serialNumber)
|
|
1907
|
+
}
|
|
1908
|
+
sendVersionInfo(info)
|
|
1909
|
+
|
|
1910
|
+
case Ar99Protocol.cmdDisplayGetBright:
|
|
1911
|
+
if let brightness = parseBrightnessResponse(payload) {
|
|
1912
|
+
DeviceStore.shared.apply("glasses", "brightness", brightness)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
case Ar99Protocol.cmdDisplaySetBright:
|
|
1916
|
+
if let brightness = parseSetBrightnessResponse(payload) {
|
|
1917
|
+
DeviceStore.shared.apply("glasses", "brightness", brightness)
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
case Ar99Protocol.cmdDisplaySetLanguage:
|
|
1921
|
+
_ = parseLanguageResponse(payload)
|
|
1922
|
+
|
|
1923
|
+
case Ar99Protocol.cmdDisplayCtrlFactoryReset:
|
|
1924
|
+
if let success = parseSuccessResponse(payload) {
|
|
1925
|
+
let confirmCode = parseFactoryResetConfirmCode(payload) ?? -1
|
|
1926
|
+
Bridge.log("AR99: factory reset response success=\(success) confirmCode=\(confirmCode)")
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
case Ar99Protocol.cmdDisplayCtrlSysFunction:
|
|
1930
|
+
if parseSuccessResponse(payload) == true {
|
|
1931
|
+
minimalModeReady = true
|
|
1932
|
+
flushPendingDisplayUpdateIfNeeded()
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
case Ar99Protocol.cmdDisplayCtrlMinimalTextStart,
|
|
1936
|
+
Ar99Protocol.cmdDisplayCtrlMinimalTextSet,
|
|
1937
|
+
Ar99Protocol.cmdDisplayCtrlMinimalTextClear,
|
|
1938
|
+
Ar99Protocol.cmdDisplayCtrlMinimalTextStop:
|
|
1939
|
+
let success = parseSuccessResponse(payload)
|
|
1940
|
+
if isResponse, success == false {
|
|
1941
|
+
if cmd == Ar99Protocol.cmdDisplayCtrlMinimalTextStart {
|
|
1942
|
+
displaySessionStarted = false
|
|
1943
|
+
} else if cmd == Ar99Protocol.cmdDisplayCtrlMinimalTextClear || cmd == Ar99Protocol.cmdDisplayCtrlMinimalTextStop {
|
|
1944
|
+
resetMinimalTextSession()
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
default:
|
|
1949
|
+
break
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
private func handleCommMessage(cmd: Int, isResponse _: Bool, payload: Data) {
|
|
1954
|
+
if cmd == Ar99Protocol.cmdCommSetTime {
|
|
1955
|
+
_ = parseSetTimeResponse(payload)
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
private func handleLegacyAudioRecordMessage(isResponse _: Bool, payload: Data) {
|
|
1960
|
+
guard let currentRecording = parseLegacyAudioRecordResponse(payload) else { return }
|
|
1961
|
+
if !currentRecording {
|
|
1962
|
+
awaitingMicDisableAck = false
|
|
1963
|
+
opusDecoder?.resetBuffer()
|
|
1964
|
+
}
|
|
1965
|
+
DeviceStore.shared.apply("glasses", "micEnabled", currentRecording)
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// MARK: - Proto parsing
|
|
1969
|
+
|
|
1970
|
+
private func parseHeader(_ data: Data) -> Ar99MessageHeader? {
|
|
1971
|
+
var reader = Ar99ProtoReader(data)
|
|
1972
|
+
var magic: UInt32 = 0
|
|
1973
|
+
var function = 0
|
|
1974
|
+
var cmd = 0
|
|
1975
|
+
var ctrl = 0
|
|
1976
|
+
var sequence = 0
|
|
1977
|
+
var payload = Data()
|
|
1978
|
+
var totalBytes = -1
|
|
1979
|
+
|
|
1980
|
+
while !reader.isAtEnd {
|
|
1981
|
+
guard let field = reader.nextField() else { break }
|
|
1982
|
+
switch field.field {
|
|
1983
|
+
case 1 where field.wire == 5:
|
|
1984
|
+
guard let value = reader.readFixed32() else { return nil }
|
|
1985
|
+
magic = value
|
|
1986
|
+
case 2 where field.wire == 0:
|
|
1987
|
+
guard let value = reader.readVarint() else { return nil }
|
|
1988
|
+
function = Int(value)
|
|
1989
|
+
case 3 where field.wire == 0:
|
|
1990
|
+
guard let value = reader.readVarint() else { return nil }
|
|
1991
|
+
cmd = Int(value)
|
|
1992
|
+
case 5 where field.wire == 0:
|
|
1993
|
+
guard let value = reader.readVarint() else { return nil }
|
|
1994
|
+
ctrl = Int(value)
|
|
1995
|
+
case 6 where field.wire == 0:
|
|
1996
|
+
guard let value = reader.readVarint() else { return nil }
|
|
1997
|
+
sequence = Int(value)
|
|
1998
|
+
case 8 where field.wire == 2:
|
|
1999
|
+
guard let value = reader.readLengthDelimited() else { return nil }
|
|
2000
|
+
payload = value
|
|
2001
|
+
totalBytes = reader.offset
|
|
2002
|
+
default:
|
|
2003
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
guard magic == Ar99Protocol.magicNumber else { return nil }
|
|
2008
|
+
return Ar99MessageHeader(
|
|
2009
|
+
function: function,
|
|
2010
|
+
cmd: cmd,
|
|
2011
|
+
ctrl: ctrl,
|
|
2012
|
+
sequence: sequence,
|
|
2013
|
+
payload: payload,
|
|
2014
|
+
totalBytes: totalBytes > 0 ? totalBytes : data.count
|
|
2015
|
+
)
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
private func parseBatteryResponse(_ payload: Data) -> Ar99BatteryInfo? {
|
|
2019
|
+
parseBatteryResponseNested(payload) ?? parseBatteryResponseFlat(payload)
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
private func parseBatteryResponseNested(_ payload: Data) -> Ar99BatteryInfo? {
|
|
2023
|
+
var reader = Ar99ProtoReader(payload)
|
|
2024
|
+
var success: Bool?
|
|
2025
|
+
var battery = -1
|
|
2026
|
+
var charging: Bool?
|
|
2027
|
+
|
|
2028
|
+
while !reader.isAtEnd {
|
|
2029
|
+
guard let field = reader.nextField() else { break }
|
|
2030
|
+
switch field.field {
|
|
2031
|
+
case 1:
|
|
2032
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2033
|
+
case 2 where field.wire == 0:
|
|
2034
|
+
battery = Int(reader.readVarint() ?? UInt64.max)
|
|
2035
|
+
case 3 where field.wire == 0:
|
|
2036
|
+
charging = (reader.readVarint() ?? 0) != 0
|
|
2037
|
+
default:
|
|
2038
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
guard success == true else { return nil }
|
|
2043
|
+
return Ar99BatteryInfo(battery: battery, charging: charging)
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private func parseBatteryResponseFlat(_ payload: Data) -> Ar99BatteryInfo? {
|
|
2047
|
+
var reader = Ar99ProtoReader(payload)
|
|
2048
|
+
var success: Bool?
|
|
2049
|
+
var battery: Int?
|
|
2050
|
+
var charging: Bool?
|
|
2051
|
+
|
|
2052
|
+
while !reader.isAtEnd {
|
|
2053
|
+
guard let field = reader.nextField() else { break }
|
|
2054
|
+
switch field.field {
|
|
2055
|
+
case 1:
|
|
2056
|
+
if field.wire == 2, let raw = reader.readLengthDelimited() {
|
|
2057
|
+
success = parseCommonResult(raw)?.success
|
|
2058
|
+
} else if field.wire == 0, let value = reader.readVarint() {
|
|
2059
|
+
if value == 0 || value == 1 {
|
|
2060
|
+
success = value == 1
|
|
2061
|
+
} else if battery == nil, value <= 100 {
|
|
2062
|
+
battery = Int(value)
|
|
2063
|
+
}
|
|
2064
|
+
} else {
|
|
2065
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2066
|
+
}
|
|
2067
|
+
case 2 where field.wire == 0:
|
|
2068
|
+
guard let value = reader.readVarint() else { return nil }
|
|
2069
|
+
if battery == nil, value <= 100 {
|
|
2070
|
+
battery = Int(value)
|
|
2071
|
+
} else if charging == nil, value == 0 || value == 1 {
|
|
2072
|
+
charging = value == 1
|
|
2073
|
+
}
|
|
2074
|
+
case 3 where field.wire == 0:
|
|
2075
|
+
charging = (reader.readVarint() ?? 0) != 0
|
|
2076
|
+
default:
|
|
2077
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
guard let battery, success != false else { return nil }
|
|
2082
|
+
return Ar99BatteryInfo(battery: battery, charging: charging)
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
private func parseDeviceInfoResponse(_ payload: Data) -> Ar99DeviceInfo? {
|
|
2086
|
+
var reader = Ar99ProtoReader(payload)
|
|
2087
|
+
var success: Bool?
|
|
2088
|
+
var firmware: String?
|
|
2089
|
+
var version: String?
|
|
2090
|
+
var deviceName: String?
|
|
2091
|
+
var productName: String?
|
|
2092
|
+
var btMac: String?
|
|
2093
|
+
var serial: String?
|
|
2094
|
+
|
|
2095
|
+
while !reader.isAtEnd {
|
|
2096
|
+
guard let field = reader.nextField() else { break }
|
|
2097
|
+
switch field.field {
|
|
2098
|
+
case 1:
|
|
2099
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2100
|
+
case 2 where field.wire == 2:
|
|
2101
|
+
firmware = reader.readString()
|
|
2102
|
+
case 5 where field.wire == 2:
|
|
2103
|
+
version = reader.readString()
|
|
2104
|
+
case 6 where field.wire == 2:
|
|
2105
|
+
deviceName = reader.readString()
|
|
2106
|
+
case 7 where field.wire == 2:
|
|
2107
|
+
productName = reader.readString()
|
|
2108
|
+
case 8 where field.wire == 2:
|
|
2109
|
+
btMac = reader.readString()
|
|
2110
|
+
case 9 where field.wire == 2:
|
|
2111
|
+
serial = reader.readString()
|
|
2112
|
+
default:
|
|
2113
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
guard success == true else { return nil }
|
|
2118
|
+
return Ar99DeviceInfo(
|
|
2119
|
+
firmwareVersion: (version?.isEmpty == false ? version : firmware),
|
|
2120
|
+
deviceName: deviceName,
|
|
2121
|
+
productName: productName,
|
|
2122
|
+
btMac: btMac,
|
|
2123
|
+
serialNumber: serial
|
|
2124
|
+
)
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
private func parseLanguageResponse(_ payload: Data) -> Ar99LanguageResponse? {
|
|
2128
|
+
var reader = Ar99ProtoReader(payload)
|
|
2129
|
+
var success: Bool?
|
|
2130
|
+
var language: Int?
|
|
2131
|
+
|
|
2132
|
+
while !reader.isAtEnd {
|
|
2133
|
+
guard let field = reader.nextField() else { break }
|
|
2134
|
+
switch field.field {
|
|
2135
|
+
case 1:
|
|
2136
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2137
|
+
case 2 where field.wire == 0:
|
|
2138
|
+
language = Int(reader.readVarint() ?? 0)
|
|
2139
|
+
default:
|
|
2140
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
guard let success else { return nil }
|
|
2145
|
+
return Ar99LanguageResponse(success: success, language: language)
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
private func parseSetTimeResponse(_ payload: Data) -> Ar99SetTimeResponse? {
|
|
2149
|
+
var reader = Ar99ProtoReader(payload)
|
|
2150
|
+
var success: Bool?
|
|
2151
|
+
var timeValue: Int64?
|
|
2152
|
+
|
|
2153
|
+
while !reader.isAtEnd {
|
|
2154
|
+
guard let field = reader.nextField() else { break }
|
|
2155
|
+
switch field.field {
|
|
2156
|
+
case 1:
|
|
2157
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2158
|
+
case 2 where field.wire == 0:
|
|
2159
|
+
timeValue = Int64(bitPattern: reader.readVarint() ?? 0)
|
|
2160
|
+
default:
|
|
2161
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
guard let success else { return nil }
|
|
2166
|
+
return Ar99SetTimeResponse(success: success, timeValue: timeValue)
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
private func sendVersionInfo(_ info: Ar99DeviceInfo) {
|
|
2170
|
+
var values: [String: Any] = [:]
|
|
2171
|
+
if let firmwareVersion = info.firmwareVersion {
|
|
2172
|
+
values["firmwareVersion"] = firmwareVersion
|
|
2173
|
+
}
|
|
2174
|
+
if let productName = info.productName {
|
|
2175
|
+
values["deviceModel"] = productName
|
|
2176
|
+
}
|
|
2177
|
+
if let serialNumber = info.serialNumber {
|
|
2178
|
+
values["serialNumber"] = serialNumber
|
|
2179
|
+
}
|
|
2180
|
+
Bridge.sendVersionInfo(values)
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
private func parseBrightnessResponse(_ payload: Data) -> Int? {
|
|
2184
|
+
parseBrightnessPayload(payload)
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
private func parseSetBrightnessResponse(_ payload: Data) -> Int? {
|
|
2188
|
+
parseBrightnessPayload(payload)
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
private func parseBrightnessPayload(_ payload: Data) -> Int? {
|
|
2192
|
+
var reader = Ar99ProtoReader(payload)
|
|
2193
|
+
var success: Bool?
|
|
2194
|
+
var brightness: Int?
|
|
2195
|
+
while !reader.isAtEnd {
|
|
2196
|
+
guard let field = reader.nextField() else { break }
|
|
2197
|
+
switch field.field {
|
|
2198
|
+
case 1:
|
|
2199
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2200
|
+
case 2 where field.wire == 0:
|
|
2201
|
+
brightness = Int(reader.readVarint() ?? 0)
|
|
2202
|
+
default:
|
|
2203
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return success == true ? brightness : nil
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
private func parseSuccessResponse(_ payload: Data) -> Bool? {
|
|
2210
|
+
var reader = Ar99ProtoReader(payload)
|
|
2211
|
+
var success: Bool?
|
|
2212
|
+
while !reader.isAtEnd {
|
|
2213
|
+
guard let field = reader.nextField() else { break }
|
|
2214
|
+
if field.field == 1 {
|
|
2215
|
+
success = readSuccessField(reader: &reader, wire: field.wire)
|
|
2216
|
+
} else {
|
|
2217
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return success
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
private func parseFactoryResetConfirmCode(_ payload: Data) -> Int? {
|
|
2224
|
+
var reader = Ar99ProtoReader(payload)
|
|
2225
|
+
var confirmCode: Int?
|
|
2226
|
+
while !reader.isAtEnd {
|
|
2227
|
+
guard let field = reader.nextField() else { break }
|
|
2228
|
+
switch field.field {
|
|
2229
|
+
case 1:
|
|
2230
|
+
_ = readSuccessField(reader: &reader, wire: field.wire)
|
|
2231
|
+
case 2 where field.wire == 0:
|
|
2232
|
+
confirmCode = Int(reader.readVarint() ?? 0)
|
|
2233
|
+
default:
|
|
2234
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return confirmCode
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
private func parseLegacyAudioRecordResponse(_ payload: Data) -> Bool? {
|
|
2241
|
+
var reader = Ar99ProtoReader(payload)
|
|
2242
|
+
var success: Bool?
|
|
2243
|
+
var start: Bool?
|
|
2244
|
+
while !reader.isAtEnd {
|
|
2245
|
+
guard let field = reader.nextField() else { break }
|
|
2246
|
+
switch field.field {
|
|
2247
|
+
case 1 where field.wire == 0:
|
|
2248
|
+
success = (reader.readVarint() ?? 0) != 0
|
|
2249
|
+
case 2 where field.wire == 0:
|
|
2250
|
+
start = (reader.readVarint() ?? 0) != 0
|
|
2251
|
+
default:
|
|
2252
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
guard success == true, let start else { return nil }
|
|
2256
|
+
return !start
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
private func readSuccessField(reader: inout Ar99ProtoReader, wire: Int) -> Bool? {
|
|
2260
|
+
if wire == 0 {
|
|
2261
|
+
return (reader.readVarint() ?? 0) != 0
|
|
2262
|
+
}
|
|
2263
|
+
if wire == 2, let raw = reader.readLengthDelimited() {
|
|
2264
|
+
return parseCommonResult(raw)?.success
|
|
2265
|
+
}
|
|
2266
|
+
_ = reader.skipField(wire: wire)
|
|
2267
|
+
return nil
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
private func parseCommonResult(_ raw: Data) -> Ar99CommonResult? {
|
|
2271
|
+
var reader = Ar99ProtoReader(raw)
|
|
2272
|
+
var success = false
|
|
2273
|
+
var errorCode = 0
|
|
2274
|
+
var errorMessage = ""
|
|
2275
|
+
while !reader.isAtEnd {
|
|
2276
|
+
guard let field = reader.nextField() else { break }
|
|
2277
|
+
switch field.field {
|
|
2278
|
+
case 1 where field.wire == 0:
|
|
2279
|
+
success = (reader.readVarint() ?? 0) != 0
|
|
2280
|
+
case 2 where field.wire == 0:
|
|
2281
|
+
errorCode = Int(reader.readVarint() ?? 0)
|
|
2282
|
+
case 3 where field.wire == 2:
|
|
2283
|
+
errorMessage = reader.readString() ?? ""
|
|
2284
|
+
default:
|
|
2285
|
+
guard reader.skipField(wire: field.wire) else { return nil }
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
return Ar99CommonResult(success: success, errorCode: errorCode, errorMessage: errorMessage)
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// MARK: - Advertisement parsing
|
|
2292
|
+
|
|
2293
|
+
private func parseAdvertisement(localName: String?, manufacturerData: Data?) -> Ar99ParsedAdvertisement? {
|
|
2294
|
+
var parsed: Ar99ParsedAdvertisement?
|
|
2295
|
+
if let data = manufacturerData {
|
|
2296
|
+
for base in [0, 2] where base + 20 <= data.count {
|
|
2297
|
+
let projectName = trimAscii4(data, offset: base)
|
|
2298
|
+
guard matchesAr99(name: nil, projectName: projectName) else { continue }
|
|
2299
|
+
parsed = Ar99ParsedAdvertisement(
|
|
2300
|
+
bleName: localName,
|
|
2301
|
+
projectName: projectName,
|
|
2302
|
+
bleAddress: xorMac6ToColon(data, offset: base + 8),
|
|
2303
|
+
btAddress: xorMac6ToColon(data, offset: base + 14),
|
|
2304
|
+
serialNumber: parseAr99SerialNumber(data, base: base)
|
|
2305
|
+
)
|
|
2306
|
+
break
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if let parsed {
|
|
2310
|
+
return parsed
|
|
2311
|
+
}
|
|
2312
|
+
if matchesAr99(name: localName, projectName: nil) {
|
|
2313
|
+
return Ar99ParsedAdvertisement(
|
|
2314
|
+
bleName: localName,
|
|
2315
|
+
projectName: nil,
|
|
2316
|
+
bleAddress: nil,
|
|
2317
|
+
btAddress: nil,
|
|
2318
|
+
serialNumber: nil
|
|
2319
|
+
)
|
|
2320
|
+
}
|
|
2321
|
+
return nil
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
private func matchesAr99(name _: String?, projectName: String?) -> Bool {
|
|
2325
|
+
isSupportedProjectName(projectName)
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
private func isSupportedProjectName(_ projectName: String?) -> Bool {
|
|
2329
|
+
guard let projectName = projectName?.trimmingCharacters(in: .whitespacesAndNewlines), !projectName.isEmpty else {
|
|
2330
|
+
return false
|
|
2331
|
+
}
|
|
2332
|
+
return supportedProjectNames.contains { $0.caseInsensitiveCompare(projectName) == .orderedSame }
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
private func trimAscii4(_ data: Data, offset: Int) -> String {
|
|
2336
|
+
let bytes = Array(data)
|
|
2337
|
+
guard offset < bytes.count else { return "" }
|
|
2338
|
+
var end = min(offset + 4, bytes.count)
|
|
2339
|
+
while end > offset, bytes[end - 1] == 0 {
|
|
2340
|
+
end -= 1
|
|
2341
|
+
}
|
|
2342
|
+
guard end > offset else { return "" }
|
|
2343
|
+
return String(data: Data(bytes[offset ..< end]), encoding: .utf8) ?? ""
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private func xorMac6ToColon(_ data: Data, offset: Int) -> String? {
|
|
2347
|
+
let bytes = Array(data)
|
|
2348
|
+
guard offset + 6 <= bytes.count else { return nil }
|
|
2349
|
+
return (0 ..< 6)
|
|
2350
|
+
.map { String(format: "%02X", (bytes[offset + $0] ^ 0xAD) & 0xFF) }
|
|
2351
|
+
.joined(separator: ":")
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
private func parseAr99SerialNumber(_ data: Data, base: Int) -> String? {
|
|
2355
|
+
let bytes = Array(data)
|
|
2356
|
+
let serialOffset = base + 20
|
|
2357
|
+
if serialOffset + 12 <= bytes.count,
|
|
2358
|
+
!isAllZeroBytes(bytes, offset: serialOffset, length: 12),
|
|
2359
|
+
isAsciiDigits(bytes, offset: serialOffset, length: 12)
|
|
2360
|
+
{
|
|
2361
|
+
return safeAscii(bytes, offset: serialOffset, length: 12)
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if serialOffset + 6 <= bytes.count {
|
|
2365
|
+
if !isAllZeroBytes(bytes, offset: serialOffset, length: 6),
|
|
2366
|
+
isAsciiDigits(bytes, offset: serialOffset, length: 6)
|
|
2367
|
+
{
|
|
2368
|
+
return safeAscii(bytes, offset: serialOffset, length: 6)
|
|
2369
|
+
}
|
|
2370
|
+
if !isAllZeroBytes(bytes, offset: serialOffset, length: 6) {
|
|
2371
|
+
return bytes6ToHex12(bytes, offset: serialOffset)
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
return nil
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
private func isAsciiDigits(_ bytes: [UInt8], offset: Int, length: Int) -> Bool {
|
|
2379
|
+
guard offset + length <= bytes.count else { return false }
|
|
2380
|
+
return (0 ..< length).allSatisfy { bytes[offset + $0] >= 48 && bytes[offset + $0] <= 57 }
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
private func isAllZeroBytes(_ bytes: [UInt8], offset: Int, length: Int) -> Bool {
|
|
2384
|
+
guard offset + length <= bytes.count else { return true }
|
|
2385
|
+
let slice = bytes[offset ..< offset + length]
|
|
2386
|
+
return slice.allSatisfy { $0 == 0 } || slice.allSatisfy { $0 == 48 }
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
private func safeAscii(_ bytes: [UInt8], offset: Int, length: Int) -> String? {
|
|
2390
|
+
guard offset + length <= bytes.count else { return nil }
|
|
2391
|
+
return String(data: Data(bytes[offset ..< offset + length]), encoding: .utf8)
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
private func bytes6ToHex12(_ bytes: [UInt8], offset: Int) -> String? {
|
|
2395
|
+
guard offset + 6 <= bytes.count else { return nil }
|
|
2396
|
+
return (0 ..< 6).map { String(format: "%02X", bytes[offset + $0]) }.joined()
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
private func buildDisplayName(
|
|
2400
|
+
fallbackName: String,
|
|
2401
|
+
advertisement: Ar99ParsedAdvertisement?,
|
|
2402
|
+
deviceAddress: String?
|
|
2403
|
+
) -> String {
|
|
2404
|
+
if let serial = advertisement?.serialNumber?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
2405
|
+
!serial.isEmpty
|
|
2406
|
+
{
|
|
2407
|
+
return "SN: \(serial)"
|
|
2408
|
+
}
|
|
2409
|
+
let bleName = advertisement?.bleName ?? fallbackName
|
|
2410
|
+
let legacyNameSerial = extractLegacyNameSerial(bleName)
|
|
2411
|
+
if !legacyNameSerial.isEmpty {
|
|
2412
|
+
return "SN: \(legacyNameSerial)"
|
|
2413
|
+
}
|
|
2414
|
+
if let deviceAddress = deviceAddress?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
2415
|
+
!deviceAddress.isEmpty
|
|
2416
|
+
{
|
|
2417
|
+
return "MAC: \(deviceAddress)"
|
|
2418
|
+
}
|
|
2419
|
+
return fallbackName
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
private func extractTargetSerial(_ target: String) -> String {
|
|
2423
|
+
let trimmed = normalizeDisplayIdentifier(target)
|
|
2424
|
+
guard !trimmed.isEmpty else { return "" }
|
|
2425
|
+
if let underscore = trimmed.lastIndex(of: "_"), underscore < trimmed.index(before: trimmed.endIndex) {
|
|
2426
|
+
return String(trimmed[trimmed.index(after: underscore)...]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2427
|
+
}
|
|
2428
|
+
return trimmed
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
private func normalizeDisplayIdentifier(_ value: String) -> String {
|
|
2432
|
+
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2433
|
+
let upper = trimmed.uppercased()
|
|
2434
|
+
if upper.hasPrefix("SN:") {
|
|
2435
|
+
return String(trimmed.dropFirst(3)).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2436
|
+
}
|
|
2437
|
+
if upper.hasPrefix("MAC:") {
|
|
2438
|
+
return String(trimmed.dropFirst(4)).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2439
|
+
}
|
|
2440
|
+
return trimmed
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
private func extractLegacyNameSerial(_ bleName: String?) -> String {
|
|
2444
|
+
guard let bleName else { return "" }
|
|
2445
|
+
let trimmed = bleName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2446
|
+
guard let underscore = trimmed.firstIndex(of: "_"), underscore < trimmed.index(before: trimmed.endIndex) else {
|
|
2447
|
+
return ""
|
|
2448
|
+
}
|
|
2449
|
+
return String(trimmed[trimmed.index(after: underscore)...]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2450
|
+
}
|
|
2451
|
+
}
|