@mentra/bluetooth-sdk 0.1.0

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 (278) hide show
  1. package/README.md +278 -0
  2. package/android/build.gradle +167 -0
  3. package/android/lc3Lib/CMakeLists.txt +6 -0
  4. package/android/lc3Lib/build.gradle +109 -0
  5. package/android/lc3Lib/proguard-rules.pro +33 -0
  6. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  7. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  8. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  26. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  81. package/android/lc3Lib/src/main/java/com/mentra/lc3Lib/Lc3Cpp.java +38 -0
  82. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  83. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  86. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  119. package/android/settings.gradle +1 -0
  120. package/android/src/main/AndroidManifest.xml +40 -0
  121. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +528 -0
  122. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +610 -0
  123. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +1616 -0
  124. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +314 -0
  125. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothModels.kt +1710 -0
  126. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +554 -0
  127. package/android/src/main/java/com/mentra/bluetoothsdk/ObservableStore.kt +68 -0
  128. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  129. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +934 -0
  130. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +175 -0
  131. package/android/src/main/java/com/mentra/bluetoothsdk/services/PhoneMic.kt +1109 -0
  132. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +3974 -0
  133. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +3551 -0
  134. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +1327 -0
  135. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +6959 -0
  136. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +1655 -0
  137. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +53675 -0
  138. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +202 -0
  139. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +224 -0
  140. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +340 -0
  141. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +465 -0
  142. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +221 -0
  143. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AES.java +106 -0
  144. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  145. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  146. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +179 -0
  147. package/android/src/main/java/com/mentra/bluetoothsdk/utils/Constants.kt +44 -0
  148. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G1Text.kt +436 -0
  149. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  150. package/android/src/main/java/com/mentra/bluetoothsdk/utils/IncidentLogBleUploadService.java +76 -0
  151. package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +791 -0
  152. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +186 -0
  153. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +589 -0
  154. package/android/src/main/java/com/mentra/bluetoothsdk/utils/PhoneAudioMonitor.kt +303 -0
  155. package/android/src/main/java/com/mentra/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  156. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  157. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  158. package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  159. package/android/src/main/res/values/strings.xml +4 -0
  160. package/app.plugin.js +1 -0
  161. package/build/BluetoothSdk.types.d.ts +498 -0
  162. package/build/BluetoothSdk.types.d.ts.map +1 -0
  163. package/build/BluetoothSdk.types.js +23 -0
  164. package/build/BluetoothSdk.types.js.map +1 -0
  165. package/build/BluetoothSdkModule.d.ts +71 -0
  166. package/build/BluetoothSdkModule.d.ts.map +1 -0
  167. package/build/BluetoothSdkModule.js +106 -0
  168. package/build/BluetoothSdkModule.js.map +1 -0
  169. package/build/index.d.ts +3 -0
  170. package/build/index.d.ts.map +1 -0
  171. package/build/index.js +5 -0
  172. package/build/index.js.map +1 -0
  173. package/expo-module.config.json +9 -0
  174. package/ios/BluetoothSdkModule.swift +665 -0
  175. package/ios/MentraBluetoothSDK.podspec +77 -0
  176. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  177. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  178. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  179. package/ios/Packages/CoreObjC/attdet.c +92 -0
  180. package/ios/Packages/CoreObjC/attdet.h +44 -0
  181. package/ios/Packages/CoreObjC/bits.c +375 -0
  182. package/ios/Packages/CoreObjC/bits.h +315 -0
  183. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  184. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  185. package/ios/Packages/CoreObjC/common.h +151 -0
  186. package/ios/Packages/CoreObjC/energy.c +70 -0
  187. package/ios/Packages/CoreObjC/energy.h +43 -0
  188. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  189. package/ios/Packages/CoreObjC/lc3.c +704 -0
  190. package/ios/Packages/CoreObjC/lc3.h +313 -0
  191. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  192. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  193. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  194. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  195. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  196. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  197. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  198. package/ios/Packages/CoreObjC/mdct.c +469 -0
  199. package/ios/Packages/CoreObjC/mdct.h +57 -0
  200. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  201. package/ios/Packages/CoreObjC/meson.build +61 -0
  202. package/ios/Packages/CoreObjC/plc.c +61 -0
  203. package/ios/Packages/CoreObjC/plc.h +57 -0
  204. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  205. package/ios/Packages/CoreObjC/sns.c +880 -0
  206. package/ios/Packages/CoreObjC/sns.h +103 -0
  207. package/ios/Packages/CoreObjC/spec.c +907 -0
  208. package/ios/Packages/CoreObjC/spec.h +119 -0
  209. package/ios/Packages/CoreObjC/tables.c +3457 -0
  210. package/ios/Packages/CoreObjC/tables.h +94 -0
  211. package/ios/Packages/CoreObjC/tns.c +457 -0
  212. package/ios/Packages/CoreObjC/tns.h +99 -0
  213. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  214. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  215. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +1659 -0
  216. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +162 -0
  217. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +1852 -0
  218. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +674 -0
  219. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +44 -0
  220. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  221. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  222. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  223. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  224. package/ios/Packages/VAD/Common/VAD/VADQuality.swift +41 -0
  225. package/ios/Packages/VAD/Common/VAD/VADState.swift +26 -0
  226. package/ios/Packages/VAD/Common/VAD/VADStrategy.swift +29 -0
  227. package/ios/Packages/VAD/Common/VAD/VADType.swift +14 -0
  228. package/ios/Packages/VAD/Data/Configuration.swift +68 -0
  229. package/ios/Packages/VAD/Data/FrameSize.swift +39 -0
  230. package/ios/Packages/VAD/Data/Record.swift +13 -0
  231. package/ios/Packages/VAD/Data/Result.swift +22 -0
  232. package/ios/Packages/VAD/Data/SampleRate.swift +48 -0
  233. package/ios/Packages/VAD/Silero/Model/silero_vad.onnx +0 -0
  234. package/ios/Packages/VAD/Silero/SileroVAD.swift +284 -0
  235. package/ios/Packages/VAD/Silero/SileroVADStrategy.swift +64 -0
  236. package/ios/Packages/libbz2/module.modulemap +5 -0
  237. package/ios/Packages/libbz2/shim.h +1 -0
  238. package/ios/Source/Bridge.swift +353 -0
  239. package/ios/Source/Bridging-Header.h +16 -0
  240. package/ios/Source/DeviceManager.swift +1478 -0
  241. package/ios/Source/DeviceStore.swift +295 -0
  242. package/ios/Source/MentraBluetoothSDK.swift +2439 -0
  243. package/ios/Source/ObservableStore.swift +88 -0
  244. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  245. package/ios/Source/controllers/ControllerManager.swift +187 -0
  246. package/ios/Source/controllers/R1.swift +827 -0
  247. package/ios/Source/services/PhoneMic.swift +662 -0
  248. package/ios/Source/sgcs/Frame.swift +651 -0
  249. package/ios/Source/sgcs/G1.swift +2505 -0
  250. package/ios/Source/sgcs/G2.swift +3730 -0
  251. package/ios/Source/sgcs/Mach1.swift +543 -0
  252. package/ios/Source/sgcs/MentraLive.swift +4708 -0
  253. package/ios/Source/sgcs/MentraNex.swift +2479 -0
  254. package/ios/Source/sgcs/SGCManager.swift +243 -0
  255. package/ios/Source/sgcs/Simulated.swift +270 -0
  256. package/ios/Source/sgcs/mentraos_ble.pb.swift +3811 -0
  257. package/ios/Source/stt/STTTools.swift +154 -0
  258. package/ios/Source/stt/SherpaOnnxTranscriber.swift +401 -0
  259. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  260. package/ios/Source/utils/Constants.swift +74 -0
  261. package/ios/Source/utils/Enums.swift +95 -0
  262. package/ios/Source/utils/G1Text.swift +2067 -0
  263. package/ios/Source/utils/JSCExperiment.swift +241 -0
  264. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  265. package/ios/Source/utils/MessageChunker.swift +164 -0
  266. package/ios/Source/utils/Models.swift +135 -0
  267. package/ios/Source/utils/PhoneAudioMonitor.swift +226 -0
  268. package/ios/Source/utils/TarBz2Extractor.swift +206 -0
  269. package/package.json +78 -0
  270. package/plugin/build/index.d.ts +6 -0
  271. package/plugin/build/index.js +12 -0
  272. package/plugin/build/withAndroid.d.ts +4 -0
  273. package/plugin/build/withAndroid.js +80 -0
  274. package/plugin/build/withIos.d.ts +4 -0
  275. package/plugin/build/withIos.js +64 -0
  276. package/src/BluetoothSdk.types.ts +581 -0
  277. package/src/BluetoothSdkModule.ts +259 -0
  278. package/src/index.ts +4 -0
