@glassly/bluetooth-sdk 0.1.0-dev.26

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