@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,2505 @@
1
+ //
2
+ // G1.swift
3
+ // MentraOS_Manager
4
+ //
5
+ // Created by Matthew Fosse on 3/3/25.
6
+ //
7
+
8
+ import Combine
9
+ import CoreBluetooth
10
+ import Foundation
11
+ import UIKit
12
+
13
+ extension Data {
14
+ func chunked(into size: Int) -> [Data] {
15
+ var chunks = [Data]()
16
+ var index = 0
17
+ while index < count {
18
+ let chunkSize = Swift.min(size, count - index)
19
+ let chunk = subdata(in: index ..< (index + chunkSize))
20
+ chunks.append(chunk)
21
+ index += chunkSize
22
+ }
23
+ return chunks
24
+ }
25
+
26
+ func hexEncodedString() -> String {
27
+ return map { String(format: "%02x", $0) }.joined(separator: " ")
28
+ // return map { String(format: "%02x", $0) }.joined(separator: ", ")
29
+ }
30
+
31
+ /// Extension for CRC32 calculation
32
+ var crc32: UInt32 {
33
+ return withUnsafeBytes { bytes in
34
+ let buffer = bytes.bindMemory(to: UInt8.self)
35
+ var crc: UInt32 = 0xFFFF_FFFF
36
+
37
+ for byte in buffer {
38
+ crc ^= UInt32(byte)
39
+ for _ in 0 ..< 8 {
40
+ if crc & 1 == 1 {
41
+ crc = (crc >> 1) ^ 0xEDB8_8320
42
+ } else {
43
+ crc >>= 1
44
+ }
45
+ }
46
+ }
47
+
48
+ return ~crc
49
+ }
50
+ }
51
+
52
+ /// Initialize Data from hex string
53
+ init?(hexString: String) {
54
+ let cleanHex = hexString.replacingOccurrences(of: " ", with: "")
55
+ guard cleanHex.count % 2 == 0 else { return nil }
56
+
57
+ var data = Data()
58
+ var index = cleanHex.startIndex
59
+
60
+ while index < cleanHex.endIndex {
61
+ let nextIndex = cleanHex.index(index, offsetBy: 2)
62
+ let byteString = cleanHex[index ..< nextIndex]
63
+ guard let byte = UInt8(byteString, radix: 16) else { return nil }
64
+ data.append(byte)
65
+ index = nextIndex
66
+ }
67
+
68
+ self = data
69
+ }
70
+ }
71
+
72
+ public struct QuickNote: Equatable {
73
+ let id: UUID
74
+ let text: String
75
+ let timestamp: Date
76
+
77
+ public static func == (lhs: QuickNote, rhs: QuickNote) -> Bool {
78
+ return lhs.id == rhs.id
79
+ }
80
+ }
81
+
82
+ struct BufferedCommand {
83
+ let chunks: [[UInt8]]
84
+ let sendLeft: Bool
85
+ let sendRight: Bool
86
+ let waitTime: Int
87
+ let ignoreAck: Bool
88
+ let chunkTimeMs: Int
89
+ let lastFrameMs: Int
90
+
91
+ init(
92
+ chunks: [[UInt8]], sendLeft: Bool = true, sendRight: Bool = true, waitTime: Int = -1,
93
+ ignoreAck: Bool = false, chunkTimeMs: Int = 10, lastFrameMs: Int = 0
94
+ ) {
95
+ self.chunks = chunks
96
+ self.sendLeft = sendLeft
97
+ self.sendRight = sendRight
98
+ self.waitTime = waitTime
99
+ self.ignoreAck = ignoreAck
100
+ self.chunkTimeMs = chunkTimeMs
101
+ self.lastFrameMs = lastFrameMs
102
+ }
103
+ }
104
+
105
+ /// Simple struct to hold app info
106
+ struct AppInfo {
107
+ let id: String
108
+ let name: String
109
+ }
110
+
111
+ enum GlassesError: Error {
112
+ case missingGlasses(String)
113
+ }
114
+
115
+ /// Dedicated actor for timer management
116
+ actor HeartbeatManager {
117
+ private var task: Task<Void, Never>?
118
+ private let intervalSeconds: TimeInterval
119
+
120
+ init(intervalSeconds: TimeInterval = 20) {
121
+ self.intervalSeconds = intervalSeconds
122
+ }
123
+
124
+ func start(onTick: @escaping @Sendable () async -> Void) {
125
+ stop()
126
+
127
+ task = Task {
128
+ while !Task.isCancelled {
129
+ do {
130
+ try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
131
+ } catch {
132
+ break
133
+ }
134
+ guard !Task.isCancelled else { break }
135
+ await onTick()
136
+ }
137
+ }
138
+ }
139
+
140
+ func stop() {
141
+ task?.cancel()
142
+ task = nil
143
+ }
144
+ }
145
+
146
+ /// Dedicated actor for command queue (you already have this partially)
147
+ actor CommandQueue {
148
+ private var commands: [BufferedCommand] = []
149
+ private var continuation: CheckedContinuation<BufferedCommand, Never>?
150
+
151
+ func enqueue(_ command: BufferedCommand) {
152
+ if let continuation {
153
+ self.continuation = nil
154
+ continuation.resume(returning: command)
155
+ } else {
156
+ commands.append(command)
157
+ }
158
+ }
159
+
160
+ func dequeue() async -> BufferedCommand {
161
+ if let command = commands.first {
162
+ commands.removeFirst()
163
+ return command
164
+ }
165
+
166
+ return await withCheckedContinuation { continuation in
167
+ self.continuation = continuation
168
+ }
169
+ }
170
+ }
171
+
172
+ /// Actor for managing pending ACKs
173
+ actor AckManager {
174
+ private var pending: [String: CheckedContinuation<Bool, Never>] = [:]
175
+
176
+ func waitForAck(
177
+ key: String,
178
+ timeoutMs: Int,
179
+ onRegistered: @escaping @Sendable () -> Void,
180
+ onTimeout: @escaping @Sendable () -> Void = {}
181
+ ) async -> Bool {
182
+ return await withCheckedContinuation { continuation in
183
+ pending[key] = continuation
184
+
185
+ // Now it's safe to send — registration is complete
186
+ onRegistered()
187
+
188
+ // Start timeout
189
+ Task { [weak self] in
190
+ try? await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000)
191
+
192
+ guard let self else { return }
193
+ if let timedOut = await self.removeIfPending(key: key) {
194
+ onTimeout()
195
+ timedOut.resume(returning: false)
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ func receiveAck(key: String) -> Bool {
202
+ if let continuation = pending.removeValue(forKey: key) {
203
+ continuation.resume(returning: true)
204
+ return true
205
+ }
206
+ return false
207
+ }
208
+
209
+ private func removeIfPending(key: String) -> CheckedContinuation<Bool, Never>? {
210
+ return pending.removeValue(forKey: key)
211
+ }
212
+ }
213
+
214
+ /// Actor for reconnection logic
215
+ actor ReconnectionManager {
216
+ private var task: Task<Void, Never>?
217
+ private let intervalSeconds: TimeInterval
218
+ private var attempts = 0
219
+ private let maxAttempts: Int // -1 for unlimited
220
+
221
+ init(intervalSeconds: TimeInterval = 30, maxAttempts: Int = -1) {
222
+ self.intervalSeconds = intervalSeconds
223
+ self.maxAttempts = maxAttempts
224
+ }
225
+
226
+ var isRunning: Bool {
227
+ task != nil && task?.isCancelled == false
228
+ }
229
+
230
+ var attemptCount: Int {
231
+ attempts
232
+ }
233
+
234
+ func start(onAttempt: @escaping @Sendable () async -> Bool) {
235
+ stop()
236
+ attempts = 0
237
+
238
+ task = Task {
239
+ while !Task.isCancelled {
240
+ if maxAttempts > 0, attempts >= maxAttempts {
241
+ Bridge.log("G1: Max reconnection attempts (\(maxAttempts)) reached")
242
+ break
243
+ }
244
+
245
+ attempts += 1
246
+ Bridge.log("G1: Reconnection attempt \(attempts)")
247
+
248
+ let shouldStop = await onAttempt()
249
+
250
+ if shouldStop {
251
+ Bridge.log("G1: Reconnection successful, stopping")
252
+ break
253
+ }
254
+
255
+ do {
256
+ try await Task.sleep(nanoseconds: UInt64(intervalSeconds * 1_000_000_000))
257
+ } catch {
258
+ break
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ func stop() {
265
+ task?.cancel()
266
+ task = nil
267
+ attempts = 0
268
+ }
269
+ }
270
+
271
+ @MainActor
272
+ class G1: NSObject, SGCManager {
273
+ func sendIncidentId(_: String, apiBaseUrl _: String?) {}
274
+
275
+ func sendGalleryMode() {}
276
+
277
+ func sendButtonMaxRecordingTime(_: Int) {}
278
+
279
+ func requestPhoto(
280
+ _: String, appId _: String, size _: String?, webhookUrl _: String?, authToken _: String?,
281
+ compress _: String?, flash _: Bool, sound _: Bool
282
+ ) {}
283
+
284
+ func startStream(_: [String: Any]) {}
285
+
286
+ func stopStream() {}
287
+
288
+ func sendStreamKeepAlive(_: [String: Any]) {}
289
+
290
+ func startVideoRecording(requestId _: String, save _: Bool, flash _: Bool, sound _: Bool) {}
291
+
292
+ func stopVideoRecording(requestId _: String) {}
293
+
294
+ func sendButtonPhotoSettings() {}
295
+
296
+ func sendButtonVideoRecordingSettings() {}
297
+
298
+ func sendButtonMaxRecordingTime() {}
299
+
300
+ func sendButtonCameraLedSetting() {}
301
+
302
+ func sendCameraFovSetting() {}
303
+
304
+ func showDashboard() {}
305
+
306
+ func setSilentMode(_: Bool) {}
307
+
308
+ func requestWifiScan() {}
309
+
310
+ func sendWifiCredentials(_: String, _: String) {}
311
+
312
+ func forgetWifiNetwork(_: String) {}
313
+
314
+ func sendHotspotState(_: Bool) {}
315
+
316
+ func sendUserEmailToGlasses(_: String) {}
317
+
318
+ func queryGalleryStatus() {}
319
+
320
+ func sendOtaStart() {}
321
+ func sendOtaQueryStatus() {}
322
+
323
+ func ping() {}
324
+ func dbg1() {}
325
+ func dbg2() {}
326
+ func connectController() {}
327
+ func disconnectController() {}
328
+
329
+ func requestVersionInfo() {
330
+ Bridge.log("G1: requestVersionInfo - not supported on G1")
331
+ }
332
+
333
+ var connectionState: String = ConnTypes.DISCONNECTED
334
+
335
+ func sendJson(_: [String: Any], wakeUp _: Bool, requireAck _: Bool) {}
336
+
337
+ var type = DeviceTypes.G1
338
+ var hasMic = true
339
+
340
+ // TODO: we probably don't need this
341
+ @objc static func requiresMainQueueSetup() -> Bool {
342
+ return true
343
+ }
344
+
345
+ // Duplicate BMP prevention with timeout
346
+ private var isDisplayingBMP = false
347
+ private var lastBMPStartTime = Date()
348
+
349
+ // Frame synchronization for animations
350
+ private var lastFrameTime = Date()
351
+ private var frameSequence = 0
352
+
353
+ // Animation Batching (iOS-Controlled Timing)
354
+ private var animationFrames: [String] = []
355
+ private var animationTimer: Timer?
356
+ private var currentFrameIndex: Int = 0
357
+ private var animationInterval: TimeInterval = 1.650 // Default 1650ms
358
+ private var animationRepeat: Bool = false
359
+ private var isAnimationRunning: Bool = false
360
+
361
+ // L/R Synchronization - Track BLE write completions
362
+ private var pendingWriteCompletions: [CBCharacteristic: CheckedContinuation<Bool, Never>] = [:]
363
+ private var pendingAckCompletions: [String: CheckedContinuation<Bool, Never>] = [:]
364
+ private let ackCompletionsQueue = DispatchQueue(
365
+ label: "com.erg1.ackCompletions", attributes: .concurrent
366
+ )
367
+ private var writeCompletionCount = 0
368
+
369
+ private var _fullyBooted: Bool = false
370
+ var fullyBooted: Bool {
371
+ get { DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false }
372
+ set {
373
+ let oldValue = DeviceStore.shared.get("glasses", "fullyBooted") as? Bool ?? false
374
+ DeviceStore.shared.apply("glasses", "fullyBooted", newValue)
375
+ if !newValue {
376
+ // Reset battery levels when disconnected
377
+ batteryLevel = -1
378
+ leftBatteryLevel = -1
379
+ rightBatteryLevel = -1
380
+ }
381
+ }
382
+ }
383
+
384
+ private var connected: Bool {
385
+ get { DeviceStore.shared.get("glasses", "connected") as? Bool ?? false }
386
+ set { DeviceStore.shared.apply("glasses", "connected", newValue) }
387
+ }
388
+
389
+ var leftReady: Bool = false
390
+ var rightReady: Bool = false
391
+
392
+ @Published var compressedVoiceData: Data = .init()
393
+ @Published var aiListening: Bool = false
394
+ @Published var quickNotes: [QuickNote] = []
395
+ @Published var leftBatteryLevel: Int = -1
396
+ @Published var rightBatteryLevel: Int = -1
397
+
398
+ private var batteryLevel: Int {
399
+ get { DeviceStore.shared.get("glasses", "batteryLevel") as? Int ?? -1 }
400
+ set { DeviceStore.shared.apply("glasses", "batteryLevel", newValue) }
401
+ }
402
+
403
+ private var caseCharging: Bool {
404
+ get { DeviceStore.shared.get("glasses", "caseCharging") as? Bool ?? false }
405
+ set { DeviceStore.shared.apply("glasses", "caseCharging", newValue) }
406
+ }
407
+
408
+ private var caseOpen: Bool {
409
+ get { DeviceStore.shared.get("glasses", "caseOpen") as? Bool ?? true }
410
+ set { DeviceStore.shared.apply("glasses", "caseOpen", newValue) }
411
+ }
412
+
413
+ private var caseRemoved: Bool {
414
+ get { DeviceStore.shared.get("glasses", "caseRemoved") as? Bool ?? true }
415
+ set { DeviceStore.shared.apply("glasses", "caseRemoved", newValue) }
416
+ }
417
+
418
+ var isDisconnecting = false
419
+
420
+ private let heartbeatManager = HeartbeatManager()
421
+ private let commandQueue = CommandQueue()
422
+ private let reconnectionManager = ReconnectionManager()
423
+ private let ackManager = AckManager()
424
+ private var globalCounter: UInt8 = 0
425
+ private var heartbeatCounter: UInt8 = 0
426
+
427
+ enum AiMode: String {
428
+ case AI_REQUESTED
429
+ case AI_MIC_ON
430
+ case AI_IDLE
431
+ }
432
+
433
+ let UART_SERVICE_UUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
434
+ let UART_TX_CHAR_UUID = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
435
+ let UART_RX_CHAR_UUID = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
436
+
437
+ // Constants
438
+ var DEVICE_SEARCH_ID = "NOT_SET"
439
+ let DELAY_BETWEEN_CHUNKS_SEND: UInt64 = 16_000_000 // 16ms
440
+ let DELAY_BETWEEN_SENDS_MS: UInt64 = 8_000_000 // 8ms
441
+ let INITIAL_CONNECTION_DELAY_MS: UInt64 = 350_000_000 // 350ms
442
+ var textHelper = G1Text()
443
+ var msgId = 100
444
+
445
+ static let _bluetoothQueue = DispatchQueue(label: "BluetoothG1", qos: .userInitiated)
446
+
447
+ private var aiMode: AiMode = .AI_IDLE {
448
+ didSet {
449
+ if aiMode == .AI_MIC_ON {
450
+ aiListening = true
451
+ } else {
452
+ aiListening = false
453
+ }
454
+ }
455
+ }
456
+
457
+ private var centralManager: CBCentralManager?
458
+
459
+ private var leftPeripheral: CBPeripheral?
460
+ private var rightPeripheral: CBPeripheral?
461
+ private var connectedDevices: [String: (CBPeripheral?, CBPeripheral?)] = [:]
462
+ var lastConnectionTimestamp: Date = .distantPast
463
+ private var leftInitialized: Bool = false
464
+ private var rightInitialized: Bool = false
465
+
466
+ private var leftGlassUUID: UUID? {
467
+ get {
468
+ if let uuidString = UserDefaults.standard.string(forKey: "leftGlassUUID") {
469
+ return UUID(uuidString: uuidString)
470
+ }
471
+ return nil
472
+ }
473
+ set {
474
+ if let newValue = newValue {
475
+ UserDefaults.standard.set(newValue.uuidString, forKey: "leftGlassUUID")
476
+ } else {
477
+ UserDefaults.standard.removeObject(forKey: "leftGlassUUID")
478
+ }
479
+ }
480
+ }
481
+
482
+ private var rightGlassUUID: UUID? {
483
+ get {
484
+ if let uuidString = UserDefaults.standard.string(forKey: "rightGlassUUID") {
485
+ return UUID(uuidString: uuidString)
486
+ }
487
+ return nil
488
+ }
489
+ set {
490
+ if let newValue = newValue {
491
+ UserDefaults.standard.set(newValue.uuidString, forKey: "rightGlassUUID")
492
+ } else {
493
+ UserDefaults.standard.removeObject(forKey: "rightGlassUUID")
494
+ }
495
+ }
496
+ }
497
+
498
+ override init() {
499
+ super.init()
500
+ startHeartbeat()
501
+ startCommandProcessor()
502
+ }
503
+
504
+ func forget() {
505
+ Task {
506
+ await heartbeatManager.stop()
507
+ await reconnectionManager.stop()
508
+ }
509
+
510
+ // Disconnect BLE peripherals before clearing references
511
+ if let left = leftPeripheral {
512
+ centralManager?.cancelPeripheralConnection(left)
513
+ }
514
+ if let right = rightPeripheral {
515
+ centralManager?.cancelPeripheralConnection(right)
516
+ }
517
+
518
+ // Clear all references
519
+ leftGlassUUID = nil
520
+ rightGlassUUID = nil
521
+ leftPeripheral = nil
522
+ rightPeripheral = nil
523
+ DEVICE_SEARCH_ID = "NOT_SET"
524
+
525
+ // Clean up central manager delegate
526
+ centralManager?.delegate = nil
527
+ }
528
+
529
+ deinit {
530
+ // Note: Can't call async from deinit, but Tasks will be cancelled
531
+ // when the class is deallocated anyway
532
+ centralManager?.delegate = nil
533
+ leftPeripheral?.delegate = nil
534
+ rightPeripheral?.delegate = nil
535
+ Bridge.log("G1: Manager deinitialized")
536
+ }
537
+
538
+ func cleanup() {
539
+ // TODO:
540
+ }
541
+
542
+ private func startHeartbeat() {
543
+ Task {
544
+ await heartbeatManager.start { [weak self] in
545
+ await self?.sendHeartbeat()
546
+ }
547
+ }
548
+ }
549
+
550
+ private func startCommandProcessor() {
551
+ Task {
552
+ while !Task.isCancelled {
553
+ let command = await commandQueue.dequeue()
554
+ await processCommand(command)
555
+ }
556
+ }
557
+ }
558
+
559
+ // MARK: - Serial Number and Color Detection
560
+
561
+ /// Decodes Even G1 serial number to extract style and color information
562
+ /// - Parameter serialNumber: The full serial number (e.g., "S110LABD020021")
563
+ /// - Returns: Tuple containing (style, color) or ("Unknown", "Unknown") if invalid
564
+ static func decodeEvenG1SerialNumber(_ serialNumber: String) -> (style: String, color: String) {
565
+ guard serialNumber.count >= 6 else {
566
+ return ("Unknown", "Unknown")
567
+ }
568
+
569
+ // Style mapping: 2nd character (index 1)
570
+ let style: String
571
+ let styleChar = serialNumber[serialNumber.index(serialNumber.startIndex, offsetBy: 2)]
572
+ switch styleChar {
573
+ case "0":
574
+ style = "Round"
575
+ case "1":
576
+ style = "Rectangular"
577
+ default:
578
+ style = "Round"
579
+ }
580
+
581
+ // Color mapping: 5th character (index 4)
582
+ let color: String
583
+ let colorChar = serialNumber[serialNumber.index(serialNumber.startIndex, offsetBy: 5)]
584
+ switch colorChar {
585
+ case "A":
586
+ color = "Grey"
587
+ case "B":
588
+ color = "Brown"
589
+ case "C":
590
+ color = "Green"
591
+ default:
592
+ color = "Grey"
593
+ }
594
+
595
+ return (style, color)
596
+ }
597
+
598
+ /// Decodes serial number from manufacturer data bytes
599
+ /// - Parameter manufacturerData: The manufacturer data bytes
600
+ /// - Returns: Decoded serial number string or nil if not found
601
+ private func decodeSerialFromManufacturerData(_ manufacturerData: Data) -> String? {
602
+ guard manufacturerData.count >= 10 else {
603
+ return nil
604
+ }
605
+
606
+ // Convert bytes to ASCII string
607
+ var serialBuilder = ""
608
+ for byte in manufacturerData {
609
+ if byte == 0x00 {
610
+ // Stop at null terminator
611
+ break
612
+ }
613
+ if byte >= 0x20, byte <= 0x7E {
614
+ // Only include CoreCommsService.logable ASCII characters
615
+ serialBuilder.append(Character(UnicodeScalar(byte)))
616
+ }
617
+ }
618
+
619
+ let decodedString = serialBuilder.trimmingCharacters(in: .whitespacesAndNewlines)
620
+
621
+ // Check if it looks like a valid Even G1 serial number
622
+ if decodedString.count >= 12,
623
+ decodedString.hasPrefix("S1") || decodedString.hasPrefix("100")
624
+ || decodedString.hasPrefix("110")
625
+ {
626
+ return decodedString
627
+ }
628
+
629
+ return nil
630
+ }
631
+
632
+ /// Emits serial number information to React Native
633
+ private func emitSerialNumberInfo(serialNumber: String, style: String, color: String) {
634
+ let eventBody: [String: Any] = [
635
+ "serialNumber": serialNumber,
636
+ "style": style,
637
+ "color": color,
638
+ ]
639
+
640
+ // Convert to JSON string for CoreMessageEvent
641
+ do {
642
+ let jsonData = try JSONSerialization.data(withJSONObject: eventBody, options: [])
643
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
644
+ // Bridge.sendTypedMessage("glasses_serial_number", body: eventBody)
645
+
646
+ Bridge.log(
647
+ "G1: 📱 Emitted serial number info: \(serialNumber), Style: \(style), Color: \(color)"
648
+ )
649
+ }
650
+ } catch {
651
+ Bridge.log("G1: Error creating serial number JSON: \(error)")
652
+ }
653
+ }
654
+
655
+ // @@@ REACT NATIVE FUNCTIONS @@@
656
+
657
+ /// this scans for glasses to connect to and only connnects if SEARCH_ID is set
658
+ func startScan() -> Bool {
659
+ if centralManager == nil {
660
+ centralManager = CBCentralManager(
661
+ delegate: self, queue: G1._bluetoothQueue,
662
+ options: ["CBCentralManagerOptionShowPowerAlertKey": 0]
663
+ )
664
+ }
665
+
666
+ isDisconnecting = false // reset intentional disconnect flag
667
+ guard centralManager!.state == .poweredOn else {
668
+ Bridge.log("G1: Attempting to scan but bluetooth is not powered on.")
669
+ return false
670
+ }
671
+
672
+ // send our already connected devices to RN:
673
+ let devices = getConnectedDevices()
674
+ Bridge.log("G1: connnectedDevices.count: (\(devices.count))")
675
+ for device in devices {
676
+ if let name = device.name {
677
+ Bridge.log("G1: Connected to device: \(name)")
678
+ if name.contains("_L_") && name.contains(DEVICE_SEARCH_ID) {
679
+ leftPeripheral = device
680
+ device.delegate = self
681
+ device.discoverServices([UART_SERVICE_UUID])
682
+ } else if name.contains("_R_") && name.contains(DEVICE_SEARCH_ID) {
683
+ rightPeripheral = device
684
+ device.delegate = self
685
+ device.discoverServices([UART_SERVICE_UUID])
686
+ }
687
+ emitDiscoveredDevice(name)
688
+ }
689
+ }
690
+
691
+ // First try: Connect by UUID (works in background)
692
+ if connectByUUID() {
693
+ Bridge.log("G1: 🔄 Found and attempting to connect to stored glasses UUIDs")
694
+ // Wait for connection to complete - no need to scan
695
+ return true
696
+ }
697
+
698
+ let scanOptions: [String: Any] = [
699
+ CBCentralManagerScanOptionAllowDuplicatesKey: false, // Don't allow duplicate advertisements
700
+ ]
701
+
702
+ centralManager!.scanForPeripherals(withServices: nil, options: scanOptions)
703
+ return true
704
+ }
705
+
706
+ func connectById(_ id: String) {
707
+ DEVICE_SEARCH_ID = "_" + id + "_"
708
+ startScan()
709
+ }
710
+
711
+ func findCompatibleDevices() {
712
+ DEVICE_SEARCH_ID = "NOT_SET"
713
+ startScan()
714
+ }
715
+
716
+ /// connect to glasses we've discovered:
717
+ @objc func RN_connectGlasses() -> Bool {
718
+ Bridge.log("RN_connectGlasses()")
719
+
720
+ if let side = leftPeripheral {
721
+ Bridge.log("G1: connecting to left glass: \(side.name ?? "(unknown)")")
722
+ centralManager!.connect(side, options: nil)
723
+ }
724
+
725
+ if let side = rightPeripheral {
726
+ Bridge.log("G1: connecting to right glass: \(side.name ?? "(unknown)")")
727
+ centralManager!.connect(side, options: nil)
728
+ }
729
+
730
+ // just return if we don't have both a left and right arm:
731
+ guard leftPeripheral != nil && rightPeripheral != nil else {
732
+ return false
733
+ }
734
+
735
+ Bridge.log(
736
+ "G1: found both glasses \(leftPeripheral!.name ?? "(unknown)"), \(rightPeripheral!.name ?? "(unknown)") stopping scan"
737
+ )
738
+ stopScan()
739
+
740
+ // get battery status:
741
+ getBatteryStatus()
742
+ return true
743
+ }
744
+
745
+ @objc func RN_sendText(_ text: String) {
746
+ Task {
747
+ let displayText = "\(text)"
748
+ guard let textData = displayText.data(using: .utf8) else { return }
749
+
750
+ var command: [UInt8] = [
751
+ 0x4E, // SEND_RESULT command
752
+ 0x00, // sequence number
753
+ 0x01, // total packages
754
+ 0x00, // current package
755
+ 0x71, // screen status (0x70 Text Show | 0x01 New Content)
756
+ 0x00, // char position 0
757
+ 0x00, // char position 1
758
+ 0x01, // page number
759
+ 0x01, // max pages
760
+ ]
761
+ command.append(contentsOf: Array(textData))
762
+ self.queueChunks([command])
763
+
764
+ // await sendTextWall(text)
765
+
766
+ // // await createQuickNoteIfNeeded(text)
767
+ // // await sendQuickNotesToGlasses()
768
+ }
769
+
770
+ // @@@@@@@@ just for testing:
771
+ // Task {
772
+ // msgId += 1
773
+ // let ncsNotification = NCSNotification(
774
+ // msgId: msgId,
775
+ // appIdentifier: "io.heckel.ntfy",
776
+ // title: "Notification Title",
777
+ // subtitle: "Notification Subtitle",
778
+ // message: text,
779
+ // displayName: "Example App"
780
+ // )
781
+ //
782
+ // let notification = G1Notification(ncsNotification: ncsNotification)
783
+ // let encodedChunks = await notification.constructNotification()
784
+ // CoreCommsService.log("encodedChunks: \(encodedChunks.count)")
785
+ // self.queueChunks(encodedChunks)
786
+ // }
787
+ }
788
+
789
+ func sendTextWall(_ text: String) {
790
+ let chunks = textHelper.createTextWallChunks(text)
791
+ queueChunks(chunks, sleepAfterMs: 10)
792
+ }
793
+
794
+ func createQuickNoteIfNeeded(_ text: String) async {
795
+ if quickNotes.count == 0 {
796
+ await addQuickNote(text)
797
+ } else {
798
+ await updateQuickNote(id: quickNotes[0].id, newText: text)
799
+ }
800
+ }
801
+
802
+ func sendDoubleTextWall(_ top: String, _ bottom: String) {
803
+ let chunks = textHelper.createDoubleTextWallChunks(textTop: top, textBottom: bottom)
804
+ queueChunks(chunks, sleepAfterMs: 10)
805
+
806
+ // quick note testing:
807
+ // Task {
808
+ // await createQuickNoteIfNeeded(top + "\n" + bottom)
809
+ // await sendQuickNotesToGlasses()
810
+ // }
811
+ }
812
+
813
+ private func sendQuickNotesToGlasses() async {
814
+ // guard let rightGlass = rightPeripheral,
815
+ // let leftGlass = leftPeripheral,
816
+ // let rightTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: rightGlass),
817
+ // let leftTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: leftGlass) else {
818
+ // return
819
+ // }
820
+
821
+ // if !self.isHeadUp {
822
+ // return
823
+ // }
824
+
825
+ // First, clear all existing notes
826
+ // for noteNumber in 1...2 {
827
+ let noteNumber = 1
828
+ var command = Data()
829
+ command.append(Commands.QUICK_NOTE_ADD.rawValue)
830
+ command.append(0x10) // Fixed length for delete command
831
+ command.append(0x00) // Fixed byte
832
+ command.append(0xE0) // Version byte for delete
833
+ command.append(contentsOf: [0x03, 0x01, 0x00, 0x01, 0x00]) // Fixed bytes
834
+ command.append(UInt8(noteNumber)) // Note number to delete
835
+ command.append(contentsOf: [0x00, 0x01, 0x00, 0x01, 0x00, 0x00]) // Fixed bytes for delete
836
+
837
+ // // Send delete command to both glasses with proper timing
838
+ // rightGlass.writeValue(command, for: rightTxChar, type: .withResponse)
839
+ // try? await Task.sleep(nanoseconds: 50 * 1_000_000)
840
+ // leftGlass.writeValue(command, for: leftTxChar, type: .withResponse)
841
+ // try? await Task.sleep(nanoseconds: 150 * 1_000_000)
842
+
843
+ // convert command to array of UInt8
844
+ let commandArray = command.map { $0 }
845
+ queueChunks([commandArray])
846
+ // }
847
+
848
+ // Then add all current notes
849
+ for (index, note) in quickNotes.prefix(4).enumerated() {
850
+ let slotNumber = index + 1
851
+
852
+ guard let textData = note.text.data(using: .utf8),
853
+ let nameData = "Quick Note2".data(using: .utf8)
854
+ else {
855
+ continue
856
+ }
857
+
858
+ // Calculate payload length
859
+ let fixedBytes: [UInt8] = [0x03, 0x01, 0x00, 0x01, 0x00]
860
+ let versionByte = UInt8(
861
+ Date().timeIntervalSince1970.truncatingRemainder(dividingBy: 256)
862
+ )
863
+ let payloadLength =
864
+ 1 // Fixed byte
865
+ + 1 // Version byte
866
+ + fixedBytes.count // Fixed bytes sequence
867
+ + 1 // Note number
868
+ + 1 // Fixed byte 2
869
+ + 1 // Name length
870
+ + nameData.count // Name bytes
871
+ + 1 // Text length
872
+ + 1 // Fixed byte after text length
873
+ + textData.count // Text bytes
874
+ + 2 // Final bytes
875
+
876
+ // Build command
877
+ var command = Data()
878
+ command.append(Commands.QUICK_NOTE_ADD.rawValue)
879
+ command.append(UInt8(payloadLength & 0xFF))
880
+ command.append(0x00) // Fixed byte
881
+ command.append(versionByte)
882
+ command.append(contentsOf: fixedBytes)
883
+ command.append(UInt8(slotNumber))
884
+ command.append(0x01) // Fixed byte 2
885
+ command.append(UInt8(nameData.count))
886
+ command.append(nameData)
887
+ command.append(UInt8(textData.count))
888
+ command.append(0x00) // Fixed byte
889
+ command.append(textData)
890
+
891
+ // convert command to array of UInt8
892
+ let commandArray = command.map { $0 }
893
+ queueChunks([commandArray])
894
+ }
895
+ }
896
+
897
+ func addQuickNote(_ text: String) async {
898
+ let note = QuickNote(id: UUID(), text: text, timestamp: Date())
899
+ quickNotes.append(note)
900
+ }
901
+
902
+ func updateQuickNote(id: UUID, newText: String) async {
903
+ if let index = quickNotes.firstIndex(where: { $0.id == id }) {
904
+ quickNotes[index] = QuickNote(id: id, text: newText, timestamp: Date())
905
+ }
906
+ }
907
+
908
+ func removeQuickNote(id: UUID) async {
909
+ quickNotes.removeAll { $0.id == id }
910
+ }
911
+
912
+ func clearQuickNotes() async {
913
+ quickNotes.removeAll()
914
+ }
915
+
916
+ /// only set to true when we receive init_ack response from the glasses
917
+ func setReadiness(left: Bool?, right: Bool?) {
918
+ let prevLeftReady = leftReady
919
+ let prevRightReady = rightReady
920
+
921
+ if left != nil {
922
+ leftReady = left!
923
+ if !prevLeftReady, leftReady {
924
+ Bridge.log("Left ready!")
925
+ }
926
+ }
927
+ if right != nil {
928
+ rightReady = right!
929
+ if !prevRightReady, rightReady {
930
+ Bridge.log("Right ready!")
931
+ }
932
+ }
933
+
934
+ // CoreCommsService.log("g1Ready set to \(leftReady) \(rightReady) \(leftReady && rightReady) left: \(left), right: \(right)")
935
+ fullyBooted = leftReady && rightReady
936
+ connected = leftReady && rightReady
937
+ if fullyBooted {
938
+ stopReconnectionTimer()
939
+ }
940
+ }
941
+
942
+ func stopScan() {
943
+ centralManager!.stopScan()
944
+ Bridge.log("G1: Stopped scanning for devices")
945
+ }
946
+
947
+ func getSerialNumberInfo() -> [String: Any] {
948
+ return [
949
+ "serialNumber": serialNumber ?? "",
950
+ "style": style ?? "",
951
+ "color": color ?? "",
952
+ ]
953
+ }
954
+
955
+ func getConnectedBluetoothName() -> String? {
956
+ // Return the name of the first connected peripheral (either left or right)
957
+ if let leftName = leftPeripheral?.name {
958
+ return leftName
959
+ }
960
+ return rightPeripheral?.name
961
+ }
962
+
963
+ func disconnect() {
964
+ isDisconnecting = true
965
+ leftGlassUUID = nil
966
+ rightGlassUUID = nil
967
+ stopReconnectionTimer()
968
+
969
+ if let left = leftPeripheral {
970
+ centralManager!.cancelPeripheralConnection(left)
971
+ }
972
+
973
+ if let right = rightPeripheral {
974
+ centralManager!.cancelPeripheralConnection(right)
975
+ }
976
+
977
+ leftPeripheral = nil
978
+ rightPeripheral = nil
979
+ setReadiness(left: false, right: false)
980
+ Bridge.log("G1: Disconnected from glasses")
981
+ }
982
+
983
+ private func attemptSend(cmd: BufferedCommand, side: String) async {
984
+ var maxAttempts = 5
985
+ var attempts = 0
986
+ var success = false
987
+ let chunks = cmd.chunks
988
+
989
+ while attempts < maxAttempts, !success {
990
+ if attempts > 0 {
991
+ Bridge.log("G1: trying again to send to:\(side): \(attempts)")
992
+ }
993
+ let data = Data(chunks[0])
994
+ // CoreCommsService.log("SEND (\(side)) \(data.hexEncodedString())")
995
+
996
+ if isDisconnecting {
997
+ // forget whatever we were doing since we're disconnecting:
998
+ break
999
+ }
1000
+
1001
+ for i in 0 ..< chunks.count - 1 {
1002
+ let chunk = chunks[i]
1003
+
1004
+ let firstFewBytes = String(Data(chunk).hexEncodedString().prefix(16))
1005
+ // CoreCommsService.log("SEND (\(side)) \(firstFewBytes)")
1006
+ await sendCommandToSideWithoutResponse(chunk, side: side)
1007
+ try? await Task.sleep(nanoseconds: UInt64(cmd.chunkTimeMs) * 1_000_000) // 8ms
1008
+ }
1009
+
1010
+ let lastChunk = chunks.last!
1011
+
1012
+ var sequenceNumber = -1
1013
+
1014
+ // if this is a text chunk, set the sequence to the 2nd byte of the chunk:
1015
+ if lastChunk[0] == Commands.BLE_REQ_EVENAI.rawValue {
1016
+ sequenceNumber = Int(lastChunk[1])
1017
+ }
1018
+
1019
+ if lastChunk[0] == Commands.CRC_CHECK.rawValue {
1020
+ sequenceNumber = Int(lastChunk[1])
1021
+ }
1022
+
1023
+ if cmd.lastFrameMs > 0 {
1024
+ try? await Task.sleep(nanoseconds: UInt64(cmd.lastFrameMs) * 1_000_000) // 100ms
1025
+ }
1026
+
1027
+ let firstFewBytes = String(Data(lastChunk).hexEncodedString().prefix(16)).uppercased()
1028
+ // don't log if it starts with 25 (heartbeat):
1029
+ if lastChunk[0] != Commands.BLE_REQ_HEARTBEAT.rawValue {
1030
+ // Bridge.log("SEND (\(side)) \(firstFewBytes)")
1031
+ }
1032
+
1033
+ // if (lastChunk[0] == 0x4E) {
1034
+ // sequenceNumber = Int(lastChunk[1])
1035
+ // }
1036
+
1037
+ // CoreCommsService.log("G1: SENDING with sequenceNumber: \(sequenceNumber)")
1038
+
1039
+ // for heartbeats, don't retry and assume success since the glasses don't respond:
1040
+ if lastChunk[0] == Commands.BLE_REQ_HEARTBEAT.rawValue {
1041
+ success = true
1042
+ await sendCommandToSideWithoutResponse(lastChunk, side: side)
1043
+ } else {
1044
+ success = await sendCommandToSide2(
1045
+ lastChunk, side: side, attemptNumber: attempts, sequenceNumber: sequenceNumber
1046
+ )
1047
+ }
1048
+
1049
+ // CoreCommsService.log("command success: \(success)")
1050
+ // if (!success) {
1051
+ // CoreCommsService.log("G1: timed out waiting for \(s)")
1052
+ // }
1053
+ // await sendCommandToSideWithoutResponse(lastChunk, side: side)
1054
+ // success = true
1055
+
1056
+ attempts += 1
1057
+ if !success, attempts >= maxAttempts {
1058
+ Bridge.log("G1: ❌ Command timed out!")
1059
+ startReconnectionTimer()
1060
+ break
1061
+ }
1062
+
1063
+ if success {
1064
+ stopReconnectionTimer()
1065
+ // setReadiness(left: true, right: true)
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ /// Process a single number with timeouts
1071
+ private func processCommand(_ command: BufferedCommand) async {
1072
+ if command.chunks.isEmpty {
1073
+ Bridge.log("G1: @@@ chunks was empty! @@@")
1074
+ return
1075
+ }
1076
+
1077
+ // Send to both sides in parallel
1078
+ await withTaskGroup(of: Void.self) { group in
1079
+ if command.sendLeft {
1080
+ group.addTask {
1081
+ await self.attemptSend(cmd: command, side: "L")
1082
+ }
1083
+ }
1084
+
1085
+ if command.sendRight {
1086
+ group.addTask {
1087
+ await self.attemptSend(cmd: command, side: "R")
1088
+ }
1089
+ }
1090
+
1091
+ // Wait for all tasks to complete
1092
+ await group.waitForAll()
1093
+ }
1094
+
1095
+ if command.waitTime > 0 {
1096
+ // wait waitTime milliseconds before moving on to the next command:
1097
+ try? await Task.sleep(nanoseconds: UInt64(command.waitTime) * 1_000_000)
1098
+ } else {
1099
+ // sleep for a min amount of time unless otherwise specified
1100
+ try? await Task.sleep(nanoseconds: 8 * 1_000_000) // Xms
1101
+ }
1102
+ }
1103
+
1104
+ private func waitForSemaphore(semaphore: DispatchSemaphore, timeout: TimeInterval) -> Bool {
1105
+ let result = semaphore.wait(timeout: .now() + timeout)
1106
+ return result == .success
1107
+ }
1108
+
1109
+ private func findCharacteristic(uuid: CBUUID, peripheral: CBPeripheral) -> CBCharacteristic? {
1110
+ for service in peripheral.services ?? [] {
1111
+ for characteristic in service.characteristics ?? [] {
1112
+ if characteristic.uuid == uuid {
1113
+ return characteristic
1114
+ }
1115
+ }
1116
+ }
1117
+ return nil
1118
+ }
1119
+
1120
+ private func getConnectedDevices() -> [CBPeripheral] {
1121
+ return centralManager!.retrieveConnectedPeripherals(withServices: [
1122
+ UART_SERVICE_UUID,
1123
+ ])
1124
+ }
1125
+
1126
+ private func handleAck(from peripheral: CBPeripheral, success: Bool, sequenceNumber: Int = -1) {
1127
+ guard success else { return }
1128
+
1129
+ let side = peripheral == leftPeripheral ? "L" : "R"
1130
+ let key = sequenceNumber == -1 ? side : "\(side)-\(sequenceNumber)"
1131
+
1132
+ Task {
1133
+ let wasWaiting = await ackManager.receiveAck(key: key)
1134
+ if wasWaiting {
1135
+ // Bridge.log("G1: ✅ ACK received for \(key)")
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ private func handleNotification(from peripheral: CBPeripheral, data: Data) {
1141
+ guard let command = data.first else { return } // ensure the data isn't empty
1142
+
1143
+ let side = peripheral == leftPeripheral ? "L" : "R"
1144
+ let s = peripheral == leftPeripheral ? "L" : "R"
1145
+ // Bridge.log("G1: RECV (\(s)) \(data.hexEncodedString())")
1146
+
1147
+ switch Commands(rawValue: command) {
1148
+ case .BLE_REQ_INIT:
1149
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1150
+ handleInitResponse(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1151
+ case .QUICK_NOTE_ADD:
1152
+ handleAck(from: peripheral, success: data[1] == 0x10 || data[1] == 0x43)
1153
+ case .BLE_REQ_MIC_ON:
1154
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1155
+ case .BRIGHTNESS:
1156
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1157
+ case .BLE_EXIT_ALL_FUNCTIONS:
1158
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1159
+ case .WHITELIST:
1160
+ // TODO: ios no idea why the glasses send 0xCB before sending ACK: (CB == continue!)
1161
+ handleAck(
1162
+ from: peripheral,
1163
+ success: data[1] == 0xCB || data[1] == CommandResponse.ACK.rawValue
1164
+ )
1165
+ case .DASHBOARD_LAYOUT_COMMAND:
1166
+ // 0x06 seems arbitrary :/
1167
+ handleAck(from: peripheral, success: data[1] == 0x06)
1168
+ case .DASHBOARD_SHOW:
1169
+ handleAck(
1170
+ from: peripheral, success: data[1] == 0x07 || data[1] == 0x90 || data[1] == 0x0C
1171
+ )
1172
+ case .HEAD_UP_ANGLE:
1173
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1174
+ case .CRC_CHECK:
1175
+ handleAck(from: peripheral, success: true, sequenceNumber: Int(data[1]))
1176
+ case .BMP_END:
1177
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1178
+ case .SILENT_MODE:
1179
+ handleAck(from: peripheral, success: data[1] == CommandResponse.ACK.rawValue)
1180
+ case .BLE_REQ_TRANSFER_MIC_DATA:
1181
+ // compressedVoiceData = data
1182
+ // skip the first 2 bytes:
1183
+ let lc3Data = data.subdata(in: 2 ..< data.count)
1184
+ DeviceManager.shared.handleGlassesMicData(lc3Data)
1185
+ // CoreCommsService.log("G1: Got voice data: " + String(data.count))
1186
+ case .UNK_1:
1187
+ handleAck(from: peripheral, success: true)
1188
+ case .UNK_2:
1189
+ handleAck(from: peripheral, success: true)
1190
+ case .BLE_REQ_HEARTBEAT:
1191
+ Bridge.log("heartbeatCounter: \(heartbeatCounter) data[1]: \(data[1])")
1192
+ handleAck(from: peripheral, success: data[1] == heartbeatCounter - 1)
1193
+ case .BLE_REQ_BATTERY:
1194
+ // TODO: ios handle semaphores correctly here
1195
+ // battery info
1196
+ guard data.count >= 6 && data[1] == 0x66 else {
1197
+ break
1198
+ }
1199
+
1200
+ handleAck(from: peripheral, success: data[1] == 0x66)
1201
+
1202
+ // Response format: 2C 66 [battery%] [flags] [voltage_low] [voltage_high] ...
1203
+ let batteryPercent = Int(data[2])
1204
+ let flags = data[3]
1205
+ let voltageLow = Int(data[4])
1206
+ let voltageHigh = Int(data[5])
1207
+ let rawVoltage = (voltageHigh << 8) | voltageLow
1208
+ let voltage = rawVoltage / 10 // Scale down by 10 to get actual millivolts
1209
+
1210
+ // CoreCommsService.log("G1: Raw battery data - Battery: \(batteryPercent)%, Voltage: \(voltage)mV, Flags: 0x\(String(format: "%02X", flags))")
1211
+
1212
+ // if left, update left battery level, if right, update right battery level
1213
+ if peripheral == leftPeripheral {
1214
+ if leftBatteryLevel != batteryPercent {
1215
+ Bridge.log("G1: Left glass battery: \(batteryPercent)%")
1216
+ leftBatteryLevel = batteryPercent
1217
+ }
1218
+ } else if peripheral == rightPeripheral {
1219
+ if rightBatteryLevel != batteryPercent {
1220
+ Bridge.log("G1: Right glass battery: \(batteryPercent)%")
1221
+ rightBatteryLevel = batteryPercent
1222
+ }
1223
+ }
1224
+
1225
+ // update the main battery level as the lower of the two
1226
+ let newBatteryLevel = min(leftBatteryLevel, rightBatteryLevel)
1227
+ if batteryLevel != newBatteryLevel {
1228
+ batteryLevel = min(leftBatteryLevel, rightBatteryLevel)
1229
+ }
1230
+ case .BLE_REQ_EVENAI:
1231
+ guard data.count > 1 else { break }
1232
+ handleAck(
1233
+ from: peripheral, success: data[1] == CommandResponse.ACK.rawValue,
1234
+ sequenceNumber: Int(data[2])
1235
+ )
1236
+ case .BLE_REQ_DEVICE_ORDER:
1237
+ let order = data[1]
1238
+ switch DeviceOrders(rawValue: order) {
1239
+ case .HEAD_UP:
1240
+ Bridge.log("G1: HEAD_UP")
1241
+ DeviceStore.shared.apply("glasses", "headUp", true)
1242
+ case .HEAD_UP2:
1243
+ Bridge.log("G1: HEAD_UP2")
1244
+ DeviceStore.shared.apply("glasses", "headUp", true)
1245
+ // case .HEAD_DOWN:
1246
+ // CoreCommsService.log("HEAD_DOWN")
1247
+ // break
1248
+ case .HEAD_DOWN2:
1249
+ Bridge.log("G1: HEAD_DOWN2")
1250
+ DeviceStore.shared.apply("glasses", "headUp", false)
1251
+ case .ACTIVATED:
1252
+ Bridge.log("G1: ACTIVATED")
1253
+ case .SILENCED:
1254
+ Bridge.log("G1: SILENCED")
1255
+ case .DISPLAY_READY:
1256
+ Bridge.log("G1: DISPLAY_READY")
1257
+ // sendInitCommand(to: peripheral)// experimental
1258
+ case .TRIGGER_FOR_AI:
1259
+ Bridge.log("G1: TRIGGER AI")
1260
+ case .TRIGGER_FOR_STOP_RECORDING:
1261
+ Bridge.log("G1: STOP RECORDING")
1262
+ case .TRIGGER_CHANGE_PAGE:
1263
+ Bridge.log("G1: TRIGGER_CHANGE_PAGE")
1264
+ case .CASE_REMOVED:
1265
+ Bridge.log("G1: REMOVED FROM CASE")
1266
+ caseRemoved = true
1267
+ case .CASE_REMOVED2:
1268
+ Bridge.log("G1: REMOVED FROM CASE2")
1269
+ caseRemoved = true
1270
+ case .CASE_OPEN:
1271
+ caseOpen = true
1272
+ caseRemoved = false
1273
+ Bridge.log("G1: CASE OPEN")
1274
+ case .CASE_CLOSED:
1275
+ caseOpen = false
1276
+ caseRemoved = false
1277
+ Bridge.log("G1: CASE CLOSED")
1278
+ case .CASE_CHARGING_STATUS:
1279
+ guard data.count >= 3 else { break }
1280
+ let status = data[2]
1281
+ if status == 0x01 {
1282
+ caseCharging = true
1283
+ Bridge.log("G1: CASE CHARGING")
1284
+ } else {
1285
+ caseCharging = false
1286
+ Bridge.log("G1: CASE NOT CHARGING")
1287
+ }
1288
+ case .CASE_CHARGE_INFO:
1289
+ Bridge.log("G1: CASE CHARGE INFO")
1290
+ guard data.count >= 3 else { break }
1291
+ if Int(data[2]) != -1 {
1292
+ let newCaseBatteryLevel = Int(data[2])
1293
+ DeviceStore.shared.apply("glasses", "caseBatteryLevel", newCaseBatteryLevel)
1294
+ Bridge.log("G1: Case battery level: \(newCaseBatteryLevel)%")
1295
+ } else {
1296
+ Bridge.log("G1: Case battery level was -1")
1297
+ }
1298
+ case .DOUBLE_TAP:
1299
+ Bridge.log("G1: DOUBLE TAP / display turned off")
1300
+ // Task {
1301
+ //// RN_sendText("DOUBLE TAP DETECTED")
1302
+ //// queueChunks([[UInt8(0x00), UInt8(0x01)]])
1303
+ // try? await Task.sleep(nanoseconds: 1500 * 1_000_000) // 2s delay after sending
1304
+ // sendInit()
1305
+ // clearState()
1306
+ // }
1307
+ default:
1308
+ // Core.log("G1: Received device order: \(data.subdata(in: 1..<data.count).hexEncodedString())")
1309
+ break
1310
+ }
1311
+ default:
1312
+ Bridge.log("G1: received from G1(not handled): \(data.hexEncodedString())")
1313
+ }
1314
+ }
1315
+ }
1316
+
1317
+ // MARK: Commands
1318
+
1319
+ extension G1 {
1320
+ /// Handle whitelist functionality
1321
+ func getWhitelistChunks() -> [[UInt8]] {
1322
+ // Define the hardcoded whitelist JSON
1323
+ let apps = [
1324
+ ["id": "com.mentra.os", "name": "MentraOS"],
1325
+ ["id": "io.heckel.ntfy", "name": "ntfy"],
1326
+ ]
1327
+ let whitelistJson = createWhitelistJson(apps: apps)
1328
+
1329
+ Bridge.log("G1: Creating chunks for hardcoded whitelist: \(whitelistJson)")
1330
+
1331
+ // Convert JSON to bytes and split into chunks
1332
+ return createWhitelistChunks(json: whitelistJson)
1333
+ }
1334
+
1335
+ private func createWhitelistJson(apps: [[String: String]]) -> String {
1336
+ do {
1337
+ // Create app list array
1338
+ var appList: [[String: Any]] = []
1339
+ for app in apps {
1340
+ let appDict: [String: Any] = [
1341
+ "id": app["id"] ?? "",
1342
+ "name": app["name"] ?? "",
1343
+ ]
1344
+ appList.append(appDict)
1345
+ }
1346
+
1347
+ // Create the whitelist dictionary
1348
+ let whitelistDict: [String: Any] = [
1349
+ "calendar_enable": true,
1350
+ "call_enable": true,
1351
+ "msg_enable": true,
1352
+ "ios_mail_enable": true,
1353
+ "app": [
1354
+ "list": appList,
1355
+ "enable": true,
1356
+ ],
1357
+ ]
1358
+
1359
+ // Convert to JSON string
1360
+ let jsonData = try JSONSerialization.data(withJSONObject: whitelistDict, options: [])
1361
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
1362
+ return jsonString
1363
+ } else {
1364
+ return "{}"
1365
+ }
1366
+ } catch {
1367
+ Bridge.log("G1: Error creating whitelist JSON: \(error.localizedDescription)")
1368
+ return "{}"
1369
+ }
1370
+ }
1371
+
1372
+ /// Helper function to split JSON into chunks
1373
+ private func createWhitelistChunks(json: String) -> [[UInt8]] {
1374
+ let MAX_CHUNK_SIZE = 180 - 4 // Reserve space for the header
1375
+ guard let jsonData = json.data(using: .utf8) else { return [] }
1376
+
1377
+ let totalChunks = Int(ceil(Double(jsonData.count) / Double(MAX_CHUNK_SIZE)))
1378
+ var chunks: [Data] = []
1379
+
1380
+ Bridge.log("G1: jsonData.count = \(jsonData.count), totalChunks = \(totalChunks)")
1381
+
1382
+ for i in 0 ..< totalChunks {
1383
+ let start = i * MAX_CHUNK_SIZE
1384
+ let end = min(start + MAX_CHUNK_SIZE, jsonData.count)
1385
+ let range = start ..< end
1386
+ let payloadChunk = jsonData.subdata(in: range)
1387
+
1388
+ // Create the header: [WHITELIST_CMD, total_chunks, chunk_index]
1389
+ var headerData = Data()
1390
+ headerData.append(Commands.WHITELIST.rawValue)
1391
+ headerData.append(UInt8(totalChunks))
1392
+ headerData.append(UInt8(i))
1393
+
1394
+ // Combine header and payload
1395
+ var chunkData = Data()
1396
+ chunkData.append(headerData)
1397
+ chunkData.append(payloadChunk)
1398
+
1399
+ chunks.append(chunkData)
1400
+ }
1401
+
1402
+ var uintChunks: [[UInt8]] = []
1403
+ for chunk in chunks {
1404
+ uintChunks.append(Array(chunk))
1405
+ }
1406
+ return uintChunks
1407
+ // return chunks.flatMap { Array($0) }
1408
+ }
1409
+
1410
+ func exitAllFunctions(to peripheral: CBPeripheral, characteristic: CBCharacteristic) {
1411
+ var data = Data()
1412
+ data.append(Commands.BLE_EXIT_ALL_FUNCTIONS.rawValue)
1413
+ peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
1414
+ }
1415
+
1416
+ private func sendInitCommand(to peripheral: CBPeripheral) {
1417
+ let initData = Data([Commands.BLE_REQ_INIT.rawValue, 0x01])
1418
+ let initDataArray = initData.map { UInt8($0) }
1419
+
1420
+ if leftPeripheral == peripheral {
1421
+ queueChunks([initDataArray], sendLeft: true, sendRight: false)
1422
+ } else if rightPeripheral == peripheral {
1423
+ queueChunks([initDataArray], sendLeft: false, sendRight: true)
1424
+ }
1425
+ }
1426
+
1427
+ private func sendInit() {
1428
+ let initData = Data([Commands.BLE_REQ_INIT.rawValue, 0x01])
1429
+ let initDataArray = initData.map { UInt8($0) }
1430
+ queueChunks([initDataArray])
1431
+ }
1432
+
1433
+ func exit() {
1434
+ let exitData = Data([Commands.BLE_EXIT_ALL_FUNCTIONS.rawValue])
1435
+ let exitDataArray = exitData.map { UInt8($0) }
1436
+ queueChunks([exitDataArray])
1437
+ }
1438
+
1439
+ func sendShutdown() {
1440
+ Bridge.log("sendShutdown - not supported on G1")
1441
+ }
1442
+
1443
+ func sendReboot() {
1444
+ Bridge.log("sendReboot - not supported on G1")
1445
+ }
1446
+
1447
+ func sendRgbLedControl(
1448
+ requestId: String, packageName _: String?, action _: String, color _: String?,
1449
+ ontime _: Int, offtime _: Int, count _: Int
1450
+ ) {
1451
+ Bridge.log("sendRgbLedControl - not supported on G1")
1452
+ Bridge.sendRgbLedControlResponse(
1453
+ requestId: requestId, success: false, error: "device_not_supported"
1454
+ )
1455
+ }
1456
+
1457
+ /// don't call semaphore signals here as it's handled elswhere:
1458
+ private func handleInitResponse(from peripheral: CBPeripheral, success: Bool) {
1459
+ if peripheral == leftPeripheral {
1460
+ leftInitialized = success
1461
+ // CoreCommsService.log("G1: Left arm initialized: \(success)")
1462
+ setReadiness(left: true, right: nil)
1463
+ } else if peripheral == rightPeripheral {
1464
+ rightInitialized = success
1465
+ // CoreCommsService.log("G1: Right arm initialized: \(success)")
1466
+ setReadiness(left: nil, right: true)
1467
+ }
1468
+
1469
+ // Only proceed if both glasses are initialized
1470
+ if leftInitialized, rightInitialized {
1471
+ setReadiness(left: true, right: true)
1472
+ }
1473
+ }
1474
+
1475
+ private func sendHeartbeat() {
1476
+ incrementHeartbeatCounter()
1477
+
1478
+ var heartbeatData = Data()
1479
+ // heartbeatData.append(Commands.BLE_REQ_HEARTBEAT.rawValue)
1480
+ // heartbeatData.append(UInt8(0x02 & 0xFF))
1481
+ heartbeatData.append(Commands.BLE_REQ_HEARTBEAT.rawValue)
1482
+ heartbeatData.append(UInt8(heartbeatCounter & 0xFF))
1483
+
1484
+ var heartbeatArray = heartbeatData.map { UInt8($0) }
1485
+
1486
+ if fullyBooted {
1487
+ queueChunks([heartbeatArray])
1488
+ }
1489
+
1490
+ // Periodically request battery status
1491
+ if leftBatteryLevel == -1 || rightBatteryLevel == -1 || heartbeatCounter % 10 == 0 {
1492
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
1493
+ self?.getBatteryStatus()
1494
+ }
1495
+ }
1496
+ // if let txChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: peripheral) {
1497
+ // let hexString = heartbeatData.map { String(format: "%02X", $0) }.joined()
1498
+ // peripheral.writeValue(heartbeatData, for: txChar, type: .withoutResponse)
1499
+ // }
1500
+ }
1501
+
1502
+ func sendCommandToSide(_ command: [UInt8], side: String) async {
1503
+ // Convert to Data
1504
+ let commandData = Data(command)
1505
+ // CoreCommsService.log("G1: Sending command to glasses: \(paddedCommand.map { String(format: "%02X", $0) }.joined(separator: " "))")
1506
+ // CoreCommsService.log("G1: SEND (\(side)) \(commandData.hexEncodedString())")
1507
+
1508
+ if side == "L" {
1509
+ // send to left
1510
+ if let leftPeripheral = leftPeripheral,
1511
+ let characteristic = leftPeripheral.services?
1512
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1513
+ .characteristics?
1514
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1515
+ {
1516
+ leftPeripheral.writeValue(commandData, for: characteristic, type: .withResponse)
1517
+ }
1518
+ } else {
1519
+ // send to right
1520
+ if let rightPeripheral = rightPeripheral,
1521
+ let characteristic = rightPeripheral.services?
1522
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1523
+ .characteristics?
1524
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1525
+ {
1526
+ rightPeripheral.writeValue(commandData, for: characteristic, type: .withResponse)
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ private func sendCommandToSide2(
1532
+ _ command: [UInt8],
1533
+ side: String,
1534
+ attemptNumber: Int = 0,
1535
+ sequenceNumber: Int = -1
1536
+ ) async -> Bool {
1537
+ let startTime = Date()
1538
+ let commandData = Data(command)
1539
+
1540
+ let peripheral: CBPeripheral?
1541
+ let characteristic: CBCharacteristic?
1542
+
1543
+ if side == "L" {
1544
+ peripheral = leftPeripheral
1545
+ characteristic = leftPeripheral?.services?
1546
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1547
+ .characteristics?
1548
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1549
+ } else {
1550
+ peripheral = rightPeripheral
1551
+ characteristic = rightPeripheral?.services?
1552
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1553
+ .characteristics?
1554
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1555
+ }
1556
+
1557
+ guard let peripheral, let characteristic else {
1558
+ Bridge.log("G1: ⚠️ peripheral/characteristic not found, resuming immediately")
1559
+ return false
1560
+ }
1561
+
1562
+ let key = sequenceNumber == -1 ? side : "\(side)-\(sequenceNumber)"
1563
+ let waitTimeMs = Int((0.3 + (0.2 * Double(attemptNumber))) * 1000)
1564
+
1565
+ return await ackManager.waitForAck(key: key, timeoutMs: waitTimeMs) {
1566
+ peripheral.writeValue(commandData, for: characteristic, type: .withResponse)
1567
+ } onTimeout: {
1568
+ let elapsed = Date().timeIntervalSince(startTime) * 1000
1569
+ Bridge.log("G1: ⚠️ ACK timeout for \(key) after \(String(format: "%.0f", elapsed))ms")
1570
+ }
1571
+ }
1572
+
1573
+ /// FAST BLE TRANSMISSION (.withoutResponse)
1574
+ func sendCommandToSideWithoutResponse(_ command: [UInt8], side: String) async {
1575
+ // Convert to Data
1576
+ let commandData = Data(command)
1577
+
1578
+ if side == "L" {
1579
+ // send to left
1580
+ if let leftPeripheral = leftPeripheral,
1581
+ let characteristic = leftPeripheral.services?
1582
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1583
+ .characteristics?
1584
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1585
+ {
1586
+ // Fast approach: .withoutResponse for speed
1587
+ leftPeripheral.writeValue(commandData, for: characteristic, type: .withoutResponse)
1588
+ }
1589
+ } else {
1590
+ // send to right
1591
+ if let rightPeripheral = rightPeripheral,
1592
+ let characteristic = rightPeripheral.services?
1593
+ .first(where: { $0.uuid == UART_SERVICE_UUID })?
1594
+ .characteristics?
1595
+ .first(where: { $0.uuid == UART_TX_CHAR_UUID })
1596
+ {
1597
+ // Fast approach: .withoutResponse for speed
1598
+ rightPeripheral.writeValue(commandData, for: characteristic, type: .withoutResponse)
1599
+ }
1600
+ }
1601
+
1602
+ // No waiting for ACK - fire and forget for speed
1603
+ }
1604
+
1605
+ func queueChunks(
1606
+ _ chunks: [[UInt8]], sendLeft: Bool = true, sendRight: Bool = true, sleepAfterMs: Int = 0,
1607
+ ignoreAck: Bool = false, chunkTimeMs: Int = 8, lastFrameMs: Int = 100
1608
+ ) {
1609
+ let bufferedCommand = BufferedCommand(
1610
+ chunks: chunks, sendLeft: sendLeft, sendRight: sendRight, waitTime: sleepAfterMs,
1611
+ ignoreAck: ignoreAck, chunkTimeMs: chunkTimeMs, lastFrameMs: lastFrameMs
1612
+ )
1613
+ Task {
1614
+ await commandQueue.enqueue(bufferedCommand)
1615
+ }
1616
+ }
1617
+
1618
+ func sendWhitelist() {
1619
+ Bridge.log("G1: sendWhitelist()")
1620
+ let whitelistChunks = getWhitelistChunks()
1621
+ queueChunks(whitelistChunks, sendLeft: true, sendRight: true, sleepAfterMs: 100)
1622
+ }
1623
+
1624
+ func setBrightness(_ level: Int, autoMode: Bool = false) {
1625
+ // Convert from percentage (0-100) to the correct range (0-41)
1626
+ let mappedLevel = min(41, max(0, Int((Double(level) / 100.0) * 41.0)))
1627
+
1628
+ // Create and capture the UInt8 value
1629
+ let brightnessLevel = UInt8(mappedLevel)
1630
+
1631
+ // Call the async function from a non-async context
1632
+ Task {
1633
+ let success = await setBrightnessRaw(brightnessLevel, autoMode: autoMode)
1634
+ if !success {
1635
+ NSLog("Failed to set brightness to level \(level)% (mapped to \(mappedLevel))")
1636
+ }
1637
+ }
1638
+ }
1639
+
1640
+ func setBrightnessRaw(_ level: UInt8, autoMode: Bool = false) async -> Bool {
1641
+ Bridge.log("G1: setBrightness()")
1642
+ // Ensure level is between 0x00 and 0x29 (0-41)
1643
+ var lvl: UInt8 = level
1644
+ if level > 0x29 {
1645
+ lvl = 0x29
1646
+ }
1647
+
1648
+ let command: [UInt8] = [Commands.BRIGHTNESS.rawValue, lvl, autoMode ? 0x01 : 0x00]
1649
+ queueChunks([command])
1650
+
1651
+ // buried data point testing:
1652
+ // let command: [UInt8] = [0x3E]
1653
+ // queueChunks([command])
1654
+
1655
+ // // Send to both glasses with proper timing
1656
+ // if let rightGlass = rightPeripheral,
1657
+ // let rightTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: rightGlass) {
1658
+ // rightGlass.writeValue(Data(command), for: rightTxChar, type: .withResponse)
1659
+ // try? await Task.sleep(nanoseconds: 50 * 1_000_000) // 50ms delay
1660
+ // }
1661
+ //
1662
+ // if let leftGlass = leftPeripheral,
1663
+ // let leftTxChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: leftGlass) {
1664
+ // leftGlass.writeValue(Data(command), for: leftTxChar, type: .withResponse)
1665
+ // }
1666
+
1667
+ return true
1668
+ }
1669
+
1670
+ func setHeadUpAngle(_ angle: Int) {
1671
+ var agl: Int = angle
1672
+ if angle < 0 {
1673
+ agl = 0
1674
+ } else if angle > 60 {
1675
+ agl = 60
1676
+ }
1677
+
1678
+ // Call the async function from a non-async context
1679
+ Task {
1680
+ await setHeadUpAngleRaw(UInt8(agl))
1681
+ }
1682
+ }
1683
+
1684
+ func setHeadUpAngleRaw(_ angle: UInt8) async {
1685
+ Bridge.log("G1: setHeadUpAngle()")
1686
+ let command: [UInt8] = [Commands.HEAD_UP_ANGLE.rawValue, angle, 0x01]
1687
+ queueChunks([command])
1688
+ }
1689
+
1690
+ func getBatteryStatus() {
1691
+ Bridge.log("G1: getBatteryStatus()")
1692
+ let command: [UInt8] = [Commands.BLE_REQ_BATTERY.rawValue, 0x01]
1693
+ queueChunks([command])
1694
+ }
1695
+
1696
+ func setSilentMode(_ enabled: Bool) async {
1697
+ let command: [UInt8] = [Commands.SILENT_MODE.rawValue, enabled ? 0x0C : 0x0A, 0x00]
1698
+ queueChunks([command])
1699
+ }
1700
+
1701
+ func setDashboardPosition(_ height: Int, _ depth: Int) {
1702
+ Task {
1703
+ await setDashboardPositionRaw(UInt8(height), UInt8(depth))
1704
+ }
1705
+ }
1706
+
1707
+ func incrementGlobalCounter() {
1708
+ if globalCounter < 255 {
1709
+ globalCounter += 1
1710
+ } else {
1711
+ globalCounter = 0
1712
+ }
1713
+ }
1714
+
1715
+ func incrementHeartbeatCounter() {
1716
+ if heartbeatCounter < 255 {
1717
+ heartbeatCounter += 1
1718
+ } else {
1719
+ heartbeatCounter = 0
1720
+ }
1721
+ }
1722
+
1723
+ func setDashboardPositionRaw(_ height: UInt8, _ depth: UInt8) async -> Bool {
1724
+ let h: UInt8 = min(max(height, 0), 8)
1725
+ let d: UInt8 = min(max(depth, 1), 9)
1726
+
1727
+ incrementGlobalCounter()
1728
+
1729
+ // Build dashboard position command
1730
+ var command = Data()
1731
+ command.append(Commands.DASHBOARD_LAYOUT_COMMAND.rawValue)
1732
+ command.append(0x08) // Length
1733
+ command.append(0x00) // Sequence
1734
+ command.append(globalCounter & 0xFF) // Fixed value
1735
+ command.append(0x02) // Fixed value
1736
+ command.append(0x01) // State ON
1737
+ command.append(h) // height
1738
+ command.append(d) // depth
1739
+
1740
+ // while command.count < 20 {
1741
+ // command.append(0x00)
1742
+ // }
1743
+
1744
+ // convert command to array of UInt8
1745
+ let commandArray = command.map { $0 }
1746
+ queueChunks([commandArray])
1747
+ return true
1748
+ }
1749
+
1750
+ func setMicEnabled(_ enabled: Bool) {
1751
+ Bridge.log("G1: setMicEnabled() \(enabled)")
1752
+ DeviceStore.shared.apply("glasses", "micEnabled", enabled)
1753
+ var micOnData = Data()
1754
+ micOnData.append(Commands.BLE_REQ_MIC_ON.rawValue)
1755
+ if enabled {
1756
+ micOnData.append(0x01)
1757
+ } else {
1758
+ micOnData.append(0x00)
1759
+ }
1760
+
1761
+ let micOnDataArray: [UInt8] = micOnData.map { UInt8($0) }
1762
+
1763
+ queueChunks([micOnDataArray], sendLeft: false, sendRight: true)
1764
+
1765
+ // if let txChar = findCharacteristic(uuid: UART_TX_CHAR_UUID, peripheral: peripheral) {
1766
+ // peripheral.writeValue(micOnData, for: txChar, type: .withResponse)
1767
+ // }
1768
+ }
1769
+
1770
+ func sortMicRanking(list: [String]) -> [String] {
1771
+ return list
1772
+ }
1773
+
1774
+ // MARK: - Bitmap Conversion
1775
+
1776
+ /// Convert arbitrary image data (PNG/JPEG) to G1-compatible 1-bit BMP format.
1777
+ /// Mirrors G2.convertToG2Bmp() for local miniapp bitmap display support.
1778
+ /// G1 uses 1-bit monochrome BMP at 640x400 (576x135 active area).
1779
+ func convertToG1Bmp(_ imageData: Data, width: Int = 576, height: Int = 135) -> Data? {
1780
+ guard let image = UIImage(data: imageData), let cgImage = image.cgImage else {
1781
+ Bridge.log("G1: convertToG1Bmp - could not decode image")
1782
+ return nil
1783
+ }
1784
+
1785
+ let srcWidth = cgImage.width
1786
+ let srcHeight = cgImage.height
1787
+
1788
+ // Scale to fit within target dimensions, maintaining aspect ratio
1789
+ let scale = min(Double(width) / Double(srcWidth), Double(height) / Double(srcHeight))
1790
+ let scaledW = Int(Double(srcWidth) * scale)
1791
+ let scaledH = Int(Double(srcHeight) * scale)
1792
+ let offsetX = (width - scaledW) / 2
1793
+ let offsetY = (height - scaledH) / 2
1794
+
1795
+ Bridge.log("G1: convertToG1Bmp - input \(srcWidth)x\(srcHeight) -> scaled \(scaledW)x\(scaledH) in \(width)x\(height)")
1796
+
1797
+ // Render to 8-bit grayscale
1798
+ guard let ctx = CGContext(
1799
+ data: nil,
1800
+ width: width,
1801
+ height: height,
1802
+ bitsPerComponent: 8,
1803
+ bytesPerRow: width,
1804
+ space: CGColorSpaceCreateDeviceGray(),
1805
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
1806
+ ) else {
1807
+ Bridge.log("G1: convertToG1Bmp - failed to create CGContext")
1808
+ return nil
1809
+ }
1810
+
1811
+ ctx.setFillColor(gray: 0, alpha: 1)
1812
+ ctx.fill(CGRect(x: 0, y: 0, width: width, height: height))
1813
+ ctx.draw(cgImage, in: CGRect(x: offsetX, y: offsetY, width: scaledW, height: scaledH))
1814
+
1815
+ guard let renderedImage = ctx.makeImage(),
1816
+ let pixels = renderedImage.dataProvider?.data as Data? else {
1817
+ Bridge.log("G1: convertToG1Bmp - failed to get pixel data")
1818
+ return nil
1819
+ }
1820
+
1821
+ // Convert grayscale pixels to 1-bit BMP
1822
+ let rowBytes = ((width + 31) / 32) * 4 // BMP rows are DWORD-aligned
1823
+ let pixelDataSize = rowBytes * height
1824
+ let headerSize = 62 // 14 file header + 40 DIB header + 8 color table
1825
+
1826
+ var bmp = Data()
1827
+
1828
+ // BMP File Header (14 bytes)
1829
+ let fileSize = UInt32(headerSize + pixelDataSize)
1830
+ bmp.append(contentsOf: [0x42, 0x4D]) // "BM"
1831
+ bmp.append(contentsOf: withUnsafeBytes(of: fileSize.littleEndian) { Array($0) })
1832
+ bmp.append(contentsOf: [0x00, 0x00, 0x00, 0x00]) // reserved
1833
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt32(headerSize).littleEndian) { Array($0) })
1834
+
1835
+ // DIB Header (BITMAPINFOHEADER, 40 bytes)
1836
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt32(40).littleEndian) { Array($0) })
1837
+ bmp.append(contentsOf: withUnsafeBytes(of: Int32(width).littleEndian) { Array($0) })
1838
+ bmp.append(contentsOf: withUnsafeBytes(of: Int32(height).littleEndian) { Array($0) })
1839
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt16(1).littleEndian) { Array($0) }) // planes
1840
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt16(1).littleEndian) { Array($0) }) // bpp
1841
+ bmp.append(contentsOf: [UInt8](repeating: 0, count: 4)) // compression = BI_RGB
1842
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt32(pixelDataSize).littleEndian) { Array($0) })
1843
+ bmp.append(contentsOf: withUnsafeBytes(of: Int32(3780).littleEndian) { Array($0) }) // X ppm
1844
+ bmp.append(contentsOf: withUnsafeBytes(of: Int32(3780).littleEndian) { Array($0) }) // Y ppm
1845
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt32(2).littleEndian) { Array($0) }) // colors used
1846
+ bmp.append(contentsOf: withUnsafeBytes(of: UInt32(2).littleEndian) { Array($0) }) // important colors
1847
+
1848
+ // Color table: index 0 = black, index 1 = white
1849
+ bmp.append(contentsOf: [0x00, 0x00, 0x00, 0x00]) // black
1850
+ bmp.append(contentsOf: [0xFF, 0xFF, 0xFF, 0x00]) // white
1851
+
1852
+ // Pixel data (bottom-up row order for BMP)
1853
+ for row in (0..<height).reversed() {
1854
+ var rowData = [UInt8](repeating: 0, count: rowBytes)
1855
+ for col in 0..<width {
1856
+ let pixelIndex = row * width + col
1857
+ let gray = pixels[pixelIndex]
1858
+ if gray >= 128 {
1859
+ let byteIndex = col / 8
1860
+ let bitIndex = 7 - (col % 8)
1861
+ rowData[byteIndex] |= UInt8(1 << bitIndex)
1862
+ }
1863
+ }
1864
+ bmp.append(contentsOf: rowData)
1865
+ }
1866
+
1867
+ Bridge.log("G1: convertToG1Bmp - produced \(bmp.count) byte BMP")
1868
+ return bmp
1869
+ }
1870
+
1871
+ // MARK: - Enhanced BMP Display Methods
1872
+
1873
+ func displayBitmap(base64ImageData: String) async -> Bool {
1874
+ guard let bmpData = Data(base64Encoded: base64ImageData) else {
1875
+ Bridge.log("G1: Failed to decode base64 image data")
1876
+ return false
1877
+ }
1878
+
1879
+ Bridge.log("G1: ✅ Successfully decoded base64 image data to \(bmpData.count) bytes")
1880
+ let invertedBmpData = invertBmpPixels(bmpData)
1881
+ let result = await sendBmp(bmpData: invertedBmpData)
1882
+ Bridge.log("G1: 🖼️ Single frame: Transmission \(result ? "SUCCESS" : "FAILED")")
1883
+ return result
1884
+ }
1885
+
1886
+ func clearDisplay() {
1887
+ Bridge.log("G1: clearDisplay() - Using space")
1888
+ sendTextWall(" ")
1889
+ }
1890
+
1891
+ /// Create a simple test BMP pattern in hex format
1892
+ private func createTestBMPHex() -> String {
1893
+ // BMP header for 576x135 1-bit monochrome (from our working data)
1894
+ let header =
1895
+ "424d36260000000000003e0000002800000040020000870000000100010000000000f82500c40e0000c40e00000200000002000000000000ffffff00"
1896
+
1897
+ // Create a simple pattern: alternating lines
1898
+ var pixelData = ""
1899
+ let bytesPerRow = 72 // 576 pixels / 8 bits per byte
1900
+
1901
+ for row in 0 ..< 135 {
1902
+ for col in 0 ..< bytesPerRow {
1903
+ // Create a pattern: every other row is different
1904
+ if row % 10 < 5 {
1905
+ pixelData += "ff" // White line
1906
+ } else {
1907
+ pixelData += col % 4 == 0 ? "00" : "ff" // Pattern line
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ return header + pixelData
1913
+ }
1914
+
1915
+ private func invertBmpPixels(_ bmpData: Data) -> Data {
1916
+ guard bmpData.count > 62 else {
1917
+ Bridge.log("G1: BMP data too small to contain pixel data")
1918
+ return bmpData
1919
+ }
1920
+
1921
+ // BMP header is 62 bytes for your format (14 byte file header + 40 byte DIB header + 8 byte color table)
1922
+ let headerSize = 62
1923
+ var invertedData = Data(bmpData.prefix(headerSize)) // Keep header unchanged
1924
+
1925
+ // Invert the pixel data (everything after the header)
1926
+ let pixelData = bmpData.dropFirst(headerSize)
1927
+
1928
+ for byte in pixelData {
1929
+ // Invert each byte (flip all bits)
1930
+ let invertedByte = ~byte
1931
+ invertedData.append(invertedByte)
1932
+ }
1933
+
1934
+ Bridge.log("G1: Inverted BMP pixels: \(pixelData.count) bytes processed")
1935
+ return invertedData
1936
+ }
1937
+
1938
+ /// Core MentraOS-compatible BMP display implementation
1939
+ private func sendBmp(bmpData: Data) async -> Bool {
1940
+ // Frame timing validation for animation smoothness
1941
+ let currentTime = Date()
1942
+ let timeSinceLastFrame = currentTime.timeIntervalSince(lastFrameTime)
1943
+
1944
+ // Update frame tracking
1945
+ frameSequence += 1
1946
+ lastFrameTime = currentTime
1947
+
1948
+ Bridge.log(
1949
+ "G1: 🎬 Frame \(frameSequence): \(String(format: "%.0f", timeSinceLastFrame * 1000))ms since last frame"
1950
+ )
1951
+
1952
+ // MentraOS constants - exact match
1953
+ let packLen = 194 // Exact chunk size from MentraOS
1954
+ let iosDelayMs = 8 // iOS delay from MentraOS
1955
+ let addressBytes: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // Address from MentraOS
1956
+
1957
+ // // Debug: Check bmpData integrity before chunking
1958
+ // let pixelDataStart = 62
1959
+ // if bmpData.count > pixelDataStart + 50 {
1960
+ // let beforeChunkSample = Array(bmpData[pixelDataStart..<(pixelDataStart + 20)])
1961
+ // let beforeChunkHex = beforeChunkSample.map { String(format: "%02X", $0) }.joined(separator: " ")
1962
+ // CoreCommsService.log("G1: 🔍 Before chunking - pixel data sample (bytes 62-82): \(beforeChunkHex)")
1963
+ // }
1964
+
1965
+ // Create chunks exactly like MentraOS
1966
+ var multiPacks: [Data] = []
1967
+ var index = 0
1968
+ while index < bmpData.count {
1969
+ let end = min(index + packLen, bmpData.count)
1970
+ let singlePack = bmpData.subdata(in: index ..< end)
1971
+
1972
+ // Debug first few chunks to see where corruption happens
1973
+ if index < 600 { // First 3 chunks (194 * 3 = 582)
1974
+ let chunkSample = Array(singlePack.prefix(20))
1975
+ let chunkHex = chunkSample.map { String(format: "%02X", $0) }.joined(separator: " ")
1976
+ Bridge.log("G1: 🔍 Chunk creation - index \(index), sample: \(chunkHex)")
1977
+ }
1978
+
1979
+ multiPacks.append(singlePack)
1980
+ index += packLen
1981
+ }
1982
+
1983
+ Bridge.log("G1: Created \(multiPacks.count) packs from BMP data (MentraOS format)")
1984
+
1985
+ var chunks: [[UInt8]] = []
1986
+
1987
+ // add bmp data chunks:
1988
+ for (packIndex, pack) in multiPacks.enumerated() {
1989
+ let packData: Data
1990
+ if packIndex == 0 {
1991
+ // First package includes address: [0x15, index, address...]
1992
+ var firstPacketData = Data([0x15, UInt8(packIndex & 0xFF)])
1993
+ firstPacketData.append(Data(addressBytes))
1994
+ firstPacketData.append(pack)
1995
+ packData = firstPacketData
1996
+ } else {
1997
+ // Subsequent packages: [0x15, index, data...]
1998
+ var packetData = Data([0x15, UInt8(packIndex & 0xFF)])
1999
+ packetData.append(pack)
2000
+ packData = packetData
2001
+ }
2002
+
2003
+ chunks.append(Array(packData))
2004
+ }
2005
+
2006
+ // queueChunks(chunks)
2007
+ // chunks.removeAll()
2008
+
2009
+ // add end command:
2010
+ chunks.append([0x20, 0x0D, 0x0E])
2011
+
2012
+ queueChunks(chunks, sleepAfterMs: 25, lastFrameMs: 100)
2013
+ chunks.removeAll()
2014
+
2015
+ // CRC validation like MentraOS - frame 1 should be 0x1914adcf
2016
+ var imageWithAddress = Data(addressBytes)
2017
+ imageWithAddress.append(bmpData)
2018
+
2019
+ // Calculate CRC32-XZ like MentraOS (not standard CRC32)
2020
+ let crc32Value = calculateCRC32XZ(data: imageWithAddress)
2021
+ let crcBytes = Data([
2022
+ UInt8((crc32Value >> 24) & 0xFF),
2023
+ UInt8((crc32Value >> 16) & 0xFF),
2024
+ UInt8((crc32Value >> 8) & 0xFF),
2025
+ UInt8(crc32Value & 0xFF),
2026
+ ])
2027
+
2028
+ var crcCommand = Data([0x16])
2029
+ crcCommand.append(crcBytes)
2030
+
2031
+ chunks.append(Array(crcCommand))
2032
+ queueChunks(chunks)
2033
+ return true
2034
+ }
2035
+
2036
+ /// Helper function to calculate CRC32-XZ like MentraOS (matches Dart crclib)
2037
+ private func calculateCRC32XZ(data: Data) -> UInt32 {
2038
+ // CRC32-XZ table-based implementation (matches Dart crclib exactly)
2039
+ let polynomial: UInt32 = 0x04C1_1DB7
2040
+ var crc: UInt32 = 0xFFFF_FFFF
2041
+
2042
+ // Build CRC table for efficiency (matches crclib behavior)
2043
+ var table: [UInt32] = Array(repeating: 0, count: 256)
2044
+ for i in 0 ..< 256 {
2045
+ var entry = UInt32(i) << 24
2046
+ for _ in 0 ..< 8 {
2047
+ if (entry & 0x8000_0000) != 0 {
2048
+ entry = (entry << 1) ^ polynomial
2049
+ } else {
2050
+ entry <<= 1
2051
+ }
2052
+ }
2053
+ table[i] = entry
2054
+ }
2055
+
2056
+ // Calculate CRC using table lookup (matches MentraOS's crclib)
2057
+ for byte in data {
2058
+ let tableIndex = Int((crc >> 24) ^ UInt32(byte)) & 0xFF
2059
+ crc = (crc << 8) ^ table[tableIndex]
2060
+ }
2061
+
2062
+ return ~crc
2063
+ }
2064
+
2065
+ /// Helper function to calculate CRC32 (simple implementation)
2066
+ private func calculateCRC32(data: Data) -> UInt32 {
2067
+ let polynomial: UInt32 = 0xEDB8_8320
2068
+ var crc: UInt32 = 0xFFFF_FFFF
2069
+
2070
+ for byte in data {
2071
+ crc ^= UInt32(byte)
2072
+ for _ in 0 ..< 8 {
2073
+ if (crc & 1) != 0 {
2074
+ crc = (crc >> 1) ^ polynomial
2075
+ } else {
2076
+ crc = crc >> 1
2077
+ }
2078
+ }
2079
+ }
2080
+
2081
+ return ~crc
2082
+ }
2083
+
2084
+ /// Create BMP chunks with MentraOS-compatible headers
2085
+ private func createBmpChunks(from bmpData: Data, chunkSize: Int) -> [[UInt8]] {
2086
+ var chunks: [[UInt8]] = []
2087
+ let glassesAddress: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // MentraOS uses address 0x1c
2088
+
2089
+ let totalChunks = (bmpData.count + chunkSize - 1) / chunkSize
2090
+
2091
+ for i in 0 ..< totalChunks {
2092
+ let start = i * chunkSize
2093
+ let end = min(start + chunkSize, bmpData.count)
2094
+ let chunkData = bmpData.subdata(in: start ..< end)
2095
+
2096
+ var chunk: [UInt8] = []
2097
+
2098
+ // First chunk needs address bytes
2099
+ if i == 0 {
2100
+ chunk.append(0x15) // Command
2101
+ chunk.append(UInt8(i & 0xFF)) // Sequence
2102
+ chunk.append(contentsOf: glassesAddress) // Address
2103
+ chunk.append(contentsOf: chunkData)
2104
+ } else {
2105
+ chunk.append(0x15) // Command
2106
+ chunk.append(UInt8(i & 0xFF)) // Sequence
2107
+ chunk.append(contentsOf: chunkData)
2108
+ }
2109
+
2110
+ chunks.append(chunk)
2111
+ }
2112
+
2113
+ return chunks
2114
+ }
2115
+
2116
+ /// Send CRC with retry logic
2117
+ private func sendBmpCrcWithRetry(
2118
+ bmpData: Data,
2119
+ sendLeft: Bool,
2120
+ sendRight: Bool,
2121
+ maxAttempts: Int,
2122
+ timeoutMs: Int
2123
+ ) async -> Bool {
2124
+ // Create data with address for CRC calculation (MentraOS pattern)
2125
+ let glassesAddress: [UInt8] = [0x00, 0x1C, 0x00, 0x00] // Same address as in chunks
2126
+ var dataWithAddress = Data(glassesAddress)
2127
+ dataWithAddress.append(bmpData)
2128
+
2129
+ // Calculate CRC32 (simplified - in a real implementation, use proper CRC32-XZ)
2130
+ let crcValue = dataWithAddress.crc32
2131
+
2132
+ // Create CRC command packet
2133
+ var crcCommand: [UInt8] = [0x16] // CRC command
2134
+ crcCommand.append(UInt8((crcValue >> 24) & 0xFF))
2135
+ crcCommand.append(UInt8((crcValue >> 16) & 0xFF))
2136
+ crcCommand.append(UInt8((crcValue >> 8) & 0xFF))
2137
+ crcCommand.append(UInt8(crcValue & 0xFF))
2138
+
2139
+ Bridge.log("G1: Sending CRC command, CRC value: \(String(format: "%08x", crcValue))")
2140
+
2141
+ // Send CRC with retry
2142
+ for _ in 0 ..< maxAttempts {
2143
+ queueChunks([crcCommand], sendLeft: sendLeft, sendRight: sendRight)
2144
+
2145
+ // Wait for CRC command to process
2146
+ try? await Task.sleep(nanoseconds: UInt64(timeoutMs * 1_000_000))
2147
+
2148
+ // For now, assume success (in a real implementation, you'd check for ACK)
2149
+ Bridge.log("G1: CRC command sent successfully")
2150
+ return true
2151
+ }
2152
+ // Bridge.log("G1: CRC command failed, attempt \(attempt + 1)")
2153
+ Bridge.log("G1: Failed to send CRC command after \(maxAttempts) attempts")
2154
+ return false
2155
+ }
2156
+ }
2157
+
2158
+ // MARK: BLE Stubs
2159
+
2160
+ extension G1: CBCentralManagerDelegate, CBPeripheralDelegate {
2161
+ func getWriteCharacteristic(for peripheral: CBPeripheral?) -> CBCharacteristic? {
2162
+ guard let peripheral = peripheral else { return nil }
2163
+ for service in peripheral.services ?? [] {
2164
+ if service.uuid == UART_SERVICE_UUID {
2165
+ for characteristic in service.characteristics ?? []
2166
+ where characteristic.uuid == UART_TX_CHAR_UUID
2167
+ {
2168
+ return characteristic
2169
+ }
2170
+ }
2171
+ }
2172
+ return nil
2173
+ }
2174
+
2175
+ func extractIdNumber(_ string: String) -> Int? {
2176
+ // Pattern to match "G1_" followed by digits, followed by "_"
2177
+ let pattern = "G1_(\\d+)_"
2178
+
2179
+ // Create a regular expression
2180
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
2181
+ return nil
2182
+ }
2183
+
2184
+ // Look for matches in the input string
2185
+ let range = NSRange(string.startIndex ..< string.endIndex, in: string)
2186
+ guard let match = regex.firstMatch(in: string, options: [], range: range) else {
2187
+ return nil
2188
+ }
2189
+
2190
+ // Extract the captured group (the digits)
2191
+ if let matchRange = Range(match.range(at: 1), in: string) {
2192
+ let idString = String(string[matchRange])
2193
+ return Int(idString)
2194
+ }
2195
+
2196
+ return nil
2197
+ }
2198
+
2199
+ func emitDiscoveredDevice(_ name: String) {
2200
+ if name.contains("_L_") || name.contains("_R_") {
2201
+ // exampleName = "Even G1_74_L_57863C", "Even G1_3_L_57863C", "Even G1_100_L_57863C"
2202
+ guard let deviceName = extractIdNumber(name) else {
2203
+ Bridge.log("Failed to extract ID number from device name: \(name)")
2204
+ return
2205
+ }
2206
+ let deviceModel = "Even Realities G1"
2207
+ let dName = "\(deviceName)"
2208
+ Bridge.sendDiscoveredDevice(deviceModel, dName)
2209
+ } else {
2210
+ Bridge.log("Unknown device type: \(name)")
2211
+ }
2212
+ }
2213
+
2214
+ /// On BT discovery, automatically connect to both arms if we have them:
2215
+ func centralManager(
2216
+ _: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any],
2217
+ rssi _: NSNumber
2218
+ ) {
2219
+ guard let name = peripheral.name else { return }
2220
+ guard name.contains("Even G1") else { return }
2221
+
2222
+ Bridge.log("G1: found peripheral: \(name) - SEARCH_ID: \(DEVICE_SEARCH_ID)")
2223
+
2224
+ // Only process serial number for devices that match our search ID
2225
+ if name.contains(DEVICE_SEARCH_ID) {
2226
+ // Extract manufacturer data to decode serial number
2227
+ if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey]
2228
+ as? Data
2229
+ {
2230
+ Bridge.log("G1: 📱 Found manufacturer data: \(manufacturerData.hexEncodedString())")
2231
+
2232
+ // Try to decode serial number from manufacturer data
2233
+ if let decodedSerial = decodeSerialFromManufacturerData(manufacturerData) {
2234
+ Bridge.log("G1: 📱 Decoded serial number: \(decodedSerial)")
2235
+
2236
+ // Decode style and color from serial number
2237
+ let (decodedStyle, decodedColor) = G1.decodeEvenG1SerialNumber(decodedSerial)
2238
+ Bridge.log("G1: 📱 Style: \(style), Color: \(color)")
2239
+
2240
+ // Store the information
2241
+ DeviceStore.shared.apply("glasses", "serialNumber", decodedSerial)
2242
+ DeviceStore.shared.apply("glasses", "style", decodedStyle)
2243
+ DeviceStore.shared.apply("glasses", "color", decodedColor)
2244
+ } else {
2245
+ Bridge.log("G1: 📱 Could not decode serial number from manufacturer data")
2246
+ }
2247
+ } else {
2248
+ Bridge.log("G1: 📱 No manufacturer data found in advertisement")
2249
+ }
2250
+ }
2251
+
2252
+ if name.contains("_L_"), name.contains(DEVICE_SEARCH_ID) {
2253
+ Bridge.log("G1: Found left arm: \(name)")
2254
+ leftPeripheral = peripheral
2255
+ } else if name.contains("_R_"), name.contains(DEVICE_SEARCH_ID) {
2256
+ Bridge.log("G1: Found right arm: \(name)")
2257
+ rightPeripheral = peripheral
2258
+ }
2259
+
2260
+ emitDiscoveredDevice(name)
2261
+
2262
+ if leftPeripheral != nil, rightPeripheral != nil {
2263
+ // central.stopScan()
2264
+ RN_connectGlasses()
2265
+ }
2266
+ }
2267
+
2268
+ func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
2269
+ // BLE operations stay on _bluetoothQueue (where this callback runs)
2270
+ peripheral.delegate = self
2271
+ peripheral.discoverServices([UART_SERVICE_UUID])
2272
+
2273
+ DispatchQueue.main.async { [weak self] in
2274
+ guard let self = self else { return }
2275
+
2276
+ Bridge.log("G1: device connected: \(peripheral.name ?? "Unknown")")
2277
+
2278
+ if peripheral == self.leftPeripheral || (peripheral.name?.contains("_L_") ?? false) {
2279
+ self.leftGlassUUID = peripheral.identifier
2280
+ self.leftPeripheral = peripheral
2281
+ }
2282
+
2283
+ if peripheral == self.rightPeripheral || (peripheral.name?.contains("_R_") ?? false) {
2284
+ self.rightGlassUUID = peripheral.identifier
2285
+ self.rightPeripheral = peripheral
2286
+ }
2287
+
2288
+ self.lastConnectionTimestamp = Date()
2289
+ Bridge.log("G1: Connected to peripheral: \(peripheral.name ?? "Unknown")")
2290
+ }
2291
+ }
2292
+
2293
+ func centralManager(
2294
+ _: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error _: (any Error)?
2295
+ ) {
2296
+ let side =
2297
+ peripheral == leftPeripheral
2298
+ ? "LEFT" : peripheral == rightPeripheral ? "RIGHT" : "unknown"
2299
+ Bridge.log("G1: @@@@@ \(side) PERIPHERAL DISCONNECTED @@@@@")
2300
+
2301
+ // only reconnect if we're not intentionally disconnecting:
2302
+ if isDisconnecting {
2303
+ return
2304
+ }
2305
+
2306
+ if peripheral == leftPeripheral || peripheral == rightPeripheral {
2307
+ // force reconnection to both before considering us ready again:
2308
+ leftPeripheral = nil
2309
+ rightPeripheral = nil
2310
+ setReadiness(left: false, right: false)
2311
+ startReconnectionTimer() // Start periodic reconnection attempts
2312
+ }
2313
+ }
2314
+
2315
+ private func startReconnectionTimer() {
2316
+ Task {
2317
+ await reconnectionManager.start { [weak self] in
2318
+ guard let self else { return false }
2319
+
2320
+ // Check if already connected
2321
+ if await MainActor.run(body: { self.fullyBooted }) {
2322
+ Bridge.log("G1: Already connected, stopping reconnection")
2323
+ return true // Returning true stops the reconnection loop
2324
+ }
2325
+
2326
+ Bridge.log("G1: Attempting reconnection...")
2327
+
2328
+ // Attempt to reconnect
2329
+ await MainActor.run {
2330
+ self.startScan()
2331
+ }
2332
+
2333
+ // Return false to keep trying, true if connected
2334
+ return false
2335
+ }
2336
+ }
2337
+ }
2338
+
2339
+ private func stopReconnectionTimer() {
2340
+ Task {
2341
+ await reconnectionManager.stop()
2342
+ }
2343
+ }
2344
+
2345
+ /// Connect by UUID
2346
+ func connectByUUID() -> Bool {
2347
+ // don't do this if we don't have a search id set:
2348
+ if DEVICE_SEARCH_ID == "NOT_SET" || DEVICE_SEARCH_ID.isEmpty {
2349
+ Bridge.log("G1: 🔵 No DEVICE_SEARCH_ID set, skipping connect by UUID")
2350
+ return false
2351
+ }
2352
+
2353
+ Bridge.log("G1: 🔵 Attempting to connect by UUID")
2354
+ var foundAny = false
2355
+
2356
+ if let leftUUID = leftGlassUUID {
2357
+ Bridge.log("G1: 🔵 Found stored left glass UUID: \(leftUUID.uuidString)")
2358
+ let leftDevices = centralManager!.retrievePeripherals(withIdentifiers: [leftUUID])
2359
+
2360
+ if let leftDevice = leftDevices.first {
2361
+ Bridge.log(
2362
+ "G1: 🔵 Successfully retrieved left glass: \(leftDevice.name ?? "Unknown")"
2363
+ )
2364
+ foundAny = true
2365
+ leftPeripheral = leftDevice
2366
+ leftDevice.delegate = self
2367
+ centralManager!.connect(
2368
+ leftDevice,
2369
+ options: [
2370
+ CBConnectPeripheralOptionNotifyOnConnectionKey: true,
2371
+ CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
2372
+ ]
2373
+ )
2374
+ }
2375
+ }
2376
+
2377
+ if let rightUUID = rightGlassUUID {
2378
+ Bridge.log("G1: 🔵 Found stored right glass UUID: \(rightUUID.uuidString)")
2379
+ let rightDevices = centralManager!.retrievePeripherals(withIdentifiers: [rightUUID])
2380
+
2381
+ if let rightDevice = rightDevices.first {
2382
+ Bridge.log(
2383
+ "G1: 🔵 Successfully retrieved right glass: \(rightDevice.name ?? "Unknown")"
2384
+ )
2385
+ foundAny = true
2386
+ rightPeripheral = rightDevice
2387
+ rightDevice.delegate = self
2388
+ centralManager!.connect(
2389
+ rightDevice,
2390
+ options: [
2391
+ CBConnectPeripheralOptionNotifyOnConnectionKey: true,
2392
+ CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
2393
+ ]
2394
+ )
2395
+ }
2396
+ }
2397
+
2398
+ return foundAny
2399
+ }
2400
+
2401
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
2402
+ if let services = peripheral.services {
2403
+ for service in services where service.uuid == UART_SERVICE_UUID {
2404
+ peripheral.discoverCharacteristics(
2405
+ [UART_TX_CHAR_UUID, UART_RX_CHAR_UUID], for: service
2406
+ )
2407
+ }
2408
+ }
2409
+ }
2410
+
2411
+ /// Update peripheral(_:didDiscoverCharacteristicsFor:error:) to set services waiters
2412
+ func peripheral(
2413
+ _ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService,
2414
+ error _: Error?
2415
+ ) {
2416
+ guard let characteristics = service.characteristics else { return }
2417
+
2418
+ if service.uuid.isEqual(UART_SERVICE_UUID) {
2419
+ for characteristic in characteristics {
2420
+ if characteristic.uuid == UART_TX_CHAR_UUID {
2421
+ sendInitCommand(to: peripheral)
2422
+ } else if characteristic.uuid == UART_RX_CHAR_UUID {
2423
+ peripheral.setNotifyValue(true, for: characteristic)
2424
+
2425
+ // enable notification (needed for pairing from scracth!)
2426
+ Thread.sleep(forTimeInterval: 0.5) // 500ms delay
2427
+ let CLIENT_CHARACTERISTIC_CONFIG_UUID = CBUUID(
2428
+ string: "00002902-0000-1000-8000-00805f9b34fb"
2429
+ )
2430
+ if let descriptor = characteristic.descriptors?.first(where: {
2431
+ $0.uuid == CLIENT_CHARACTERISTIC_CONFIG_UUID
2432
+ }) {
2433
+ let value = Data([0x01, 0x00]) // ENABLE_NOTIFICATION_VALUE in iOS
2434
+ peripheral.writeValue(value, for: descriptor)
2435
+ } else {
2436
+ Bridge.log("PROC_QUEUE - descriptor not found")
2437
+ }
2438
+ }
2439
+ }
2440
+
2441
+ // // Mark the services as ready
2442
+ // if peripheral == leftPeripheral {
2443
+ // Bridge.log("G1: Left glass services discovered and ready")
2444
+ // setReadiness(left: true, right: nil)
2445
+ // } else if peripheral == rightPeripheral {
2446
+ // Bridge.log("G1: Right glass services discovered and ready")
2447
+ // setReadiness(left: nil, right: true)
2448
+ // }
2449
+ }
2450
+ }
2451
+
2452
+ /// called whenever bluetooth is initialized / turned on or off:
2453
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
2454
+ DispatchQueue.main.async { [weak self] in
2455
+ guard let self = self else { return }
2456
+
2457
+ if central.state == .poweredOn {
2458
+ Bridge.log("G1: Bluetooth was powered on")
2459
+ self.setReadiness(left: false, right: false)
2460
+
2461
+ if self.DEVICE_SEARCH_ID != "NOT_SET", !self.DEVICE_SEARCH_ID.isEmpty {
2462
+ self.startScan()
2463
+ }
2464
+ } else {
2465
+ Bridge.log("G1: Bluetooth was turned off.")
2466
+ }
2467
+ }
2468
+ }
2469
+
2470
+ /// called when we get data from the glasses:
2471
+ func peripheral(
2472
+ _ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
2473
+ error: Error?
2474
+ ) {
2475
+ if let error = error {
2476
+ Bridge.log("G1: Error updating value for characteristic: \(error.localizedDescription)")
2477
+ return
2478
+ }
2479
+
2480
+ guard let data = characteristic.value else {
2481
+ Bridge.log("G1: Characteristic value is nil.")
2482
+ return
2483
+ }
2484
+
2485
+ // Process the notification data
2486
+ DispatchQueue.main.async { [weak self] in
2487
+ self?.handleNotification(from: peripheral, data: data)
2488
+ }
2489
+ }
2490
+
2491
+ /// L/R Synchronization - Handle BLE write completions
2492
+ func peripheral(_ peripheral: CBPeripheral, didWriteValueFor _: CBCharacteristic, error: Error?) {
2493
+ DispatchQueue.main.async { [weak self] in
2494
+ guard let self = self else { return }
2495
+
2496
+ if let error = error {
2497
+ Bridge.log(
2498
+ "G1: ❌ BLE write error for \(peripheral.name ?? "unknown"): \(error.localizedDescription)"
2499
+ )
2500
+ } else {
2501
+ self.writeCompletionCount += 1
2502
+ }
2503
+ }
2504
+ }
2505
+ }