@@ -0,0 +1,1478 @@
1
+ //
2
+ // DeviceManager.swift
3
+ // MentraOS_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
+
14
+ struct ViewState {
15
+ var topText: String
16
+ var bottomText: String
17
+ var title: String
18
+ var layoutType: String
19
+ var text: String
20
+ var data: String?
21
+ var animationData: [String: Any]?
22
+ }
23
+
24
+ @MainActor
25
+ @objc(DeviceManager) class DeviceManager: NSObject {
26
+ static let shared = DeviceManager()
27
+
28
+ @objc static func getInstance() -> DeviceManager {
29
+ return DeviceManager.shared
30
+ }
31
+
32
+ // MARK: - Unique (iOS)
33
+
34
+ private var cancellables = Set<AnyCancellable>()
35
+ var sendStateWorkItem: DispatchWorkItem?
36
+ let sendStateQueue = DispatchQueue(label: "sendStateQueue", qos: .userInitiated)
37
+
38
+ /**
39
+ * Setup Bluetooth audio pairing after BLE connection is established
40
+ * Attempts to automatically activate Mentra Live as the system audio device
41
+ * If not paired yet, prompts user to pair in Settings
42
+ */
43
+ func setupAudioPairing(deviceName _: String) {
44
+ // Don't configure audio session - PhoneMic.swift handles that
45
+ // Just check if audio session supports Bluetooth (informational only)
46
+ if !AudioSessionMonitor.isAudioSessionConfigured() {
47
+ Bridge.log(
48
+ "Audio: Audio session not configured for Bluetooth yet - mic system will configure it when recording"
49
+ )
50
+ }
51
+
52
+ // Extract device ID pattern to match the specific device
53
+ // BLE name: "MENTRA_LIVE_BLE_ABC123"
54
+ // BT Classic could be: "MENTRA_LIVE_BLE_ABC123" or "MENTRA_LIVE_BT_ABC123"
55
+ // We need to match on the unique device ID part (e.g., "ABC123")
56
+ let audioDevicePattern = getAudioDevicePattern()
57
+
58
+ if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
59
+ Bridge.log("Audio: Device pattern is empty or simulated, returning")
60
+ return
61
+ }
62
+
63
+ // Check if device is paired (don't activate to preserve A2DP music playback)
64
+ let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
65
+
66
+ if isPaired {
67
+ // Device is paired! Don't activate it - let PhoneMic.swift activate when recording starts
68
+ Bridge.log("Audio: ✅ Mentra Live is paired (preserving A2DP for music)")
69
+ glassesBtcConnected = true
70
+ } else {
71
+ glassesBtcConnected = false
72
+ // Not found in availableInputs - not paired yet
73
+
74
+ // Start monitoring for when user pairs manually
75
+ AudioSessionMonitor.startMonitoring(devicePattern: audioDevicePattern) {
76
+ [weak self] (connected: Bool, _: String?) in
77
+ guard let self = self else { return }
78
+
79
+ if connected {
80
+ Bridge.log("Audio: ✅ Device paired and connected")
81
+ // Don't activate - let PhoneMic.swift handle that when recording starts
82
+ self.glassesBtcConnected = true
83
+ } else {
84
+ Bridge.log("Audio: Device disconnected")
85
+ self.glassesBtcConnected = false
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // MARK: - End Unique
92
+
93
+ // MARK: - Properties
94
+
95
+ var coreToken: String = ""
96
+ var coreTokenOwner: String = ""
97
+ var userEmail: String = ""
98
+ var sgc: SGCManager?
99
+ var controller: ControllerManager?
100
+
101
+ // state
102
+ // var lastStatusObj: [String: Any] = [:]
103
+
104
+ /// settings:
105
+ private var defaultWearable: String {
106
+ get { DeviceStore.shared.get("bluetooth", "default_wearable") as? String ?? "" }
107
+ set { DeviceStore.shared.apply("bluetooth", "default_wearable", newValue) }
108
+ }
109
+
110
+ private var pendingWearable: String {
111
+ get { DeviceStore.shared.get("bluetooth", "pending_wearable") as? String ?? "" }
112
+ set { DeviceStore.shared.apply("bluetooth", "pending_wearable", newValue) }
113
+ }
114
+
115
+ private var deviceName: String {
116
+ get { DeviceStore.shared.get("bluetooth", "device_name") as? String ?? "" }
117
+ set { DeviceStore.shared.apply("bluetooth", "device_name", newValue) }
118
+ }
119
+
120
+ private var deviceAddress: String {
121
+ get { DeviceStore.shared.get("bluetooth", "device_address") as? String ?? "" }
122
+ set { DeviceStore.shared.apply("bluetooth", "device_address", newValue) }
123
+ }
124
+
125
+ private var defaultController: String {
126
+ get { DeviceStore.shared.get("bluetooth", "default_controller") as? String ?? "" }
127
+ set { DeviceStore.shared.apply("bluetooth", "default_controller", newValue) }
128
+ }
129
+
130
+ private var pendingController: String {
131
+ get { DeviceStore.shared.get("bluetooth", "pending_controller") as? String ?? "" }
132
+ set { DeviceStore.shared.apply("bluetooth", "pending_controller", newValue) }
133
+ }
134
+
135
+ private var controllerDeviceName: String {
136
+ get { DeviceStore.shared.get("bluetooth", "controller_device_name") as? String ?? "" }
137
+ set { DeviceStore.shared.apply("bluetooth", "controller_device_name", newValue) }
138
+ }
139
+
140
+ private var screenDisabled: Bool {
141
+ get { DeviceStore.shared.get("bluetooth", "screen_disabled") as? Bool ?? false }
142
+ set { DeviceStore.shared.apply("bluetooth", "screen_disabled", newValue) }
143
+ }
144
+
145
+ private var preferredMic: String {
146
+ get { DeviceStore.shared.get("bluetooth", "preferred_mic") as? String ?? "auto" }
147
+ set { DeviceStore.shared.apply("bluetooth", "preferred_mic", newValue) }
148
+ }
149
+
150
+ private var autoBrightness: Bool {
151
+ get { DeviceStore.shared.get("bluetooth", "auto_brightness") as? Bool ?? true }
152
+ set { DeviceStore.shared.apply("bluetooth", "auto_brightness", newValue) }
153
+ }
154
+
155
+ private var brightness: Int {
156
+ get { DeviceStore.shared.get("bluetooth", "brightness") as? Int ?? 50 }
157
+ set { DeviceStore.shared.apply("bluetooth", "brightness", newValue) }
158
+ }
159
+
160
+ private var headUpAngle: Int {
161
+ get { DeviceStore.shared.get("bluetooth", "head_up_angle") as? Int ?? 30 }
162
+ set { DeviceStore.shared.apply("bluetooth", "head_up_angle", newValue) }
163
+ }
164
+
165
+ private var sensingEnabled: Bool {
166
+ get { DeviceStore.shared.get("bluetooth", "sensing_enabled") as? Bool ?? true }
167
+ set { DeviceStore.shared.apply("bluetooth", "sensing_enabled", newValue) }
168
+ }
169
+
170
+ private var bypassVad: Bool {
171
+ get { DeviceStore.shared.get("bluetooth", "bypass_vad") as? Bool ?? true }
172
+ set { DeviceStore.shared.apply("bluetooth", "bypass_vad", newValue) }
173
+ }
174
+
175
+ private var offlineCaptionsRunning: Bool {
176
+ get { DeviceStore.shared.get("bluetooth", "offline_captions_running") as? Bool ?? false }
177
+ set { DeviceStore.shared.apply("bluetooth", "offline_captions_running", newValue) }
178
+ }
179
+
180
+ private var localSttFallbackActive: Bool {
181
+ get { DeviceStore.shared.get("bluetooth", "local_stt_fallback_active") as? Bool ?? false }
182
+ set { DeviceStore.shared.apply("bluetooth", "local_stt_fallback_active", newValue) }
183
+ }
184
+
185
+ private var shouldSendPcm: Bool {
186
+ get { DeviceStore.shared.get("bluetooth", "should_send_pcm") as? Bool ?? false }
187
+ set { DeviceStore.shared.apply("bluetooth", "should_send_pcm", newValue) }
188
+ }
189
+
190
+ private var shouldSendLc3: Bool {
191
+ get { DeviceStore.shared.get("bluetooth", "should_send_lc3") as? Bool ?? false }
192
+ set { DeviceStore.shared.apply("bluetooth", "should_send_lc3", newValue) }
193
+ }
194
+
195
+ private var shouldSendTranscript: Bool {
196
+ get { DeviceStore.shared.get("bluetooth", "should_send_transcript") as? Bool ?? false }
197
+ set { DeviceStore.shared.apply("bluetooth", "should_send_transcript", newValue) }
198
+ }
199
+
200
+ private var contextualDashboard: Bool {
201
+ get { DeviceStore.shared.get("bluetooth", "contextual_dashboard") as? Bool ?? true }
202
+ set { DeviceStore.shared.apply("bluetooth", "contextual_dashboard", newValue) }
203
+ }
204
+
205
+ // state:
206
+
207
+ private var searching: Bool {
208
+ get { DeviceStore.shared.get("bluetooth", "searching") as? Bool ?? false }
209
+ set { DeviceStore.shared.apply("bluetooth", "searching", newValue) }
210
+ }
211
+
212
+ private var searchingController: Bool {
213
+ get { DeviceStore.shared.get("bluetooth", "searchingController") as? Bool ?? false }
214
+ set { DeviceStore.shared.apply("bluetooth", "searchingController", newValue) }
215
+ }
216
+
217
+ private var glassesBtcConnected: Bool {
218
+ get { DeviceStore.shared.get("glasses", "btcConnected") as? Bool ?? false }
219
+ set { DeviceStore.shared.apply("glasses", "btcConnected", newValue) }
220
+ }
221
+
222
+ private var micRanking: [String] {
223
+ get {
224
+ DeviceStore.shared.get("bluetooth", "micRanking") as? [String] ?? MicMap.map["auto"]!
225
+ }
226
+ set { DeviceStore.shared.apply("bluetooth", "micRanking", newValue) }
227
+ }
228
+
229
+ private var shouldSendBootingMessage: Bool {
230
+ get { DeviceStore.shared.get("bluetooth", "shouldSendBootingMessage") as? Bool ?? true }
231
+ set { DeviceStore.shared.apply("bluetooth", "shouldSendBootingMessage", newValue) }
232
+ }
233
+
234
+ private var systemMicUnavailable: Bool {
235
+ get { DeviceStore.shared.get("bluetooth", "systemMicUnavailable") as? Bool ?? false }
236
+ set { DeviceStore.shared.apply("bluetooth", "systemMicUnavailable", newValue) }
237
+ }
238
+
239
+ private var headUp: Bool {
240
+ get { DeviceStore.shared.get("glasses", "headUp") as? Bool ?? false }
241
+ set { DeviceStore.shared.apply("glasses", "headUp", newValue) }
242
+ }
243
+
244
+ private var micEnabled: Bool {
245
+ get { DeviceStore.shared.get("bluetooth", "micEnabled") as? Bool ?? false }
246
+ set { DeviceStore.shared.apply("bluetooth", "micEnabled", newValue) }
247
+ }
248
+
249
+ private var currentMic: String {
250
+ get { DeviceStore.shared.get("bluetooth", "currentMic") as? String ?? "" }
251
+ set { DeviceStore.shared.apply("bluetooth", "currentMic", newValue) }
252
+ }
253
+
254
+ private var searchResults: [[String: Any]] {
255
+ get { DeviceStore.shared.get("bluetooth", "searchResults") as? [[String: Any]] ?? [] }
256
+ set { DeviceStore.shared.apply("bluetooth", "searchResults", newValue) }
257
+ }
258
+
259
+ private var wifiScanResults: [[String: Any]] {
260
+ get { DeviceStore.shared.get("bluetooth", "wifiScanResults") as? [[String: Any]] ?? [] }
261
+ set { DeviceStore.shared.apply("bluetooth", "wifiScanResults", newValue) }
262
+ }
263
+
264
+ private var lastLog: [String] {
265
+ get { DeviceStore.shared.get("bluetooth", "lastLog") as? [String] ?? [] }
266
+ set { DeviceStore.shared.apply("bluetooth", "lastLog", newValue) }
267
+ }
268
+
269
+ private var otherBtConnected: Bool {
270
+ get { DeviceStore.shared.get("bluetooth", "otherBtConnected") as? Bool ?? false }
271
+ set { DeviceStore.shared.apply("bluetooth", "otherBtConnected", newValue) }
272
+ }
273
+
274
+ /// LC3 Audio Encoding
275
+ /// Audio output format enum
276
+ enum AudioOutputFormat { case lc3, pcm }
277
+ /// Canonical LC3 config: 16kHz sample rate, 10ms frame duration
278
+ /// Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
279
+ /// Persistent LC3 converter for encoding/decoding
280
+ var lc3Converter: PcmConverter?
281
+ /// Audio output format - defaults to LC3 for bandwidth savings
282
+ private var audioOutputFormat: AudioOutputFormat = .lc3
283
+ /// Last time we received an LC3 frame from the glasses (used by the mic
284
+ /// inactivity watchdog).
285
+ private var lastLc3Event: Date?
286
+ private var micReinitTimer: Timer?
287
+
288
+ // VAD:
289
+ private var vad: SileroVADStrategy?
290
+ private var vadBuffer = [Data]()
291
+ private var isSpeaking = false
292
+
293
+ /// STT:
294
+ private var transcriber: SherpaOnnxTranscriber?
295
+
296
+ var viewStates: [ViewState] = [
297
+ ViewState(
298
+ topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: ""
299
+ ),
300
+ ViewState(
301
+ topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
302
+ text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$"
303
+ ),
304
+ ViewState(
305
+ topText: " ", bottomText: " ", title: " ", layoutType: "text_wall", text: "",
306
+ data: nil, animationData: nil
307
+ ),
308
+ ViewState(
309
+ topText: " ", bottomText: " ", title: " ", layoutType: "text_wall",
310
+ text: "$TIME12$ $DATE$ $GBATT$ $CONNECTION_STATUS$", data: nil,
311
+ animationData: nil
312
+ ),
313
+ ]
314
+
315
+ override init() {
316
+ Bridge.log("MAN: init()")
317
+ vad = SileroVADStrategy()
318
+ super.init()
319
+
320
+ // Start memory monitoring (logs every 30s to help detect leaks)
321
+ // MemoryMonitor.start()
322
+
323
+ // Initialize SherpaOnnx Transcriber
324
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
325
+ let window = windowScene.windows.first,
326
+ let rootViewController = window.rootViewController
327
+ {
328
+ transcriber = SherpaOnnxTranscriber(context: rootViewController)
329
+ } else {
330
+ Bridge.log("Failed to create SherpaOnnxTranscriber - no root view controller found")
331
+ }
332
+
333
+ // Initialize the transcriber
334
+ if let transcriber = transcriber {
335
+ transcriber.initialize()
336
+ Bridge.log("SherpaOnnxTranscriber fully initialized")
337
+ }
338
+
339
+ Task {
340
+ self.vad?.setup(
341
+ sampleRate: .rate_16k,
342
+ frameSize: .size_1024,
343
+ quality: .normal,
344
+ silenceTriggerDurationMs: 4000,
345
+ speechTriggerDurationMs: 50
346
+ )
347
+ }
348
+
349
+ // Initialize persistent LC3 converter for unified audio encoding
350
+ lc3Converter = PcmConverter()
351
+ Bridge.log("LC3 converter initialized for unified audio encoding")
352
+
353
+ DispatchQueue.main.async { [weak self] in
354
+ guard let self = self else { return }
355
+ self.micReinitTimer = Timer.scheduledTimer(
356
+ withTimeInterval: 10.0, repeats: true
357
+ ) { [weak self] _ in
358
+ self?.checkAndReinitGlassesMic()
359
+ }
360
+ }
361
+ }
362
+
363
+ // MARK: - AUX Voice Data Handling
364
+
365
+ private func checkSetVadStatus(speaking: Bool) {
366
+ if speaking != isSpeaking {
367
+ isSpeaking = speaking
368
+ Bridge.sendVadEvent(isSpeaking)
369
+ }
370
+ }
371
+
372
+ private func convertAndSendMicLc3(_ pcmData: Data) {
373
+ guard let lc3Converter = lc3Converter else {
374
+ Bridge.log("MAN: ERROR - LC3 converter not initialized but format is LC3")
375
+ return
376
+ }
377
+ let frameSize = DeviceStore.shared.get("bluetooth", "lc3_frame_size") as! Int
378
+ let lc3Data = lc3Converter.encode(pcmData, frameSize: frameSize) as Data
379
+ guard lc3Data.count > 0 else {
380
+ Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
381
+ return
382
+ }
383
+ Bridge.sendMicLc3(lc3Data)
384
+ }
385
+
386
+ private func handleSendingPcm(_ pcmData: Data) {
387
+ // Bridge.log("MAN: handleSendingPcm() shouldSendPcm: \(shouldSendPcm) shouldSendLc3: \(shouldSendLc3)")
388
+ if shouldSendPcm {
389
+ Bridge.sendMicPcm(pcmData)
390
+ }
391
+ if shouldSendLc3 {
392
+ convertAndSendMicLc3(pcmData)
393
+ }
394
+ }
395
+
396
+ private func emptyVadBuffer() {
397
+ // go through the buffer, popping from the first element in the array (FIFO):
398
+ while !vadBuffer.isEmpty {
399
+ let chunk = vadBuffer.removeFirst()
400
+ handleSendingPcm(chunk)
401
+ }
402
+ }
403
+
404
+ private func addToVadBuffer(_ chunk: Data) {
405
+ let MAX_BUFFER_SIZE = 20
406
+ vadBuffer.append(chunk)
407
+ while vadBuffer.count > MAX_BUFFER_SIZE {
408
+ // pop from the front of the array:
409
+ vadBuffer.removeFirst()
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Handle raw LC3 audio data from glasses.
415
+ * Decodes the glasses LC3 to PCM, then forwards to handlePcm for processing.
416
+ * This matches Android behavior - glasses forward raw LC3, DeviceManager handles encoding.
417
+ */
418
+ func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20) {
419
+ lastLc3Event = Date()
420
+ guard let lc3Converter = lc3Converter else {
421
+ Bridge.log("MAN: LC3 converter not initialized")
422
+ return
423
+ }
424
+
425
+ guard lc3Data.count > 2 else {
426
+ Bridge.log("MAN: Received invalid LC3 data size: \(lc3Data.count)")
427
+ return
428
+ }
429
+
430
+ let pcmData = lc3Converter.decode(lc3Data, frameSize: frameSize) as Data
431
+ guard pcmData.count > 0 else {
432
+ Bridge.log("MAN: Failed to decode glasses LC3 audio")
433
+ return
434
+ }
435
+ // Forward to handlePcm which handles VAD and encoding
436
+ handlePcm(pcmData)
437
+ }
438
+
439
+ func handlePcm(_ pcmData: Data) {
440
+ // handle incoming PCM data from the microphone manager and feed to the VAD:
441
+ if bypassVad {
442
+ handleSendingPcm(pcmData)
443
+
444
+ // Send PCM to local transcriber (always needs raw PCM)
445
+ if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
446
+ transcriber?.acceptAudio(pcm16le: pcmData)
447
+ }
448
+ return
449
+ }
450
+
451
+ // feed PCM to the VAD:
452
+ guard let vad = vad else {
453
+ Bridge.log("VAD not initialized")
454
+ return
455
+ }
456
+
457
+ // convert audioData to Int16 array for VAD:
458
+ let pcmDataArray = pcmData.withUnsafeBytes { pointer -> [Int16] in
459
+ Array(
460
+ UnsafeBufferPointer(
461
+ start: pointer.bindMemory(to: Int16.self).baseAddress,
462
+ count: pointer.count / MemoryLayout<Int16>.stride
463
+ )
464
+ )
465
+ }
466
+
467
+ vad.checkVAD(pcm: pcmDataArray) { [weak self] state in
468
+ guard let self = self else { return }
469
+ Bridge.log("VAD State: \(state)")
470
+ }
471
+
472
+ let vadState = vad.currentState()
473
+ if vadState == .speeching {
474
+ checkSetVadStatus(speaking: true)
475
+ // first send out whatever's in the vadBuffer (if there is anything):
476
+ emptyVadBuffer()
477
+
478
+ handleSendingPcm(pcmData)
479
+
480
+ // Send PCM to local transcriber (always needs raw PCM)
481
+ if shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive {
482
+ transcriber?.acceptAudio(pcm16le: pcmData)
483
+ }
484
+ } else {
485
+ checkSetVadStatus(speaking: false)
486
+ // add to the vadBuffer (stores PCM for potential later sending):
487
+ addToVadBuffer(pcmData)
488
+ }
489
+ }
490
+
491
+ func updateMicState() {
492
+ // go through the micRanking and find the first mic that is available:
493
+ var micUsed = ""
494
+
495
+ // allow the sgc to make changes to the micRanking:
496
+ micRanking = sgc?.sortMicRanking(list: micRanking) ?? micRanking
497
+ // Bridge.log("MAN: updateMicState() micRanking: \(micRanking)")
498
+
499
+ var phoneMicUnavailable = systemMicUnavailable
500
+
501
+ let appState = UIApplication.shared.applicationState
502
+ if appState == .background {
503
+ // Bridge.log("App is in background - onboard mic unavailable to start!")
504
+ phoneMicUnavailable = true
505
+ }
506
+
507
+ if micEnabled {
508
+ for micMode in micRanking {
509
+ if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BT_CLASSIC
510
+ || micMode == MicTypes.BT
511
+ {
512
+ if PhoneMic.shared.isRecordingWithMode(micMode) {
513
+ micUsed = micMode
514
+ break
515
+ }
516
+
517
+ if phoneMicUnavailable {
518
+ continue
519
+ }
520
+
521
+ // if the phone mic is not recording, start recording:
522
+ let success = PhoneMic.shared.startMode(micMode)
523
+ Bridge.log("MAN: starting mic mode: \(micMode) -> \(success)")
524
+ if success {
525
+ micUsed = micMode
526
+ break
527
+ }
528
+ }
529
+
530
+ if micMode == MicTypes.GLASSES_CUSTOM {
531
+ // Bridge.log(
532
+ // "MAN: glasses custom mic found - hasMic: \(sgc?.hasMic ?? false), micEnabled: \(sgc?.micEnabled ?? false)"
533
+ // )
534
+ // if the glasses has a mic that's already on, mark it as used and break:
535
+ if sgc?.hasMic ?? false {
536
+ // enable the mic if it's not already on:
537
+ if sgc?.micEnabled == false {
538
+ sgc?.setMicEnabled(true)
539
+ micUsed = micMode
540
+ break
541
+ } else {
542
+ // the mic is already on, mark it as used and break:
543
+ micUsed = micMode
544
+ break
545
+ }
546
+ }
547
+ // if the glasses doesn't have a mic, continue to the next mic:
548
+ continue
549
+ }
550
+ }
551
+ }
552
+
553
+ currentMic = micUsed
554
+
555
+ // log if no mic was found:
556
+ if micUsed == "" && micEnabled {
557
+ Bridge.log("MAN: No available mic found!")
558
+ return
559
+ }
560
+
561
+ // go through and disable all mics after the first used one:
562
+ var allMics = micRanking
563
+ // add any missing mics to the list:
564
+ for micMode in MicMap.map["auto"]! {
565
+ if !allMics.contains(micMode) {
566
+ allMics.append(micMode)
567
+ }
568
+ }
569
+
570
+ for micMode in allMics {
571
+ if micMode == micUsed {
572
+ continue
573
+ }
574
+
575
+ if micMode == MicTypes.PHONE_INTERNAL || micMode == MicTypes.BT_CLASSIC
576
+ || micMode == MicTypes.BT
577
+ {
578
+ PhoneMic.shared.stopMode(micMode)
579
+ }
580
+
581
+ if micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true {
582
+ sgc?.setMicEnabled(false)
583
+ }
584
+ }
585
+ }
586
+
587
+ func setOnboardMicEnabled(_ isEnabled: Bool) {
588
+ Task {
589
+ if isEnabled {
590
+ // Just check permissions - we no longer request them directly from Swift
591
+ // Permissions should already be granted via React Native UI flow
592
+ if !(PhoneMic.shared.checkPermissions()) {
593
+ Bridge.log("Microphone permissions not granted. Cannot enable microphone.")
594
+ return
595
+ }
596
+
597
+ let success = PhoneMic.shared.startRecording()
598
+ if !success {
599
+ // fallback to glasses mic if possible:
600
+ if sgc?.hasMic ?? false {
601
+ await sgc?.setMicEnabled(true)
602
+ }
603
+ }
604
+ } else {
605
+ PhoneMic.shared.stopRecording()
606
+ }
607
+ }
608
+ }
609
+
610
+ // MARK: - Glasses Commands
611
+
612
+ private func playStartupSequence() {
613
+ Bridge.log("MAN: playStartupSequence()")
614
+ // Arrow frames for the animation
615
+ let arrowFrames = ["↑", "↗", "↑", "↖"]
616
+
617
+ let delay = 0.25 // Frame delay in seconds
618
+ let totalCycles = 2 // Number of animation cycles
619
+
620
+ // Variables to track animation state
621
+ var frameIndex = 0
622
+ var cycles = 0
623
+
624
+ // Create a dispatch queue for the animation
625
+ let animationQueue = DispatchQueue.global(qos: .userInteractive)
626
+
627
+ /// Function to display the current animation frame
628
+ func displayFrame() {
629
+ // Check if we've completed all cycles
630
+ if cycles >= totalCycles {
631
+ // End animation with final message
632
+ sgc?.sendTextWall(" /// MentraOS Connected \\\\\\")
633
+ animationQueue.asyncAfter(deadline: .now() + 1.0) {
634
+ self.sgc?.clearDisplay()
635
+ }
636
+ return
637
+ }
638
+
639
+ // Display current animation frame
640
+ let frameText =
641
+ " \(arrowFrames[frameIndex]) MentraOS Booting \(arrowFrames[frameIndex])"
642
+ sgc?.sendTextWall(frameText)
643
+
644
+ // Move to next frame
645
+ frameIndex = (frameIndex + 1) % arrowFrames.count
646
+
647
+ // Count completed cycles
648
+ if frameIndex == 0 {
649
+ cycles += 1
650
+ }
651
+
652
+ // Schedule next frame
653
+ animationQueue.asyncAfter(deadline: .now() + delay) {
654
+ displayFrame()
655
+ }
656
+ }
657
+
658
+ // Start the animation after a short initial delay
659
+ animationQueue.asyncAfter(deadline: .now() + 0.35) {
660
+ displayFrame()
661
+ }
662
+ }
663
+
664
+ // MARK: - Auxiliary Commands
665
+
666
+ func initSGC(_ wearable: String) {
667
+ Bridge.log("Initializing manager for wearable: \(wearable)")
668
+ if sgc != nil && sgc?.type != wearable {
669
+ Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
670
+ sgc?.cleanup()
671
+ sgc = nil
672
+ }
673
+
674
+ if sgc != nil {
675
+ Bridge.log("MAN: SGC already initialized")
676
+ return
677
+ }
678
+ if wearable.contains(DeviceTypes.SIMULATED) {
679
+ sgc = Simulated()
680
+ } else if wearable.contains(DeviceTypes.G1) {
681
+ sgc = G1()
682
+ } else if wearable.contains(DeviceTypes.G2) {
683
+ sgc = G2()
684
+ } else if wearable.contains(DeviceTypes.LIVE) {
685
+ sgc = MentraLive()
686
+ } else if wearable.contains(DeviceTypes.MACH1) {
687
+ sgc = Mach1()
688
+ } else if wearable.contains(DeviceTypes.Z100) {
689
+ sgc = Mach1() // Z100 uses same hardware/SDK as Mach1
690
+ sgc?.type = DeviceTypes.Z100 // Override type to Z100
691
+ } else if wearable.contains(DeviceTypes.FRAME) {
692
+ // sgc = FrameManager()
693
+ }
694
+ // update device model:
695
+ DeviceStore.shared.apply("glasses", "deviceModel", sgc?.type ?? "")
696
+ }
697
+
698
+ func initController(_ controllerModel: String) {
699
+ Bridge.log("MAN: Initializing controller: \(controllerModel)")
700
+ if controller != nil && controller?.type != controllerModel {
701
+ Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
702
+ controller?.cleanup()
703
+ controller = nil
704
+ }
705
+
706
+ if controller != nil {
707
+ Bridge.log("MAN: Controller already initialized")
708
+ return
709
+ }
710
+
711
+ if controllerModel == ControllerTypes.R1 {
712
+ controller = R1()
713
+ }
714
+ }
715
+
716
+ func sendCurrentState() {
717
+ if screenDisabled {
718
+ return
719
+ }
720
+
721
+ Task {
722
+ var currentViewState: ViewState!
723
+ if headUp {
724
+ currentViewState = self.viewStates[1]
725
+ } else {
726
+ currentViewState = self.viewStates[0]
727
+ }
728
+ if headUp && !self.contextualDashboard {
729
+ currentViewState = self.viewStates[0]
730
+ }
731
+
732
+ if sgc?.type.contains(DeviceTypes.SIMULATED) ?? true {
733
+ // dont send the event to glasses that aren't there:
734
+ return
735
+ }
736
+
737
+ var fullyBooted = sgc?.fullyBooted ?? false
738
+ if !fullyBooted {
739
+ return
740
+ }
741
+
742
+ // cancel any pending clear display work item:
743
+ sendStateWorkItem?.cancel()
744
+
745
+ let layoutType = currentViewState.layoutType
746
+ switch layoutType {
747
+ case "text_wall":
748
+ let text = currentViewState.text
749
+ sgc?.sendTextWall(text)
750
+ case "double_text_wall":
751
+ let topText = currentViewState.topText
752
+ let bottomText = currentViewState.bottomText
753
+ sgc?.sendDoubleTextWall(topText, bottomText)
754
+ case "reference_card":
755
+ sgc?.sendTextWall(currentViewState.title + "\n\n" + currentViewState.text)
756
+ case "bitmap_view":
757
+ Bridge.log("MAN: Processing bitmap_view layout")
758
+ guard let data = currentViewState.data else {
759
+ Bridge.log("MAN: ERROR: bitmap_view missing data field")
760
+ return
761
+ }
762
+ Bridge.log("MAN: Processing bitmap_view with base64 data, length: \(data.count)")
763
+ await sgc?.displayBitmap(base64ImageData: data)
764
+ case "clear_view":
765
+ sgc?.clearDisplay()
766
+ default:
767
+ Bridge.log("UNHANDLED LAYOUT_TYPE \(layoutType)")
768
+ }
769
+ }
770
+ }
771
+
772
+ func parsePlaceholders(_ text: String) -> String {
773
+ let dateFormatter = DateFormatter()
774
+ dateFormatter.dateFormat = "M/dd, h:mm"
775
+ let formattedDate = dateFormatter.string(from: Date())
776
+
777
+ // 12-hour time format (with leading zeros for hours)
778
+ let time12Format = DateFormatter()
779
+ time12Format.dateFormat = "hh:mm"
780
+ let time12 = time12Format.string(from: Date())
781
+
782
+ // 24-hour time format
783
+ let time24Format = DateFormatter()
784
+ time24Format.dateFormat = "HH:mm"
785
+ let time24 = time24Format.string(from: Date())
786
+
787
+ // Current date with format MM/dd
788
+ let dateFormat = DateFormatter()
789
+ dateFormat.dateFormat = "MM/dd"
790
+ let currentDate = dateFormat.string(from: Date())
791
+
792
+ var placeholders: [String: String] = [:]
793
+ placeholders["$no_datetime$"] = formattedDate
794
+ placeholders["$DATE$"] = currentDate
795
+ placeholders["$TIME12$"] = time12
796
+ placeholders["$TIME24$"] = time24
797
+
798
+ if (sgc?.batteryLevel ?? -1) == -1 {
799
+ placeholders["$GBATT$"] = ""
800
+ } else {
801
+ placeholders["$GBATT$"] = "\(sgc!.batteryLevel)%"
802
+ }
803
+
804
+ // placeholders["$CONNECTION_STATUS$"] =
805
+ // WebSocketManager.shared.isConnected() ? "Connected" : "Disconnected"
806
+ // TODO: config:
807
+ placeholders["$CONNECTION_STATUS$"] = "Connected"
808
+
809
+ var result = text
810
+ for (key, value) in placeholders {
811
+ result = result.replacingOccurrences(of: key, with: value)
812
+ }
813
+
814
+ return result
815
+ }
816
+
817
+ private func checkAndReinitGlassesMic() {
818
+ // if the glasses mic is marked as enabled (and the glasses are connected), but our last known lc3 event is from > 5 seconds ago, reinitialize the mic:
819
+ let glassesMicEnabled = DeviceStore.shared.get("glasses", "micEnabled") as? Bool ?? false
820
+ let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
821
+ if !glassesMicEnabled || !glassesConnected {
822
+ return
823
+ }
824
+
825
+ let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
826
+ if timeSinceLastLc3Event > 5 {
827
+ Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
828
+ sgc?.setMicEnabled(true)
829
+ }
830
+ }
831
+
832
+ func getAudioDevicePattern() -> String {
833
+ let audioDevicePattern: String
834
+ if let idRange = deviceName.range(of: "_BLE_", options: .caseInsensitive) {
835
+ // Extract the ID after "_BLE_" (e.g., "ABC123")
836
+ audioDevicePattern = String(deviceName[idRange.upperBound...])
837
+ } else if let idRange = deviceName.range(of: "_BT_", options: .caseInsensitive) {
838
+ // Extract the ID after "_BT_"
839
+ audioDevicePattern = String(deviceName[idRange.upperBound...])
840
+ } else {
841
+ // Fallback: use the full device name
842
+ audioDevicePattern = deviceName
843
+ }
844
+ return audioDevicePattern
845
+ }
846
+
847
+ func checkCurrentAudioDevice() {
848
+ let audioDevicePattern = getAudioDevicePattern()
849
+ Bridge.log("MAN: checkCurrentAudioDevice: audioDevicePattern: \(audioDevicePattern)")
850
+
851
+ if audioDevicePattern.isEmpty || audioDevicePattern == DeviceTypes.SIMULATED {
852
+ glassesBtcConnected = false
853
+ Bridge.log("MAN: Audio device pattern is empty or simulated, returning")
854
+ return
855
+ }
856
+
857
+ // check if the device disconnected:
858
+ let isConnected = AudioSessionMonitor.isAudioDeviceConnected(
859
+ devicePattern: audioDevicePattern
860
+ )
861
+
862
+ if !isConnected {
863
+ Bridge.log("MAN: Device '\(deviceName)' disconnected")
864
+ glassesBtcConnected = false
865
+
866
+ let isOtherDeviceConnected = AudioSessionMonitor.isOtherAudioDeviceConnected(
867
+ devicePattern: audioDevicePattern
868
+ )
869
+ if isOtherDeviceConnected {
870
+ Bridge.log("MAN: Other device connected, returning")
871
+ otherBtConnected = true
872
+ }
873
+ return
874
+ }
875
+
876
+ let isPaired = AudioSessionMonitor.isDevicePaired(devicePattern: audioDevicePattern)
877
+ if isPaired {
878
+ let session = AVAudioSession.sharedInstance()
879
+ let deviceName = session.availableInputs?.first(where: {
880
+ $0.portName.localizedCaseInsensitiveContains(audioDevicePattern)
881
+ })?.portName
882
+ Bridge.log("MAN: ✅ Successfully detected newly paired device '\(deviceName)'")
883
+ glassesBtcConnected = true
884
+ } else {
885
+ glassesBtcConnected = false
886
+ }
887
+ }
888
+
889
+ func onRouteChange(
890
+ reason: AVAudioSession.RouteChangeReason, availableInputs: [AVAudioSessionPortDescription]
891
+ ) {
892
+ Bridge.log("MAN: onRouteChange: reason: \(reason)")
893
+ Bridge.log("MAN: onRouteChange: inputs: \(availableInputs)")
894
+
895
+ // check if our deviceName is connected:
896
+ // (return if deviceName is empty):
897
+ if deviceName.isEmpty {
898
+ Bridge.log("MAN: Device name is empty, returning")
899
+ return
900
+ }
901
+
902
+ // Add small delay to let iOS populate availableInputs
903
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
904
+ guard let self = self else { return }
905
+ checkCurrentAudioDevice()
906
+ }
907
+
908
+ updateMicState()
909
+ }
910
+
911
+ func onInterruption(began: Bool) {
912
+ Bridge.log("MAN: Interruption: \(began)")
913
+ systemMicUnavailable = began
914
+ updateMicState()
915
+ }
916
+
917
+ func restartTranscriber() {
918
+ Bridge.log("MAN: Restarting SherpaOnnxTranscriber via command")
919
+ transcriber?.restart()
920
+ }
921
+
922
+ // MARK: - connection state management
923
+
924
+ func handleDeviceReady() {
925
+ guard let sgc else {
926
+ Bridge.log("MAN: SGC is nil, returning")
927
+ return
928
+ }
929
+ Bridge.log("MAN: handleDeviceReady(): \(sgc.type)")
930
+
931
+ pendingWearable = ""
932
+ defaultWearable = sgc.type
933
+ searching = false
934
+
935
+ // Show welcome message on first connect for all display glasses
936
+ if shouldSendBootingMessage {
937
+ Task {
938
+ sgc.sendTextWall("// MentraOS Connected")
939
+ try? await Task.sleep(nanoseconds: 3_000_000_000) // 1 second
940
+ sgc.clearDisplay()
941
+ }
942
+ shouldSendBootingMessage = false
943
+ }
944
+
945
+ // Call device-specific setup handlers
946
+ if defaultWearable.contains(DeviceTypes.G1) {
947
+ handleG1Ready()
948
+ } else if defaultWearable.contains(DeviceTypes.G2) {
949
+ // handleG2Ready()
950
+ } else if defaultWearable.contains(DeviceTypes.MACH1) {
951
+ handleMach1Ready()
952
+ } else if defaultWearable.contains(DeviceTypes.Z100) {
953
+ handleMach1Ready() // Z100 uses same initialization as Mach1
954
+ }
955
+
956
+ // check current audio device:
957
+ checkCurrentAudioDevice()
958
+
959
+ // save the default_wearable now that we're connected:
960
+ Bridge.saveSetting("default_wearable", defaultWearable)
961
+ Bridge.saveSetting("device_name", deviceName)
962
+ Bridge.saveSetting("device_address", deviceAddress)
963
+
964
+ // Re-apply display height after reconnection
965
+ let h = DeviceStore.shared.get("bluetooth", "dashboard_height") as? Int ?? 4
966
+ let d = NexDashboardDisplayWire.clampDepthFromStore(
967
+ DeviceStore.shared.get("bluetooth", "dashboard_depth")
968
+ )
969
+ sgc.setDashboardPosition(h, d)
970
+ }
971
+
972
+ func handleControllerReady() {
973
+ guard let controller else {
974
+ Bridge.log("MAN: Controller is nil, returning")
975
+ return
976
+ }
977
+ Bridge.log("MAN: handleControllerReady(): \(controller.type)")
978
+
979
+ pendingController = ""
980
+ defaultController = controller.type
981
+ searching = false
982
+
983
+ // save the default_controller now that we're connected:
984
+ Bridge.saveSetting("default_controller", defaultController)
985
+ Bridge.saveSetting("controller_device_name", controllerDeviceName)
986
+ }
987
+
988
+ func handleControllerDisconnected() {
989
+ Bridge.log("MAN: Controller disconnected")
990
+ }
991
+
992
+ private func handleG1Ready() {
993
+ // G1-specific setup and configuration
994
+ Task {
995
+ // give the glasses some extra time to finish booting:
996
+ try? await Task.sleep(nanoseconds: 1_000_000_000)
997
+ await sgc?.setSilentMode(false) // turn off silent mode
998
+ await sgc?.getBatteryStatus()
999
+
1000
+ // send loaded settings to glasses:
1001
+ try? await Task.sleep(nanoseconds: 400_000_000)
1002
+ sgc?.setHeadUpAngle(headUpAngle)
1003
+ try? await Task.sleep(nanoseconds: 400_000_000)
1004
+ sgc?.setBrightness(brightness, autoMode: autoBrightness)
1005
+ try? await Task.sleep(nanoseconds: 400_000_000)
1006
+ // self.g1Manager?.RN_setDashboardPosition(self.dashboardHeight, self.dashboardDepth)
1007
+ // try? await Task.sleep(nanoseconds: 400_000_000)
1008
+ // playStartupSequence()
1009
+ }
1010
+ }
1011
+
1012
+ private func handleMach1Ready() {}
1013
+
1014
+ func handleDeviceDisconnected() {
1015
+ Bridge.log("MAN: Device disconnected")
1016
+ // setMicState(shouldSendPcData, shouldSendTranscript, false)
1017
+ // shouldSendBootingMessage = true // Reset for next first connect
1018
+ }
1019
+
1020
+ // MARK: - Network Command handlers
1021
+
1022
+ func displayText(_ params: [String: Any]) {
1023
+ guard let text = params["text"] as? String else {
1024
+ Bridge.log("MAN: display_text missing text parameter")
1025
+ return
1026
+ }
1027
+
1028
+ Bridge.log("MAN: Displaying text: \(text)")
1029
+ sgc?.sendTextWall(text)
1030
+ }
1031
+
1032
+ func displayEvent(_ event: [String: Any]) {
1033
+ guard let view = event["view"] as? String else {
1034
+ Bridge.log("MAN: invalid view")
1035
+ return
1036
+ }
1037
+ let isDashboard = view == "dashboard"
1038
+
1039
+ var stateIndex = 0
1040
+ if isDashboard {
1041
+ stateIndex = 1
1042
+ } else {
1043
+ stateIndex = 0
1044
+ }
1045
+
1046
+ let layout = event["layout"] as! [String: Any]
1047
+ let layoutType = layout["layoutType"] as! String
1048
+ var text = layout["text"] as? String ?? " "
1049
+ var topText = layout["topText"] as? String ?? " "
1050
+ var bottomText = layout["bottomText"] as? String ?? " "
1051
+ var title = layout["title"] as? String ?? " "
1052
+ var data = layout["data"] as? String ?? ""
1053
+
1054
+ text = parsePlaceholders(text)
1055
+ topText = parsePlaceholders(topText)
1056
+ bottomText = parsePlaceholders(bottomText)
1057
+ title = parsePlaceholders(title)
1058
+
1059
+ var newViewState = ViewState(
1060
+ topText: topText, bottomText: bottomText, title: title, layoutType: layoutType,
1061
+ text: text, data: data, animationData: nil
1062
+ )
1063
+
1064
+ if layoutType == "bitmap_animation" {
1065
+ if let frames = layout["frames"] as? [String],
1066
+ let interval = layout["interval"] as? Double
1067
+ {
1068
+ let animationData: [String: Any] = [
1069
+ "frames": frames,
1070
+ "interval": interval,
1071
+ "repeat": layout["repeat"] as? Bool ?? true,
1072
+ ]
1073
+ newViewState.animationData = animationData
1074
+ Bridge.log(
1075
+ "MAN: Parsed bitmap_animation with \(frames.count) frames, interval: \(interval)ms"
1076
+ )
1077
+ } else {
1078
+ Bridge.log("MAN: ERROR: bitmap_animation missing frames or interval")
1079
+ }
1080
+ }
1081
+
1082
+ let cS = viewStates[stateIndex]
1083
+ let nS = newViewState
1084
+ let currentState =
1085
+ cS.layoutType + cS.text + cS.topText + cS.bottomText + cS.title + (cS.data ?? "")
1086
+ let newState =
1087
+ nS.layoutType + nS.text + nS.topText + nS.bottomText + nS.title + (nS.data ?? "")
1088
+
1089
+ if currentState == newState {
1090
+ // Core.log("MAN: View state is the same, skipping update")
1091
+ return
1092
+ }
1093
+
1094
+ // Bridge.log("MAN: Updating view state \(stateIndex) with \(layoutType) \(text) \(topText) \(bottomText)")
1095
+
1096
+ viewStates[stateIndex] = newViewState
1097
+
1098
+ let hUp = headUp && contextualDashboard
1099
+ // send the state we just received if the user is currently in that state:
1100
+ if stateIndex == 0 && !hUp {
1101
+ sendCurrentState()
1102
+ } else if stateIndex == 1 && hUp {
1103
+ sendCurrentState()
1104
+ }
1105
+ }
1106
+
1107
+ func showDashboard() {
1108
+ sgc?.showDashboard()
1109
+ }
1110
+
1111
+ func ping() {
1112
+ sgc?.ping()
1113
+ }
1114
+
1115
+ func dbg1() {
1116
+ sgc?.disconnectController()
1117
+ connectDefaultController()
1118
+ }
1119
+
1120
+ func dbg2() {}
1121
+
1122
+ func startStream(_ message: [String: Any]) {
1123
+ Bridge.log("MAN: startStream: \(message)")
1124
+ sgc?.startStream(message)
1125
+ }
1126
+
1127
+ func stopStream() {
1128
+ Bridge.log("MAN: stopStream")
1129
+ sgc?.stopStream()
1130
+ }
1131
+
1132
+ func keepStreamAlive(_ message: [String: Any]) {
1133
+ Bridge.log("MAN: sendStreamKeepAlive: \(message)")
1134
+ sgc?.sendStreamKeepAlive(message)
1135
+ }
1136
+
1137
+ func requestWifiScan() {
1138
+ Bridge.log("MAN: Requesting wifi scan")
1139
+ DeviceStore.shared.apply("bluetooth", "wifiScanResults", [])
1140
+ sgc?.requestWifiScan()
1141
+ }
1142
+
1143
+ func sendIncidentId(_ incidentId: String, apiBaseUrl: String? = nil) {
1144
+ Bridge.log("MAN: Sending incidentId to glasses for log upload: \(incidentId)")
1145
+ sgc?.sendIncidentId(incidentId, apiBaseUrl: apiBaseUrl)
1146
+ }
1147
+
1148
+ func sendWifiCredentials(_ ssid: String, _ password: String) {
1149
+ Bridge.log("MAN: Sending wifi credentials: \(ssid) \(password)")
1150
+ sgc?.sendWifiCredentials(ssid, password)
1151
+ }
1152
+
1153
+ func forgetWifiNetwork(_ ssid: String) {
1154
+ Bridge.log("MAN: Forgetting wifi network: \(ssid)")
1155
+ sgc?.forgetWifiNetwork(ssid)
1156
+ }
1157
+
1158
+ func setHotspotState(_ enabled: Bool) {
1159
+ Bridge.log("MAN: 🔥 Setting glasses hotspot state: \(enabled)")
1160
+ sgc?.sendHotspotState(enabled)
1161
+ }
1162
+
1163
+ func queryGalleryStatus() {
1164
+ Bridge.log("MAN: 📸 Querying gallery status from glasses")
1165
+ sgc?.queryGalleryStatus()
1166
+ }
1167
+
1168
+ /// Send OTA start command to glasses.
1169
+ /// Called when user approves an update (onboarding or background mode).
1170
+ /// Triggers glasses to begin download and installation.
1171
+ func sendOtaStart() {
1172
+ Bridge.log("MAN: 📱 Sending OTA start command to glasses")
1173
+ sgc?.sendOtaStart()
1174
+ }
1175
+
1176
+ func sendOtaQueryStatus() {
1177
+ Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
1178
+ (sgc as? MentraLive)?.sendOtaQueryStatus()
1179
+ }
1180
+
1181
+ /// Request version info from glasses.
1182
+ /// Glasses will respond with version_info message containing build number, firmware version, etc.
1183
+ func requestVersionInfo() {
1184
+ Bridge.log("MAN: 📱 Requesting version info from glasses")
1185
+ sgc?.requestVersionInfo()
1186
+ }
1187
+
1188
+ /// Send shutdown command to glasses.
1189
+ /// This will initiate a graceful shutdown of the device.
1190
+ func sendShutdown() {
1191
+ Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
1192
+ sgc?.sendShutdown()
1193
+ }
1194
+
1195
+ /// Send reboot command to glasses.
1196
+ /// This will initiate a reboot of the device.
1197
+ func sendReboot() {
1198
+ Bridge.log("MAN: 🔄 Sending reboot command to glasses")
1199
+ sgc?.sendReboot()
1200
+ }
1201
+
1202
+ func startVideoRecording(_ requestId: String, _ save: Bool, _ flash: Bool, _ sound: Bool) {
1203
+ Bridge.log(
1204
+ "MAN: onStartVideoRecording: requestId=\(requestId), save=\(save), flash=\(flash), sound=\(sound)"
1205
+ )
1206
+ sgc?.startVideoRecording(requestId: requestId, save: save, flash: flash, sound: sound)
1207
+ }
1208
+
1209
+ func stopVideoRecording(_ requestId: String) {
1210
+ Bridge.log("MAN: onStopVideoRecording: requestId=\(requestId)")
1211
+ sgc?.stopVideoRecording(requestId: requestId)
1212
+ }
1213
+
1214
+ func setMicState() {
1215
+ let willSendPcm = shouldSendPcm || shouldSendLc3
1216
+ let willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
1217
+ micEnabled = willSendPcm || willSendTranscript
1218
+ updateMicState()
1219
+ }
1220
+
1221
+ func rgbLedControl(
1222
+ requestId: String,
1223
+ packageName: String?,
1224
+ action: String,
1225
+ color: String?,
1226
+ ontime: Int,
1227
+ offtime: Int,
1228
+ count: Int
1229
+ ) {
1230
+ sgc?.sendRgbLedControl(
1231
+ requestId: requestId,
1232
+ packageName: packageName,
1233
+ action: action,
1234
+ color: color,
1235
+ ontime: ontime,
1236
+ offtime: offtime,
1237
+ count: count
1238
+ )
1239
+ }
1240
+
1241
+ /// Mentra Live only: K900 `cs_getvol` / `sr_getvol` (step volume 0–15).
1242
+ func getGlassesMediaVolume() async throws -> [String: Any] {
1243
+ guard let live = sgc as? MentraLive else {
1244
+ throw NSError(
1245
+ domain: "DeviceManager",
1246
+ code: 100,
1247
+ userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
1248
+ )
1249
+ }
1250
+ return try await withCheckedThrowingContinuation {
1251
+ (continuation: CheckedContinuation<[String: Any], Error>) in
1252
+ live.getGlassesMediaVolume { result in
1253
+ continuation.resume(with: result)
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ /// Mentra Live only: K900 `cs_vol` / `sr_vol`.
1259
+ func setGlassesMediaVolume(level: Int) async throws -> [String: Any] {
1260
+ guard let live = sgc as? MentraLive else {
1261
+ throw NSError(
1262
+ domain: "DeviceManager",
1263
+ code: 100,
1264
+ userInfo: [NSLocalizedDescriptionKey: "unsupported_device"]
1265
+ )
1266
+ }
1267
+ return try await withCheckedThrowingContinuation {
1268
+ (continuation: CheckedContinuation<[String: Any], Error>) in
1269
+ live.setGlassesMediaVolume(level: level) { result in
1270
+ continuation.resume(with: result)
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ func photoRequest(
1276
+ _ requestId: String,
1277
+ _ appId: String,
1278
+ _ size: String,
1279
+ _ webhookUrl: String?,
1280
+ _ authToken: String?,
1281
+ _ compress: String?,
1282
+ _ flash: Bool,
1283
+ _ sound: Bool
1284
+ ) {
1285
+ Bridge.log(
1286
+ "MAN: onPhotoRequest: \(requestId), \(appId), \(webhookUrl), size=\(size), compress=\(compress ?? "none"), flash=\(flash), sound=\(sound)"
1287
+ )
1288
+ sgc?.requestPhoto(
1289
+ requestId, appId: appId, size: size, webhookUrl: webhookUrl, authToken: authToken,
1290
+ compress: compress, flash: flash, sound: sound
1291
+ )
1292
+ }
1293
+
1294
+ func connectDefault() {
1295
+ if defaultWearable.isEmpty {
1296
+ Bridge.log("MAN: No default wearable, returning")
1297
+ return
1298
+ }
1299
+ if deviceName.isEmpty {
1300
+ Bridge.log("MAN: No device name, returning")
1301
+ return
1302
+ }
1303
+ initSGC(defaultWearable)
1304
+ searching = true
1305
+ sgc?.connectById(deviceName)
1306
+ connectDefaultController()
1307
+ }
1308
+
1309
+ func connectDefaultController() {
1310
+ if defaultController.isEmpty {
1311
+ Bridge.log("MAN: No default controller, returning")
1312
+ return
1313
+ }
1314
+ if controllerDeviceName.isEmpty {
1315
+ Bridge.log("MAN: No controller device name, returning")
1316
+ return
1317
+ }
1318
+ initController(defaultController)
1319
+ searchingController = true
1320
+ controller?.connectById(controllerDeviceName)
1321
+ }
1322
+
1323
+ func connectByName(_ dName: String) {
1324
+ Bridge.log("MAN: Connecting to wearable: \(dName)")
1325
+ var name = dName
1326
+
1327
+ // use stored device name if available:
1328
+ if dName.isEmpty && !deviceName.isEmpty {
1329
+ name = deviceName
1330
+ }
1331
+
1332
+ if pendingWearable.isEmpty, defaultWearable.isEmpty {
1333
+ Bridge.log("MAN: No pending or default wearable, returning")
1334
+ return
1335
+ }
1336
+
1337
+ if pendingWearable.isEmpty, !defaultWearable.isEmpty {
1338
+ Bridge.log("MAN: No pending wearable, using default wearable: \(defaultWearable)")
1339
+ pendingWearable = defaultWearable
1340
+ }
1341
+
1342
+ // if the pending wearable is a controller, don't disconnect, use the controller manager to connect
1343
+ if ControllerTypes.ALL.contains(pendingWearable) {
1344
+ controller?.disconnect()
1345
+ controller?.connectById(name)
1346
+ return
1347
+ }
1348
+
1349
+ Task {
1350
+ disconnect()
1351
+ try? await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms
1352
+ self.searching = true
1353
+ self.deviceName = name
1354
+
1355
+ initSGC(self.pendingWearable)
1356
+ sgc?.connectById(self.deviceName)
1357
+ }
1358
+ }
1359
+
1360
+ func connectDevice(_ deviceModel: String, _ deviceName: String) {
1361
+ Bridge.log("MAN: Connecting to device: \(deviceModel) \(deviceName)")
1362
+ if DeviceTypes.ALL.contains(deviceModel) {
1363
+ pendingWearable = deviceModel
1364
+ initSGC(pendingWearable)
1365
+ sgc?.connectById(deviceName)
1366
+ return
1367
+ }
1368
+ if ControllerTypes.ALL.contains(deviceModel) {
1369
+ pendingWearable = deviceModel
1370
+ initController(deviceModel)
1371
+ controller?.connectById(deviceName)
1372
+ return
1373
+ }
1374
+ Bridge.log("MAN: No compatible device model, returning")
1375
+ }
1376
+
1377
+ func connectSimulated() {
1378
+ defaultWearable = DeviceTypes.SIMULATED
1379
+ deviceName = DeviceTypes.SIMULATED
1380
+ initSGC(defaultWearable)
1381
+ handleDeviceReady()
1382
+ }
1383
+
1384
+ func disconnect() {
1385
+ sgc?.clearDisplay() // clear the screen
1386
+ sgc?.disconnect()
1387
+ sgc = nil // Clear the SGC reference after disconnect
1388
+ searching = false
1389
+ micEnabled = false
1390
+ updateMicState()
1391
+ shouldSendBootingMessage = true // Reset for next first connect
1392
+ // clear glasses properties:
1393
+ DeviceStore.shared.apply("glasses", "deviceModel", "")
1394
+ DeviceStore.shared.apply("glasses", "fullyBooted", false)
1395
+ DeviceStore.shared.apply("glasses", "connected", false)
1396
+ // disconnect the controller as well:
1397
+ searchingController = false
1398
+ DeviceStore.shared.apply("glasses", "controllerConnected", false)
1399
+ controller?.disconnect()
1400
+ controller = nil // Clear the controller reference after disconnect
1401
+ }
1402
+
1403
+ func disconnectController() {
1404
+ searchingController = false
1405
+ // disconnect the controller from the glasses if applicable:
1406
+ sgc?.disconnectController()
1407
+ controller?.disconnect()
1408
+ controller = nil // Clear the controller reference after disconnect
1409
+ }
1410
+
1411
+ func forget() {
1412
+ Bridge.log("MAN: Forgetting smart glasses")
1413
+ // Call forget first to stop timers/handlers/reconnect logic
1414
+ sgc?.forget()
1415
+ disconnect()
1416
+ // Clear state
1417
+ defaultWearable = ""
1418
+ deviceName = ""
1419
+ deviceAddress = ""
1420
+ Bridge.saveSetting("default_wearable", "")
1421
+ Bridge.saveSetting("device_name", "")
1422
+ Bridge.saveSetting("device_address", "")
1423
+ }
1424
+
1425
+ func forgetController() {
1426
+ Bridge.log("MAN: Forgetting controller")
1427
+ controller?.forget()
1428
+ disconnectController()
1429
+ // Clear state
1430
+ defaultController = ""
1431
+ controllerDeviceName = ""
1432
+ Bridge.saveSetting("controller_device_name", "")
1433
+ Bridge.saveSetting("default_controller", "")
1434
+ DeviceStore.shared.apply("glasses", "controllerConnected", false)
1435
+ }
1436
+
1437
+ func findCompatibleDevices(_ deviceModel: String) {
1438
+ Bridge.log("MAN: Searching for compatible device names for: \(deviceModel)")
1439
+
1440
+ // reset the search results:
1441
+ searchResults = []
1442
+
1443
+ if DeviceTypes.ALL.contains(deviceModel) {
1444
+ pendingWearable = deviceModel
1445
+ }
1446
+
1447
+ if ControllerTypes.ALL.contains(deviceModel) {
1448
+ pendingWearable = deviceModel
1449
+ }
1450
+
1451
+ if ControllerTypes.ALL.contains(deviceModel) {
1452
+ initController(deviceModel)
1453
+ controller?.findCompatibleDevices()
1454
+ return
1455
+ }
1456
+
1457
+ initSGC(pendingWearable)
1458
+ sgc?.findCompatibleDevices()
1459
+ }
1460
+
1461
+ func stopScan() {
1462
+ controller?.stopScan()
1463
+ sgc?.stopScan()
1464
+ DeviceStore.shared.apply("bluetooth", "searching", false)
1465
+ DeviceStore.shared.apply("bluetooth", "searchingController", false)
1466
+ }
1467
+
1468
+ func cleanup() {
1469
+ // Clean up transcriber resources
1470
+ transcriber?.shutdown()
1471
+ transcriber = nil
1472
+
1473
+ // Clean up LC3 converter
1474
+ lc3Converter = nil
1475
+
1476
+ cancellables.removeAll()
1477
+ }
1478
+ }