@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,1616 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ import android.bluetooth.BluetoothAdapter
4
+ import android.content.BroadcastReceiver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentFilter
8
+ import android.content.pm.PackageManager
9
+ import android.os.Build
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import androidx.core.content.ContextCompat
13
+ import com.mentra.bluetoothsdk.controllers.ControllerManager
14
+ import com.mentra.bluetoothsdk.controllers.R1
15
+ import com.mentra.bluetoothsdk.services.ForegroundService
16
+ import com.mentra.bluetoothsdk.services.PhoneMic
17
+ import com.mentra.bluetoothsdk.sgcs.G1
18
+ import com.mentra.bluetoothsdk.sgcs.G2
19
+ import com.mentra.bluetoothsdk.sgcs.Mach1
20
+ import com.mentra.bluetoothsdk.sgcs.MentraLive
21
+ import com.mentra.bluetoothsdk.sgcs.MentraNex
22
+ import com.mentra.bluetoothsdk.sgcs.SGCManager
23
+ import com.mentra.bluetoothsdk.sgcs.Simulated
24
+ import com.mentra.bluetoothsdk.utils.ControllerTypes
25
+ import com.mentra.bluetoothsdk.utils.DeviceTypes
26
+ import com.mentra.bluetoothsdk.utils.MicMap
27
+ import com.mentra.bluetoothsdk.utils.MicTypes
28
+ import com.mentra.lc3Lib.Lc3Cpp
29
+ import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
30
+ import java.text.SimpleDateFormat
31
+ import java.util.*
32
+ import java.util.concurrent.CountDownLatch
33
+ import java.util.concurrent.ExecutorService
34
+ import java.util.concurrent.Executors
35
+ import java.util.concurrent.TimeUnit
36
+ import kotlin.jvm.JvmStatic
37
+
38
+ class DeviceManager {
39
+ companion object {
40
+
41
+ @Volatile private var _instance: DeviceManager? = null
42
+
43
+ @JvmStatic
44
+ fun getInstance(): DeviceManager {
45
+ return _instance
46
+ ?: synchronized(this) { _instance ?: DeviceManager().also { _instance = it } }
47
+ }
48
+ }
49
+
50
+ // MARK: - Unique (Android)
51
+ private var serviceStarted = false
52
+ private val mainHandler = Handler(Looper.getMainLooper())
53
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
54
+ private var sendStateWorkItem: Runnable? = null
55
+ private var phoneMic: PhoneMic? = null
56
+
57
+ // Track last known permissions
58
+ private var lastHadBluetoothPermission = false
59
+ private var lastHadMicrophonePermission = false
60
+ private var permissionReceiver: BroadcastReceiver? = null
61
+ private val handler = Handler(Looper.getMainLooper())
62
+ private var permissionCheckRunnable: Runnable? = null
63
+
64
+ // Bluetooth adapter state monitoring (detects BT toggle from control center)
65
+ private var bluetoothStateReceiver: BroadcastReceiver? = null
66
+ private var isBluetoothStateReceiverRegistered = false
67
+
68
+ // MARK: - End Unique
69
+
70
+ // MARK: - Properties
71
+ var sgc: SGCManager? = null
72
+ var controller: ControllerManager? = null
73
+
74
+ // settings:
75
+ private var defaultWearable: String
76
+ get() = DeviceStore.store.get("bluetooth", "default_wearable") as? String ?: ""
77
+ set(value) = DeviceStore.apply("bluetooth", "default_wearable", value)
78
+
79
+ private var pendingWearable: String
80
+ get() = DeviceStore.store.get("bluetooth", "pending_wearable") as? String ?: ""
81
+ set(value) = DeviceStore.apply("bluetooth", "pending_wearable", value)
82
+
83
+ public var deviceName: String
84
+ get() = DeviceStore.store.get("bluetooth", "device_name") as? String ?: ""
85
+ set(value) = DeviceStore.apply("bluetooth", "device_name", value)
86
+
87
+ public var deviceAddress: String
88
+ get() = DeviceStore.store.get("bluetooth", "device_address") as? String ?: ""
89
+ set(value) = DeviceStore.apply("bluetooth", "device_address", value)
90
+
91
+ private var defaultController: String
92
+ get() = DeviceStore.store.get("bluetooth", "default_controller") as? String ?: ""
93
+ set(value) = DeviceStore.apply("bluetooth", "default_controller", value)
94
+
95
+ private var pendingController: String
96
+ get() = DeviceStore.store.get("bluetooth", "pending_controller") as? String ?: ""
97
+ set(value) = DeviceStore.apply("bluetooth", "pending_controller", value)
98
+
99
+ private var controllerDeviceName: String
100
+ get() = DeviceStore.store.get("bluetooth", "controller_device_name") as? String ?: ""
101
+ set(value) = DeviceStore.apply("bluetooth", "controller_device_name", value)
102
+
103
+ private var searchingController: Boolean
104
+ get() = DeviceStore.store.get("bluetooth", "searchingController") as? Boolean ?: false
105
+ set(value) = DeviceStore.apply("bluetooth", "searchingController", value)
106
+
107
+ private var screenDisabled: Boolean
108
+ get() = DeviceStore.store.get("bluetooth", "screen_disabled") as? Boolean ?: false
109
+ set(value) = DeviceStore.apply("bluetooth", "screen_disabled", value)
110
+
111
+ private var preferredMic: String
112
+ get() = DeviceStore.store.get("bluetooth", "preferred_mic") as? String ?: "auto"
113
+ set(value) = DeviceStore.apply("bluetooth", "preferred_mic", value)
114
+
115
+ private var autoBrightness: Boolean
116
+ get() = DeviceStore.store.get("bluetooth", "auto_brightness") as? Boolean ?: true
117
+ set(value) = DeviceStore.apply("bluetooth", "auto_brightness", value)
118
+
119
+ private var brightness: Int
120
+ get() = DeviceStore.store.get("bluetooth", "brightness") as? Int ?: 50
121
+ set(value) = DeviceStore.apply("bluetooth", "brightness", value)
122
+
123
+ private var headUpAngle: Int
124
+ get() = DeviceStore.store.get("bluetooth", "head_up_angle") as? Int ?: 30
125
+ set(value) = DeviceStore.apply("bluetooth", "head_up_angle", value)
126
+
127
+ private var sensingEnabled: Boolean
128
+ get() = DeviceStore.store.get("bluetooth", "sensing_enabled") as? Boolean ?: true
129
+ set(value) = DeviceStore.apply("bluetooth", "sensing_enabled", value)
130
+
131
+ public var powerSavingMode: Boolean
132
+ get() = DeviceStore.store.get("bluetooth", "power_saving_mode") as? Boolean ?: false
133
+ set(value) = DeviceStore.apply("bluetooth", "power_saving_mode", value)
134
+
135
+ private var bypassVad: Boolean
136
+ get() = DeviceStore.store.get("bluetooth", "bypass_vad") as? Boolean ?: true
137
+ set(value) = DeviceStore.apply("bluetooth", "bypass_vad", value)
138
+
139
+ private var offlineCaptionsRunning: Boolean
140
+ get() = DeviceStore.store.get("bluetooth", "offline_captions_running") as? Boolean ?: false
141
+ set(value) = DeviceStore.apply("bluetooth", "offline_captions_running", value)
142
+
143
+ private var localSttFallbackActive: Boolean
144
+ get() = DeviceStore.store.get("bluetooth", "local_stt_fallback_active") as? Boolean ?: false
145
+ set(value) = DeviceStore.apply("bluetooth", "local_stt_fallback_active", value)
146
+
147
+ private var shouldSendPcm: Boolean
148
+ get() = DeviceStore.store.get("bluetooth", "should_send_pcm") as? Boolean ?: false
149
+ set(value) = DeviceStore.apply("bluetooth", "should_send_pcm", value)
150
+
151
+ private var shouldSendLc3: Boolean
152
+ get() = DeviceStore.store.get("bluetooth", "should_send_lc3") as? Boolean ?: false
153
+ set(value) = DeviceStore.apply("bluetooth", "should_send_lc3", value)
154
+
155
+ private var shouldSendTranscript: Boolean
156
+ get() = DeviceStore.store.get("bluetooth", "should_send_transcript") as? Boolean ?: false
157
+ set(value) = DeviceStore.apply("bluetooth", "should_send_transcript", value)
158
+
159
+ private var contextualDashboard: Boolean
160
+ get() = DeviceStore.store.get("bluetooth", "contextual_dashboard") as? Boolean ?: true
161
+ set(value) = DeviceStore.apply("bluetooth", "contextual_dashboard", value)
162
+
163
+ private var dashboardHeight: Int
164
+ get() = (DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)?.toInt() ?: 4
165
+ set(value) = DeviceStore.apply("bluetooth", "dashboard_height", value)
166
+
167
+ private var dashboardDepth: Int
168
+ get() = (DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)?.toInt() ?: 2
169
+ set(value) = DeviceStore.apply("bluetooth", "dashboard_depth", value)
170
+
171
+ private var galleryMode: Boolean
172
+ get() = DeviceStore.store.get("bluetooth", "gallery_mode") as? Boolean ?: true
173
+ set(value) = DeviceStore.apply("bluetooth", "gallery_mode", value)
174
+
175
+ // state:
176
+ private var searching: Boolean
177
+ get() = DeviceStore.store.get("bluetooth", "searching") as? Boolean ?: false
178
+ set(value) = DeviceStore.apply("bluetooth", "searching", value)
179
+
180
+ private var glassesBtcConnected: Boolean
181
+ get() = DeviceStore.store.get("glasses", "btcConnected") as? Boolean ?: false
182
+ set(value) = DeviceStore.apply("glasses", "btcConnected", value)
183
+
184
+ public var micRanking: MutableList<String>
185
+ get() =
186
+ (DeviceStore.store.get("bluetooth", "micRanking") as? List<*>)
187
+ ?.mapNotNull { it as? String }
188
+ ?.toMutableList()
189
+ ?: MicMap.map["auto"]?.toMutableList() ?: mutableListOf()
190
+ set(value) = DeviceStore.apply("bluetooth", "micRanking", value)
191
+
192
+ private var shouldSendBootingMessage: Boolean
193
+ get() = DeviceStore.store.get("bluetooth", "shouldSendBootingMessage") as? Boolean ?: true
194
+ set(value) = DeviceStore.apply("bluetooth", "shouldSendBootingMessage", value)
195
+
196
+ // Guard against duplicate ready callbacks firing back-to-back.
197
+ private var lastReadyHandledAtMs: Long = 0L
198
+ private var lastReadyHandledKey: String = ""
199
+
200
+ private var systemMicUnavailable: Boolean
201
+ get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
202
+ set(value) = DeviceStore.apply("bluetooth", "systemMicUnavailable", value)
203
+
204
+ public var headUp: Boolean
205
+ get() = DeviceStore.store.get("glasses", "headUp") as? Boolean ?: false
206
+ set(value) = DeviceStore.apply("glasses", "headUp", value)
207
+
208
+ private var micEnabled: Boolean
209
+ get() = DeviceStore.store.get("bluetooth", "micEnabled") as? Boolean ?: false
210
+ set(value) = DeviceStore.apply("bluetooth", "micEnabled", value)
211
+
212
+ private var currentMic: String
213
+ get() = DeviceStore.store.get("bluetooth", "currentMic") as? String ?: ""
214
+ set(value) = DeviceStore.apply("bluetooth", "currentMic", value)
215
+
216
+ private var searchResults: List<Any>
217
+ get() = DeviceStore.store.get("bluetooth", "searchResults") as? List<Any> ?: emptyList()
218
+ set(value) = DeviceStore.apply("bluetooth", "searchResults", value)
219
+
220
+ private var wifiScanResults: List<Any>
221
+ get() = DeviceStore.store.get("bluetooth", "wifiScanResults") as? List<Any> ?: emptyList()
222
+ set(value) = DeviceStore.apply("bluetooth", "wifiScanResults", value)
223
+
224
+ private var lastLog: MutableList<String>
225
+ get() = DeviceStore.store.get("bluetooth", "lastLog") as? MutableList<String> ?: mutableListOf()
226
+ set(value) = DeviceStore.apply("bluetooth", "lastLog", value)
227
+
228
+ // LC3 Audio Encoding
229
+ // Audio output format enum
230
+ enum class AudioOutputFormat {
231
+ LC3,
232
+ PCM
233
+ }
234
+ // Canonical LC3 config: 16kHz sample rate, 10ms frame duration
235
+ // Frame size is configurable: 20 bytes (16kbps), 40 bytes (32kbps), 60 bytes (48kbps)
236
+ private var lc3EncoderPtr: Long = 0
237
+ private var lc3DecoderPtr: Long = 0
238
+ private val lc3Lock = Any()
239
+ // Audio output format - defaults to LC3 for bandwidth savings
240
+ private var audioOutputFormat: AudioOutputFormat = AudioOutputFormat.LC3
241
+ private var lastLc3Event: Long? = null
242
+ private var micReinitRunnable: Runnable? = null
243
+
244
+ // VAD
245
+ private val vadBuffer = mutableListOf<ByteArray>()
246
+ private var isSpeaking = false
247
+
248
+ // STT
249
+ private var transcriber: SherpaOnnxTranscriber? = null
250
+
251
+ // View states
252
+ private val viewStates = mutableListOf<ViewState>()
253
+
254
+ init {
255
+ Bridge.log("DeviceManager: init()")
256
+ initializeViewStates()
257
+ startForegroundService()
258
+ // setupPermissionMonitoring()
259
+ setupBluetoothStateMonitoring()
260
+ phoneMic = PhoneMic.getInstance()
261
+ // Initialize local STT transcriber
262
+ try {
263
+ val context = Bridge.getContext()
264
+ transcriber = SherpaOnnxTranscriber(context)
265
+ transcriber?.setTranscriptListener(
266
+ object : SherpaOnnxTranscriber.TranscriptListener {
267
+ override fun onPartialResult(text: String, language: String) {
268
+ Bridge.log("STT: Partial result: $text")
269
+ Bridge.sendLocalTranscription(text, false, language)
270
+ }
271
+
272
+ override fun onFinalResult(text: String, language: String) {
273
+ Bridge.log("STT: Final result: $text")
274
+ Bridge.sendLocalTranscription(text, true, language)
275
+ }
276
+ }
277
+ )
278
+ transcriber?.initialize()
279
+ Bridge.log("SherpaOnnxTranscriber fully initialized")
280
+ } catch (e: Exception) {
281
+ Bridge.log("Failed to initialize SherpaOnnxTranscriber: ${e.message}")
282
+ transcriber = null
283
+ }
284
+
285
+ // Initialize LC3 encoder/decoder for unified audio encoding
286
+ try {
287
+ Lc3Cpp.init()
288
+ lc3EncoderPtr = Lc3Cpp.initEncoder()
289
+ lc3DecoderPtr = Lc3Cpp.initDecoder()
290
+ Bridge.log("LC3 encoder/decoder initialized successfully")
291
+ } catch (e: Exception) {
292
+ Bridge.log("Failed to initialize LC3 encoder/decoder: ${e.message}")
293
+ lc3EncoderPtr = 0
294
+ lc3DecoderPtr = 0
295
+ }
296
+
297
+ // Mic reinit check every 10 seconds
298
+ val micReinitR =
299
+ object : Runnable {
300
+ override fun run() {
301
+ checkAndReinitGlassesMic()
302
+ mainHandler.postDelayed(this, 10_000)
303
+ }
304
+ }
305
+ micReinitRunnable = micReinitR
306
+ mainHandler.postDelayed(micReinitR, 10_000)
307
+ }
308
+
309
+ private fun checkAndReinitGlassesMic() {
310
+ // 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:
311
+ val glassesMicEnabled = DeviceStore.get("glasses", "micEnabled") as? Boolean ?: false
312
+ val glassesConnected = DeviceStore.get("glasses", "connected") as? Boolean ?: false
313
+ if (!glassesMicEnabled || !glassesConnected) {
314
+ return
315
+ }
316
+
317
+ // When no frame has ever been received, treat elapsed as "forever" so we
318
+ // actually attempt recovery (was 0 before, which made the watchdog a no-op).
319
+ val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
320
+ if (timeSinceLastLc3Event > 5000) {
321
+ Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
322
+ sgc?.setMicEnabled(true)
323
+ }
324
+ }
325
+
326
+ // MARK: - Unique (Android)
327
+ private fun setupPermissionMonitoring() {
328
+ val context = Bridge.getContext()
329
+
330
+ // Store initial permission state
331
+ lastHadBluetoothPermission = checkBluetoothPermission(context)
332
+ lastHadMicrophonePermission = checkMicrophonePermission(context)
333
+
334
+ Bridge.log(
335
+ "MAN: Initial permissions - BT: $lastHadBluetoothPermission, Mic: $lastHadMicrophonePermission"
336
+ )
337
+
338
+ // Create receiver for package changes (fires when permissions change)
339
+ permissionReceiver =
340
+ object : BroadcastReceiver() {
341
+ override fun onReceive(context: Context?, intent: Intent?) {
342
+ if (intent?.action == Intent.ACTION_PACKAGE_CHANGED &&
343
+ intent.data?.schemeSpecificPart == context?.packageName
344
+ ) {
345
+
346
+ Bridge.log("MAN: Package changed, checking permissions...")
347
+ checkPermissionChanges()
348
+ }
349
+ }
350
+ }
351
+
352
+ // Register the receiver
353
+ try {
354
+ val filter =
355
+ IntentFilter().apply {
356
+ addAction(Intent.ACTION_PACKAGE_CHANGED)
357
+ addDataScheme("package")
358
+ }
359
+ context.registerReceiver(permissionReceiver, filter)
360
+ Bridge.log("MAN: Permission monitoring started")
361
+ } catch (e: Exception) {
362
+ Bridge.log("MAN: Failed to register permission receiver: ${e.message}")
363
+ }
364
+
365
+ // Also set up a periodic check as backup (some devices don't fire PACKAGE_CHANGED reliably)
366
+ // startPeriodicPermissionCheck()
367
+ }
368
+
369
+ private fun startPeriodicPermissionCheck() {
370
+ permissionCheckRunnable =
371
+ object : Runnable {
372
+ override fun run() {
373
+ checkPermissionChanges()
374
+ handler.postDelayed(this, 10000) // Check every 10 seconds
375
+ }
376
+ }
377
+ handler.postDelayed(permissionCheckRunnable!!, 10000)
378
+ }
379
+
380
+ private fun checkPermissionChanges() {
381
+ val context = Bridge.getContext()
382
+
383
+ val currentHasBluetoothPermission = checkBluetoothPermission(context)
384
+ val currentHasMicrophonePermission = checkMicrophonePermission(context)
385
+
386
+ var permissionsChanged = false
387
+
388
+ if (currentHasBluetoothPermission != lastHadBluetoothPermission) {
389
+ Bridge.log(
390
+ "MAN: Bluetooth permission changed: $lastHadBluetoothPermission -> $currentHasBluetoothPermission"
391
+ )
392
+ lastHadBluetoothPermission = currentHasBluetoothPermission
393
+ permissionsChanged = true
394
+ }
395
+
396
+ if (currentHasMicrophonePermission != lastHadMicrophonePermission) {
397
+ Bridge.log(
398
+ "MAN: Microphone permission changed: $lastHadMicrophonePermission -> $currentHasMicrophonePermission"
399
+ )
400
+ lastHadMicrophonePermission = currentHasMicrophonePermission
401
+ permissionsChanged = true
402
+ }
403
+
404
+ if (permissionsChanged && !currentHasBluetoothPermission) {
405
+ Bridge.log("MAN: Bluetooth permission revoked disconnecting glasses")
406
+ disconnect()
407
+ }
408
+
409
+ if (permissionsChanged && serviceStarted) {
410
+ Bridge.log("MAN: Permissions changed, restarting service")
411
+ restartForegroundService()
412
+ }
413
+ }
414
+
415
+ private fun checkBluetoothPermission(context: Context): Boolean {
416
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
417
+ val connect = ContextCompat.checkSelfPermission(
418
+ context,
419
+ android.Manifest.permission.BLUETOOTH_CONNECT
420
+ ) == PackageManager.PERMISSION_GRANTED
421
+ val scan = ContextCompat.checkSelfPermission(
422
+ context,
423
+ android.Manifest.permission.BLUETOOTH_SCAN
424
+ ) == PackageManager.PERMISSION_GRANTED
425
+ connect && scan
426
+ } else {
427
+ ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) ==
428
+ PackageManager.PERMISSION_GRANTED
429
+ }
430
+ }
431
+
432
+ private fun hasBluetoothPermissions(): Boolean {
433
+ return checkBluetoothPermission(Bridge.getContext())
434
+ }
435
+
436
+ private fun checkMicrophonePermission(context: Context): Boolean {
437
+ return ContextCompat.checkSelfPermission(
438
+ context,
439
+ android.Manifest.permission.RECORD_AUDIO
440
+ ) == PackageManager.PERMISSION_GRANTED
441
+ }
442
+
443
+ private fun setupBluetoothStateMonitoring() {
444
+ val context = Bridge.getContext()
445
+
446
+ bluetoothStateReceiver =
447
+ object : BroadcastReceiver() {
448
+ override fun onReceive(context: Context?, intent: Intent?) {
449
+ if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
450
+
451
+ val state =
452
+ intent.getIntExtra(
453
+ BluetoothAdapter.EXTRA_STATE,
454
+ BluetoothAdapter.ERROR
455
+ )
456
+ when (state) {
457
+ BluetoothAdapter.STATE_OFF -> {
458
+ Bridge.log("MAN: Bluetooth turned OFF (control center or settings)")
459
+ disconnect()
460
+ }
461
+ BluetoothAdapter.STATE_TURNING_OFF -> {
462
+ Bridge.log("MAN: Bluetooth turning off...")
463
+ }
464
+ BluetoothAdapter.STATE_ON -> {
465
+ Bridge.log("MAN: Bluetooth turned ON")
466
+ // Auto-reconnect to last known device if we have one
467
+ if (defaultWearable.isNotEmpty() && deviceName.isNotEmpty()) {
468
+ Bridge.log(
469
+ "MAN: Bluetooth restored, attempting reconnect to: $deviceName"
470
+ )
471
+ handler.postDelayed(
472
+ { connectDefault() },
473
+ 2000
474
+ ) // Small delay to let BT stack stabilize
475
+ }
476
+ }
477
+ BluetoothAdapter.STATE_TURNING_ON -> {
478
+ Bridge.log("MAN: Bluetooth turning on...")
479
+ }
480
+ }
481
+ }
482
+ }
483
+
484
+ try {
485
+ val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
486
+ context.registerReceiver(bluetoothStateReceiver, filter)
487
+ isBluetoothStateReceiverRegistered = true
488
+ Bridge.log("MAN: Bluetooth state monitoring started")
489
+ } catch (e: Exception) {
490
+ Bridge.log("MAN: Failed to register Bluetooth state receiver: ${e.message}")
491
+ }
492
+ }
493
+
494
+ private fun stopBluetoothStateMonitoring() {
495
+ if (isBluetoothStateReceiverRegistered && bluetoothStateReceiver != null) {
496
+ try {
497
+ Bridge.getContext().unregisterReceiver(bluetoothStateReceiver)
498
+ } catch (e: Exception) {
499
+ Bridge.log("MAN: Error unregistering Bluetooth state receiver: ${e.message}")
500
+ }
501
+ isBluetoothStateReceiverRegistered = false
502
+ }
503
+ }
504
+
505
+ private fun startForegroundService() {
506
+ val context = Bridge.getContext()
507
+
508
+ try {
509
+ Bridge.log("MAN: Starting foreground service")
510
+ val serviceIntent = Intent(context, ForegroundService::class.java)
511
+
512
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
513
+ context.startForegroundService(serviceIntent)
514
+ } else {
515
+ context.startService(serviceIntent)
516
+ }
517
+
518
+ serviceStarted = true
519
+ Bridge.log("MAN: Foreground service started")
520
+ } catch (e: Exception) {
521
+ Bridge.log("MAN: Failed to start service: ${e.message}")
522
+ }
523
+ }
524
+
525
+ private fun restartForegroundService() {
526
+ val context = Bridge.getContext()
527
+
528
+ try {
529
+ // Stop the service
530
+ val stopIntent = Intent(context, ForegroundService::class.java)
531
+ context.stopService(stopIntent)
532
+
533
+ // Small delay
534
+ Thread.sleep(100)
535
+
536
+ // Start it again with new permissions
537
+ val startIntent = Intent(context, ForegroundService::class.java)
538
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
539
+ context.startForegroundService(startIntent)
540
+ } else {
541
+ context.startService(startIntent)
542
+ }
543
+
544
+ Bridge.log("MAN: Service restarted with updated permissions")
545
+ } catch (e: Exception) {
546
+ Bridge.log("MAN: Failed to restart service: ${e.message}")
547
+ }
548
+ }
549
+
550
+ private fun initializeViewStates() {
551
+ viewStates.clear()
552
+
553
+ // Matching Swift's 4 view states exactly
554
+ viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
555
+ viewStates.add(
556
+ ViewState(
557
+ " ",
558
+ " ",
559
+ " ",
560
+ "text_wall",
561
+ "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
562
+ null,
563
+ null
564
+ )
565
+ )
566
+ viewStates.add(ViewState(" ", " ", " ", "text_wall", "", null, null))
567
+ viewStates.add(
568
+ ViewState(
569
+ " ",
570
+ " ",
571
+ " ",
572
+ "text_wall",
573
+ "\$TIME12$ \$DATE$ \$GBATT$ \$CONNECTION_STATUS$",
574
+ null,
575
+ null
576
+ )
577
+ )
578
+ }
579
+
580
+ private fun statesEqual(s1: ViewState, s2: ViewState): Boolean {
581
+ val state1 =
582
+ "${s1.layoutType}${s1.text}${s1.topText}${s1.bottomText}${s1.title}${s1.data ?: ""}"
583
+ val state2 =
584
+ "${s2.layoutType}${s2.text}${s2.topText}${s2.bottomText}${s2.title}${s2.data ?: ""}"
585
+ return state1 == state2
586
+ }
587
+
588
+ private fun Map<String, Any>.getString(key: String, defaultValue: String): String {
589
+ return (this[key] as? String) ?: defaultValue
590
+ }
591
+
592
+ // Inner classes
593
+
594
+ data class ViewState(
595
+ var topText: String,
596
+ var bottomText: String,
597
+ var title: String,
598
+ var layoutType: String,
599
+ var text: String,
600
+ var data: String?,
601
+ var animationData: Map<String, Any>?
602
+ )
603
+ // MARK: - End Unique
604
+
605
+ // MARK: - Voice Data Handling
606
+
607
+ private fun checkSetVadStatus(speaking: Boolean) {
608
+ if (speaking != isSpeaking) {
609
+ isSpeaking = speaking
610
+ Bridge.sendVadEvent(isSpeaking)
611
+ }
612
+ }
613
+
614
+ private fun convertAndSendMicLc3(pcmData: ByteArray) {
615
+ synchronized(lc3Lock) {
616
+ if (lc3EncoderPtr == 0L) {
617
+ Bridge.log("MAN: ERROR - LC3 encoder not initialized but format is LC3")
618
+ return
619
+ }
620
+ val lc3FrameSize = (DeviceStore.store.get("bluetooth", "lc3_frame_size") as Number).toInt()
621
+ val lc3Data = Lc3Cpp.encodeLC3(lc3EncoderPtr, pcmData, lc3FrameSize)
622
+ if (lc3Data == null || lc3Data.isEmpty()) {
623
+ Bridge.log("MAN: ERROR - LC3 encoding returned empty data")
624
+ return
625
+ }
626
+ Bridge.sendMicLc3(lc3Data)
627
+ }
628
+ }
629
+
630
+ private fun handleSendingPcm(pcmData: ByteArray) {
631
+ if (shouldSendPcm) {
632
+ Bridge.sendMicPcm(pcmData)
633
+ }
634
+ if (shouldSendLc3) {
635
+ convertAndSendMicLc3(pcmData)
636
+ }
637
+ }
638
+
639
+ private fun emptyVadBuffer() {
640
+ while (vadBuffer.isNotEmpty()) {
641
+ val chunk = vadBuffer.removeAt(0)
642
+ handleSendingPcm(chunk) // Uses our encoder, not Bridge directly
643
+ }
644
+ }
645
+
646
+ private fun addToVadBuffer(chunk: ByteArray) {
647
+ val MAX_BUFFER_SIZE = 20
648
+ vadBuffer.add(chunk)
649
+ while (vadBuffer.size > MAX_BUFFER_SIZE) {
650
+ vadBuffer.removeAt(0)
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Handle raw LC3 audio data from glasses. Decodes the glasses LC3, then passes to handlePcm for
656
+ * canonical LC3 encoding. Note: frameSize here is for glasses→phone decoding, NOT for
657
+ * phone→cloud encoding.
658
+ */
659
+ fun handleGlassesMicData(rawLC3Data: ByteArray, frameSize: Int = 40) {
660
+ lastLc3Event = System.currentTimeMillis()
661
+ val pcmData: ByteArray?
662
+ synchronized(lc3Lock) {
663
+ if (lc3DecoderPtr == 0L) {
664
+ Bridge.log("MAN: LC3 decoder not initialized, cannot process glasses audio")
665
+ return
666
+ }
667
+
668
+ try {
669
+ // Decode glasses LC3 to PCM (glasses may use different LC3 configs)
670
+ pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, rawLC3Data, frameSize)
671
+ } catch (e: Exception) {
672
+ Bridge.log("MAN: Failed to decode glasses LC3: ${e.message}")
673
+ return
674
+ }
675
+ }
676
+ if (pcmData != null && pcmData.isNotEmpty()) {
677
+ // Re-encode to canonical LC3 via handlePcm (outside lock to avoid deadlock)
678
+ handlePcm(pcmData)
679
+ } else {
680
+ Bridge.log("MAN: LC3 decode returned empty data")
681
+ }
682
+ }
683
+
684
+ fun handlePcm(pcmData: ByteArray) {
685
+ handleSendingPcm(pcmData)
686
+
687
+ // Send PCM to local transcriber (always needs raw PCM)
688
+ if (shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive) {
689
+ transcriber?.acceptAudio(pcmData)
690
+ }
691
+ }
692
+
693
+ // turns a single mic on and turns off all other mics:
694
+ private var isUpdatingMicState = false
695
+ private var pendingMicStateUpdate = false
696
+
697
+ internal fun updateMicState() {
698
+ // Guard against re-entrant calls from onRouteChange callbacks
699
+ if (isUpdatingMicState) {
700
+ pendingMicStateUpdate = true
701
+ return
702
+ }
703
+ isUpdatingMicState = true
704
+ pendingMicStateUpdate = false
705
+
706
+ try {
707
+ updateMicStateInternal()
708
+ } finally {
709
+ isUpdatingMicState = false
710
+ // If a re-entrant call was requested, run it now
711
+ if (pendingMicStateUpdate) {
712
+ pendingMicStateUpdate = false
713
+ updateMicState()
714
+ }
715
+ }
716
+ }
717
+
718
+ private fun updateMicStateInternal() {
719
+ // go through the micRanking and find the first mic that is available:
720
+ var micUsed: String = ""
721
+
722
+ // allow the sgc to make changes to the micRanking:
723
+ micRanking = sgc?.sortMicRanking(micRanking) ?: micRanking
724
+ Bridge.log("MAN: updateMicState() micRanking: $micRanking")
725
+
726
+ if (micEnabled) {
727
+
728
+ for (micMode in micRanking) {
729
+ if (micMode == MicTypes.PHONE_INTERNAL ||
730
+ micMode == MicTypes.BT_CLASSIC ||
731
+ micMode == MicTypes.BT
732
+ ) {
733
+
734
+ if (phoneMic?.isRecordingWithMode(micMode) == true) {
735
+ micUsed = micMode
736
+ break
737
+ }
738
+
739
+ if (systemMicUnavailable) {
740
+ Bridge.log("MAN: systemMicUnavailable, continuing to next mic")
741
+ continue
742
+ }
743
+
744
+ // if the phone mic is not recording, start recording:
745
+ val success = phoneMic?.startMode(micMode) ?: false
746
+ Bridge.log("MAN: starting mic mode: $micMode -> $success")
747
+ if (success) {
748
+ micUsed = micMode
749
+ break
750
+ }
751
+ }
752
+
753
+ if (micMode == MicTypes.GLASSES_CUSTOM) {
754
+ if (sgc?.hasMic == true) {
755
+ // enable the mic if it's not already on:
756
+ if (sgc?.micEnabled == false) {
757
+ sgc?.setMicEnabled(true)
758
+ micUsed = micMode
759
+ break
760
+ } else {
761
+ // the mic is already on, mark it as used and break:
762
+ micUsed = micMode
763
+ break
764
+ }
765
+ }
766
+ // if the glasses doesn't have a mic, continue to the next mic:
767
+ continue
768
+ }
769
+ }
770
+ }
771
+
772
+ currentMic = micUsed
773
+
774
+ if (micUsed == "" && micEnabled) {
775
+ Bridge.log("MAN: No available mic found!")
776
+ return
777
+ }
778
+
779
+ // go through and disable all mics after the first used one:
780
+ val allMics = micRanking
781
+ // add any missing mics to the list:
782
+ for (micMode in MicMap.map["auto"]!!) {
783
+ if (!allMics.contains(micMode)) {
784
+ allMics.add(micMode)
785
+ }
786
+ }
787
+ for (micMode in allMics) {
788
+ if (micMode == micUsed) {
789
+ continue
790
+ }
791
+
792
+ if (micMode == MicTypes.PHONE_INTERNAL ||
793
+ micMode == MicTypes.BT_CLASSIC ||
794
+ micMode == MicTypes.BT
795
+ ) {
796
+ phoneMic?.stopMode(micMode)
797
+ }
798
+
799
+ if (micMode == MicTypes.GLASSES_CUSTOM && sgc?.hasMic == true && sgc?.micEnabled == true
800
+ ) {
801
+ sgc?.setMicEnabled(false)
802
+ }
803
+ }
804
+ }
805
+
806
+ private fun setOnboardMicEnabled(enabled: Boolean) {
807
+ Bridge.log("MAN: setOnboardMicEnabled(): $enabled")
808
+ if (enabled) {
809
+ phoneMic?.startRecording()
810
+ } else {
811
+ phoneMic?.stopRecording()
812
+ }
813
+ }
814
+
815
+ fun sendCurrentState() {
816
+ val hUp = DeviceStore.get("glasses", "headUp") as? Boolean ?: false
817
+ // Bridge.log("MAN: sendCurrentState(): $isHeadUp")
818
+ if (screenDisabled) {
819
+ return
820
+ }
821
+
822
+ // executor.execute {
823
+ var currentViewState: ViewState
824
+ if (hUp) {
825
+ currentViewState = viewStates[1]
826
+ } else {
827
+ currentViewState = viewStates[0]
828
+ }
829
+
830
+ if (hUp && !contextualDashboard) {
831
+ currentViewState = viewStates[0]
832
+ }
833
+
834
+ if (sgc?.type?.contains(DeviceTypes.SIMULATED) == true) {
835
+ // dont send the event to glasses that aren't there:
836
+ return
837
+ }
838
+
839
+ var fullyBooted = sgc?.fullyBooted ?: false
840
+ if (!fullyBooted) {
841
+ Bridge.log("MAN: DeviceManager.sendCurrentState(): sgc not ready")
842
+ return
843
+ }
844
+
845
+ // Cancel any pending clear display work item
846
+ // sendStateWorkItem?.let { mainHandler.removeCallbacks(it) }
847
+
848
+ // Bridge.log("MAN: parsing layoutType: ${currentViewState.layoutType}")
849
+ // Bridge.log(
850
+ // "MAN: viewState text: '${currentViewState.text}' (len=${currentViewState.text.length})"
851
+ // )
852
+
853
+ when (currentViewState.layoutType) {
854
+ "text_wall" -> {
855
+ sgc?.sendTextWall(currentViewState.text)
856
+ }
857
+ "double_text_wall" -> {
858
+ sgc?.sendDoubleTextWall(currentViewState.topText, currentViewState.bottomText)
859
+ }
860
+ "reference_card" -> {
861
+ sgc?.sendTextWall("${currentViewState.title}\n\n${currentViewState.text}")
862
+ }
863
+ "bitmap_view" -> {
864
+ currentViewState.data?.let { data -> sgc?.displayBitmap(data) }
865
+ }
866
+ "clear_view" -> sgc?.clearDisplay()
867
+ else -> Bridge.log("MAN: UNHANDLED LAYOUT_TYPE ${currentViewState.layoutType}")
868
+ }
869
+ // }
870
+ }
871
+
872
+ private fun parsePlaceholders(text: String): String {
873
+ val dateFormatter = SimpleDateFormat("M/dd, h:mm", Locale.getDefault())
874
+ val formattedDate = dateFormatter.format(Date())
875
+
876
+ val time12Format = SimpleDateFormat("hh:mm", Locale.getDefault())
877
+ val time12 = time12Format.format(Date())
878
+
879
+ val time24Format = SimpleDateFormat("HH:mm", Locale.getDefault())
880
+ val time24 = time24Format.format(Date())
881
+
882
+ val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault())
883
+ val currentDate = dateFormat.format(Date())
884
+
885
+ val placeholders =
886
+ mapOf(
887
+ "\$no_datetime$" to formattedDate,
888
+ "\$DATE$" to currentDate,
889
+ "\$TIME12$" to time12,
890
+ "\$TIME24$" to time24,
891
+ "\$GBATT$" to
892
+ (sgc?.batteryLevel?.let { if (it == -1) "" else "$it%" } ?: ""),
893
+ "\$CONNECTION_STATUS$" to "Connected"
894
+ )
895
+
896
+ return placeholders.entries.fold(text) { result, (key, value) ->
897
+ result.replace(key, value)
898
+ }
899
+ }
900
+
901
+ private fun appendLog(entry: String) {
902
+ lastLog = (lastLog + entry).takeLast(100).toMutableList()
903
+ }
904
+
905
+ fun onRouteChange(reason: String, availableInputs: List<String>) {
906
+ Bridge.log("MAN: onRouteChange: reason: $reason")
907
+ Bridge.log("MAN: onRouteChange: inputs: $availableInputs")
908
+
909
+ // Handle external app conflicts - automatically switch to glasses mic if available
910
+ when (reason) {
911
+ "external_app_recording" -> {
912
+ // Another app is using the microphone
913
+ systemMicUnavailable = true
914
+ Bridge.log("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
915
+ appendLog("MAN: MIC_UNAVAILABLE: TRUE external_app_recording")
916
+ }
917
+ "audio_focus_available" -> {
918
+ // Audio focus is available again
919
+ systemMicUnavailable = false
920
+ Bridge.log("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
921
+ appendLog("MAN: MIC_UNAVAILABLE: FALSE audio_focus_available")
922
+ }
923
+ "external_app_stopped" -> {
924
+ // External app stopped recording
925
+ systemMicUnavailable = false
926
+ Bridge.log("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
927
+ appendLog("MAN: MIC_UNAVAILABLE: FALSE external_app_stopped")
928
+ }
929
+ "phone_call_interruption" -> {
930
+ // Phone call started - mark mic as unavailable
931
+ systemMicUnavailable = true
932
+ Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
933
+ appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_interruption")
934
+ }
935
+ "phone_call_ended" -> {
936
+ // Phone call ended - mark mic as available again
937
+ systemMicUnavailable = false
938
+ Bridge.log("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
939
+ appendLog("MAN: MIC_UNAVAILABLE: FALSE phone_call_ended")
940
+ }
941
+ "phone_call_active" -> {
942
+ // Tried to start recording while phone call already active
943
+ systemMicUnavailable = true
944
+ Bridge.log("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
945
+ appendLog("MAN: MIC_UNAVAILABLE: TRUE phone_call_active")
946
+ }
947
+ "audio_focus_denied" -> {
948
+ // Another app has audio focus
949
+ systemMicUnavailable = true
950
+ Bridge.log("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
951
+ appendLog("MAN: MIC_UNAVAILABLE: TRUE audio_focus_denied")
952
+ }
953
+ "permission_denied" -> {
954
+ // Microphone permission not granted
955
+ systemMicUnavailable = true
956
+ Bridge.log("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
957
+ appendLog("MAN: MIC_UNAVAILABLE: TRUE permission_denied")
958
+ // Don't trigger fallback - need to request permission from user
959
+ }
960
+ "audio_route_changed" -> {
961
+ // Audio route changed
962
+ // systemMicUnavailable = false
963
+ Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
964
+ appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN audio_route_changed")
965
+ }
966
+ "recording_started" -> {
967
+ // this is an event from the PhoneMic saying we have started recording
968
+ // Audio recording started
969
+ // systemMicUnavailable = true
970
+ Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
971
+ appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_started")
972
+ }
973
+ "recording_stopped" -> {
974
+ // this is an event from the PhoneMic saying we have stopped recording
975
+ // Audio recording stopped
976
+ // systemMicUnavailable = false
977
+ Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
978
+ appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN recording_stopped")
979
+ }
980
+ else -> {
981
+ // Other route changes (headset plug/unplug, BT connect/disconnect, etc.)
982
+ // Just log for now - may want to handle these in the future
983
+ Bridge.log("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
984
+ appendLog("MAN: MIC_UNAVAILABLE: UNKNOWN other: $reason")
985
+ // systemMicUnavailable = false
986
+ }
987
+ }
988
+
989
+ updateMicState()
990
+ }
991
+
992
+ fun onInterruption(began: Boolean) {
993
+ Bridge.log("MAN: Interruption: $began")
994
+ systemMicUnavailable = began
995
+ updateMicState()
996
+ }
997
+
998
+ // MARK: - Auxiliary Commands
999
+
1000
+ fun initSGC(wearable: String) {
1001
+ Bridge.log("Initializing manager for wearable: $wearable")
1002
+ if (sgc != null && sgc?.type != wearable) {
1003
+ Bridge.log("MAN: Manager already initialized, cleaning up previous sgc")
1004
+ Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
1005
+ sgc?.cleanup()
1006
+ sgc = null
1007
+ }
1008
+
1009
+ if (sgc != null) {
1010
+ Bridge.log("MAN: SGC already initialized")
1011
+ return
1012
+ }
1013
+
1014
+ if (wearable.contains(DeviceTypes.SIMULATED)) {
1015
+ sgc = Simulated()
1016
+ } else if (wearable.contains(DeviceTypes.G1)) {
1017
+ sgc = G1()
1018
+ } else if (wearable.contains(DeviceTypes.G2)) {
1019
+ sgc = G2()
1020
+ } else if (wearable.contains(DeviceTypes.LIVE)) {
1021
+ sgc = MentraLive()
1022
+ } else if (wearable.contains(DeviceTypes.NEX)) {
1023
+ sgc = MentraNex()
1024
+ } else if (wearable.contains(DeviceTypes.MACH1)) {
1025
+ sgc = Mach1()
1026
+ } else if (wearable.contains(DeviceTypes.Z100)) {
1027
+ sgc = Mach1() // Z100 uses same hardware/SDK as Mach1
1028
+ sgc?.type = DeviceTypes.Z100 // Override type to Z100
1029
+ } else if (wearable.contains(DeviceTypes.FRAME)) {
1030
+ // sgc = FrameManager()
1031
+ }
1032
+ // update device model:
1033
+ DeviceStore.apply("glasses", "deviceModel", sgc?.type ?: "")
1034
+ }
1035
+
1036
+ fun initController(controllerType: String) {
1037
+ Bridge.log("MAN: Initializing controller: $controllerType")
1038
+ if (controller != null && controller?.type != controllerType) {
1039
+ Bridge.log("MAN: Controller already initialized, cleaning up previous controller")
1040
+ controller?.cleanup()
1041
+ controller = null
1042
+ }
1043
+
1044
+ if (controller != null) {
1045
+ Bridge.log("MAN: Controller already initialized")
1046
+ return
1047
+ }
1048
+
1049
+ if (controllerType == ControllerTypes.R1) {
1050
+ controller = R1()
1051
+ }
1052
+ }
1053
+
1054
+ fun restartTranscriber() {
1055
+ Bridge.log("MAN: Restarting transcriber via command")
1056
+ transcriber?.restart()
1057
+ }
1058
+
1059
+ // MARK: - connection state management
1060
+
1061
+ fun handleDeviceReady() {
1062
+ if (sgc == null) {
1063
+ Bridge.log("MAN: SGC is null, returning")
1064
+ return
1065
+ }
1066
+
1067
+ val readyKey = "${sgc?.type}:${deviceName}"
1068
+ val now = System.currentTimeMillis()
1069
+ if (readyKey == lastReadyHandledKey && now - lastReadyHandledAtMs < 2000) {
1070
+ Bridge.log("MAN: handleDeviceReady() duplicate suppressed for $readyKey")
1071
+ return
1072
+ }
1073
+ lastReadyHandledKey = readyKey
1074
+ lastReadyHandledAtMs = now
1075
+
1076
+ Bridge.log("MAN: handleDeviceReady() ${sgc?.type}")
1077
+ pendingWearable = ""
1078
+ defaultWearable = sgc?.type ?: ""
1079
+ searching = false
1080
+
1081
+ // Apply dashboard position before any boot text so content doesn't jump.
1082
+ sgc?.setDashboardPosition(dashboardHeight, dashboardDepth)
1083
+
1084
+ // Show welcome message on first connect for all display glasses
1085
+ if (shouldSendBootingMessage) {
1086
+ shouldSendBootingMessage = false
1087
+ executor.execute {
1088
+ sgc?.sendTextWall("// MentraOS Connected")
1089
+ Thread.sleep(3000)
1090
+ sgc?.clearDisplay()
1091
+ }
1092
+ }
1093
+
1094
+ // Call device-specific setup handlers
1095
+ if (defaultWearable.contains(DeviceTypes.G1)) {
1096
+ handleG1Ready()
1097
+ } else if (defaultWearable.contains(DeviceTypes.MACH1)) {
1098
+ handleMach1Ready()
1099
+ } else if (defaultWearable.contains(DeviceTypes.Z100)) {
1100
+ handleMach1Ready() // Z100 uses same initialization as Mach1
1101
+ }
1102
+
1103
+ // Re-apply microphone settings after reconnection
1104
+ // Cache was cleared on disconnect, so this will definitely send commands
1105
+ Bridge.log("MAN: Re-applying microphone settings after reconnection")
1106
+ updateMicState()
1107
+
1108
+ // send to the server our battery status:
1109
+ Bridge.sendBatteryStatus(sgc?.batteryLevel ?: -1, false)
1110
+
1111
+ // save the default_wearable now that we're connected:
1112
+ Bridge.saveSetting("default_wearable", defaultWearable)
1113
+ Bridge.saveSetting("device_name", deviceName)
1114
+ Bridge.saveSetting("device_address", deviceAddress)
1115
+ }
1116
+
1117
+ private fun handleG1Ready() {
1118
+ // G1-specific setup (if any needed in the future)
1119
+ // Note: G1-specific settings like silent mode, battery status,
1120
+ // head up angle, brightness, etc. could be configured here
1121
+ }
1122
+
1123
+ private fun handleMach1Ready() {
1124
+ // Mach1-specific setup (if any needed in the future)
1125
+ }
1126
+
1127
+ fun handleDeviceDisconnected() {
1128
+ Bridge.log("MAN: Device disconnected")
1129
+ DeviceStore.apply("glasses", "headUp", false)
1130
+ }
1131
+
1132
+ fun handleControllerReady() {
1133
+ val c = controller
1134
+ if (c == null) {
1135
+ Bridge.log("MAN: Controller is nil, returning")
1136
+ return
1137
+ }
1138
+ Bridge.log("MAN: handleControllerReady(): ${c.type}")
1139
+
1140
+ pendingController = ""
1141
+ defaultController = c.type
1142
+ searching = false
1143
+
1144
+ // save the default_controller now that we're connected:
1145
+ Bridge.saveSetting("default_controller", defaultController)
1146
+ Bridge.saveSetting("controller_device_name", controllerDeviceName)
1147
+ }
1148
+
1149
+ fun handleControllerDisconnected() {
1150
+ Bridge.log("MAN: Controller disconnected")
1151
+ }
1152
+
1153
+ // MARK: - Network Command handlers
1154
+
1155
+ fun displayText(params: Map<String, Any>) {
1156
+ (params["text"] as? String)?.let { text ->
1157
+ Bridge.log("MAN: Displaying text: $text")
1158
+ sgc?.sendTextWall(text)
1159
+ }
1160
+ }
1161
+
1162
+ fun clearDisplay() {
1163
+ Bridge.log("MAN: Clearing Display")
1164
+ sgc?.clearDisplay()
1165
+ }
1166
+
1167
+ fun displayEvent(event: Map<String, Any>) {
1168
+ val view = event["view"] as? String
1169
+ if (view == null) {
1170
+ Bridge.log("MAN: Invalid view")
1171
+ return
1172
+ }
1173
+
1174
+ val isDashboard = view == "dashboard"
1175
+ val stateIndex = if (isDashboard) 1 else 0
1176
+
1177
+ @Suppress("UNCHECKED_CAST") val layout = event["layout"] as? Map<String, Any> ?: return
1178
+
1179
+ val layoutType = layout["layoutType"] as? String
1180
+ val text = parsePlaceholders(layout.getString("text", " "))
1181
+ val topText = parsePlaceholders(layout.getString("topText", " "))
1182
+ val bottomText = parsePlaceholders(layout.getString("bottomText", " "))
1183
+ val title = parsePlaceholders(layout.getString("title", " "))
1184
+ val data = layout["data"] as? String
1185
+
1186
+ var newViewState = ViewState(topText, bottomText, title, layoutType ?: "", text, data, null)
1187
+
1188
+ val currentState = viewStates[stateIndex]
1189
+
1190
+ if (statesEqual(currentState, newViewState)) {
1191
+ return
1192
+ }
1193
+
1194
+ viewStates[stateIndex] = newViewState
1195
+ val hUp = headUp && contextualDashboard
1196
+ // send the state we just received if the user is currently in that state:
1197
+ if (stateIndex == 0 && !hUp) {
1198
+ sendCurrentState()
1199
+ } else if (stateIndex == 1 && hUp) {
1200
+ sendCurrentState()
1201
+ }
1202
+ }
1203
+
1204
+ fun showDashboard() {
1205
+ sgc?.showDashboard()
1206
+ }
1207
+
1208
+ fun ping() {
1209
+ sgc?.ping()
1210
+ }
1211
+
1212
+ fun dbg1() {
1213
+ Bridge.log("MAN: dbg1()")
1214
+ sgc?.dbg1()
1215
+ }
1216
+
1217
+ fun dbg2() {
1218
+ Bridge.log("MAN: dbg2()")
1219
+ sgc?.dbg2()
1220
+ }
1221
+
1222
+ fun startStream(message: MutableMap<String, Any>) {
1223
+ Bridge.log("MAN: startStream")
1224
+ sgc?.startStream(message)
1225
+ }
1226
+
1227
+ fun stopStream() {
1228
+ Bridge.log("MAN: stopStream")
1229
+ sgc?.stopStream()
1230
+ }
1231
+
1232
+ fun keepStreamAlive(message: MutableMap<String, Any>) {
1233
+ Bridge.log("MAN: keepStreamAlive: (message)")
1234
+ sgc?.sendStreamKeepAlive(message)
1235
+ }
1236
+
1237
+ fun requestWifiScan() {
1238
+ Bridge.log("MAN: Requesting wifi scan")
1239
+ DeviceStore.apply("bluetooth", "wifiScanResults", emptyList<Any>())
1240
+ sgc?.requestWifiScan()
1241
+ }
1242
+
1243
+ fun sendIncidentId(incidentId: String, apiBaseUrl: String? = null) {
1244
+ Bridge.log("MAN: Sending incidentId to glasses for log upload: $incidentId")
1245
+ sgc?.sendIncidentId(incidentId, apiBaseUrl)
1246
+ }
1247
+
1248
+ fun sendWifiCredentials(ssid: String, password: String) {
1249
+ Bridge.log("MAN: Sending wifi credentials: $ssid")
1250
+ sgc?.sendWifiCredentials(ssid, password)
1251
+ }
1252
+
1253
+ fun forgetWifiNetwork(ssid: String) {
1254
+ Bridge.log("MAN: Forgetting wifi network: $ssid")
1255
+ sgc?.forgetWifiNetwork(ssid)
1256
+ }
1257
+
1258
+ fun setHotspotState(enabled: Boolean) {
1259
+ Bridge.log("MAN: Setting glasses hotspot state: $enabled")
1260
+ sgc?.sendHotspotState(enabled)
1261
+ }
1262
+
1263
+ fun queryGalleryStatus() {
1264
+ Bridge.log("MAN: Querying gallery status from glasses")
1265
+ sgc?.queryGalleryStatus()
1266
+ }
1267
+
1268
+ /**
1269
+ * Send OTA start command to glasses. Called when user approves an update (onboarding or
1270
+ * background mode). Triggers glasses to begin download and installation.
1271
+ */
1272
+ fun sendOtaStart() {
1273
+ Bridge.log("MAN: 📱 Sending OTA start command to glasses")
1274
+ (sgc as? MentraLive)?.sendOtaStart()
1275
+ }
1276
+
1277
+ fun sendOtaQueryStatus() {
1278
+ Bridge.log("MAN: 📱 Sending OTA query status command to glasses")
1279
+ (sgc as? MentraLive)?.sendOtaQueryStatus()
1280
+ }
1281
+
1282
+ /**
1283
+ * Read glasses media step volume (0–15) via K900 on Mentra Live only. Blocks until response,
1284
+ * error, or timeout (used from JS AsyncFunction on a worker thread).
1285
+ */
1286
+ fun getGlassesMediaVolumeBlocking(): Map<String, Any> {
1287
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1288
+ val latch = CountDownLatch(1)
1289
+ var result: Map<String, Any>? = null
1290
+ var error: String? = null
1291
+ live.getGlassesMediaVolume(
1292
+ { m ->
1293
+ result = m
1294
+ latch.countDown()
1295
+ },
1296
+ { e ->
1297
+ error = e
1298
+ latch.countDown()
1299
+ }
1300
+ )
1301
+ val completed = latch.await(5, TimeUnit.SECONDS)
1302
+ if (!completed) {
1303
+ throw IllegalStateException("glasses_volume_timeout")
1304
+ }
1305
+ error?.let { throw IllegalStateException(it) }
1306
+ return result ?: throw IllegalStateException("glasses_volume_empty")
1307
+ }
1308
+
1309
+ /** Set glasses media step volume (0–15) via K900 on Mentra Live only. */
1310
+ fun setGlassesMediaVolumeBlocking(level: Int): Map<String, Any> {
1311
+ val live = sgc as? MentraLive ?: throw IllegalStateException("unsupported_device")
1312
+ val latch = CountDownLatch(1)
1313
+ var result: Map<String, Any>? = null
1314
+ var error: String? = null
1315
+ live.setGlassesMediaVolume(
1316
+ level,
1317
+ { m ->
1318
+ result = m
1319
+ latch.countDown()
1320
+ },
1321
+ { e ->
1322
+ error = e
1323
+ latch.countDown()
1324
+ }
1325
+ )
1326
+ val completed = latch.await(5, TimeUnit.SECONDS)
1327
+ if (!completed) {
1328
+ throw IllegalStateException("glasses_volume_timeout")
1329
+ }
1330
+ error?.let { throw IllegalStateException(it) }
1331
+ return result ?: throw IllegalStateException("glasses_volume_empty")
1332
+ }
1333
+
1334
+ /**
1335
+ * Request version info from glasses. Glasses will respond with version_info message containing
1336
+ * build number, firmware version, etc.
1337
+ */
1338
+ fun requestVersionInfo() {
1339
+ Bridge.log("MAN: 📱 Requesting version info from glasses")
1340
+ sgc?.requestVersionInfo()
1341
+ }
1342
+
1343
+ /** Send shutdown command to glasses. This will initiate a graceful shutdown of the device. */
1344
+ fun sendShutdown() {
1345
+ Bridge.log("MAN: 🔌 Sending shutdown command to glasses")
1346
+ sgc?.sendShutdown()
1347
+ }
1348
+
1349
+ /** Send reboot command to glasses. This will initiate a reboot of the device. */
1350
+ fun sendReboot() {
1351
+ Bridge.log("MAN: 🔄 Sending reboot command to glasses")
1352
+ sgc?.sendReboot()
1353
+ }
1354
+
1355
+ fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {
1356
+ Bridge.log(
1357
+ "MAN: onStartVideoRecording: requestId=$requestId, save=$save, flash=$flash, sound=$sound"
1358
+ )
1359
+ sgc?.startVideoRecording(requestId, save, flash, sound)
1360
+ }
1361
+
1362
+ fun stopVideoRecording(requestId: String) {
1363
+ Bridge.log("MAN: onStopVideoRecording: requestId=$requestId")
1364
+ sgc?.stopVideoRecording(requestId)
1365
+ }
1366
+
1367
+ fun setMicState() {
1368
+ val willSendPcm = shouldSendPcm || shouldSendLc3
1369
+ val willSendTranscript = shouldSendTranscript || offlineCaptionsRunning || localSttFallbackActive
1370
+ micEnabled = willSendPcm || willSendTranscript
1371
+ vadBuffer.clear()
1372
+ updateMicState()
1373
+ }
1374
+
1375
+ fun photoRequest(
1376
+ requestId: String,
1377
+ appId: String,
1378
+ size: String,
1379
+ webhookUrl: String,
1380
+ authToken: String?,
1381
+ compress: String,
1382
+ flash: Boolean,
1383
+ sound: Boolean
1384
+ ) {
1385
+ Bridge.log(
1386
+ "MAN: onPhotoRequest: $requestId, $appId, $size, compress=$compress, flash=$flash, sound=$sound"
1387
+ )
1388
+ sgc?.requestPhoto(requestId, appId, size, webhookUrl, authToken, compress, flash, sound)
1389
+ }
1390
+
1391
+ fun rgbLedControl(
1392
+ requestId: String,
1393
+ packageName: String?,
1394
+ action: String,
1395
+ color: String?,
1396
+ ontime: Int,
1397
+ offtime: Int,
1398
+ count: Int
1399
+ ) {
1400
+ Bridge.log("MAN: RGB LED control: action=$action, color=$color, requestId=$requestId")
1401
+ sgc?.sendRgbLedControl(requestId, packageName, action, color, ontime, offtime, count)
1402
+ }
1403
+
1404
+ fun connectDefault() {
1405
+ if (defaultWearable.isEmpty()) {
1406
+ Bridge.log("MAN: No default wearable, returning")
1407
+ return
1408
+ }
1409
+ if (deviceName.isEmpty()) {
1410
+ Bridge.log("MAN: No device name, returning")
1411
+ return
1412
+ }
1413
+ if (!hasBluetoothPermissions()) {
1414
+ // Auto-reconnect paths (boot, BT toggle, app launch before perm flow)
1415
+ // may fire before user has granted runtime Bluetooth permissions on Android 12+.
1416
+ // Bail out instead of crashing with SecurityException on startScan / getRemoteName.
1417
+ Bridge.log("MAN: connectDefault skipped — bluetooth runtime permissions not granted")
1418
+ return
1419
+ }
1420
+ initSGC(defaultWearable)
1421
+ searching = true
1422
+ sgc?.connectById(deviceName)
1423
+ connectDefaultController()
1424
+ }
1425
+
1426
+ fun connectDefaultController() {
1427
+ if (defaultController.isEmpty()) {
1428
+ Bridge.log("MAN: No default controller, returning")
1429
+ return
1430
+ }
1431
+ if (controllerDeviceName.isEmpty()) {
1432
+ Bridge.log("MAN: No controller device name, returning")
1433
+ return
1434
+ }
1435
+ if (!hasBluetoothPermissions()) {
1436
+ Bridge.log("MAN: connectDefaultController skipped — bluetooth runtime permissions not granted")
1437
+ return
1438
+ }
1439
+ initController(defaultController)
1440
+ searchingController = true
1441
+ controller?.connectById(controllerDeviceName)
1442
+ }
1443
+
1444
+ fun connectByName(dName: String) {
1445
+ Bridge.log("MAN: Connecting to wearable: $dName")
1446
+
1447
+ var name = dName
1448
+
1449
+ // use stored device name if available:
1450
+ if (dName.isEmpty() && !deviceName.isEmpty()) {
1451
+ name = deviceName
1452
+ }
1453
+
1454
+ if (pendingWearable.isEmpty() && defaultWearable.isEmpty()) {
1455
+ Bridge.log("MAN: No pending or default wearable, returning")
1456
+ return
1457
+ }
1458
+
1459
+ if (pendingWearable.isEmpty() && !defaultWearable.isEmpty()) {
1460
+ Bridge.log("MAN: No pending wearable, using default wearable")
1461
+ pendingWearable = defaultWearable
1462
+ }
1463
+
1464
+ // if the pending wearable is a controller, don't disconnect the glasses;
1465
+ // route through the controller manager instead
1466
+ if (ControllerTypes.ALL.contains(pendingWearable)) {
1467
+ controller?.disconnect()
1468
+ initController(pendingWearable)
1469
+ controller?.connectById(name)
1470
+ return
1471
+ }
1472
+
1473
+ disconnect()
1474
+ Thread.sleep(100)
1475
+ searching = true
1476
+ deviceName = name
1477
+
1478
+ initSGC(pendingWearable)
1479
+ sgc?.connectById(deviceName)
1480
+ }
1481
+
1482
+ fun connectDevice(deviceModel: String, deviceName: String) {
1483
+ Bridge.log("MAN: Connecting to device: $deviceModel $deviceName")
1484
+ if (DeviceTypes.ALL.contains(deviceModel)) {
1485
+ pendingWearable = deviceModel
1486
+ initSGC(pendingWearable)
1487
+ sgc?.connectById(deviceName)
1488
+ return
1489
+ }
1490
+ if (ControllerTypes.ALL.contains(deviceModel)) {
1491
+ pendingWearable = deviceModel
1492
+ initController(deviceModel)
1493
+ controller?.connectById(deviceName)
1494
+ return
1495
+ }
1496
+ Bridge.log("MAN: No compatible device model, returning")
1497
+ }
1498
+
1499
+ fun connectSimulated() {
1500
+ defaultWearable = DeviceTypes.SIMULATED
1501
+ deviceName = DeviceTypes.SIMULATED
1502
+ initSGC(defaultWearable)
1503
+ handleDeviceReady()
1504
+ }
1505
+
1506
+ fun disconnect() {
1507
+ sgc?.clearDisplay()
1508
+ sgc?.disconnect()
1509
+ sgc = null // Clear the SGC reference after disconnect
1510
+ searching = false
1511
+ micEnabled = false
1512
+ updateMicState()
1513
+ shouldSendBootingMessage = true // Reset for next first connect
1514
+ // clear glasses properties:
1515
+ DeviceStore.apply("glasses", "deviceModel", "")
1516
+ DeviceStore.apply("glasses", "fullyBooted", false)
1517
+ DeviceStore.apply("glasses", "connected", false)
1518
+ // disconnect the controller as well:
1519
+ searchingController = false
1520
+ DeviceStore.apply("glasses", "controllerConnected", false)
1521
+ controller?.disconnect()
1522
+ controller = null
1523
+ }
1524
+
1525
+ fun disconnectController() {
1526
+ searchingController = false
1527
+ // disconnect the controller from the glasses if applicable:
1528
+ sgc?.disconnectController()
1529
+ controller?.disconnect()
1530
+ controller = null
1531
+ }
1532
+
1533
+ fun forget() {
1534
+ Bridge.log("MAN: Forgetting smart glasses")
1535
+
1536
+ // Call forget first to stop timers/handlers/reconnect logic
1537
+ sgc?.forget()
1538
+
1539
+ // Then disconnect to close connections
1540
+ disconnect()
1541
+
1542
+ // Clear state
1543
+ defaultWearable = ""
1544
+ deviceName = ""
1545
+ deviceAddress = ""
1546
+ Bridge.saveSetting("default_wearable", "")
1547
+ Bridge.saveSetting("device_name", "")
1548
+ Bridge.saveSetting("device_address", "")
1549
+ }
1550
+
1551
+ fun forgetController() {
1552
+ Bridge.log("MAN: Forgetting controller")
1553
+ controller?.forget()
1554
+ disconnectController()
1555
+ // Clear state
1556
+ defaultController = ""
1557
+ controllerDeviceName = ""
1558
+ Bridge.saveSetting("controller_device_name", "")
1559
+ Bridge.saveSetting("default_controller", "")
1560
+ DeviceStore.apply("glasses", "controllerConnected", false)
1561
+ }
1562
+
1563
+ fun findCompatibleDevices(deviceModel: String) {
1564
+ Bridge.log("MAN: Searching for compatible device names for: $deviceModel")
1565
+
1566
+ // reset the search results:
1567
+ searchResults = emptyList()
1568
+
1569
+ if (DeviceTypes.ALL.contains(deviceModel)) {
1570
+ pendingWearable = deviceModel
1571
+ }
1572
+
1573
+ if (ControllerTypes.ALL.contains(deviceModel)) {
1574
+ pendingWearable = deviceModel
1575
+ initController(deviceModel)
1576
+ controller?.findCompatibleDevices()
1577
+ return
1578
+ }
1579
+
1580
+ initSGC(pendingWearable)
1581
+ Bridge.log("MAN: sgc initialized, calling findCompatibleDevices")
1582
+ sgc?.findCompatibleDevices()
1583
+ }
1584
+
1585
+ fun stopScan() {
1586
+ controller?.stopScan()
1587
+ sgc?.stopScan()
1588
+ DeviceStore.apply("bluetooth", "searching", false)
1589
+ DeviceStore.apply("bluetooth", "searchingController", false)
1590
+ }
1591
+
1592
+ // MARK: Cleanup
1593
+ fun cleanup() {
1594
+ stopBluetoothStateMonitoring()
1595
+
1596
+ micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
1597
+ micReinitRunnable = null
1598
+
1599
+ // Clean up transcriber resources
1600
+ transcriber?.shutdown()
1601
+ transcriber = null
1602
+
1603
+ // Clean up LC3 encoder/decoder (synchronized to prevent use-after-free
1604
+ // if the recording thread is mid-encode/decode)
1605
+ synchronized(lc3Lock) {
1606
+ if (lc3EncoderPtr != 0L) {
1607
+ Lc3Cpp.freeEncoder(lc3EncoderPtr)
1608
+ lc3EncoderPtr = 0
1609
+ }
1610
+ if (lc3DecoderPtr != 0L) {
1611
+ Lc3Cpp.freeDecoder(lc3DecoderPtr)
1612
+ lc3DecoderPtr = 0
1613
+ }
1614
+ }
1615
+ }
1616
+ }