@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,827 @@
1
+ //
2
+ // R1.swift
3
+ // MentraOS_Manager
4
+ //
5
+ // Even Realities R1 Smart Ring controller
6
+ // Protocol reverse-engineered from BTSnoop captures and firmware analysis
7
+ //
8
+
9
+ import Combine
10
+ import CoreBluetooth
11
+ import Foundation
12
+
13
+ // MARK: - R1 BLE Constants
14
+
15
+ private enum R1BLE {
16
+ /// Ring custom service
17
+ static let SERVICE_UUID = CBUUID(string: "BAE80001-4F05-4503-8E65-3AF1F7329D1F")
18
+
19
+ // Channel 1
20
+ static let WRITE_CHAR_1 = CBUUID(string: "BAE80010-4F05-4503-8E65-3AF1F7329D1F")
21
+ static let NOTIFY_CHAR_1 = CBUUID(string: "BAE80011-4F05-4503-8E65-3AF1F7329D1F")
22
+
23
+ // Channel 2
24
+ static let WRITE_CHAR_2 = CBUUID(string: "BAE80012-4F05-4503-8E65-3AF1F7329D1F")
25
+ static let NOTIFY_CHAR_2 = CBUUID(string: "BAE80013-4F05-4503-8E65-3AF1F7329D1F")
26
+
27
+ // Standard battery service (ring may expose this)
28
+ static let BATTERY_SERVICE = CBUUID(string: "180F")
29
+ static let BATTERY_LEVEL_CHAR = CBUUID(string: "2A19")
30
+
31
+ /// Name filters for scanning
32
+ static let NAME_FILTERS = ["EVEN R1", "BCL60"]
33
+
34
+ // Init sequence config writes
35
+ static let CONFIG_FC = Data([0xFC])
36
+ static let CONFIG_11 = Data([0x11])
37
+
38
+ // BleRing1 command header (cmd, module, subCmd) for advStart
39
+ // From RE: BleRing1Cmd_system=0, BleRing1Module_system=0, BleRing1SubCmd_advStart=9
40
+ static let CMD_SYSTEM: UInt8 = 0x00
41
+ static let MODULE_SYSTEM: UInt8 = 0x00
42
+ static let SUBCMD_ADV_START: UInt8 = 0x09
43
+
44
+ /// Gesture protocol marker
45
+ static let GESTURE_MARKER: UInt8 = 0xFF
46
+
47
+ static let SCAN_TIMEOUT: TimeInterval = 15.0
48
+ }
49
+
50
+ // MARK: - R1 Gesture Types
51
+
52
+ private enum R1Gesture: String {
53
+ case hold
54
+ case singleTap = "single_tap"
55
+ case doubleTap = "double_tap"
56
+ case swipeUp = "swipe_up"
57
+ case swipeDown = "swipe_down"
58
+
59
+ /// Parse gesture from notification data: [0xFF, type, param]
60
+ static func parse(from data: Data) -> R1Gesture? {
61
+ guard data.count >= 3, data[0] == R1BLE.GESTURE_MARKER else { return nil }
62
+ switch data[1] {
63
+ case 0x03:
64
+ return .hold
65
+ case 0x04:
66
+ return data[2] == 0x01 ? .singleTap : data[2] == 0x02 ? .doubleTap : nil
67
+ case 0x05:
68
+ return data[2] < 0x80 ? .swipeUp : .swipeDown
69
+ default:
70
+ return nil
71
+ }
72
+ }
73
+ }
74
+
75
+ // MARK: - R1 Controller
76
+
77
+ @MainActor
78
+ class R1: NSObject, ControllerManager {
79
+ var type = ControllerTypes.R1
80
+ let hasMic = false // R1 ring has no microphone
81
+
82
+ // Connection state
83
+ private var centralManager: CBCentralManager?
84
+ private var ringPeripheral: CBPeripheral?
85
+ private var isDisconnecting = false
86
+
87
+ // BLE characteristics
88
+ private var writeChar1: CBCharacteristic?
89
+ private var notifyChar1: CBCharacteristic?
90
+ private var writeChar2: CBCharacteristic?
91
+ private var notifyChar2: CBCharacteristic?
92
+ private var batteryLevelChar: CBCharacteristic?
93
+ private var notifySubscriptionCount = 0
94
+ private var initSequenceRun = false
95
+
96
+ /// Device search
97
+ var DEVICE_SEARCH_ID = "NOT_SET"
98
+
99
+ /// persisted state for ease of reconnection / background connection:
100
+ /// we could store these elsewhere to be like other settings / state, but in practice they will only ever be set and used here
101
+ /// Stored UUID for background reconnection
102
+ private var ringUUID: UUID? {
103
+ get { UserDefaults.standard.string(forKey: "r1_ringUUID").flatMap { UUID(uuidString: $0) } }
104
+ set {
105
+ if let v = newValue {
106
+ UserDefaults.standard.set(v.uuidString, forKey: "r1_ringUUID")
107
+ } else {
108
+ UserDefaults.standard.removeObject(forKey: "r1_ringUUID")
109
+ }
110
+ }
111
+ }
112
+
113
+ /// maps peripheral.name to 6-byte ring MAC address:
114
+ private var ringMacAddressMap: [String: Data] {
115
+ get {
116
+ UserDefaults.standard.dictionary(forKey: "r1_ringMacAddressMap") as? [String: Data]
117
+ ?? [:]
118
+ }
119
+ set { UserDefaults.standard.set(newValue, forKey: "r1_ringMacAddressMap") }
120
+ }
121
+
122
+ private var ringMacAddress: String? {
123
+ get { UserDefaults.standard.string(forKey: "r1_ringMacAddress") }
124
+ set { UserDefaults.standard.set(newValue, forKey: "r1_ringMacAddress") }
125
+ }
126
+
127
+ /// Reconnection
128
+ private let reconnectionManager = R1ReconnectionManager()
129
+
130
+ /// Battery
131
+ @Published private var _batteryLevel: Int = -1 {
132
+ didSet {
133
+ if _batteryLevel != oldValue && _batteryLevel >= 0 {
134
+ DeviceStore.shared.apply("glasses", "controllerBatteryLevel", _batteryLevel)
135
+ // Bridge.sendBatteryStatus(level: _batteryLevel, charging: isCharging)
136
+ }
137
+ }
138
+ }
139
+
140
+ private var isCharging = false
141
+
142
+ /// Heartbeat
143
+ private var heartbeatTimer: Timer?
144
+
145
+ static let _bluetoothQueue = DispatchQueue(label: "BluetoothR1", qos: .userInitiated)
146
+
147
+ // MARK: - Init
148
+
149
+ override init() {
150
+ super.init()
151
+ }
152
+
153
+ deinit {
154
+ centralManager?.delegate = nil
155
+ ringPeripheral?.delegate = nil
156
+ }
157
+
158
+ // MARK: - BLE Scanning
159
+
160
+ @discardableResult
161
+ private func startScan() -> Bool {
162
+ Bridge.log("R1: startScan()")
163
+ if centralManager == nil {
164
+ centralManager = CBCentralManager(
165
+ delegate: self, queue: R1._bluetoothQueue,
166
+ options: [CBCentralManagerOptionShowPowerAlertKey: 0]
167
+ )
168
+ }
169
+
170
+ isDisconnecting = false
171
+ guard centralManager!.state == .poweredOn else {
172
+ Bridge.log("R1: Bluetooth not powered on")
173
+ return false
174
+ }
175
+
176
+ // Try UUID-based reconnection first
177
+ if connectByUUID() {
178
+ return true
179
+ }
180
+
181
+ centralManager!.scanForPeripherals(
182
+ withServices: nil,
183
+ options: [
184
+ CBCentralManagerScanOptionAllowDuplicatesKey: false,
185
+ ]
186
+ )
187
+ return true
188
+ }
189
+
190
+ func stopScan() {
191
+ centralManager?.stopScan()
192
+ }
193
+
194
+ private func connectByUUID() -> Bool {
195
+ // don't do this if we don't have a search id set:
196
+ if DEVICE_SEARCH_ID == "NOT_SET" || DEVICE_SEARCH_ID.isEmpty {
197
+ Bridge.log("R1: 🔵 No DEVICE_SEARCH_ID set, skipping connect by UUID")
198
+ return false
199
+ }
200
+ guard let uuid = ringUUID else { return false }
201
+ guard let ringMac = ringMacAddress else { return false }
202
+ guard let peripheral = centralManager?.retrievePeripherals(withIdentifiers: [uuid]).first
203
+ else { return false }
204
+
205
+ ringPeripheral = peripheral
206
+ peripheral.delegate = self
207
+ centralManager?.connect(peripheral, options: nil)
208
+ Bridge.log("R1: Reconnecting by UUID to \(peripheral.name ?? "ring")")
209
+ return true
210
+ }
211
+
212
+ private func matchesNameFilter(_ name: String?) -> Bool {
213
+ guard let name = name else { return false }
214
+ return R1BLE.NAME_FILTERS.contains(where: { name.contains($0) })
215
+ }
216
+
217
+ /// Extract a device identifier from the ring name (e.g. "EVEN R1_CEC5BA" -> "CEC5BA")
218
+ private func extractRingId(_ name: String) -> String? {
219
+ if let range = name.range(of: "R1_") {
220
+ let id = String(name[range.upperBound...])
221
+ return id.isEmpty ? nil : id
222
+ }
223
+ return nil
224
+ }
225
+
226
+ // MARK: - Init Sequence
227
+
228
+ /// Runs after notify subscriptions are confirmed.
229
+ /// Sends 0xFC and 0x11 config writes to initialize the ring.
230
+ private func runInitSequence() {
231
+ guard !initSequenceRun else { return }
232
+ initSequenceRun = true
233
+
234
+ Bridge.log("R1: Running init sequence")
235
+
236
+ // Write init commands to both write characteristics
237
+ let writeChars = [writeChar1, writeChar2].compactMap { $0 }
238
+ guard !writeChars.isEmpty else {
239
+ Bridge.log("R1: No write characteristics found, skipping init")
240
+ markConnected()
241
+ return
242
+ }
243
+
244
+ for wc in writeChars {
245
+ ringPeripheral?.writeValue(R1BLE.CONFIG_FC, for: wc, type: .withoutResponse)
246
+ }
247
+
248
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
249
+ guard let self = self else { return }
250
+ for wc in writeChars {
251
+ self.ringPeripheral?.writeValue(R1BLE.CONFIG_11, for: wc, type: .withoutResponse)
252
+ }
253
+ self.markConnected()
254
+ }
255
+ }
256
+
257
+ /// Tells the ring to start advertising / connect to the glasses.
258
+ /// Sends BleRing1 advStart (cmd=0, module=0, subCmd=9) with the 6-byte glasses MAC as payload
259
+ /// to WRITE_CHAR_2 (BAE80012-…). Reverse-engineered from the Even Realities mobile app
260
+ /// (BleRing1CmdProto::advStart -> BleRing1CmdPublicExt.sendCmd).
261
+ private func connectToGlasses() {
262
+ let glassesMac = (DeviceStore.shared.get("glasses", "btMacAddress") as? String)
263
+ ?? UserDefaults.standard.string(forKey: "glasses_btMacAddress")
264
+
265
+ guard let glassesMac else {
266
+ Bridge.log("R1: connectToGlasses: no glasses MAC")
267
+ return
268
+ }
269
+
270
+ guard let macBytes = parseMac(glassesMac) else {
271
+ Bridge.log("R1: connectToGlasses: could not parse glasses MAC")
272
+ return
273
+ }
274
+
275
+ // Cache so we can reconnect even before the glasses are scanned.
276
+ UserDefaults.standard.set(glassesMac, forKey: "glasses_btMacAddress")
277
+
278
+ guard let wc = writeChar2 ?? writeChar1 else {
279
+ Bridge.log("R1: connectToGlasses: no write characteristic")
280
+ return
281
+ }
282
+
283
+ var payload = Data([R1BLE.CMD_SYSTEM, R1BLE.MODULE_SYSTEM, R1BLE.SUBCMD_ADV_START])
284
+ payload.append(macBytes)
285
+ Bridge.log("R1: advStart sent")
286
+ ringPeripheral?.writeValue(payload, for: wc, type: .withoutResponse)
287
+ }
288
+
289
+ /// Parse a MAC string like "AA:BB:CC:DD:EE:FF" or "AABBCCDDEEFF" into 6 raw bytes.
290
+ private func parseMac(_ s: String) -> Data? {
291
+ let cleaned = s.replacingOccurrences(of: ":", with: "")
292
+ .replacingOccurrences(of: "-", with: "")
293
+ guard cleaned.count == 12 else { return nil }
294
+ var out = Data(); out.reserveCapacity(6)
295
+ var idx = cleaned.startIndex
296
+ for _ in 0 ..< 6 {
297
+ let next = cleaned.index(idx, offsetBy: 2)
298
+ guard let byte = UInt8(cleaned[idx ..< next], radix: 16) else { return nil }
299
+ out.append(byte)
300
+ idx = next
301
+ }
302
+ return out
303
+ }
304
+
305
+ private func markConnected() {
306
+ Task { await reconnectionManager.stop() }
307
+ Bridge.log("R1: Ring connected")
308
+
309
+ if let name = ringPeripheral?.name, let id = extractRingId(name) {
310
+ DeviceStore.shared.apply("bluetooth", "controller_device_name", id)
311
+ }
312
+
313
+ guard let mac = ringMacAddress else {
314
+ Bridge.log("R1: No ring MAC address found")
315
+ return
316
+ }
317
+ DeviceStore.shared.apply("glasses", "controllerMacAddress", mac)
318
+
319
+ DeviceStore.shared.apply("glasses", "controllerConnected", true)
320
+ // DeviceStore.shared.apply("glasses", "controllerFullyBooted", true)
321
+
322
+ // tell the ring to connect to the glasses if we have it's mac address:
323
+ connectToGlasses()
324
+
325
+ // after a second, connect the glasses to the controller if needed:
326
+ Task {
327
+ try? await Task.sleep(nanoseconds: 1_000_000_000)
328
+ await DeviceManager.shared.sgc?.connectController()
329
+ }
330
+
331
+ startHeartbeat()
332
+ }
333
+
334
+ // MARK: - Heartbeat
335
+
336
+ private func startHeartbeat() {
337
+ heartbeatTimer?.invalidate()
338
+ // Simple keepalive: re-read battery every 30 seconds
339
+ heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) {
340
+ [weak self] _ in
341
+ DispatchQueue.main.async {
342
+ guard let self = self, DeviceStore.shared.get("glasses", "controllerConnected") as? Bool ?? false else { return }
343
+ // Read battery if we have the standard battery char
344
+ if let char = self.batteryLevelChar {
345
+ self.ringPeripheral?.readValue(for: char)
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ private func stopHeartbeat() {
352
+ heartbeatTimer?.invalidate()
353
+ heartbeatTimer = nil
354
+ }
355
+
356
+ // MARK: - Incoming Data Handling
357
+
358
+ private func handleNotification(from characteristic: CBCharacteristic, data: Data) {
359
+ let hex = data.map { String(format: "%02X", $0) }.joined(separator: " ")
360
+ Bridge.log("R1: \(String(characteristic.uuid.uuidString.suffix(4))) -> \(hex)")
361
+
362
+ let timestamp = Int64(Date().timeIntervalSince1970 * 1000)
363
+
364
+ // Gesture: [0xFF, type, param]
365
+ if data.count >= 3, data[0] == R1BLE.GESTURE_MARKER {
366
+ if let gesture = R1Gesture.parse(from: data) {
367
+ Bridge.log("R1: Gesture: \(gesture.rawValue)")
368
+ Bridge.sendTouchEvent(
369
+ deviceModel: ControllerTypes.R1,
370
+ gestureName: gesture.rawValue,
371
+ timestamp: timestamp
372
+ )
373
+ } else {
374
+ Bridge.log(
375
+ "R1: Unknown gesture type=0x\(String(format: "%02X", data[1])) param=0x\(String(format: "%02X", data[2]))"
376
+ )
377
+ }
378
+ return
379
+ }
380
+
381
+ // Battery: 2 bytes, first byte is percentage
382
+ if data.count == 2, data[0] <= 100 {
383
+ _batteryLevel = Int(data[0])
384
+ Bridge.log("R1: Battery: \(data[0])%")
385
+ return
386
+ }
387
+
388
+ // State: single byte (0x01=ready, 0x00=menu)
389
+ if data.count == 1 {
390
+ let state = data[0] == 0x01 ? "ready" : data[0] == 0x00 ? "menu" : "unknown(\(data[0]))"
391
+ Bridge.log("R1: State: \(state)")
392
+ return
393
+ }
394
+
395
+ // Longer data: check for embedded gesture marker
396
+ if data.count > 3, let ffIndex = data.firstIndex(of: R1BLE.GESTURE_MARKER),
397
+ ffIndex + 2 < data.count
398
+ {
399
+ let gestureData = Data(data[ffIndex ... ffIndex + 2])
400
+ if let gesture = R1Gesture.parse(from: gestureData) {
401
+ Bridge.log("R1: Embedded gesture: \(gesture.rawValue)")
402
+ Bridge.sendTouchEvent(
403
+ deviceModel: ControllerTypes.R1,
404
+ gestureName: gesture.rawValue,
405
+ timestamp: timestamp
406
+ )
407
+ return
408
+ }
409
+ }
410
+ }
411
+
412
+ // MARK: - Connection State Reset
413
+
414
+ private func resetConnectionState() {
415
+ ringPeripheral = nil
416
+ writeChar1 = nil
417
+ notifyChar1 = nil
418
+ writeChar2 = nil
419
+ notifyChar2 = nil
420
+ batteryLevelChar = nil
421
+ notifySubscriptionCount = 0
422
+ initSequenceRun = false
423
+ ringMacAddress = nil
424
+ DeviceStore.shared.apply("glasses", "controllerConnected", false)
425
+ DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
426
+ }
427
+
428
+ // MARK: - Reconnection
429
+
430
+ private func startReconnectionTimer() {
431
+ Task {
432
+ await reconnectionManager.start { [weak self] in
433
+ guard let self else { return false }
434
+ if await MainActor.run(body: { DeviceStore.shared.get("glasses", "controllerConnected") as? Bool ?? false }) {
435
+ return true // already connected
436
+ }
437
+ Bridge.log("R1: Attempting reconnection...")
438
+ await MainActor.run { self.startScan() }
439
+ return false // keep trying
440
+ }
441
+ }
442
+ }
443
+
444
+ // MARK: - ControllerManager Protocol
445
+
446
+ func findCompatibleDevices() {
447
+ Bridge.log("R1: findCompatibleDevices()")
448
+ DEVICE_SEARCH_ID = "NOT_SET"
449
+ startScan()
450
+ }
451
+
452
+ func connectById(_ id: String) {
453
+ Bridge.log("R1: connectById(\(id))")
454
+ DEVICE_SEARCH_ID = id
455
+ startScan()
456
+ }
457
+
458
+ func disconnect() {
459
+ Bridge.log("R1: disconnect()")
460
+ isDisconnecting = true
461
+ stopHeartbeat()
462
+ Task { await reconnectionManager.stop() }
463
+
464
+ if let peripheral = ringPeripheral {
465
+ centralManager?.cancelPeripheralConnection(peripheral)
466
+ }
467
+ resetConnectionState()
468
+ }
469
+
470
+ func forget() {
471
+ disconnect()
472
+ ringUUID = nil
473
+ ringMacAddress = nil
474
+ DEVICE_SEARCH_ID = "NOT_SET"
475
+ centralManager?.delegate = nil
476
+ }
477
+
478
+ func cleanup() {
479
+ disconnect()
480
+ }
481
+
482
+ func getConnectedBluetoothName() -> String? {
483
+ return ringPeripheral?.name
484
+ }
485
+
486
+ func ping() {
487
+ if let char = batteryLevelChar {
488
+ ringPeripheral?.readValue(for: char)
489
+ }
490
+ }
491
+
492
+ func getBatteryStatus() {
493
+ if let char = batteryLevelChar {
494
+ ringPeripheral?.readValue(for: char)
495
+ }
496
+ }
497
+
498
+ // MARK: - No-op implementations (ring has no display/camera/wifi/mic)
499
+
500
+ func sendIncidentId(_: String, apiBaseUrl _: String?) {}
501
+ func setMicEnabled(_: Bool) {}
502
+ func sortMicRanking(list: [String]) -> [String] { return list }
503
+ func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
504
+ func requestPhoto(
505
+ _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
506
+ compress _: String?, flash _: Bool, sound _: Bool
507
+ ) {}
508
+ func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
509
+ func stopVideoRecording(requestId _: String) {}
510
+ func startStream(_: [String: Any]) {}
511
+ func stopStream() {}
512
+ func sendStreamKeepAlive(_: [String: Any]) {}
513
+ func sendButtonPhotoSettings() {}
514
+ func sendButtonVideoRecordingSettings() {}
515
+ func sendButtonMaxRecordingTime() {}
516
+ func sendButtonCameraLedSetting() {}
517
+ func setBrightness(_: Int, autoMode _: Bool) {}
518
+ func clearDisplay() {}
519
+ func sendTextWall(_: String) {}
520
+ func sendDoubleTextWall(_: String, _: String) {}
521
+ func displayBitmap(base64ImageData _: String) async -> Bool { return false }
522
+ func showDashboard() {}
523
+ func setDashboardPosition(_: Int, _: Int) {}
524
+ func setHeadUpAngle(_: Int) {}
525
+ func setSilentMode(_: Bool) {}
526
+ func exit() {}
527
+ func sendShutdown() {
528
+ disconnect()
529
+ }
530
+
531
+ func sendReboot() {}
532
+ func sendRgbLedControl(
533
+ requestId _: String, packageName _: String?, action _: String, color _: String?, ontime _: Int,
534
+ offtime _: Int, count _: Int
535
+ ) {}
536
+ func requestWifiScan() {}
537
+ func sendWifiCredentials(_: String, _: String) {}
538
+ func forgetWifiNetwork(_: String) {}
539
+ func sendHotspotState(_: Bool) {}
540
+ func sendOtaStart() {}
541
+ func sendOtaQueryStatus() {}
542
+ func sendUserEmailToGlasses(_: String) {}
543
+ func queryGalleryStatus() {}
544
+ func sendGalleryMode() {}
545
+ func requestVersionInfo() {}
546
+ }
547
+
548
+ // MARK: - CBCentralManagerDelegate
549
+
550
+ extension R1: CBCentralManagerDelegate {
551
+ nonisolated func centralManagerDidUpdateState(_ central: CBCentralManager) {
552
+ let state = central.state
553
+ DispatchQueue.main.async { [weak self] in
554
+ guard let self = self else { return }
555
+ Bridge.log("R1: Bluetooth state: \(state.rawValue)")
556
+ if state == .poweredOn {
557
+ _ = self.startScan()
558
+ }
559
+ }
560
+ }
561
+
562
+ nonisolated func centralManager(
563
+ _ central: CBCentralManager,
564
+ didDiscover peripheral: CBPeripheral,
565
+ advertisementData: [String: Any],
566
+ rssi RSSI: NSNumber
567
+ ) {
568
+ let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String
569
+ let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
570
+
571
+ DispatchQueue.main.async { [weak self] in
572
+ guard let self = self else { return }
573
+ guard self.matchesNameFilter(name) else { return }
574
+
575
+ Bridge.log(
576
+ "R1: Discovered: \(name ?? "?") (RSSI: \(RSSI)) mfgData: \(mfgData?.map { String(format: "%02X", $0) }.joined(separator: " ") ?? "none")"
577
+ )
578
+
579
+ // Extract ring MAC from manufacturer data if available and store to a map name:mac
580
+ if let mfgData = mfgData {
581
+ Bridge.log(
582
+ "R1: mfgData: \(mfgData.map { String(format: "%02X", $0) }.joined(separator: " "))"
583
+ )
584
+ if mfgData.count >= 6 {
585
+ self.ringMacAddressMap[name ?? ""] = Data(mfgData.suffix(6))
586
+ }
587
+ }
588
+
589
+ // Emit discovered device
590
+ if let name = name, let id = self.extractRingId(name) {
591
+ Bridge.sendDiscoveredDevice(ControllerTypes.R1, id)
592
+ }
593
+
594
+ // If scan-only mode, don't auto-connect
595
+ guard self.DEVICE_SEARCH_ID != "NOT_SET" else { return }
596
+
597
+ // If search ID is specific, check it matches the ring name/id
598
+ if let name = name, let id = self.extractRingId(name),
599
+ self.DEVICE_SEARCH_ID != id && !name.contains(self.DEVICE_SEARCH_ID)
600
+ {
601
+ return
602
+ }
603
+
604
+ if self.ringPeripheral == nil {
605
+ self.ringPeripheral = peripheral
606
+ peripheral.delegate = self
607
+ central.connect(peripheral, options: nil)
608
+ self.stopScan()
609
+ Bridge.log("R1: Connecting to \(name ?? "ring")")
610
+ }
611
+ }
612
+ }
613
+
614
+ nonisolated func centralManager(
615
+ _: CBCentralManager, didConnect peripheral: CBPeripheral
616
+ ) {
617
+ DispatchQueue.main.async { [weak self] in
618
+ guard let self = self else { return }
619
+ Bridge.log("R1: Connected to \(peripheral.name ?? "ring")")
620
+
621
+ self.ringUUID = peripheral.identifier
622
+
623
+ guard let name = peripheral.name, let mac = self.ringMacAddressMap[name] else {
624
+ Bridge.log("R1: No MAC stored in map found for \(peripheral.name ?? "ring")")
625
+ // stop the scan, disconnect, remove the uuid, and try again as we need the mac address to connect:
626
+ self.disconnect()
627
+ self.ringUUID = nil
628
+ // we are still searching!:
629
+ DeviceStore.shared.apply("glasses", "controllerConnected", false)
630
+ DeviceStore.shared.apply("glasses", "controllerFullyBooted", false)
631
+ DeviceStore.shared.apply("glasses", "controllerSearching", true)
632
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
633
+ self.startScan()
634
+ }
635
+ return
636
+ }
637
+
638
+ self.ringMacAddress = mac.map { String(format: "%02X", $0) }.joined(separator: ":")
639
+
640
+ // Discover all services
641
+ peripheral.discoverServices(nil)
642
+ }
643
+ }
644
+
645
+ nonisolated func centralManager(
646
+ _: CBCentralManager, didFailToConnect _: CBPeripheral, error: Error?
647
+ ) {
648
+ DispatchQueue.main.async { [weak self] in
649
+ guard let self = self else { return }
650
+ Bridge.log("R1: Failed to connect: \(error?.localizedDescription ?? "unknown")")
651
+ self.resetConnectionState()
652
+ }
653
+ }
654
+
655
+ nonisolated func centralManager(
656
+ _: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error: Error?
657
+ ) {
658
+ DispatchQueue.main.async { [weak self] in
659
+ guard let self = self else { return }
660
+ Bridge.log("R1: Disconnected: \(error?.localizedDescription ?? "clean")")
661
+
662
+ self.stopHeartbeat()
663
+
664
+ if self.isDisconnecting { return }
665
+
666
+ self.resetConnectionState()
667
+ // self.startReconnectionTimer()
668
+ }
669
+ }
670
+ }
671
+
672
+ // MARK: - CBPeripheralDelegate
673
+
674
+ extension R1: CBPeripheralDelegate {
675
+ nonisolated func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
676
+ guard let services = peripheral.services else { return }
677
+ for service in services {
678
+ // Discover ALL characteristics on every service
679
+ peripheral.discoverCharacteristics(nil, for: service)
680
+ }
681
+ }
682
+
683
+ nonisolated func peripheral(
684
+ _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?
685
+ ) {
686
+ guard let characteristics = service.characteristics else { return }
687
+
688
+ DispatchQueue.main.async { [weak self] in
689
+ guard let self = self else { return }
690
+
691
+ for char in characteristics {
692
+ Bridge.log("R1: char discovered: \(char.uuid)")
693
+ let props = char.properties
694
+ var propStr: [String] = []
695
+ if props.contains(.read) { propStr.append("read") }
696
+ if props.contains(.write) { propStr.append("write") }
697
+ if props.contains(.writeWithoutResponse) { propStr.append("writeNoResp") }
698
+ if props.contains(.notify) { propStr.append("notify") }
699
+ if props.contains(.indicate) { propStr.append("indicate") }
700
+ Bridge.log("R1: char \(char.uuid) props=[\(propStr.joined(separator: ","))]")
701
+
702
+ // Store known characteristics
703
+ switch char.uuid {
704
+ case R1BLE.WRITE_CHAR_1:
705
+ self.writeChar1 = char
706
+ case R1BLE.NOTIFY_CHAR_1:
707
+ self.notifyChar1 = char
708
+ case R1BLE.WRITE_CHAR_2:
709
+ self.writeChar2 = char
710
+ case R1BLE.NOTIFY_CHAR_2:
711
+ self.notifyChar2 = char
712
+ case R1BLE.BATTERY_LEVEL_CHAR:
713
+ self.batteryLevelChar = char
714
+ default:
715
+ break
716
+ }
717
+
718
+ // Subscribe to any notify/indicate characteristic
719
+ if props.contains(.notify) || props.contains(.indicate) {
720
+ peripheral.setNotifyValue(true, for: char)
721
+ }
722
+
723
+ // Read any readable characteristic (triggers pairing if encrypted)
724
+ if props.contains(.read) {
725
+ peripheral.readValue(for: char)
726
+ }
727
+ }
728
+ }
729
+ }
730
+
731
+ nonisolated func peripheral(
732
+ _: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic,
733
+ error: Error?
734
+ ) {
735
+ Bridge.log("R1: didUpdateNotificationStateFor: \(characteristic.uuid)")
736
+ DispatchQueue.main.async { [weak self] in
737
+ guard let self = self else { return }
738
+
739
+ if let error = error {
740
+ Bridge.log(
741
+ "R1: Notify error on \(characteristic.uuid): \(error.localizedDescription)"
742
+ )
743
+ return
744
+ }
745
+ Bridge.log("R1: Notify enabled on \(characteristic.uuid)")
746
+
747
+ self.notifySubscriptionCount += 1
748
+
749
+ // Run init after subscribing to at least 2 notify characteristics
750
+ if self.notifySubscriptionCount >= 2 && !self.initSequenceRun {
751
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
752
+ self?.runInitSequence()
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ nonisolated func peripheral(
759
+ _: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
760
+ error: Error?
761
+ ) {
762
+ Bridge.log("R1: didUpdateValueFor1: \(characteristic.uuid)")
763
+ guard let data = characteristic.value, !data.isEmpty, error == nil else { return }
764
+ Bridge.log("R1: didUpdateValueFor: \(characteristic.uuid) data: \(data.toHexString())")
765
+
766
+ DispatchQueue.main.async { [weak self] in
767
+ guard let self = self else { return }
768
+
769
+ // Standard battery service
770
+ if characteristic.uuid == R1BLE.BATTERY_LEVEL_CHAR {
771
+ self._batteryLevel = Int(data[0])
772
+ Bridge.log("R1: Battery (std): \(data[0])%")
773
+ return
774
+ }
775
+
776
+ self.handleNotification(from: characteristic, data: data)
777
+ }
778
+ }
779
+
780
+ nonisolated func peripheral(
781
+ _: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?
782
+ ) {
783
+ Bridge.log("R1: didWriteValueFor: \(characteristic.uuid)")
784
+ if let error = error {
785
+ DispatchQueue.main.async {
786
+ Bridge.log(
787
+ "R1: Write error on \(characteristic.uuid): \(error.localizedDescription)"
788
+ )
789
+ }
790
+ }
791
+ }
792
+ }
793
+
794
+ // MARK: - R1 Reconnection Manager
795
+
796
+ actor R1ReconnectionManager {
797
+ private var task: Task<Void, Never>?
798
+ private let intervalSeconds: TimeInterval
799
+ private var attempts = 0
800
+ private let maxAttempts: Int
801
+
802
+ init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
803
+ self.intervalSeconds = intervalSeconds
804
+ self.maxAttempts = maxAttempts
805
+ }
806
+
807
+ func start(onAttempt: @escaping @Sendable () async -> Bool) {
808
+ stop()
809
+ attempts = 0
810
+ task = Task {
811
+ while !Task.isCancelled {
812
+ try? await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
813
+ if Task.isCancelled { break }
814
+ attempts += 1
815
+ if maxAttempts > 0, attempts > maxAttempts { break }
816
+ let done = await onAttempt()
817
+ if done { break }
818
+ }
819
+ }
820
+ }
821
+
822
+ func stop() {
823
+ task?.cancel()
824
+ task = nil
825
+ attempts = 0
826
+ }
827
+ }