@glassly/bluetooth-sdk 0.1.0-dev.26

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