@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.
Files changed (548) hide show
  1. package/README.md +563 -0
  2. package/android/build.gradle +450 -0
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/CMakeLists.txt +6 -0
  5. package/android/lc3Lib/build.gradle +212 -0
  6. package/android/lc3Lib/proguard-rules.pro +33 -0
  7. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  8. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  26. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  81. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  82. package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
  83. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  86. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  119. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  120. package/android/settings.gradle +4 -0
  121. package/android/silero/LICENSE.md +17 -0
  122. package/android/silero/build.gradle +50 -0
  123. package/android/silero/proguard-rules.pro +24 -0
  124. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  125. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  126. package/android/silero/src/main/assets/LICENSE +21 -0
  127. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  128. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  129. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  130. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  131. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  132. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  133. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  134. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  135. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  136. package/android/src/main/AndroidManifest.xml +42 -0
  137. package/android/src/main/assets/G2Firmware/README.md +11 -0
  138. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
  139. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
  140. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
  141. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
  142. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
  143. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
  144. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
  145. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
  146. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
  147. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  148. package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
  149. package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
  150. package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
  151. package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
  152. package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  153. package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
  154. package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
  155. package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  156. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  157. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
  158. package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
  159. package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
  160. package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
  161. package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
  162. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  163. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
  164. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  165. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
  166. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
  167. package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
  168. package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
  169. package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
  170. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
  171. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  172. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  173. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
  174. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  175. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
  176. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
  177. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
  178. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
  179. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
  180. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
  181. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
  182. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
  183. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
  184. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  185. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  186. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
  187. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  188. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
  189. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
  190. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  191. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  192. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  193. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  194. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  195. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  196. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
  197. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
  198. package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
  199. package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
  200. package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
  201. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
  202. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
  203. package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  204. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
  205. package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
  206. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
  207. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  208. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  209. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  210. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
  211. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
  212. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  213. package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
  214. package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
  215. package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  216. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  217. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
  218. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  219. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
  220. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
  221. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
  222. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
  223. package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
  224. package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  225. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  226. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  227. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  228. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  229. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  230. package/android/src/main/res/values/strings.xml +4 -0
  231. package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  232. package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  233. package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  234. package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  235. package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  236. package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  237. package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
  238. package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  239. package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  240. package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  241. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  242. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  243. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  244. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
  245. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  246. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
  247. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
  248. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
  249. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
  250. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  251. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  252. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  253. package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
  254. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  255. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
  256. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
  257. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  258. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  259. package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  260. package/android/src/test/resources/avif_with_exif.avif +0 -0
  261. package/app.plugin.js +3 -0
  262. package/build/BluetoothSdk.types.d.ts +1354 -0
  263. package/build/BluetoothSdk.types.d.ts.map +1 -0
  264. package/build/BluetoothSdk.types.js +41 -0
  265. package/build/BluetoothSdk.types.js.map +1 -0
  266. package/build/_internal.d.ts +17 -0
  267. package/build/_internal.d.ts.map +1 -0
  268. package/build/_internal.js +16 -0
  269. package/build/_internal.js.map +1 -0
  270. package/build/_private/BluetoothSdkModule.d.ts +172 -0
  271. package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
  272. package/build/_private/BluetoothSdkModule.js +300 -0
  273. package/build/_private/BluetoothSdkModule.js.map +1 -0
  274. package/build/_private/LocalNetworkModule.d.ts +38 -0
  275. package/build/_private/LocalNetworkModule.d.ts.map +1 -0
  276. package/build/_private/LocalNetworkModule.js +3 -0
  277. package/build/_private/LocalNetworkModule.js.map +1 -0
  278. package/build/_private/cameraRequestPayload.d.ts +4 -0
  279. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  280. package/build/_private/cameraRequestPayload.js +32 -0
  281. package/build/_private/cameraRequestPayload.js.map +1 -0
  282. package/build/_private/photoRequestPayload.d.ts +6 -0
  283. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  284. package/build/_private/photoRequestPayload.js +86 -0
  285. package/build/_private/photoRequestPayload.js.map +1 -0
  286. package/build/_private/sdkOtaManifest.d.ts +11 -0
  287. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  288. package/build/_private/sdkOtaManifest.js +15 -0
  289. package/build/_private/sdkOtaManifest.js.map +1 -0
  290. package/build/changelogs.d.ts +7 -0
  291. package/build/changelogs.d.ts.map +1 -0
  292. package/build/changelogs.js +46 -0
  293. package/build/changelogs.js.map +1 -0
  294. package/build/debug.d.ts +3 -0
  295. package/build/debug.d.ts.map +1 -0
  296. package/build/debug.js +8 -0
  297. package/build/debug.js.map +1 -0
  298. package/build/generated/changelogCatalog.d.ts +9 -0
  299. package/build/generated/changelogCatalog.d.ts.map +1 -0
  300. package/build/generated/changelogCatalog.js +12 -0
  301. package/build/generated/changelogCatalog.js.map +1 -0
  302. package/build/generated/releaseMetadata.d.ts +12 -0
  303. package/build/generated/releaseMetadata.d.ts.map +1 -0
  304. package/build/generated/releaseMetadata.js +10 -0
  305. package/build/generated/releaseMetadata.js.map +1 -0
  306. package/build/index.d.ts +6 -0
  307. package/build/index.d.ts.map +1 -0
  308. package/build/index.js +171 -0
  309. package/build/index.js.map +1 -0
  310. package/build/ota-server/OtaServer.types.d.ts +14 -0
  311. package/build/ota-server/OtaServer.types.d.ts.map +1 -0
  312. package/build/ota-server/OtaServer.types.js +2 -0
  313. package/build/ota-server/OtaServer.types.js.map +1 -0
  314. package/build/ota-server/OtaServerModule.d.ts +21 -0
  315. package/build/ota-server/OtaServerModule.d.ts.map +1 -0
  316. package/build/ota-server/OtaServerModule.js +3 -0
  317. package/build/ota-server/OtaServerModule.js.map +1 -0
  318. package/build/ota-server/OtaServerModule.web.d.ts +10 -0
  319. package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
  320. package/build/ota-server/OtaServerModule.web.js +12 -0
  321. package/build/ota-server/OtaServerModule.web.js.map +1 -0
  322. package/build/ota-server/index.d.ts +3 -0
  323. package/build/ota-server/index.d.ts.map +1 -0
  324. package/build/ota-server/index.js +3 -0
  325. package/build/ota-server/index.js.map +1 -0
  326. package/build/ota-transport/index.d.ts +67 -0
  327. package/build/ota-transport/index.d.ts.map +1 -0
  328. package/build/ota-transport/index.js +58 -0
  329. package/build/ota-transport/index.js.map +1 -0
  330. package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
  331. package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
  332. package/build/photo-receiver/PhotoReceiver.types.js +2 -0
  333. package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
  334. package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
  335. package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
  336. package/build/photo-receiver/PhotoReceiverModule.js +3 -0
  337. package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
  338. package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
  339. package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
  340. package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
  341. package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
  342. package/build/photo-receiver/index.d.ts +3 -0
  343. package/build/photo-receiver/index.d.ts.map +1 -0
  344. package/build/photo-receiver/index.js +3 -0
  345. package/build/photo-receiver/index.js.map +1 -0
  346. package/build/react/index.d.ts +5 -0
  347. package/build/react/index.d.ts.map +1 -0
  348. package/build/react/index.js +4 -0
  349. package/build/react/index.js.map +1 -0
  350. package/build/react/useBluetooth.d.ts +100 -0
  351. package/build/react/useBluetooth.d.ts.map +1 -0
  352. package/build/react/useBluetooth.js +167 -0
  353. package/build/react/useBluetooth.js.map +1 -0
  354. package/build/react/useBluetoothEvent.d.ts +6 -0
  355. package/build/react/useBluetoothEvent.d.ts.map +1 -0
  356. package/build/react/useBluetoothEvent.js +21 -0
  357. package/build/react/useBluetoothEvent.js.map +1 -0
  358. package/build/react/useBluetoothScan.d.ts +22 -0
  359. package/build/react/useBluetoothScan.d.ts.map +1 -0
  360. package/build/react/useBluetoothScan.js +135 -0
  361. package/build/react/useBluetoothScan.js.map +1 -0
  362. package/build/react/useBluetoothStatus.d.ts +16 -0
  363. package/build/react/useBluetoothStatus.d.ts.map +1 -0
  364. package/build/react/useBluetoothStatus.js +137 -0
  365. package/build/react/useBluetoothStatus.js.map +1 -0
  366. package/build/react/useGlassesConnection.d.ts +29 -0
  367. package/build/react/useGlassesConnection.d.ts.map +1 -0
  368. package/build/react/useGlassesConnection.js +153 -0
  369. package/build/react/useGlassesConnection.js.map +1 -0
  370. package/build/types/index.d.ts +3 -0
  371. package/build/types/index.d.ts.map +1 -0
  372. package/build/types/index.js +2 -0
  373. package/build/types/index.js.map +1 -0
  374. package/expo-module.config.json +14 -0
  375. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  376. package/ios/BluetoothSdkModule.swift +1034 -0
  377. package/ios/GlasslyBluetoothSDK.podspec +106 -0
  378. package/ios/LocalIPv4.swift +150 -0
  379. package/ios/LocalOtaServer.swift +309 -0
  380. package/ios/LocalPhotoUploadServer.swift +754 -0
  381. package/ios/OtaServerModule.swift +147 -0
  382. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  383. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  384. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  385. package/ios/Packages/CoreObjC/attdet.c +92 -0
  386. package/ios/Packages/CoreObjC/attdet.h +44 -0
  387. package/ios/Packages/CoreObjC/bits.c +375 -0
  388. package/ios/Packages/CoreObjC/bits.h +315 -0
  389. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  390. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  391. package/ios/Packages/CoreObjC/common.h +151 -0
  392. package/ios/Packages/CoreObjC/energy.c +70 -0
  393. package/ios/Packages/CoreObjC/energy.h +43 -0
  394. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  395. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  396. package/ios/Packages/CoreObjC/lc3.c +704 -0
  397. package/ios/Packages/CoreObjC/lc3.h +313 -0
  398. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  399. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  400. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  401. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  402. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  403. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  404. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  405. package/ios/Packages/CoreObjC/mdct.c +469 -0
  406. package/ios/Packages/CoreObjC/mdct.h +57 -0
  407. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  408. package/ios/Packages/CoreObjC/meson.build +61 -0
  409. package/ios/Packages/CoreObjC/plc.c +61 -0
  410. package/ios/Packages/CoreObjC/plc.h +57 -0
  411. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  412. package/ios/Packages/CoreObjC/sns.c +880 -0
  413. package/ios/Packages/CoreObjC/sns.h +103 -0
  414. package/ios/Packages/CoreObjC/spec.c +907 -0
  415. package/ios/Packages/CoreObjC/spec.h +119 -0
  416. package/ios/Packages/CoreObjC/tables.c +3457 -0
  417. package/ios/Packages/CoreObjC/tables.h +94 -0
  418. package/ios/Packages/CoreObjC/tns.c +457 -0
  419. package/ios/Packages/CoreObjC/tns.h +99 -0
  420. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  421. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  422. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
  423. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
  424. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  425. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  426. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  427. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  428. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  429. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  430. package/ios/Packages/libbz2/module.modulemap +5 -0
  431. package/ios/Packages/libbz2/shim.h +1 -0
  432. package/ios/PhotoReceiverModule.swift +129 -0
  433. package/ios/Source/BluetoothSDK.swift +2303 -0
  434. package/ios/Source/BluetoothSDKDebug.swift +12 -0
  435. package/ios/Source/BluetoothSdkDefaults.swift +29 -0
  436. package/ios/Source/Bridge.swift +708 -0
  437. package/ios/Source/Bridging-Header.h +16 -0
  438. package/ios/Source/DeviceManager.swift +2109 -0
  439. package/ios/Source/DeviceStore.swift +336 -0
  440. package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
  441. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  442. package/ios/Source/ObservableStore.swift +118 -0
  443. package/ios/Source/OtaManifest.swift +211 -0
  444. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  445. package/ios/Source/ReleaseChangelog.swift +65 -0
  446. package/ios/Source/Resources/G2Firmware/README.md +10 -0
  447. package/ios/Source/audio/AudioModels.swift +178 -0
  448. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  449. package/ios/Source/camera/CameraModels.swift +866 -0
  450. package/ios/Source/connection/ScanSession.swift +27 -0
  451. package/ios/Source/controllers/ControllerManager.swift +184 -0
  452. package/ios/Source/controllers/R1.swift +829 -0
  453. package/ios/Source/errors/BluetoothSdkError.swift +19 -0
  454. package/ios/Source/events/BluetoothEvents.swift +342 -0
  455. package/ios/Source/internal/BleTraceLogger.swift +192 -0
  456. package/ios/Source/internal/BluetoothAvailability.swift +78 -0
  457. package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
  458. package/ios/Source/internal/ValueParsing.swift +99 -0
  459. package/ios/Source/requests/DisplayRequests.swift +82 -0
  460. package/ios/Source/services/PhoneMic.swift +662 -0
  461. package/ios/Source/sgcs/Ar99.swift +2451 -0
  462. package/ios/Source/sgcs/Frame.swift +649 -0
  463. package/ios/Source/sgcs/G1.swift +2554 -0
  464. package/ios/Source/sgcs/G2.swift +6887 -0
  465. package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
  466. package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
  467. package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
  468. package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
  469. package/ios/Source/sgcs/G2Flasher.swift +773 -0
  470. package/ios/Source/sgcs/Mach1.swift +541 -0
  471. package/ios/Source/sgcs/MentraLive.swift +7011 -0
  472. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  473. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  474. package/ios/Source/sgcs/MentraNex.swift +3221 -0
  475. package/ios/Source/sgcs/Nimo.swift +1886 -0
  476. package/ios/Source/sgcs/SGCManager.swift +608 -0
  477. package/ios/Source/sgcs/Simulated.swift +274 -0
  478. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  479. package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
  480. package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
  481. package/ios/Source/status/DeviceStatus.swift +981 -0
  482. package/ios/Source/status/RuntimeState.swift +359 -0
  483. package/ios/Source/status/WifiHotspotStatus.swift +345 -0
  484. package/ios/Source/streaming/StreamModels.swift +693 -0
  485. package/ios/Source/stt/STTTools.swift +191 -0
  486. package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
  487. package/ios/Source/tts/TTSTools.swift +171 -0
  488. package/ios/Source/types/DeviceModels.swift +177 -0
  489. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  490. package/ios/Source/utils/BinaryCodec.swift +61 -0
  491. package/ios/Source/utils/BleJsonCompact.swift +405 -0
  492. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  493. package/ios/Source/utils/Constants.swift +78 -0
  494. package/ios/Source/utils/Enums.swift +95 -0
  495. package/ios/Source/utils/G1Text.swift +2096 -0
  496. package/ios/Source/utils/JSCExperiment.swift +241 -0
  497. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  498. package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
  499. package/ios/Source/utils/MessageChunker.swift +294 -0
  500. package/ios/Source/utils/Models.swift +135 -0
  501. package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
  502. package/ios/Source/utils/TarBz2Extractor.swift +257 -0
  503. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
  504. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
  505. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
  506. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
  507. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  508. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  509. package/package.json +129 -0
  510. package/plugin/build/index.d.ts +10 -0
  511. package/plugin/build/index.js +12 -0
  512. package/plugin/build/withAndroid.d.ts +3 -0
  513. package/plugin/build/withAndroid.js +190 -0
  514. package/plugin/build/withIos.d.ts +5 -0
  515. package/plugin/build/withIos.js +108 -0
  516. package/scripts/inject-ios-sdk-version.mjs +65 -0
  517. package/scripts/public-ota-api.test.mjs +37 -0
  518. package/scripts/verify-release-package.mjs +45 -0
  519. package/scripts/write-release-metadata.mjs +152 -0
  520. package/scripts/write-release-metadata.test.mjs +37 -0
  521. package/src/BluetoothSdk.types.ts +1623 -0
  522. package/src/_internal.ts +16 -0
  523. package/src/_private/BluetoothSdkModule.ts +684 -0
  524. package/src/_private/LocalNetworkModule.ts +50 -0
  525. package/src/_private/cameraRequestPayload.ts +34 -0
  526. package/src/_private/photoRequestPayload.ts +90 -0
  527. package/src/_private/sdkOtaManifest.ts +18 -0
  528. package/src/changelogs.ts +46 -0
  529. package/src/debug.ts +9 -0
  530. package/src/generated/changelogCatalog.ts +13 -0
  531. package/src/generated/releaseMetadata.ts +20 -0
  532. package/src/index.ts +321 -0
  533. package/src/ota-server/OtaServer.types.ts +10 -0
  534. package/src/ota-server/OtaServerModule.ts +23 -0
  535. package/src/ota-server/OtaServerModule.web.ts +17 -0
  536. package/src/ota-server/index.ts +2 -0
  537. package/src/ota-transport/index.ts +116 -0
  538. package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
  539. package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
  540. package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
  541. package/src/photo-receiver/index.ts +2 -0
  542. package/src/react/index.ts +21 -0
  543. package/src/react/useBluetooth.ts +309 -0
  544. package/src/react/useBluetoothEvent.ts +35 -0
  545. package/src/react/useBluetoothScan.ts +173 -0
  546. package/src/react/useBluetoothStatus.ts +180 -0
  547. package/src/react/useGlassesConnection.ts +202 -0
  548. 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
+ }