@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,934 @@
1
+ package com.mentra.bluetoothsdk.controllers
2
+
3
+ import android.Manifest
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothGatt
7
+ import android.bluetooth.BluetoothGattCallback
8
+ import android.bluetooth.BluetoothGattCharacteristic
9
+ import android.bluetooth.BluetoothGattDescriptor
10
+ import android.bluetooth.le.BluetoothLeScanner
11
+ import android.bluetooth.le.ScanCallback
12
+ import android.bluetooth.le.ScanResult
13
+ import android.bluetooth.le.ScanSettings
14
+ import android.content.Context
15
+ import android.content.pm.PackageManager
16
+ import android.os.Build
17
+ import android.os.Handler
18
+ import android.os.Looper
19
+ import androidx.core.content.ContextCompat
20
+ import com.mentra.bluetoothsdk.Bridge
21
+ import com.mentra.bluetoothsdk.DeviceManager
22
+ import com.mentra.bluetoothsdk.DeviceStore
23
+ import com.mentra.bluetoothsdk.utils.ControllerTypes
24
+ import java.util.UUID
25
+ import kotlinx.coroutines.CoroutineScope
26
+ import kotlinx.coroutines.Dispatchers
27
+ import kotlinx.coroutines.Job
28
+ import kotlinx.coroutines.delay
29
+ import kotlinx.coroutines.isActive
30
+ import kotlinx.coroutines.launch
31
+
32
+ // MARK: - R1 BLE Constants
33
+
34
+ private object R1BLE {
35
+ val SERVICE_UUID: UUID = UUID.fromString("BAE80001-4F05-4503-8E65-3AF1F7329D1F")
36
+ val WRITE_CHAR_1: UUID = UUID.fromString("BAE80010-4F05-4503-8E65-3AF1F7329D1F")
37
+ val NOTIFY_CHAR_1: UUID = UUID.fromString("BAE80011-4F05-4503-8E65-3AF1F7329D1F")
38
+ val WRITE_CHAR_2: UUID = UUID.fromString("BAE80012-4F05-4503-8E65-3AF1F7329D1F")
39
+ val NOTIFY_CHAR_2: UUID = UUID.fromString("BAE80013-4F05-4503-8E65-3AF1F7329D1F")
40
+ val BATTERY_SERVICE: UUID = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb")
41
+ val BATTERY_LEVEL_CHAR: UUID = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")
42
+ val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
43
+
44
+ val NAME_FILTERS = arrayOf("EVEN R1", "BCL60")
45
+
46
+ val CONFIG_FC = byteArrayOf(0xFC.toByte())
47
+ val CONFIG_11 = byteArrayOf(0x11.toByte())
48
+
49
+ // BleRing1 command header (cmd, module, subCmd) for advStart.
50
+ // From RE: BleRing1Cmd_system=0, BleRing1Module_system=0, BleRing1SubCmd_advStart=9
51
+ const val CMD_SYSTEM: Byte = 0x00
52
+ const val MODULE_SYSTEM: Byte = 0x00
53
+ const val SUBCMD_ADV_START: Byte = 0x09
54
+
55
+ const val GESTURE_MARKER: Byte = 0xFF.toByte()
56
+ }
57
+
58
+ // MARK: - R1 Gesture Types
59
+
60
+ private enum class R1Gesture(val rawValue: String) {
61
+ HOLD("hold"),
62
+ SINGLE_TAP("single_tap"),
63
+ DOUBLE_TAP("double_tap"),
64
+ SWIPE_UP("swipe_up"),
65
+ SWIPE_DOWN("swipe_down");
66
+
67
+ companion object {
68
+ /** Parse gesture from notification data: [0xFF, type, param] */
69
+ fun parse(data: ByteArray): R1Gesture? {
70
+ if (data.size < 3 || data[0] != R1BLE.GESTURE_MARKER) return null
71
+ return when (data[1]) {
72
+ 0x03.toByte() -> HOLD
73
+ 0x04.toByte() -> when (data[2]) {
74
+ 0x01.toByte() -> SINGLE_TAP
75
+ 0x02.toByte() -> DOUBLE_TAP
76
+ else -> null
77
+ }
78
+ 0x05.toByte() -> if ((data[2].toInt() and 0xFF) < 0x80) SWIPE_UP else SWIPE_DOWN
79
+ else -> null
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ // MARK: - R1 Controller
86
+
87
+ class R1 : ControllerManager() {
88
+
89
+ // Connection state
90
+ private val appContext: Context = Bridge.getContext()
91
+ private val prefs = appContext.getSharedPreferences("r1_prefs", Context.MODE_PRIVATE)
92
+
93
+ private var bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
94
+ private var scanner: BluetoothLeScanner? = null
95
+ private var scanCallback: ScanCallback? = null
96
+
97
+ private var ringGatt: BluetoothGatt? = null
98
+ private var isDisconnecting = false
99
+
100
+ // BLE characteristics
101
+ private var writeChar1: BluetoothGattCharacteristic? = null
102
+ private var notifyChar1: BluetoothGattCharacteristic? = null
103
+ private var writeChar2: BluetoothGattCharacteristic? = null
104
+ private var notifyChar2: BluetoothGattCharacteristic? = null
105
+ private var batteryLevelChar: BluetoothGattCharacteristic? = null
106
+ private var notifySubscriptionCount = 0
107
+ private var initSequenceRun = false
108
+
109
+ // Android serializes CCCD writes and reads — queue and drain in callbacks
110
+ private val pendingDescriptorWrites = ArrayDeque<BluetoothGattDescriptor>()
111
+ private val pendingReads = ArrayDeque<BluetoothGattCharacteristic>()
112
+ private var descriptorWriteInFlight = false
113
+ private var readInFlight = false
114
+
115
+ // Device search
116
+ private var deviceSearchId: String = "NOT_SET"
117
+
118
+ // BLE handle used by Android for reconnection (BluetoothDevice.address — may be a
119
+ // resolvable/random address depending on bonding state; not the ring's public MAC).
120
+ private var ringBleAddress: String?
121
+ get() = prefs.getString("r1_ringBleAddress", null)
122
+ set(value) {
123
+ prefs.edit().apply {
124
+ if (value == null) remove("r1_ringBleAddress") else putString("r1_ringBleAddress", value)
125
+ apply()
126
+ }
127
+ }
128
+
129
+ // Public ring MAC parsed from advertisement manufacturer data (last 6 bytes), formatted
130
+ // as "AA:BB:CC:DD:EE:FF". This is what gets published to DeviceStore/controllerMacAddress.
131
+ private var ringMacAddress: String?
132
+ get() = prefs.getString("r1_ringMacAddress", null)
133
+ set(value) {
134
+ prefs.edit().apply {
135
+ if (value == null) remove("r1_ringMacAddress") else putString("r1_ringMacAddress", value)
136
+ apply()
137
+ }
138
+ }
139
+
140
+ // peripheral name -> 6-byte ring MAC (hex string), populated from mfgData on every scan.
141
+ // Mirrors iOS ringMacAddressMap so reconnects re-validate freshness instead of trusting
142
+ // a stale stored MAC.
143
+ private fun loadRingMacAddressMap(): MutableMap<String, String> {
144
+ val raw = prefs.getString("r1_ringMacAddressMap", null) ?: return mutableMapOf()
145
+ val out = mutableMapOf<String, String>()
146
+ for (entry in raw.split(';')) {
147
+ if (entry.isEmpty()) continue
148
+ val idx = entry.indexOf('=')
149
+ if (idx <= 0) continue
150
+ out[entry.substring(0, idx)] = entry.substring(idx + 1)
151
+ }
152
+ return out
153
+ }
154
+
155
+ private fun saveRingMacAddressMap(map: Map<String, String>) {
156
+ val raw = map.entries.joinToString(";") { "${it.key}=${it.value}" }
157
+ prefs.edit().putString("r1_ringMacAddressMap", raw).apply()
158
+ }
159
+
160
+ private fun putRingMacInMap(name: String, mac: String) {
161
+ val map = loadRingMacAddressMap()
162
+ map[name] = mac
163
+ saveRingMacAddressMap(map)
164
+ }
165
+
166
+ private fun getRingMacFromMap(name: String): String? = loadRingMacAddressMap()[name]
167
+
168
+ // Reconnection (defined but currently unwired — matches iOS which leaves it commented)
169
+ private val reconnectionManager = R1ReconnectionManager()
170
+
171
+ // Battery
172
+ private var _batteryLevel: Int = -1
173
+ set(value) {
174
+ val old = field
175
+ field = value
176
+ if (value != old && value >= 0) {
177
+ DeviceStore.apply("glasses", "controllerBatteryLevel", value)
178
+ }
179
+ }
180
+
181
+ // Heartbeat
182
+ private val mainHandler = Handler(Looper.getMainLooper())
183
+ private var heartbeatRunnable: Runnable? = null
184
+
185
+ init {
186
+ type = ControllerTypes.R1
187
+ hasMic = false
188
+ }
189
+
190
+ // MARK: - Permissions
191
+
192
+ private fun hasScanPermission(): Boolean {
193
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
194
+ return ContextCompat.checkSelfPermission(
195
+ appContext, Manifest.permission.BLUETOOTH_SCAN
196
+ ) == PackageManager.PERMISSION_GRANTED
197
+ }
198
+
199
+ private fun hasConnectPermission(): Boolean {
200
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
201
+ return ContextCompat.checkSelfPermission(
202
+ appContext, Manifest.permission.BLUETOOTH_CONNECT
203
+ ) == PackageManager.PERMISSION_GRANTED
204
+ }
205
+
206
+ // MARK: - BLE Scanning
207
+
208
+ private fun startScan(): Boolean {
209
+ Bridge.log("R1: startScan()")
210
+ val adapter = bluetoothAdapter ?: BluetoothAdapter.getDefaultAdapter().also { bluetoothAdapter = it }
211
+ if (adapter == null) {
212
+ Bridge.log("R1: No Bluetooth adapter")
213
+ return false
214
+ }
215
+ if (!adapter.isEnabled) {
216
+ Bridge.log("R1: Bluetooth not powered on")
217
+ return false
218
+ }
219
+ if (!hasScanPermission() || !hasConnectPermission()) {
220
+ Bridge.log("R1: Missing Bluetooth permissions")
221
+ return false
222
+ }
223
+
224
+ // Already connected — don't start a new scan
225
+ if (ringGatt != null) {
226
+ Bridge.log("R1: Already connected, skipping scan")
227
+ return true
228
+ }
229
+
230
+ isDisconnecting = false
231
+
232
+ // Stop any prior scan before starting a new one (avoids leaking ScanCallback)
233
+ stopScan()
234
+
235
+ // Try address-based reconnection first
236
+ if (connectByBleAddress()) {
237
+ return true
238
+ }
239
+
240
+ val s = adapter.bluetoothLeScanner
241
+ if (s == null) {
242
+ Bridge.log("R1: BluetoothLeScanner not available")
243
+ return false
244
+ }
245
+ scanner = s
246
+
247
+ val cb = object : ScanCallback() {
248
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
249
+ if (result == null) return
250
+ handleScanResult(result)
251
+ }
252
+
253
+ override fun onScanFailed(errorCode: Int) {
254
+ Bridge.log("R1: Scan failed: $errorCode")
255
+ }
256
+ }
257
+ scanCallback = cb
258
+ val settings = ScanSettings.Builder()
259
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
260
+ .build()
261
+ try {
262
+ s.startScan(null, settings, cb)
263
+ } catch (e: SecurityException) {
264
+ Bridge.log("R1: startScan SecurityException: ${e.message}")
265
+ return false
266
+ }
267
+ return true
268
+ }
269
+
270
+ override fun stopScan() {
271
+ val s = scanner ?: return
272
+ val cb = scanCallback ?: return
273
+ try {
274
+ s.stopScan(cb)
275
+ } catch (e: SecurityException) {
276
+ Bridge.log("R1: stopScan SecurityException: ${e.message}")
277
+ }
278
+ scanCallback = null
279
+ }
280
+
281
+ private fun connectByBleAddress(): Boolean {
282
+ if (deviceSearchId == "NOT_SET" || deviceSearchId.isEmpty()) {
283
+ Bridge.log("R1: No deviceSearchId set, skipping connect by address")
284
+ return false
285
+ }
286
+ val address = ringBleAddress ?: return false
287
+ val adapter = bluetoothAdapter ?: return false
288
+ if (ringGatt != null) {
289
+ Bridge.log("R1: connectByBleAddress skipped — already connected")
290
+ return true
291
+ }
292
+ val device = try {
293
+ adapter.getRemoteDevice(address)
294
+ } catch (e: IllegalArgumentException) {
295
+ Bridge.log("R1: Invalid stored BLE address: $address")
296
+ return false
297
+ }
298
+ try {
299
+ ringGatt = device.connectGatt(appContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
300
+ Bridge.log("R1: Reconnecting by address to ${device.name ?: address}")
301
+ } catch (e: SecurityException) {
302
+ Bridge.log("R1: connectGatt SecurityException: ${e.message}")
303
+ return false
304
+ }
305
+ return true
306
+ }
307
+
308
+ private fun matchesNameFilter(name: String?): Boolean {
309
+ if (name == null) return false
310
+ return R1BLE.NAME_FILTERS.any { name.contains(it) }
311
+ }
312
+
313
+ /** Extract a device identifier from the ring name (e.g. "EVEN R1_CEC5BA" -> "CEC5BA") */
314
+ private fun extractRingId(name: String): String? {
315
+ val idx = name.indexOf("R1_")
316
+ if (idx < 0) return null
317
+ val id = name.substring(idx + 3)
318
+ return if (id.isEmpty()) null else id
319
+ }
320
+
321
+ private fun handleScanResult(result: ScanResult) {
322
+ val device = result.device ?: return
323
+ val advertisedName = result.scanRecord?.deviceName
324
+ val deviceName: String? = try {
325
+ device.name ?: advertisedName
326
+ } catch (e: SecurityException) {
327
+ advertisedName
328
+ }
329
+ mainHandler.post {
330
+ // Already connected — ignore further scan results
331
+ if (ringGatt != null) {
332
+ stopScan()
333
+ return@post
334
+ }
335
+ if (!matchesNameFilter(deviceName)) return@post
336
+
337
+ val mfgMap = result.scanRecord?.manufacturerSpecificData
338
+ val mfgBytes: ByteArray? = if (mfgMap != null && mfgMap.size() > 0) mfgMap.valueAt(0) else null
339
+ val mfgHex = mfgBytes?.joinToString(" ") { String.format("%02X", it) } ?: "none"
340
+ Bridge.log("R1: Discovered: ${deviceName ?: "?"} (RSSI: ${result.rssi}) mfgData: $mfgHex")
341
+
342
+ // Extract ring MAC from manufacturer data (last 6 bytes) and store name->MAC map.
343
+ // Android's manufacturerSpecificData strips the 2-byte company ID; iOS keeps it. The
344
+ // resulting "last 6 bytes" land in reversed byte order between platforms (verified in
345
+ // the field), so reverse here to match iOS's stored string ("1B:08:26:8E:0E:E6").
346
+ if (deviceName != null && mfgBytes != null && mfgBytes.size >= 6) {
347
+ val tail = mfgBytes.copyOfRange(mfgBytes.size - 6, mfgBytes.size)
348
+ val macStr = tail.joinToString(":") { String.format("%02X", it) }
349
+ putRingMacInMap(deviceName, macStr)
350
+ }
351
+
352
+ // Emit discovered device
353
+ val id = deviceName?.let { extractRingId(it) }
354
+ if (id != null) {
355
+ Bridge.sendDiscoveredDevice(ControllerTypes.R1, id)
356
+ }
357
+
358
+ // Scan-only mode: don't auto-connect
359
+ if (deviceSearchId == "NOT_SET") return@post
360
+
361
+ // If search ID is specific, check it matches
362
+ val name = deviceName ?: return@post
363
+ val parsedId = extractRingId(name)
364
+ if (parsedId != deviceSearchId && !name.contains(deviceSearchId)) return@post
365
+
366
+ if (ringGatt == null) {
367
+ ringBleAddress = device.address
368
+ try {
369
+ ringGatt = device.connectGatt(appContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
370
+ } catch (e: SecurityException) {
371
+ Bridge.log("R1: connectGatt SecurityException: ${e.message}")
372
+ return@post
373
+ }
374
+ stopScan()
375
+ Bridge.log("R1: Connecting to ${name}")
376
+ }
377
+ }
378
+ }
379
+
380
+ // MARK: - Init Sequence
381
+
382
+ private fun runInitSequence() {
383
+ if (initSequenceRun) return
384
+ initSequenceRun = true
385
+
386
+ Bridge.log("R1: Running init sequence")
387
+
388
+ val writeChars = listOfNotNull(writeChar1, writeChar2)
389
+ if (writeChars.isEmpty()) {
390
+ Bridge.log("R1: No write characteristics found, skipping init")
391
+ markConnected()
392
+ return
393
+ }
394
+
395
+ writeChars.forEach { writeNoResponse(it, R1BLE.CONFIG_FC) }
396
+
397
+ mainHandler.postDelayed({
398
+ writeChars.forEach { writeNoResponse(it, R1BLE.CONFIG_11) }
399
+ markConnected()
400
+ }, 200)
401
+ }
402
+
403
+ private fun writeNoResponse(char: BluetoothGattCharacteristic, data: ByteArray) {
404
+ char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
405
+ char.value = data
406
+ try {
407
+ ringGatt?.writeCharacteristic(char)
408
+ } catch (e: SecurityException) {
409
+ Bridge.log("R1: writeCharacteristic SecurityException: ${e.message}")
410
+ }
411
+ }
412
+
413
+ private fun markConnected() {
414
+ reconnectionManager.stop()
415
+ Bridge.log("R1: Ring connected")
416
+
417
+ val gatt = ringGatt
418
+ val connectedName = try { gatt?.device?.name } catch (e: SecurityException) { null }
419
+ if (connectedName != null) {
420
+ extractRingId(connectedName)?.let {
421
+ DeviceStore.apply("bluetooth", "controller_device_name", it)
422
+ }
423
+ }
424
+
425
+ val mac = ringMacAddress
426
+ if (mac == null) {
427
+ Bridge.log("R1: No ring MAC address found")
428
+ return
429
+ }
430
+ DeviceStore.apply("glasses", "controllerMacAddress", mac)
431
+ DeviceStore.apply("glasses", "controllerConnected", true)
432
+ // DeviceStore.apply("glasses", "controllerFullyBooted", true)
433
+
434
+ // tell the ring to connect to the glasses if we have its mac address:
435
+ connectToGlasses()
436
+
437
+ // after a second, connect the glasses to the controller if needed:
438
+ CoroutineScope(Dispatchers.Main).launch {
439
+ delay(1000)
440
+ DeviceManager.getInstance().sgc?.connectController()
441
+ }
442
+
443
+ startHeartbeat()
444
+ }
445
+
446
+ /**
447
+ * Tells the ring to start advertising / connect to the glasses.
448
+ * Sends BleRing1 advStart (cmd=0, module=0, subCmd=9) with the 6-byte glasses MAC as payload
449
+ * to WRITE_CHAR_2 (BAE80012-…). Reverse-engineered from the Even Realities mobile app
450
+ * (BleRing1CmdProto::advStart -> BleRing1CmdPublicExt.sendCmd).
451
+ *
452
+ * TODO: BleRing1CmdPublicExt.sendCmd may add additional outer framing (length/seq/CRC) around
453
+ * the 3-byte header + MAC. If the ring rejects this raw payload, decode the wrapper.
454
+ */
455
+ private fun connectToGlasses() {
456
+ // Try DeviceStore first; fall back to cached value in SharedPreferences.
457
+ val glassesMac = (DeviceStore.get("glasses", "btMacAddress") as? String)
458
+ ?: prefs.getString("glasses_btMacAddress", null)
459
+ if (glassesMac == null) {
460
+ Bridge.log("R1: connectToGlasses: no glasses MAC")
461
+ return
462
+ }
463
+ // Cache so we can reconnect even before the glasses are scanned.
464
+ prefs.edit().putString("glasses_btMacAddress", glassesMac).apply()
465
+
466
+ val macBytes = parseMac(glassesMac)
467
+ if (macBytes == null) {
468
+ Bridge.log("R1: connectToGlasses: could not parse glasses MAC")
469
+ return
470
+ }
471
+ val wc = writeChar2 ?: writeChar1
472
+ if (wc == null) {
473
+ Bridge.log("R1: connectToGlasses: no write characteristic")
474
+ return
475
+ }
476
+
477
+ val payload = byteArrayOf(R1BLE.CMD_SYSTEM, R1BLE.MODULE_SYSTEM, R1BLE.SUBCMD_ADV_START) + macBytes
478
+ Bridge.log("R1: advStart sent")
479
+
480
+ wc.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
481
+ wc.value = payload
482
+ try {
483
+ ringGatt?.writeCharacteristic(wc)
484
+ } catch (e: SecurityException) {
485
+ Bridge.log("R1: connectToGlasses writeCharacteristic SecurityException: ${e.message}")
486
+ }
487
+ }
488
+
489
+ /** Parse "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 raw bytes. */
490
+ private fun parseMac(s: String): ByteArray? {
491
+ val cleaned = s.replace(":", "").replace("-", "")
492
+ if (cleaned.length != 12) return null
493
+ val out = ByteArray(6)
494
+ for (i in 0 until 6) {
495
+ val byte = cleaned.substring(i * 2, i * 2 + 2).toIntOrNull(16) ?: return null
496
+ out[i] = byte.toByte()
497
+ }
498
+ return out
499
+ }
500
+
501
+ // MARK: - Heartbeat
502
+
503
+ private fun startHeartbeat() {
504
+ stopHeartbeat()
505
+ val r = object : Runnable {
506
+ override fun run() {
507
+ mainHandler.postDelayed(this, 30_000L)
508
+ val isConnected = DeviceStore.get("glasses", "controllerConnected") as? Boolean ?: false
509
+ if (!isConnected) return
510
+ val char = batteryLevelChar
511
+ if (char != null) {
512
+ try {
513
+ ringGatt?.readCharacteristic(char)
514
+ } catch (e: SecurityException) {
515
+ Bridge.log("R1: heartbeat read SecurityException: ${e.message}")
516
+ }
517
+ }
518
+ }
519
+ }
520
+ heartbeatRunnable = r
521
+ mainHandler.postDelayed(r, 30_000L)
522
+ }
523
+
524
+ private fun stopHeartbeat() {
525
+ heartbeatRunnable?.let { mainHandler.removeCallbacks(it) }
526
+ heartbeatRunnable = null
527
+ }
528
+
529
+ // MARK: - Incoming Data Handling
530
+
531
+ private fun handleNotification(characteristic: BluetoothGattCharacteristic, data: ByteArray) {
532
+ val hex = data.joinToString(" ") { String.format("%02X", it) }
533
+ val shortUuid = characteristic.uuid.toString().takeLast(4)
534
+ Bridge.log("R1: $shortUuid -> $hex")
535
+
536
+ val timestamp = System.currentTimeMillis()
537
+
538
+ // Gesture: [0xFF, type, param]
539
+ if (data.size >= 3 && data[0] == R1BLE.GESTURE_MARKER) {
540
+ val gesture = R1Gesture.parse(data)
541
+ if (gesture != null) {
542
+ Bridge.log("R1: Gesture: ${gesture.rawValue}")
543
+ Bridge.sendTouchEvent(ControllerTypes.R1, gesture.rawValue, timestamp)
544
+ } else {
545
+ Bridge.log(
546
+ "R1: Unknown gesture type=0x%02X param=0x%02X".format(data[1], data[2])
547
+ )
548
+ }
549
+ return
550
+ }
551
+
552
+ // Battery: 2 bytes, first byte is percentage
553
+ if (data.size == 2 && (data[0].toInt() and 0xFF) <= 100) {
554
+ _batteryLevel = data[0].toInt() and 0xFF
555
+ Bridge.log("R1: Battery: ${_batteryLevel}%")
556
+ return
557
+ }
558
+
559
+ // State: single byte (0x01=ready, 0x00=menu)
560
+ if (data.size == 1) {
561
+ val state = when (data[0]) {
562
+ 0x01.toByte() -> "ready"
563
+ 0x00.toByte() -> "menu"
564
+ else -> "unknown(${data[0].toInt() and 0xFF})"
565
+ }
566
+ Bridge.log("R1: State: $state")
567
+ return
568
+ }
569
+
570
+ // Longer data: check for embedded gesture marker
571
+ if (data.size > 3) {
572
+ val ffIndex = data.indexOfFirst { it == R1BLE.GESTURE_MARKER }
573
+ if (ffIndex >= 0 && ffIndex + 2 < data.size) {
574
+ val slice = byteArrayOf(data[ffIndex], data[ffIndex + 1], data[ffIndex + 2])
575
+ val gesture = R1Gesture.parse(slice)
576
+ if (gesture != null) {
577
+ Bridge.log("R1: Embedded gesture: ${gesture.rawValue}")
578
+ Bridge.sendTouchEvent(ControllerTypes.R1, gesture.rawValue, timestamp)
579
+ return
580
+ }
581
+ }
582
+ }
583
+ }
584
+
585
+ // MARK: - Connection State Reset
586
+
587
+ private fun resetConnectionState() {
588
+ ringGatt = null
589
+ writeChar1 = null
590
+ notifyChar1 = null
591
+ writeChar2 = null
592
+ notifyChar2 = null
593
+ batteryLevelChar = null
594
+ notifySubscriptionCount = 0
595
+ initSequenceRun = false
596
+ pendingDescriptorWrites.clear()
597
+ pendingReads.clear()
598
+ descriptorWriteInFlight = false
599
+ readInFlight = false
600
+ ringMacAddress = null
601
+ ringBleAddress = null
602
+ DeviceStore.apply("glasses", "controllerConnected", false)
603
+ DeviceStore.apply("glasses", "controllerFullyBooted", false)
604
+ }
605
+
606
+ // MARK: - GATT Callback
607
+
608
+ private val gattCallback = object : BluetoothGattCallback() {
609
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
610
+ when (newState) {
611
+ BluetoothGatt.STATE_CONNECTED -> {
612
+ val name = try { gatt.device.name } catch (e: SecurityException) { null }
613
+ Bridge.log("R1: Connected to ${name ?: "ring"}")
614
+
615
+ // Validate against the name->MAC map populated during scan. If the connected
616
+ // peripheral isn't in the map we don't have its true public MAC — drop the
617
+ // connection and rescan rather than publishing a stale/wrong MAC. Mirrors
618
+ // iOS R1.swift didConnect.
619
+ val mappedMac = name?.let { getRingMacFromMap(it) }
620
+ if (mappedMac == null) {
621
+ Bridge.log("R1: No MAC stored in map found for ${name ?: "ring"}")
622
+ mainHandler.post {
623
+ disconnect()
624
+ ringBleAddress = null
625
+ DeviceStore.apply("glasses", "controllerConnected", false)
626
+ DeviceStore.apply("glasses", "controllerFullyBooted", false)
627
+ DeviceStore.apply("glasses", "controllerSearching", true)
628
+ mainHandler.postDelayed({ startScan() }, 1000)
629
+ }
630
+ return
631
+ }
632
+ ringMacAddress = mappedMac
633
+
634
+ try {
635
+ gatt.discoverServices()
636
+ } catch (e: SecurityException) {
637
+ Bridge.log("R1: discoverServices SecurityException: ${e.message}")
638
+ }
639
+ }
640
+ BluetoothGatt.STATE_DISCONNECTED -> {
641
+ Bridge.log("R1: Disconnected (status=$status)")
642
+ mainHandler.post {
643
+ stopHeartbeat()
644
+ try { gatt.close() } catch (e: SecurityException) {}
645
+ if (isDisconnecting) return@post
646
+ resetConnectionState()
647
+ // iOS leaves reconnection timer commented; match that behavior
648
+ }
649
+ }
650
+ }
651
+ }
652
+
653
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
654
+ if (status != BluetoothGatt.GATT_SUCCESS) {
655
+ Bridge.log("R1: onServicesDiscovered status=$status")
656
+ return
657
+ }
658
+ mainHandler.post {
659
+ val services = gatt.services ?: return@post
660
+ for (service in services) {
661
+ for (char in service.characteristics) {
662
+ Bridge.log("R1: char discovered: ${char.uuid}")
663
+ val props = char.properties
664
+ val propStr = buildList {
665
+ if (props and BluetoothGattCharacteristic.PROPERTY_READ != 0) add("read")
666
+ if (props and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) add("write")
667
+ if (props and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0) add("writeNoResp")
668
+ if (props and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) add("notify")
669
+ if (props and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) add("indicate")
670
+ }.joinToString(",")
671
+ Bridge.log("R1: char ${char.uuid} props=[$propStr]")
672
+
673
+ // Store known characteristics
674
+ when (char.uuid) {
675
+ R1BLE.WRITE_CHAR_1 -> writeChar1 = char
676
+ R1BLE.NOTIFY_CHAR_1 -> notifyChar1 = char
677
+ R1BLE.WRITE_CHAR_2 -> writeChar2 = char
678
+ R1BLE.NOTIFY_CHAR_2 -> notifyChar2 = char
679
+ R1BLE.BATTERY_LEVEL_CHAR -> batteryLevelChar = char
680
+ }
681
+
682
+ // Subscribe to any notify/indicate characteristic
683
+ val notifyEligible = props and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0
684
+ val indicateEligible = props and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0
685
+ if (notifyEligible || indicateEligible) {
686
+ try {
687
+ gatt.setCharacteristicNotification(char, true)
688
+ } catch (e: SecurityException) {
689
+ Bridge.log("R1: setCharacteristicNotification SecurityException: ${e.message}")
690
+ }
691
+ val descriptor = char.getDescriptor(R1BLE.CCCD_UUID)
692
+ if (descriptor != null) {
693
+ descriptor.value = if (notifyEligible) {
694
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
695
+ } else {
696
+ BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
697
+ }
698
+ pendingDescriptorWrites.addLast(descriptor)
699
+ }
700
+ }
701
+
702
+ // Queue reads (run after CCCD queue drains)
703
+ if (props and BluetoothGattCharacteristic.PROPERTY_READ != 0) {
704
+ pendingReads.addLast(char)
705
+ }
706
+ }
707
+ }
708
+ drainDescriptorQueue(gatt)
709
+ }
710
+ }
711
+
712
+ override fun onDescriptorWrite(
713
+ gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int
714
+ ) {
715
+ mainHandler.post {
716
+ descriptorWriteInFlight = false
717
+ if (status != BluetoothGatt.GATT_SUCCESS) {
718
+ Bridge.log("R1: Descriptor write failed: $status on ${descriptor.characteristic.uuid}")
719
+ } else {
720
+ Bridge.log("R1: Notify enabled on ${descriptor.characteristic.uuid}")
721
+ notifySubscriptionCount += 1
722
+ if (notifySubscriptionCount >= 2 && !initSequenceRun) {
723
+ mainHandler.postDelayed({ runInitSequence() }, 300)
724
+ }
725
+ }
726
+ drainDescriptorQueue(gatt)
727
+ }
728
+ }
729
+
730
+ override fun onCharacteristicChanged(
731
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic
732
+ ) {
733
+ val data = characteristic.value ?: return
734
+ if (data.isEmpty()) return
735
+ mainHandler.post { handleNotification(characteristic, data) }
736
+ }
737
+
738
+ override fun onCharacteristicRead(
739
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
740
+ ) {
741
+ val data = characteristic.value
742
+ mainHandler.post {
743
+ readInFlight = false
744
+ if (status == BluetoothGatt.GATT_SUCCESS && data != null && data.isNotEmpty()) {
745
+ if (characteristic.uuid == R1BLE.BATTERY_LEVEL_CHAR) {
746
+ _batteryLevel = data[0].toInt() and 0xFF
747
+ Bridge.log("R1: Battery (std): ${_batteryLevel}%")
748
+ } else {
749
+ handleNotification(characteristic, data)
750
+ }
751
+ }
752
+ drainReadQueue(gatt)
753
+ }
754
+ }
755
+
756
+ override fun onCharacteristicWrite(
757
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
758
+ ) {
759
+ if (status != BluetoothGatt.GATT_SUCCESS) {
760
+ Bridge.log("R1: Write error on ${characteristic.uuid}: $status")
761
+ }
762
+ }
763
+ }
764
+
765
+ private fun drainDescriptorQueue(gatt: BluetoothGatt) {
766
+ if (descriptorWriteInFlight) return
767
+ val next = pendingDescriptorWrites.removeFirstOrNull()
768
+ if (next == null) {
769
+ // Once CCCD queue drained, start reads
770
+ drainReadQueue(gatt)
771
+ return
772
+ }
773
+ descriptorWriteInFlight = true
774
+ try {
775
+ gatt.writeDescriptor(next)
776
+ } catch (e: SecurityException) {
777
+ descriptorWriteInFlight = false
778
+ Bridge.log("R1: writeDescriptor SecurityException: ${e.message}")
779
+ drainDescriptorQueue(gatt)
780
+ }
781
+ }
782
+
783
+ private fun drainReadQueue(gatt: BluetoothGatt) {
784
+ if (readInFlight) return
785
+ val next = pendingReads.removeFirstOrNull() ?: return
786
+ readInFlight = true
787
+ try {
788
+ gatt.readCharacteristic(next)
789
+ } catch (e: SecurityException) {
790
+ readInFlight = false
791
+ Bridge.log("R1: readCharacteristic SecurityException: ${e.message}")
792
+ drainReadQueue(gatt)
793
+ }
794
+ }
795
+
796
+ // MARK: - ControllerManager overrides
797
+
798
+ override fun findCompatibleDevices() {
799
+ Bridge.log("R1: findCompatibleDevices()")
800
+ deviceSearchId = "NOT_SET"
801
+ startScan()
802
+ }
803
+
804
+ override fun connectById(id: String) {
805
+ Bridge.log("R1: connectById($id)")
806
+ deviceSearchId = id
807
+ startScan()
808
+ }
809
+
810
+ override fun disconnect() {
811
+ Bridge.log("R1: disconnect()")
812
+ isDisconnecting = true
813
+ stopHeartbeat()
814
+ reconnectionManager.stop()
815
+ try {
816
+ ringGatt?.disconnect()
817
+ ringGatt?.close()
818
+ } catch (e: SecurityException) {
819
+ Bridge.log("R1: disconnect SecurityException: ${e.message}")
820
+ }
821
+ resetConnectionState()
822
+ }
823
+
824
+ override fun forget() {
825
+ disconnect()
826
+ ringMacAddress = null
827
+ ringBleAddress = null
828
+ prefs.edit().remove("r1_ringMacAddressMap").apply()
829
+ deviceSearchId = "NOT_SET"
830
+ }
831
+
832
+ override fun cleanup() {
833
+ disconnect()
834
+ }
835
+
836
+ override fun getConnectedBluetoothName(): String? {
837
+ return try { ringGatt?.device?.name } catch (e: SecurityException) { null }
838
+ }
839
+
840
+ override fun ping() {
841
+ val char = batteryLevelChar ?: return
842
+ try {
843
+ ringGatt?.readCharacteristic(char)
844
+ } catch (e: SecurityException) {
845
+ Bridge.log("R1: ping SecurityException: ${e.message}")
846
+ }
847
+ }
848
+
849
+ override fun getBatteryStatus() {
850
+ val char = batteryLevelChar ?: return
851
+ try {
852
+ ringGatt?.readCharacteristic(char)
853
+ } catch (e: SecurityException) {
854
+ Bridge.log("R1: getBatteryStatus SecurityException: ${e.message}")
855
+ }
856
+ }
857
+
858
+ // MARK: - No-op implementations (ring has no display/camera/wifi/mic)
859
+
860
+ override fun sendIncidentId(incidentId: String) {}
861
+ override fun setMicEnabled(enabled: Boolean) {}
862
+ override fun sortMicRanking(list: MutableList<String>): MutableList<String> = list
863
+ override fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean, requireAck: Boolean) {}
864
+ override fun requestPhoto(
865
+ requestId: String, appId: String, size: String?, webhookUrl: String?,
866
+ authToken: String?, compress: String?, flash: Boolean, sound: Boolean
867
+ ) {}
868
+ override fun startVideoRecording(requestId: String, save: Boolean, flash: Boolean, sound: Boolean) {}
869
+ override fun stopVideoRecording(requestId: String) {}
870
+ override fun startStream(message: Map<String, Any>) {}
871
+ override fun stopStream() {}
872
+ override fun sendStreamKeepAlive(message: Map<String, Any>) {}
873
+ override fun sendButtonPhotoSettings() {}
874
+ override fun sendButtonVideoRecordingSettings() {}
875
+ override fun sendButtonMaxRecordingTime() {}
876
+ override fun sendButtonCameraLedSetting() {}
877
+ override fun setBrightness(level: Int, autoMode: Boolean) {}
878
+ override fun clearDisplay() {}
879
+ override fun sendTextWall(text: String) {}
880
+ override fun sendDoubleTextWall(top: String, bottom: String) {}
881
+ override fun displayBitmap(base64ImageData: String): Boolean = false
882
+ override fun showDashboard() {}
883
+ override fun setDashboardPosition(height: Int, depth: Int) {}
884
+ override fun setHeadUpAngle(angle: Int) {}
885
+ override fun setSilentMode(enabled: Boolean) {}
886
+ override fun exit() {}
887
+ override fun sendShutdown() { disconnect() }
888
+ override fun sendReboot() {}
889
+ override fun sendRgbLedControl(
890
+ requestId: String, packageName: String?, action: String, color: String?,
891
+ ontime: Int, offtime: Int, count: Int
892
+ ) {}
893
+ override fun requestWifiScan() {}
894
+ override fun sendWifiCredentials(ssid: String, password: String) {}
895
+ override fun forgetWifiNetwork(ssid: String) {}
896
+ override fun sendHotspotState(enabled: Boolean) {}
897
+ override fun sendOtaStart() {}
898
+ override fun sendUserEmailToGlasses(email: String) {}
899
+ override fun queryGalleryStatus() {}
900
+ override fun sendGalleryMode() {}
901
+ override fun requestVersionInfo() {}
902
+ }
903
+
904
+ // MARK: - R1 Reconnection Manager
905
+ // Coroutine-based port of Swift's R1ReconnectionManager actor.
906
+ // Currently unwired — matches iOS which leaves the reconnection timer commented out.
907
+
908
+ private class R1ReconnectionManager(
909
+ private val intervalSeconds: Long = 30L,
910
+ private val maxAttempts: Int = -1
911
+ ) {
912
+ private var job: Job? = null
913
+ private var attempts = 0
914
+
915
+ fun start(onAttempt: suspend () -> Boolean) {
916
+ stop()
917
+ attempts = 0
918
+ job = CoroutineScope(Dispatchers.Default).launch {
919
+ while (isActive) {
920
+ delay(intervalSeconds * 1_000L)
921
+ if (!isActive) break
922
+ attempts += 1
923
+ if (maxAttempts > 0 && attempts > maxAttempts) break
924
+ if (onAttempt()) break
925
+ }
926
+ }
927
+ }
928
+
929
+ fun stop() {
930
+ job?.cancel()
931
+ job = null
932
+ attempts = 0
933
+ }
934
+ }