@glassly/bluetooth-sdk 0.1.0-dev.100

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