@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,3878 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothGatt
7
+ import android.bluetooth.BluetoothGattCallback
8
+ import android.bluetooth.BluetoothGattCharacteristic
9
+ import android.bluetooth.BluetoothGattDescriptor
10
+ import android.bluetooth.BluetoothGattService
11
+ import android.bluetooth.BluetoothProfile
12
+ import android.bluetooth.le.BluetoothLeScanner
13
+ import android.bluetooth.le.ScanCallback
14
+ import android.bluetooth.le.ScanFilter
15
+ import android.bluetooth.le.ScanResult
16
+ import android.bluetooth.le.ScanSettings
17
+ import android.content.BroadcastReceiver
18
+ import android.content.Context
19
+ import android.content.Intent
20
+ import android.content.IntentFilter
21
+ import android.graphics.Bitmap
22
+ import android.graphics.BitmapFactory
23
+ import android.os.Handler
24
+ import android.os.Looper
25
+ import android.util.Log
26
+ import android.util.SparseArray
27
+
28
+ import java.io.IOException
29
+ import java.io.InputStream
30
+ import java.util.concurrent.BlockingQueue
31
+ import java.util.concurrent.CountDownLatch
32
+ import java.util.concurrent.TimeUnit
33
+ import java.util.concurrent.ExecutorService
34
+ import java.util.concurrent.Executors
35
+
36
+ //BMP
37
+ import java.util.ArrayList
38
+ import java.util.concurrent.LinkedBlockingQueue
39
+ import java.util.zip.CRC32
40
+ import java.nio.ByteBuffer
41
+
42
+ import com.google.gson.Gson
43
+
44
+ import org.greenrobot.eventbus.EventBus
45
+ import org.json.JSONArray
46
+ import org.json.JSONException
47
+ import org.json.JSONObject
48
+
49
+ import java.lang.reflect.Method
50
+ import java.nio.charset.StandardCharsets
51
+ import java.util.Arrays
52
+ import java.util.UUID
53
+ import java.util.concurrent.Semaphore
54
+ import java.util.regex.Matcher
55
+ import java.util.regex.Pattern
56
+ import java.util.HashMap
57
+ import java.util.HashSet
58
+
59
+ // Glassly
60
+ import com.glassly.bluetoothsdk.DeviceManager
61
+ import com.glassly.bluetoothsdk.Bridge
62
+ import com.glassly.bluetoothsdk.PhotoRequest
63
+ import com.glassly.bluetoothsdk.utils.DeviceTypes
64
+ import com.glassly.bluetoothsdk.utils.BitmapJavaUtils
65
+
66
+ import com.glassly.bluetoothsdk.utils.BitmapJavaUtils.convertBitmapTo1BitBmpBytes
67
+
68
+ import com.glassly.bluetoothsdk.utils.G1Text
69
+ import com.glassly.bluetoothsdk.utils.SmartGlassesConnectionState
70
+ import com.glassly.lc3Lib.Lc3Cpp
71
+ import com.glassly.bluetoothsdk.DeviceStore
72
+
73
+ class G1 : SGCManager() {
74
+
75
+ companion object {
76
+ private const val TAG = "WearableAi_EvenRealitiesG1SGC"
77
+ const val SHARED_PREFS_NAME = "EvenRealitiesPrefs"
78
+
79
+ const val LEFT_DEVICE_KEY = "SavedG1LeftName"
80
+ const val RIGHT_DEVICE_KEY = "SavedG1RightName"
81
+
82
+ private val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
83
+ private val UART_TX_CHAR_UUID: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
84
+ private val UART_RX_CHAR_UUID: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
85
+ private val CLIENT_CHARACTERISTIC_CONFIG_UUID: UUID = UUID
86
+ .fromString("00002902-0000-1000-8000-00805f9b34fb")
87
+ private const val SAVED_G1_ID_KEY = "SAVED_G1_ID_KEY"
88
+
89
+ private const val DELAY_BETWEEN_SENDS_MS = 5L // not using now
90
+ private const val DELAY_BETWEEN_CHUNKS_SEND = 5L // super small just in case
91
+ private const val DELAY_BETWEEN_ACTIONS_SEND = 250L // not using now
92
+ private const val HEARTBEAT_INTERVAL_MS = 15000L
93
+ private const val MICBEAT_INTERVAL_MS = (1000L * 60) * 30 // micbeat every 30 minutes
94
+
95
+ private const val BASE_RECONNECT_DELAY_MS = 3000L // Start with 3 seconds
96
+ private const val MAX_RECONNECT_DELAY_MS = 60000L
97
+
98
+ private const val BOND_RETRY_DELAY_MS = 5000L // 5-second backoff
99
+
100
+ private const val CONNECTION_TIMEOUT_MS = 10000L // 10 seconds
101
+
102
+ private const val DEBOUNCE_DELAY_MS = 270L // Minimum time between chunk sends
103
+
104
+ private const val RIGHT_CONNECTION_RETRY_DELAY = 1000L // 1 second
105
+
106
+ private const val INITIAL_CONNECTION_DELAY_MS = 350L // Adjust this value as needed
107
+
108
+ private const val NOTIFICATION = 0x4B // Notification command
109
+
110
+ // Constants for text wall display
111
+ private const val TEXT_COMMAND = 0x4E // Text command
112
+ private const val DISPLAY_WIDTH = 488
113
+ private const val DISPLAY_USE_WIDTH = 488 // How much of the display to use
114
+ private const val FONT_MULTIPLIER = 1 / 50.0f
115
+ private const val OLD_FONT_SIZE = 21 // Font size
116
+ private const val FONT_DIVIDER = 2.0f
117
+ private const val LINES_PER_SCREEN = 5 // Lines per screen
118
+ private const val MAX_CHUNK_SIZE = 176 // Maximum chunk size for BLE packets
119
+ // private static final int INDENT_SPACES = 32; // Number of spaces to indent
120
+ // text
121
+
122
+ // handle white list stuff
123
+ private const val WHITELIST_CMD = 0x04 // Command ID for whitelist
124
+
125
+ // BMP handling
126
+
127
+ // Add these class variables
128
+ private const val BMP_CHUNK_SIZE = 194
129
+ private val GLASSES_ADDRESS = byteArrayOf(0x00, 0x1c, 0x00, 0x00)
130
+ private val END_COMMAND = byteArrayOf(0x20, 0x0d, 0x0e)
131
+ private const val MAX_BMP_RETRY_ATTEMPTS = 10
132
+ private const val BMP_RETRY_DELAY_MS = 1000L
133
+
134
+ // Platform-specific timing (matching Flutter implementation)
135
+ // private static final long ANDROID_CHUNK_DELAY_MS = 5;
136
+ private const val ANDROID_CHUNK_DELAY_MS = 8L
137
+ private const val IOS_CHUNK_DELAY_MS = 8L
138
+ private const val END_COMMAND_TIMEOUT_MS = 3000L
139
+ private const val CRC_COMMAND_TIMEOUT_MS = 3000L
140
+ private const val CHUNK_SEND_TIMEOUT_MS = 5000L
141
+
142
+ // Optimized bitmap display flags
143
+ private const val USE_OPTIMIZED_BITMAP_DISPLAY = true
144
+ private const val USE_PARALLEL_BITMAP_WRITES = true
145
+
146
+ @JvmStatic
147
+ fun savePreferredG1DeviceId(context: Context?, deviceName: String?) {
148
+ Bridge.saveSetting("deviceName", deviceName!!)
149
+ }
150
+
151
+ @JvmStatic
152
+ fun deleteEvenSharedPreferences(context: Context?) {
153
+ // savePreferredG1DeviceId(context, null);
154
+ // SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME,
155
+ // Context.MODE_PRIVATE);
156
+ // prefs.edit().clear().apply();
157
+ // Bridge.log("G1: Nuked EvenRealities SharedPreferences");
158
+ }
159
+
160
+ private fun bytesToHex(bytes: ByteArray): String {
161
+ val sb = StringBuilder()
162
+ for (b in bytes) {
163
+ sb.append(String.format("%02X ", b))
164
+ }
165
+ return sb.toString().trim()
166
+ }
167
+
168
+ private fun bytesToUtf8(bytes: ByteArray): String {
169
+ return String(bytes, StandardCharsets.UTF_8)
170
+ }
171
+
172
+ /**
173
+ * Decodes Even G1 serial number to extract style and color information
174
+ *
175
+ * @param serialNumber The full serial number (e.g., "S110LABD020021")
176
+ * @return Array containing [style, color] or ["Unknown", "Unknown"] if invalid
177
+ */
178
+ @JvmStatic
179
+ fun decodeEvenG1SerialNumber(serialNumber: String?): Array<String> {
180
+ if (serialNumber == null || serialNumber.length < 6) {
181
+ return arrayOf("Unknown", "Unknown")
182
+ }
183
+
184
+ // Style mapping: 3rd character (index 2)
185
+ val style: String
186
+ when (serialNumber[1]) {
187
+ '0' ->
188
+ style = "Round"
189
+ '1' ->
190
+ style = "Rectangular"
191
+ else ->
192
+ style = "Round"
193
+ }
194
+
195
+ // Color mapping: 5th character (index 4)
196
+ val color: String
197
+ when (serialNumber[4]) {
198
+ 'A' ->
199
+ color = "Grey"
200
+ 'B' ->
201
+ color = "Brown"
202
+ 'C' ->
203
+ color = "Green"
204
+ else ->
205
+ color = "Grey"
206
+ }
207
+
208
+ return arrayOf(style, color)
209
+ }
210
+ }
211
+
212
+ private var heartbeatCount = 0
213
+ private var micBeatCount = 0
214
+ private var bluetoothAdapter: BluetoothAdapter? = null
215
+
216
+ private var isKilled = false
217
+
218
+ private var context: Context? = null
219
+ private var leftGlassGatt: BluetoothGatt? = null
220
+ private var rightGlassGatt: BluetoothGatt? = null
221
+ private var leftTxChar: BluetoothGattCharacteristic? = null
222
+ private var rightTxChar: BluetoothGattCharacteristic? = null
223
+ private var leftRxChar: BluetoothGattCharacteristic? = null
224
+ private var rightRxChar: BluetoothGattCharacteristic? = null
225
+ // NOTE: named connectionState in G1.java; renamed because Kotlin can't shadow
226
+ // SGCManager's public val connectionState with a private field of another type.
227
+ private var g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
228
+ // Executor for parallel bitmap operations (declared before init so the
229
+ // constructor body can assign it; in G1.java this field lived in the BMP section)
230
+ private var bitmapExecutor: ExecutorService? = null
231
+ private val handler = Handler(Looper.getMainLooper())
232
+ private val queryBatteryStatusHandler = Handler(Looper.getMainLooper())
233
+ private val sendBrightnessCommandHandler = Handler(Looper.getMainLooper())
234
+ private var connectHandler = Handler(Looper.getMainLooper())
235
+ private var reconnectHandler = Handler(Looper.getMainLooper())
236
+ private var characteristicHandler = Handler(Looper.getMainLooper())
237
+ private val sendSemaphore = Semaphore(1)
238
+ private var isLeftConnected = false
239
+ private var isRightConnected = false
240
+ private var currentSeq = 0
241
+ private var stopper = false
242
+ private var debugStopper = false
243
+ private var shouldUseAutoBrightness = false
244
+ private var updatingScreen = false
245
+
246
+
247
+ private var batteryLeft = -1
248
+ private var batteryRight = -1
249
+ private var leftReconnectAttempts = 0
250
+ private var rightReconnectAttempts = 0
251
+ private var reconnectAttempts = 0 // Counts the number of reconnect attempts
252
+
253
+ // heartbeat sender
254
+ private var heartbeatHandler = Handler()
255
+ private var findCompatibleDevicesHandler: Handler? = null
256
+ private var isScanningForCompatibleDevices = false
257
+ private var isScanning = false
258
+
259
+ private var heartbeatRunnable: Runnable? = null
260
+
261
+ // mic heartbeat turn on
262
+ private var micBeatHandler = Handler()
263
+ private var micBeatRunnable: Runnable? = null
264
+
265
+ // white list sender
266
+ private var whiteListHandler = Handler()
267
+ private var whiteListedAlready = false
268
+
269
+ // mic enable Handler
270
+ private var micEnableHandler = Handler()
271
+ private var isMicrophoneEnabled = false // Track current microphone state
272
+
273
+ // notification period sender
274
+ private var notificationHandler = Handler()
275
+ private var notificationRunnable: Runnable? = null
276
+ private var notifysStarted = false
277
+ private var notificationNum = 10
278
+
279
+ // text wall periodic sender
280
+ private var textWallHandler = Handler()
281
+ private var textWallRunnable: Runnable? = null
282
+ private var textWallsStarted = false
283
+ private var textWallNum = 10
284
+
285
+ // pairing logic
286
+ private var isLeftPairing = false
287
+ private var isRightPairing = false
288
+ private var isLeftBonded = false
289
+ private var isRightBonded = false
290
+ private var leftDevice: BluetoothDevice? = null
291
+ private var rightDevice: BluetoothDevice? = null
292
+ private var leftDeviceName: String? = null // Store name separately since BluetoothDevice.getName() can become null
293
+ private var rightDeviceName: String? = null // Store name separately since BluetoothDevice.getName() can become null
294
+ private var preferredG1Id: String? = null
295
+ private var pendingSavedG1LeftName: String? = null
296
+ private var pendingSavedG1RightName: String? = null
297
+ private var savedG1LeftName: String? = null
298
+ private var savedG1RightName: String? = null
299
+ private var preferredG1DeviceId: String? = null
300
+
301
+ // handler to turn off screen
302
+ // Handler goHomeHandler;
303
+ // Runnable goHomeRunnable;
304
+
305
+ // Retry handler
306
+ var retryBondHandler: Handler? = null
307
+
308
+ // remember when we connected
309
+ private var lastConnectionTimestamp = 0L
310
+
311
+ // Handlers for connection timeouts
312
+ private val leftConnectionTimeoutHandler = Handler(Looper.getMainLooper())
313
+ private val rightConnectionTimeoutHandler = Handler(Looper.getMainLooper())
314
+
315
+ // Runnable tasks for handling timeouts
316
+ private var leftConnectionTimeoutRunnable: Runnable? = null
317
+ private var rightConnectionTimeoutRunnable: Runnable? = null
318
+ private var isBondingReceiverRegistered = false
319
+ private var shouldUseGlassesMic = false
320
+ private var lastThingDisplayedWasAnImage = false
321
+
322
+ // Serial number and style/color information
323
+ // NOTE: G1.java declared public fields serialNumber/style/color here, but they were
324
+ // never read or written; in Kotlin they would clash with SGCManager's val properties,
325
+ // so they are omitted.
326
+
327
+ // lock writing until the last write is successful
328
+ // fonts in G1
329
+ private var g1Text: G1Text? = null
330
+
331
+ @Volatile private var lastSendTimestamp = 0L
332
+ private var lc3DecoderPtr = 0L
333
+
334
+ init {
335
+ this.type = DeviceTypes.G1
336
+ this.hasMic = true // G1 has a built-in microphone
337
+ DeviceStore.apply("glasses", "micEnabled", false)
338
+ Bridge.log("G1: G1 constructor")
339
+ this.context = Bridge.getContext()
340
+ loadPairedDeviceNames()
341
+ // goHomeHandler = new Handler();
342
+ // this.smartGlassesDevice = smartGlassesDevice;
343
+ preferredG1DeviceId = DeviceStore.get("bluetooth", "device_name") as String?
344
+ this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
345
+ this.shouldUseGlassesMic = false
346
+
347
+ // Initialize bitmap executor for parallel operations
348
+ if (USE_PARALLEL_BITMAP_WRITES) {
349
+ bitmapExecutor = Executors.newFixedThreadPool(2)
350
+ }
351
+
352
+ // setup LC3 decoder
353
+ if (lc3DecoderPtr == 0L) {
354
+ lc3DecoderPtr = Lc3Cpp.initDecoder()
355
+ }
356
+
357
+ // setup fonts
358
+ g1Text = G1Text()
359
+ DeviceStore.apply("glasses", "caseRemoved", true)
360
+ }
361
+
362
+ private val leftGattCallback: BluetoothGattCallback = createGattCallback("Left")
363
+ private val rightGattCallback: BluetoothGattCallback = createGattCallback("Right")
364
+
365
+ private fun createGattCallback(side: String): BluetoothGattCallback {
366
+ return object : BluetoothGattCallback() {
367
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
368
+ // Bridge.log("G1: ConnectionStateChanged");
369
+ // Cancel the connection timeout
370
+ if ("Left" == side && leftConnectionTimeoutRunnable != null) {
371
+ leftConnectionTimeoutHandler.removeCallbacks(leftConnectionTimeoutRunnable!!)
372
+ leftConnectionTimeoutRunnable = null
373
+ } else if ("Right" == side && rightConnectionTimeoutRunnable != null) {
374
+ rightConnectionTimeoutHandler.removeCallbacks(rightConnectionTimeoutRunnable!!)
375
+ rightConnectionTimeoutRunnable = null
376
+ }
377
+
378
+ if (status == BluetoothGatt.GATT_SUCCESS) {
379
+
380
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
381
+ Bridge.log("G1: " + side + " glass connected, discovering services...")
382
+ if ("Left" == side) {
383
+ isLeftConnected = true
384
+ leftReconnectAttempts = 0
385
+ } else {
386
+ isRightConnected = true
387
+ rightReconnectAttempts = 0
388
+ }
389
+
390
+ if (isLeftConnected && isRightConnected) {
391
+ stopScan()
392
+ Bridge.log("G1: Both glasses connected. Stopping BLE scan.")
393
+ }
394
+
395
+ Bridge.log("G1: Discover services calling...")
396
+ gatt.discoverServices()
397
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
398
+ Bridge.log("G1: Glass disconnected, stopping heartbeats")
399
+ Bridge.log("G1: Entering STATE_DISCONNECTED branch for side: " + side)
400
+
401
+ // Mark both sides as not ready (you could also clear both if one disconnects)
402
+ leftServicesWaiter.setTrue()
403
+ rightServicesWaiter.setTrue()
404
+ Bridge.log("G1: Set leftServicesWaiter and rightServicesWaiter to true.")
405
+
406
+ forceSideDisconnection()
407
+ Bridge.log("G1: Called forceSideDisconnection().")
408
+
409
+ // Stop any periodic transmissions
410
+ stopHeartbeat()
411
+ stopMicBeat()
412
+ sendQueue.clear()
413
+ Bridge.log("G1: Stopped heartbeat and mic beat; cleared sendQueue.")
414
+
415
+ updateConnectionState()
416
+ Bridge.log("G1: Updated connection state after disconnection.")
417
+
418
+ // Compute reconnection delay for both sides (here you could choose the maximum
419
+ // of the two delays or a new delay)
420
+ // long delayLeft = Math.min(BASE_RECONNECT_DELAY_MS * (1L <<
421
+ // leftReconnectAttempts), MAX_RECONNECT_DELAY_MS);
422
+ // long delayRight = Math.min(BASE_RECONNECT_DELAY_MS * (1L <<
423
+ // rightReconnectAttempts), MAX_RECONNECT_DELAY_MS);
424
+ val delay = 2000L // or choose another strategy
425
+ // Bridge.log("G1: Computed delayLeft: " + delayLeft + " ms, delayRight: " +
426
+ // delayRight + " ms. Using delay: " + delay + " ms.");
427
+
428
+ Bridge.log("G1: " +
429
+ side + " glass disconnected. Scheduling reconnection for both glasses in " + delay
430
+ + " ms (Left attempts: " + leftReconnectAttempts + ", Right attempts: "
431
+ + rightReconnectAttempts + ")")
432
+
433
+ // if (gatt.getDevice() != null) {
434
+ // // Close the current gatt connection
435
+ // Bridge.log("G1: Closing GATT connection for device: " +
436
+ // gatt.getDevice().getAddress());
437
+ // gatt.disconnect();
438
+ // gatt.close();
439
+ // Bridge.log("G1: GATT connection closed.");
440
+ // } else {
441
+ // Bridge.log("G1: No GATT device available to disconnect.");
442
+ // }
443
+
444
+ // Schedule a reconnection for both devices after the delay
445
+ reconnectHandler.postDelayed({
446
+ Bridge.log("G1: Reconnect handler triggered after delay.")
447
+ if (gatt.device != null && !isKilled) {
448
+ Bridge.log("G1: Reconnecting to both glasses. isKilled = " + isKilled)
449
+ // Assuming you have stored references to both devices:
450
+ if (leftDevice != null) {
451
+ Bridge.log("G1: Attempting to reconnect to leftDevice: " + leftDevice!!.address)
452
+ reconnectToGatt(leftDevice!!)
453
+ } else {
454
+ Bridge.log("G1: Left device reference is null.")
455
+ }
456
+ if (rightDevice != null) {
457
+ Bridge.log("G1: Attempting to reconnect to rightDevice: " + rightDevice!!.address)
458
+ reconnectToGatt(rightDevice!!)
459
+ } else {
460
+ Bridge.log("G1: Right device reference is null.")
461
+ }
462
+ } else {
463
+ Bridge.log("G1: Reconnect handler aborted: either no GATT device or system is killed.")
464
+ }
465
+ }, delay)
466
+ }
467
+ } else {
468
+ Log.e(TAG, "Unexpected connection state encountered for " + side + " glass: " + newState)
469
+ stopHeartbeat()
470
+ stopMicBeat()
471
+ sendQueue.clear()
472
+
473
+ // Mark both sides as not ready (you could also clear both if one disconnects)
474
+ leftServicesWaiter.setTrue()
475
+ rightServicesWaiter.setTrue()
476
+
477
+ Bridge.log("G1: Stopped heartbeat and mic beat; cleared sendQueue due to connection failure.")
478
+
479
+ Log.e(TAG, side + " glass connection failed with status: " + status)
480
+ if ("Left" == side) {
481
+ isLeftConnected = false
482
+ leftReconnectAttempts++
483
+ if (leftGlassGatt != null) {
484
+ leftGlassGatt!!.disconnect()
485
+ leftGlassGatt!!.close()
486
+ }
487
+ leftGlassGatt = null
488
+ } else {
489
+ isRightConnected = false
490
+ rightReconnectAttempts++
491
+ if (rightGlassGatt != null) {
492
+ rightGlassGatt!!.disconnect()
493
+ rightGlassGatt!!.close()
494
+ }
495
+ rightGlassGatt = null
496
+ }
497
+
498
+ forceSideDisconnection()
499
+ Bridge.log("G1: Called forceSideDisconnection() after connection failure.")
500
+
501
+ // Update connection state and notify frontend of disconnection
502
+ updateConnectionState()
503
+ Bridge.log("G1: Updated connection state after connection failure.")
504
+
505
+ connectHandler.postDelayed({
506
+ Bridge.log("G1: Attempting GATT connection for leftDevice immediately.")
507
+ attemptGattConnection(leftDevice)
508
+ }, 0L)
509
+
510
+ connectHandler.postDelayed({
511
+ Bridge.log("G1: Attempting GATT connection for rightDevice after 2000 ms delay.")
512
+ attemptGattConnection(rightDevice)
513
+ }, 400L)
514
+ }
515
+ }
516
+
517
+ private fun forceSideDisconnection() {
518
+ Bridge.log("G1: forceSideDisconnection() called for side: " + side)
519
+ // Force disconnection from the other side if necessary
520
+ if ("Left" == side) {
521
+ isLeftConnected = false
522
+ leftReconnectAttempts++
523
+ Bridge.log("G1: Left glass: Marked as disconnected and incremented leftReconnectAttempts to "
524
+ + leftReconnectAttempts)
525
+ if (leftGlassGatt != null) {
526
+ Bridge.log("G1: Left glass GATT exists. Disconnecting and closing leftGlassGatt.")
527
+ leftGlassGatt!!.disconnect()
528
+ leftGlassGatt!!.close()
529
+ leftGlassGatt = null
530
+ } else {
531
+ Bridge.log("G1: Left glass GATT is already null.")
532
+ }
533
+ // If right is still connected, disconnect it too
534
+ if (rightGlassGatt != null) {
535
+ Bridge.log("G1: Left glass disconnected - forcing disconnection from right glass.")
536
+ rightGlassGatt!!.disconnect()
537
+ rightGlassGatt!!.close()
538
+ rightGlassGatt = null
539
+ isRightConnected = false
540
+ rightReconnectAttempts++
541
+ Bridge.log("G1: Right glass marked as disconnected and rightReconnectAttempts incremented to "
542
+ + rightReconnectAttempts)
543
+ } else {
544
+ Bridge.log("G1: Right glass GATT already null, no action taken.")
545
+ }
546
+ } else { // side equals "Right"
547
+ isRightConnected = false
548
+ rightReconnectAttempts++
549
+ Bridge.log("G1: Right glass: Marked as disconnected and incremented rightReconnectAttempts to "
550
+ + rightReconnectAttempts)
551
+ if (rightGlassGatt != null) {
552
+ Bridge.log("G1: Right glass GATT exists. Disconnecting and closing rightGlassGatt.")
553
+ rightGlassGatt!!.disconnect()
554
+ rightGlassGatt!!.close()
555
+ rightGlassGatt = null
556
+ } else {
557
+ Bridge.log("G1: Right glass GATT is already null.")
558
+ }
559
+ // If left is still connected, disconnect it too
560
+ if (leftGlassGatt != null) {
561
+ Bridge.log("G1: Right glass disconnected - forcing disconnection from left glass.")
562
+ leftGlassGatt!!.disconnect()
563
+ leftGlassGatt!!.close()
564
+ leftGlassGatt = null
565
+ isLeftConnected = false
566
+ leftReconnectAttempts++
567
+ Bridge.log("G1: Left glass marked as disconnected and leftReconnectAttempts incremented to "
568
+ + leftReconnectAttempts)
569
+ } else {
570
+ Bridge.log("G1: Left glass GATT already null, no action taken.")
571
+ }
572
+ }
573
+ }
574
+
575
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
576
+ if (status == BluetoothGatt.GATT_SUCCESS) {
577
+ Handler(Looper.getMainLooper()).post { initG1s(gatt, side) }
578
+ }
579
+ }
580
+
581
+ override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
582
+ status: Int) {
583
+ if (status == BluetoothGatt.GATT_SUCCESS) {
584
+ // Bridge.log("G1: PROC_QUEUE - " + side + " glass write successful");
585
+ } else {
586
+ Bridge.log("G1: " + side + " glass write failed with status: " + status)
587
+
588
+ if (status == 133) {
589
+ Bridge.log("G1: GOT THAT 133 STATUS!")
590
+
591
+ }
592
+ }
593
+
594
+ // clear the waiter
595
+ if ("Left" == side) {
596
+ leftWaiter.setFalse()
597
+ } else {
598
+ rightWaiter.setFalse()
599
+ }
600
+ }
601
+
602
+ override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
603
+ Bridge.log("G1: PROC - GOT DESCRIPTOR WRITE: " + status)
604
+
605
+ // clear the waiter
606
+ if ("Left" == side) {
607
+ leftServicesWaiter.setFalse()
608
+ } else {
609
+ rightServicesWaiter.setFalse()
610
+ }
611
+ }
612
+
613
+ override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
614
+ characteristicHandler.post {
615
+ if (characteristic.uuid == UART_RX_CHAR_UUID) {
616
+ val data = characteristic.value
617
+ val deviceName = gatt.device.name
618
+ if (deviceName == null)
619
+ return@post
620
+
621
+ // Handle MIC audio data
622
+ if (data.isNotEmpty() && (data[0].toInt() and 0xFF) == 0xF1) {
623
+ // Bridge.log("G1: Lc3 Audio data received. Data: " + Arrays.toString(data) + ",
624
+ // from: " + deviceName);
625
+ val seq = data[1].toInt() and 0xFF // Sequence number
626
+ // eg. LC3 to PCM
627
+ val lc3 = Arrays.copyOfRange(data, 2, 202)
628
+ // byte[] pcmData = L3cCpp.decodeLC3(lc3);
629
+ // if (pcmData == null) {
630
+ // throw new IllegalStateException("Failed to decode LC3 data");
631
+ // }
632
+
633
+ if (deviceName.contains("R_")) {
634
+ // Forward raw LC3 to DeviceManager (matches iOS behavior)
635
+ // G1 uses 20-byte LC3 frames (default canonical config)
636
+ if (shouldUseGlassesMic) {
637
+ DeviceManager.getInstance().handleGlassesMicData(lc3, 20)
638
+ }
639
+ } else {
640
+ // Bridge.log("G1: Lc3 Audio data received. Seq: " + seq + ", Data: " +
641
+ // Arrays.toString(lc3) + ", from: " + deviceName);
642
+ }
643
+ }
644
+ // HEAD UP MOVEMENTS
645
+ else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x02) {
646
+ // Only check head movements from the right sensor
647
+ if (deviceName.contains("R_")) {
648
+ // Check for head down movement - initial F5 02 signal
649
+ Bridge.log("G1: HEAD UP MOVEMENT DETECTED")
650
+ DeviceStore.apply("glasses", "headUp", true)
651
+ }
652
+ }
653
+ // HEAD DOWN MOVEMENTS
654
+ else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x03) {
655
+ if (deviceName.contains("R_")) {
656
+ Bridge.log("G1: HEAD DOWN MOVEMENT DETECTED")
657
+ // clearBmpDisplay();
658
+ DeviceStore.apply("glasses", "headUp", false)
659
+ }
660
+ }
661
+ // DOUBLE TAP
662
+ // appears to be completely broken - clears the screen - we should not tell
663
+ // people to use the touchpads yet til this is fixed
664
+ // else if (data.length > 1 && (data[0] & 0xFF) == 0xF5 && ((data[1] & 0xFF) ==
665
+ // 0x20) || ((data[1] & 0xFF) == 0x00)) {
666
+ // boolean isRight = deviceName.contains("R_");
667
+ // Bridge.log("G1: GOT DOUBLE TAP from isRight?: " + isRight);
668
+ // EventBus.getDefault().post(new GlassesTapOutputEvent(2, isRight,
669
+ // System.currentTimeMillis()));
670
+ // }
671
+ // BATTERY RESPONSE
672
+ else if (data.size > 2 && data[0] == 0x2C.toByte() && data[1] == 0x66.toByte()) {
673
+ if (deviceName.contains("L_")) {
674
+ // Bridge.log("G1: LEFT Battery response received");
675
+ batteryLeft = data[2].toInt()
676
+ } else if (deviceName.contains("R_")) {
677
+ // Bridge.log("G1: RIGHT Battery response received");
678
+ batteryRight = data[2].toInt()
679
+ }
680
+
681
+ if (batteryLeft != -1 && batteryRight != -1) {
682
+ val minBatt = Math.min(batteryLeft, batteryRight)
683
+ // Bridge.log("G1: Minimum Battery Level: " + minBatt);
684
+ // EventBus.getDefault().post(new BatteryLevelEvent(minBatt, false));
685
+ DeviceStore.apply("glasses", "batteryLevel", minBatt)
686
+ }
687
+ }
688
+ // CASE REMOVED
689
+ else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5
690
+ && ((data[1].toInt() and 0xFF) == 0x07 || (data[1].toInt() and 0xFF) == 0x06)) {
691
+ DeviceStore.apply("glasses", "caseRemoved", true)
692
+ Bridge.log("G1: CASE REMOVED")
693
+ }
694
+ // CASE OPEN
695
+ else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x08) {
696
+ DeviceStore.apply("glasses", "caseOpen", true)
697
+ DeviceStore.apply("glasses", "caseRemoved", false)
698
+ // EventBus.getDefault()
699
+ // .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
700
+ }
701
+ // CASE CLOSED
702
+ else if (data.size > 1 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0B) {
703
+ DeviceStore.apply("glasses", "caseOpen", false)
704
+ DeviceStore.apply("glasses", "caseRemoved", false)
705
+ // EventBus.getDefault()
706
+ // .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
707
+ }
708
+ // CASE CHARGING STATUS
709
+ else if (data.size > 3 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0E) {
710
+ DeviceStore.apply("glasses", "caseCharging", (data[2].toInt() and 0xFF) == 0x01)// TODO: verify this is correct
711
+ // EventBus.getDefault()
712
+ // .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
713
+ }
714
+ // CASE CHARGING INFO
715
+ else if (data.size > 3 && (data[0].toInt() and 0xFF) == 0xF5 && (data[1].toInt() and 0xFF) == 0x0F) {
716
+ val newCaseBatteryLevel = (data[2].toInt() and 0xFF)// TODO: verify this is correct
717
+ DeviceStore.apply("glasses", "caseBatteryLevel", newCaseBatteryLevel)
718
+ // EventBus.getDefault()
719
+ // .post(new CaseEvent(caseBatteryLevel, caseCharging, caseOpen, caseRemoved));
720
+ }
721
+ // HEARTBEAT RESPONSE
722
+ else if (data.isNotEmpty() && data[0] == 0x25.toByte()) {
723
+ Bridge.log("G1: Heartbeat response received")
724
+ }
725
+ // TEXT RESPONSE
726
+ else if (data.isNotEmpty() && data[0] == 0x4E.toByte()) {
727
+ // Bridge.log("G1: Text response on side " + (deviceName.contains("L_") ? "Left" : "Right")
728
+ // + " was: " + ((data.length > 1 && (data[1] & 0xFF) == 0xC9) ? "SUCCEED" : "FAIL"));
729
+ }
730
+
731
+ // Handle other non-audio responses
732
+ else {
733
+ // Bridge.log("G1: PROC - Received other Even Realities response: " + bytesToHex(data) + ", from: "
734
+ // + deviceName);
735
+ }
736
+
737
+ // clear the waiter
738
+ // if ((data.length > 1 && (data[1] & 0xFF) == 0xC9)){
739
+ // if (deviceName.contains("L_")) {
740
+ // Bridge.log("G1: PROC - clearing LEFT waiter on success");
741
+ // leftWaiter.setFalse();
742
+ // } else {
743
+ // Bridge.log("G1: PROC - clearing RIGHT waiter on success");
744
+ // rightWaiter.setFalse();
745
+ // }
746
+ // }
747
+ }
748
+ }
749
+ }
750
+
751
+ }
752
+ }
753
+
754
+ private fun initG1s(gatt: BluetoothGatt, side: String) {
755
+ gatt.requestMtu(251) // Request a higher MTU size
756
+ Bridge.log("G1: Requested MTU size: 251")
757
+
758
+ val uartService = gatt.getService(UART_SERVICE_UUID)
759
+
760
+ if (uartService != null) {
761
+ val txChar = uartService.getCharacteristic(UART_TX_CHAR_UUID)
762
+ val rxChar = uartService.getCharacteristic(UART_RX_CHAR_UUID)
763
+
764
+ if (txChar != null) {
765
+ if ("Left" == side)
766
+ leftTxChar = txChar
767
+ else
768
+ rightTxChar = txChar
769
+ // enableNotification(gatt, txChar, side);
770
+ // txChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
771
+ Bridge.log("G1: " + side + " glass TX characteristic found")
772
+ }
773
+
774
+ if (rxChar != null) {
775
+ if ("Left" == side)
776
+ leftRxChar = rxChar
777
+ else
778
+ rightRxChar = rxChar
779
+ enableNotification(gatt, rxChar, side)
780
+ // rxChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
781
+ Bridge.log("G1: " + side + " glass RX characteristic found")
782
+ }
783
+
784
+ // Mark as connected but wait for setup below to update connection state
785
+ if ("Left" == side) {
786
+ isLeftConnected = true
787
+ Bridge.log("G1: PROC_QUEUE - left side setup complete")
788
+
789
+ // Manufacturer data decoding moved to connection start
790
+ } else {
791
+ isRightConnected = true
792
+ // Bridge.log("G1: PROC_QUEUE - right side setup complete");
793
+ }
794
+
795
+ // setup the G1s
796
+ if (isLeftConnected && isRightConnected) {
797
+ Bridge.log("G1: Sending firmware request Command")
798
+ sendDataSequentially(byteArrayOf(0x6E.toByte(), 0x74.toByte()))
799
+
800
+ Bridge.log("G1: Sending init 0x4D Command")
801
+ sendDataSequentially(byteArrayOf(0x4D.toByte(), 0xFB.toByte())) // told this is only left
802
+
803
+ Bridge.log("G1: Sending turn off wear detection command")
804
+ sendDataSequentially(byteArrayOf(0x27.toByte(), 0x00.toByte()))
805
+
806
+ Bridge.log("G1: Sending turn off silent mode Command")
807
+ sendDataSequentially(byteArrayOf(0x03.toByte(), 0x0A.toByte()))
808
+
809
+ // debug command
810
+ // Bridge.log("G1: Sending debug 0xF4 Command");
811
+ // sendDataSequentially(new byte[]{(byte) 0xF4, (byte) 0x01});
812
+
813
+ // no longer need to be staggered as we fixed the sender
814
+ // do first battery status query
815
+ queryBatteryStatusHandler.postDelayed({ queryBatteryStatus() }, 10L)
816
+
817
+ // setup brightness
818
+ val brightnessValue = (DeviceStore.get("bluetooth", "brightness") as Number).toInt()
819
+ val shouldUseAutoBrightness = DeviceStore.get("bluetooth", "auto_brightness") as Boolean
820
+ sendBrightnessCommandHandler
821
+ .postDelayed({ sendBrightnessCommand(brightnessValue, shouldUseAutoBrightness) }, 10L)
822
+
823
+ // MIC state is handled by DeviceManager.updateMicState() after reconnection
824
+ // Don't hardcode mic state here - let DeviceManager restore the user's preference
825
+
826
+ // enable our AugmentOS notification key
827
+ sendWhiteListCommand(10)
828
+
829
+ // start heartbeat
830
+ startHeartbeat(10000)
831
+
832
+ // start mic beat
833
+ // startMicBeat(30000);
834
+
835
+ showHomeScreen() // turn on the g1 display
836
+
837
+ updateConnectionState()
838
+
839
+ // start sending debug notifications
840
+ // startPeriodicNotifications(302);
841
+ // start sending debug notifications
842
+ // startPeriodicTextWall(302);
843
+ }
844
+ } else {
845
+ Bridge.log("G1: " + side + " glass UART service not found")
846
+ }
847
+ }
848
+
849
+ // working on all phones - must keep the delay
850
+ private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, side: String) {
851
+ Bridge.log("G1: PROC_QUEUE - Starting notification setup for " + side)
852
+
853
+ // Simply enable notifications
854
+ Bridge.log("G1: PROC_QUEUE - setting characteristic notification on side: " + side)
855
+ val result = gatt.setCharacteristicNotification(characteristic, true)
856
+ Bridge.log("G1: PROC_QUEUE - setCharacteristicNotification result for " + side + ": " + result)
857
+
858
+ // Set write type for the characteristic
859
+ characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
860
+ Bridge.log("G1: PROC_QUEUE - write type set for " + side)
861
+
862
+ // wait
863
+ Bridge.log("G1: PROC_QUEUE - waiting to enable it on this side: " + side)
864
+
865
+ try {
866
+ Thread.sleep(500)
867
+ } catch (e: InterruptedException) {
868
+ Bridge.log("G1: Error sending data: " + e.message)
869
+ }
870
+
871
+ Bridge.log("G1: PROC_QUEUE - get descriptor on side: " + side)
872
+ val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
873
+ if (descriptor != null) {
874
+ Bridge.log("G1: PROC_QUEUE - setting descriptor on side: " + side)
875
+ descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
876
+ val r_result = gatt.writeDescriptor(descriptor)
877
+ Bridge.log("G1: PROC_QUEUE - set descriptor on side: " + side + " with result: " + r_result)
878
+ }
879
+ }
880
+
881
+ private fun updateConnectionState() {
882
+ val previousConnectionState = connectionState
883
+ if (isLeftConnected && isRightConnected) {
884
+ g1ConnectionState = SmartGlassesConnectionState.CONNECTED
885
+ Bridge.log("G1: Both glasses connected")
886
+ lastConnectionTimestamp = System.currentTimeMillis()
887
+ DeviceStore.apply("glasses", "fullyBooted", true)
888
+ DeviceStore.apply("glasses", "connected", true)
889
+ } else if (isLeftConnected || isRightConnected) {
890
+ g1ConnectionState = SmartGlassesConnectionState.CONNECTING
891
+ Bridge.log("G1: One glass connected")
892
+ DeviceStore.apply("glasses", "fullyBooted", false)
893
+ DeviceStore.apply("glasses", "connected", false)
894
+ } else {
895
+ g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
896
+ Bridge.log("G1: No glasses connected")
897
+ DeviceStore.apply("glasses", "fullyBooted", false)
898
+ DeviceStore.apply("glasses", "connected", false)
899
+ }
900
+ }
901
+
902
+ private val bondingReceiver: BroadcastReceiver = object : BroadcastReceiver() {
903
+ override fun onReceive(context: Context, intent: Intent) {
904
+ val action = intent.action
905
+ if (BluetoothDevice.ACTION_BOND_STATE_CHANGED == action) {
906
+ val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
907
+
908
+ // Use device address to match with stored devices instead of relying on getName()
909
+ // which can be null in the bonding broadcast
910
+ val deviceAddress = device!!.address
911
+ var isLeft = false
912
+ var isRight = false
913
+ var deviceName = device.name
914
+
915
+ // Match by address with stored left/right devices from scanning
916
+ if (leftDevice != null && leftDevice!!.address == deviceAddress) {
917
+ isLeft = true
918
+ // Use stored name string (more reliable than BluetoothDevice.getName())
919
+ if (deviceName == null) {
920
+ deviceName = leftDeviceName
921
+ }
922
+ } else if (rightDevice != null && rightDevice!!.address == deviceAddress) {
923
+ isRight = true
924
+ // Use stored name string (more reliable than BluetoothDevice.getName())
925
+ if (deviceName == null) {
926
+ deviceName = rightDeviceName
927
+ }
928
+ }
929
+
930
+ // If we couldn't match this device, it's not one we're pairing with
931
+ if (!isLeft && !isRight) {
932
+ Bridge.log("G1: Bond state changed for unknown device: " + deviceAddress)
933
+ return
934
+ }
935
+
936
+ // If name is still null after checking stored devices, log and return
937
+ if (deviceName == null) {
938
+ Bridge.log("G1: Could not determine device name for address: " + deviceAddress)
939
+ return
940
+ }
941
+
942
+ val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
943
+
944
+ if (bondState == BluetoothDevice.BOND_BONDED) {
945
+ Bridge.log("G1: Bonded with device: " + deviceName + " (address: " + deviceAddress + ")")
946
+ if (isLeft) {
947
+ isLeftBonded = true
948
+ isLeftPairing = false
949
+ pendingSavedG1LeftName = deviceName
950
+ } else if (isRight) {
951
+ isRightBonded = true
952
+ isRightPairing = false
953
+ pendingSavedG1RightName = deviceName
954
+ }
955
+
956
+ // Reset both pairing flags when a device bonds successfully
957
+ // This prevents the other device from being blocked when scan restarts
958
+ isLeftPairing = false
959
+ isRightPairing = false
960
+
961
+ // Restart scan for the next device
962
+ if (!isLeftBonded || !isRightBonded) {
963
+ // if (!(isLeftBonded && !isRightBonded)){// || !doPendingPairingIdsMatch()) {
964
+ Bridge.log("G1: Restarting scan to find remaining device...")
965
+ // Add delay for vendor-specific timing issues (e.g., Motorola devices)
966
+ Handler(Looper.getMainLooper()).postDelayed({
967
+ startScan()
968
+ }, 500L) // 500ms delay
969
+ } else if (isLeftBonded && isRightBonded && !doPendingPairingIdsMatch()) {
970
+ // We've connected to two different G1s...
971
+ // Let's unpair the right, try to pair to a different one
972
+ isRightBonded = false
973
+ isRightConnected = false
974
+ isRightPairing = false
975
+ pendingSavedG1RightName = null
976
+ Bridge.log("G1: Connected to two different G1s - retry right G1 arm")
977
+ } else {
978
+ Bridge.log("G1: Both devices bonded. Proceeding with connections...")
979
+ savedG1LeftName = pendingSavedG1LeftName
980
+ savedG1RightName = pendingSavedG1RightName
981
+ savePairedDeviceNames()
982
+ stopScan()
983
+
984
+ connectHandler.postDelayed({
985
+ connectToGatt(leftDevice!!)
986
+ }, 0L)
987
+
988
+ connectHandler.postDelayed({
989
+ connectToGatt(rightDevice!!)
990
+ }, 2000L)
991
+ }
992
+ } else if (bondState == BluetoothDevice.BOND_NONE) {
993
+ Bridge.log("G1: Bonding failed for device: " + deviceName + " (address: " + deviceAddress + ")")
994
+ if (isLeft)
995
+ isLeftPairing = false
996
+ if (isRight)
997
+ isRightPairing = false
998
+
999
+ // Restart scanning to retry bonding
1000
+ if (retryBondHandler == null) {
1001
+ retryBondHandler = Handler(Looper.getMainLooper())
1002
+ }
1003
+
1004
+ retryBondHandler!!.postDelayed({
1005
+ Bridge.log("G1: Retrying scan after bond failure...")
1006
+ startScan()
1007
+ }, BOND_RETRY_DELAY_MS)
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ fun doPendingPairingIdsMatch(): Boolean {
1014
+ val leftId = parsePairingIdFromDeviceName(pendingSavedG1LeftName)
1015
+ val rightId = parsePairingIdFromDeviceName(pendingSavedG1RightName)
1016
+ Bridge.log("G1: LeftID: " + leftId)
1017
+ Bridge.log("G1: RightID: " + rightId)
1018
+
1019
+ // ok, HACKY, but if one of them is null, that means that we connected to the
1020
+ // other on a previous connect
1021
+ // this whole function shouldn't matter anymore anyway as we properly filter for
1022
+ // the device name, so it should be fine
1023
+ // in the future, the way to actually check this would be to check the final ID
1024
+ // string, which is the only one guaranteed to be unique
1025
+ if (leftId == null || rightId == null) {
1026
+ return true
1027
+ }
1028
+
1029
+ return leftId != null && leftId == rightId
1030
+ }
1031
+
1032
+ fun parsePairingIdFromDeviceName(input: String?): String? {
1033
+ if (input == null || input.isEmpty())
1034
+ return null
1035
+ // Regular expression to match the number after "G1_"
1036
+ val pattern = Pattern.compile("G1_(\\d+)_")
1037
+ val matcher = pattern.matcher(input)
1038
+
1039
+ if (matcher.find()) {
1040
+ return matcher.group(1) // Group 1 contains the number
1041
+ }
1042
+ return null // Return null if no match is found
1043
+ }
1044
+
1045
+ private fun savePairedDeviceNames() {
1046
+ // if (savedG1LeftName != null && savedG1RightName != null) {
1047
+ // context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
1048
+ // .edit()
1049
+ // .putString(LEFT_DEVICE_KEY, savedG1LeftName)
1050
+ // .putString(RIGHT_DEVICE_KEY, savedG1RightName)
1051
+ // .apply();
1052
+ // Bridge.log("G1: Saved paired device names: Left=" + savedG1LeftName + ", Right="
1053
+ // + savedG1RightName);
1054
+ // }
1055
+ }
1056
+
1057
+ private fun loadPairedDeviceNames() {
1058
+ // SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFS_NAME,
1059
+ // Context.MODE_PRIVATE);
1060
+ // savedG1LeftName = prefs.getString(LEFT_DEVICE_KEY, null);
1061
+ // savedG1RightName = prefs.getString(RIGHT_DEVICE_KEY, null);
1062
+ // Bridge.log("G1: Loaded paired device names: Left=" + savedG1LeftName + ", Right="
1063
+ // + savedG1RightName);
1064
+ }
1065
+
1066
+ private fun connectToGatt(device: BluetoothDevice?) {
1067
+ if (device == null) {
1068
+ Bridge.log("G1: Cannot connect to GATT: device is null")
1069
+ return
1070
+ }
1071
+
1072
+ Bridge.log("G1: connectToGatt called for device: " + device.name + " (" + device.address + ")")
1073
+ val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
1074
+ if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
1075
+ Bridge.log("G1: Bluetooth is disabled or not available. Cannot reconnect to glasses.")
1076
+ return
1077
+ }
1078
+
1079
+ // Reset the services waiter based on device name
1080
+ if (device.name.contains("_L_")) {
1081
+ Bridge.log("G1: Device identified as left side. Resetting leftServicesWaiter.")
1082
+ leftServicesWaiter.setTrue()
1083
+ } else {
1084
+ Bridge.log("G1: Device identified as right side. Resetting rightServicesWaiter.")
1085
+ rightServicesWaiter.setTrue()
1086
+ }
1087
+
1088
+ // Establish GATT connection based on device name and current connection state
1089
+ if (device.name.contains("_L_") && leftGlassGatt == null) {
1090
+ Bridge.log("G1: Connecting GATT to left side.")
1091
+ leftGlassGatt = device.connectGatt(context, false, leftGattCallback)
1092
+ isLeftConnected = false // Reset connection state
1093
+ Bridge.log("G1: Left GATT connection initiated. isLeftConnected set to false.")
1094
+ } else if (device.name.contains("_R_") && rightGlassGatt == null && isLeftConnected) {
1095
+ Bridge.log("G1: Connecting GATT to right side.")
1096
+ rightGlassGatt = device.connectGatt(context, false, rightGattCallback)
1097
+ isRightConnected = false // Reset connection state
1098
+ Bridge.log("G1: Right GATT connection initiated. isRightConnected set to false.")
1099
+ } else {
1100
+ Bridge.log("G1: Tried to connect to incorrect or already connected device: " + device.name)
1101
+ }
1102
+ }
1103
+
1104
+ private fun reconnectToGatt(device: BluetoothDevice) {
1105
+ if (isKilled) {
1106
+ return
1107
+ }
1108
+ connectToGatt(device) // Reuse the connectToGatt method
1109
+ }
1110
+
1111
+ private val seenDevices: MutableSet<String?> = HashSet()
1112
+
1113
+ private val modernScanCallback: ScanCallback = object : ScanCallback() {
1114
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
1115
+ val device = result.device
1116
+ val name = device.name
1117
+
1118
+ // Now you can reference the bluetoothAdapter field if needed:
1119
+ if (!bluetoothAdapter!!.isEnabled()) {
1120
+ Bridge.log("G1: Bluetooth is disabled")
1121
+ return
1122
+ }
1123
+
1124
+ // Check if G1 arm
1125
+ if (name == null || !name.contains("Even G1_")) {
1126
+ return
1127
+ }
1128
+
1129
+ // Log all available device information for debugging
1130
+ // Bridge.log("G1: === Device Information ===");
1131
+ // Bridge.log("G1: Device Name: " + name);
1132
+ // Bridge.log("G1: Device Address: " + device.getAddress());
1133
+ // Bridge.log("G1: Device Type: " + device.getType());
1134
+ // Bridge.log("G1: Device Class: " + device.getBluetoothClass());
1135
+ // Bridge.log("G1: Bond State: " + device.getBondState());
1136
+
1137
+ // Try to get additional device information using reflection
1138
+ try {
1139
+ // Try to get the full device name (might contain serial number)
1140
+ val getAliasMethod = device.javaClass.getMethod("getAlias")
1141
+ val alias = getAliasMethod.invoke(device) as String?
1142
+ // add alias to seen device set:
1143
+ if (!seenDevices.contains(alias)) {
1144
+ seenDevices.add(alias)
1145
+ } else {
1146
+ return
1147
+ }
1148
+ Bridge.log("G1: Device Alias: " + alias)
1149
+ } catch (e: Exception) {
1150
+ Bridge.log("G1: Could not get device alias: " + e.message)
1151
+ }
1152
+
1153
+ // Capture manufacturer data for left device during scanning
1154
+ if (name != null && name.contains("_L_") && result.scanRecord != null) {
1155
+ val allManufacturerData = result.scanRecord!!.manufacturerSpecificData
1156
+ for (i in 0 until allManufacturerData.size()) {
1157
+ val parsedDeviceName = parsePairingIdFromDeviceName(name)
1158
+ if (parsedDeviceName != null) {
1159
+ // Bridge.log("G1: Parsed Device Name: " + parsedDeviceName);
1160
+ }
1161
+
1162
+ val manufacturerId = allManufacturerData.keyAt(i)
1163
+ val data = allManufacturerData.valueAt(i)
1164
+ // Bridge.log("G1: Left Device Manufacturer ID " + manufacturerId + ": " + bytesToHex(data));
1165
+
1166
+ // Try to decode serial number from this manufacturer data
1167
+ val decodedSerial = decodeSerialFromManufacturerData(data)
1168
+ if (decodedSerial != null) {
1169
+ // Bridge.log("G1: LEFT DEVICE DECODED SERIAL NUMBER from ID " + manufacturerId + ": " + decodedSerial);
1170
+ val decoded = decodeEvenG1SerialNumber(decodedSerial)
1171
+ // Bridge.log("G1: LEFT DEVICE Style: " + decoded[0] + ", Color: " + decoded[1]);
1172
+
1173
+ if (preferredG1DeviceId != null && preferredG1DeviceId == parsedDeviceName) {
1174
+ // Store the information (matching iOS implementation)
1175
+ DeviceStore.apply("glasses", "serialNumber", decodedSerial)
1176
+ DeviceStore.apply("glasses", "style", decoded[0])
1177
+ DeviceStore.apply("glasses", "color", decoded[1])
1178
+
1179
+ // Emit the serial number information to React Native
1180
+ emitSerialNumberInfo(decodedSerial, decoded[0], decoded[1])
1181
+ }
1182
+ break
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ // Bridge.log("G1: PREFERRED ID: " + preferredG1DeviceId);
1188
+ if (preferredG1DeviceId == null || !name.contains("_" + preferredG1DeviceId + "_")) {
1189
+ // Bridge.log("G1: NOT PAIRED GLASSES");
1190
+ return
1191
+ }
1192
+
1193
+ Bridge.log("G1: FOUND OUR PREFERRED ID: " + preferredG1DeviceId)
1194
+
1195
+ val isLeft = name.contains("_L_")
1196
+
1197
+ // // If we already have saved device names for left/right...
1198
+ // if (savedG1LeftName != null && savedG1RightName != null) {
1199
+ // if (!(name.contains(savedG1LeftName) || name.contains(savedG1RightName))) {
1200
+ // return; // Not a matching device
1201
+ // }
1202
+ // }
1203
+
1204
+ // Identify which side (left/right) and store both device and name
1205
+ if (isLeft) {
1206
+ leftDevice = device
1207
+ leftDeviceName = name // Store name now since getName() can return null later
1208
+ } else {
1209
+ rightDevice = device
1210
+ rightDeviceName = name // Store name now since getName() can return null later
1211
+ }
1212
+
1213
+ val bondState = device.bondState
1214
+ if (bondState != BluetoothDevice.BOND_BONDED) {
1215
+ if (isLeft && !isLeftPairing && !isLeftBonded) {
1216
+ // Stop scan before initiating bond
1217
+ stopScan()
1218
+ // Bridge.log("G1: Bonding with Left Glass...");
1219
+ isLeftPairing = true
1220
+ g1ConnectionState = SmartGlassesConnectionState.BONDING
1221
+ // connectionEvent(g1ConnectionState);
1222
+ bondDevice(device)
1223
+ } else if (!isLeft && !isRightPairing && !isRightBonded) {
1224
+ // Stop scan before initiating bond
1225
+ stopScan()
1226
+ Bridge.log("G1: Attempting to bond with right device. isRightPairing=" + isRightPairing
1227
+ + ", isRightBonded=" + isRightBonded)
1228
+ isRightPairing = true
1229
+ g1ConnectionState = SmartGlassesConnectionState.BONDING
1230
+ // connectionEvent(g1ConnectionState);
1231
+ bondDevice(device)
1232
+ } else {
1233
+ Bridge.log("G1: Not running bonding - isLeft=" + isLeft + ", isLeftPairing=" + isLeftPairing +
1234
+ ", isLeftBonded=" + isLeftBonded + ", isRightPairing=" + isRightPairing +
1235
+ ", isRightBonded=" + isRightBonded + " - continuing scan for other side")
1236
+ }
1237
+ } else {
1238
+ // Already bonded
1239
+ if (isLeft) {
1240
+ isLeftBonded = true
1241
+ } else {
1242
+ isRightBonded = true
1243
+ }
1244
+
1245
+ // Both are bonded => connect to GATT
1246
+ if (leftDevice != null && rightDevice != null && isLeftBonded && isRightBonded) {
1247
+ Bridge.log("G1: Both sides bonded. Ready to connect to GATT.")
1248
+ stopScan()
1249
+
1250
+ connectHandler.postDelayed({
1251
+ attemptGattConnection(leftDevice)
1252
+ }, 0L)
1253
+
1254
+ connectHandler.postDelayed({
1255
+ attemptGattConnection(rightDevice)
1256
+ }, 2000L)
1257
+ } else {
1258
+ Bridge.log("G1: Not running a63dd")
1259
+ Bridge.log("G1: leftBonded=" + isLeftBonded + ", rightBonded=" + isRightBonded)
1260
+ Bridge.log("G1: leftDevice=" + leftDevice + ", rightDevice=" + rightDevice)
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ override fun onScanFailed(errorCode: Int) {
1266
+ Bridge.log("G1: Scan failed with error: " + errorCode)
1267
+ }
1268
+ }
1269
+
1270
+ private fun resetAllBondsAndState() {
1271
+ Bridge.log("G1: Resetting ALL bonds and internal state for complete fresh start")
1272
+
1273
+ // Remove both bonds if devices exist
1274
+ if (leftDevice != null) {
1275
+ removeBond(leftDevice!!)
1276
+ }
1277
+
1278
+ if (rightDevice != null) {
1279
+ removeBond(rightDevice!!)
1280
+ }
1281
+
1282
+ // Reset all internal state
1283
+ isLeftBonded = false
1284
+ isRightBonded = false
1285
+ isLeftPairing = false
1286
+ isRightPairing = false
1287
+ isLeftConnected = false
1288
+ isRightConnected = false
1289
+
1290
+ // Clear saved device names
1291
+ pendingSavedG1LeftName = null
1292
+ pendingSavedG1RightName = null
1293
+
1294
+ // Close any existing GATT connections
1295
+ if (leftGlassGatt != null) {
1296
+ leftGlassGatt!!.disconnect()
1297
+ leftGlassGatt!!.close()
1298
+ leftGlassGatt = null
1299
+ }
1300
+
1301
+ if (rightGlassGatt != null) {
1302
+ rightGlassGatt!!.disconnect()
1303
+ rightGlassGatt!!.close()
1304
+ rightGlassGatt = null
1305
+ }
1306
+
1307
+ // Wait briefly for bond removal to complete
1308
+ Handler(Looper.getMainLooper()).postDelayed({
1309
+ Bridge.log("G1: Restarting scan after complete bond/state reset")
1310
+ g1ConnectionState = SmartGlassesConnectionState.SCANNING
1311
+ // connectionEvent(g1ConnectionState);
1312
+ startScan()
1313
+ }, 2000L)
1314
+ }
1315
+
1316
+ /**
1317
+ * Handles a device with a valid bond
1318
+ */
1319
+ private fun handleValidBond(device: BluetoothDevice, isLeft: Boolean) {
1320
+ Bridge.log("G1: Handling valid bond for " + (if (isLeft) "left" else "right") + " glass")
1321
+
1322
+ // Update state
1323
+ if (isLeft) {
1324
+ isLeftBonded = true
1325
+ } else {
1326
+ isRightBonded = true
1327
+ }
1328
+
1329
+ // If both glasses are bonded, connect to GATT
1330
+ if (leftDevice != null && rightDevice != null && isLeftBonded && isRightBonded) {
1331
+ Bridge.log("G1: Both glasses have valid bonds - ready to connect to GATT")
1332
+
1333
+ connectHandler.postDelayed({
1334
+ attemptGattConnection(leftDevice)
1335
+ }, 0L)
1336
+
1337
+ connectHandler.postDelayed({
1338
+ attemptGattConnection(rightDevice)
1339
+ }, 2000L)
1340
+ } else {
1341
+ // Continue scanning for the other glass
1342
+ Bridge.log("Still need to find " + (if (isLeft) "right" else "left") + " glass - resuming scan")
1343
+ startScan()
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Removes an existing bond with a Bluetooth device to force fresh pairing
1349
+ */
1350
+ private fun removeBond(device: BluetoothDevice?): Boolean {
1351
+ try {
1352
+ if (device == null) {
1353
+ Bridge.log("G1: Cannot remove bond: device is null")
1354
+ return false
1355
+ }
1356
+
1357
+ val method = device.javaClass.getMethod("removeBond")
1358
+ val result = method.invoke(device) as Boolean
1359
+ Bridge.log("G1: Removing bond for device " + device.name + ", result: " + result)
1360
+ return result
1361
+ } catch (e: Exception) {
1362
+ Bridge.log("G1: Error removing bond: " + e.message)
1363
+ return false
1364
+ }
1365
+ }
1366
+
1367
+ fun connectToSmartGlasses() {
1368
+ // Register bonding receiver
1369
+ val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
1370
+ context!!.registerReceiver(bondingReceiver, filter)
1371
+ isBondingReceiverRegistered = true
1372
+
1373
+ preferredG1DeviceId = DeviceStore.get("bluetooth", "device_name") as String?
1374
+
1375
+ if (!bluetoothAdapter!!.isEnabled()) {
1376
+ return
1377
+ }
1378
+
1379
+ // Start scanning for devices
1380
+ g1ConnectionState = SmartGlassesConnectionState.SCANNING
1381
+ // connectionEvent(g1ConnectionState);
1382
+ startScan()
1383
+ }
1384
+
1385
+ private fun startScan() {
1386
+ val scanner = bluetoothAdapter!!.bluetoothLeScanner
1387
+ if (scanner == null) {
1388
+ Bridge.log("G1: BluetoothLeScanner not available.")
1389
+ return
1390
+ }
1391
+
1392
+ // Optionally, define filters if needed
1393
+ val filters = ArrayList<ScanFilter>()
1394
+ // For example, to filter by device name:
1395
+ // filters.add(new ScanFilter.Builder().setDeviceName("Even G1_").build());
1396
+
1397
+ // Set desired scan settings
1398
+ val settings = ScanSettings.Builder()
1399
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
1400
+ .build()
1401
+
1402
+ // Start scanning
1403
+ isScanning = true
1404
+ try {
1405
+ scanner.startScan(filters, settings, modernScanCallback)
1406
+ } catch (e: SecurityException) {
1407
+ // Auto-reconnect paths may fire before BLUETOOTH_SCAN is granted on Android 12+
1408
+ Bridge.log("G1: startScan SecurityException — bluetooth permission missing: " + e.message)
1409
+ isScanning = false
1410
+ g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
1411
+ return
1412
+ } catch (e: Exception) {
1413
+ Bridge.log("G1: startScan failed: " + e.message)
1414
+ isScanning = false
1415
+ g1ConnectionState = SmartGlassesConnectionState.DISCONNECTED
1416
+ return
1417
+ }
1418
+ Bridge.log("G1: CALL START SCAN - Started scanning for devices...")
1419
+
1420
+ // Ensure scanning state is immediately communicated to UI
1421
+ g1ConnectionState = SmartGlassesConnectionState.SCANNING
1422
+ // connectionEvent(g1ConnectionState);
1423
+
1424
+ // Stop the scan after some time (e.g., 10-15s instead of 60 to avoid
1425
+ // throttling)
1426
+ // handler.postDelayed(() -> stopScan(), 10000);
1427
+ }
1428
+
1429
+ override fun stopScan() {
1430
+ val scanner = bluetoothAdapter!!.bluetoothLeScanner
1431
+ if (scanner != null) {
1432
+ scanner.stopScan(modernScanCallback)
1433
+ }
1434
+ isScanning = false
1435
+ Bridge.log("G1: Stopped scanning for devices")
1436
+ }
1437
+
1438
+ private fun bondDevice(device: BluetoothDevice) {
1439
+ try {
1440
+ Bridge.log("G1: Attempting to bond with device: " + device.name)
1441
+ val method = device.javaClass.getMethod("createBond")
1442
+ method.invoke(device)
1443
+ } catch (e: Exception) {
1444
+ Bridge.log("G1: Bonding failed: " + e.message)
1445
+ }
1446
+ }
1447
+
1448
+ private var rightConnectionRetryRunnable: Runnable? = null
1449
+
1450
+ private fun attemptGattConnection(device: BluetoothDevice?) {
1451
+ // if (!isKilled)
1452
+
1453
+ if (device == null) {
1454
+ Bridge.log("G1: Cannot connect to GATT: Device is null")
1455
+ return
1456
+ }
1457
+
1458
+ val deviceName = device.name
1459
+ if (deviceName == null) {
1460
+ Bridge.log("G1: Skipping null device name: " + device.address
1461
+ + "... this means something horriffic has occured. Look into this.")
1462
+ return
1463
+ }
1464
+
1465
+ Bridge.log("G1: attemptGattConnection called for device: " + deviceName + " (" + device.address + ")")
1466
+
1467
+ // Check if both devices are bonded before attempting connection
1468
+ if (!isLeftBonded || !isRightBonded) {
1469
+ Bridge.log("G1: Cannot connect to GATT: Both devices are not bonded yet (isLeftBonded: " + isLeftBonded
1470
+ + ", isRightBonded: " + isRightBonded + ")")
1471
+ return
1472
+ }
1473
+
1474
+ g1ConnectionState = SmartGlassesConnectionState.CONNECTING
1475
+ Bridge.log("G1: Setting g1ConnectionState to CONNECTING. Notifying connectionEvent.")
1476
+ // connectionEvent(g1ConnectionState);
1477
+
1478
+ val isLeftDevice = deviceName.contains("_L_")
1479
+ val isRightDevice = deviceName.contains("_R_")
1480
+
1481
+ if (isLeftDevice) {
1482
+ connectLeftDevice(device)
1483
+ } else if (isRightDevice) {
1484
+ connectRightDevice(device)
1485
+ } else {
1486
+ Bridge.log("G1: Unknown device type: " + deviceName)
1487
+ }
1488
+ }
1489
+
1490
+ private fun connectLeftDevice(device: BluetoothDevice) {
1491
+ if (leftGlassGatt == null) {
1492
+ Bridge.log("G1: Attempting GATT connection for Left Glass...")
1493
+ leftGlassGatt = device.connectGatt(context, false, leftGattCallback)
1494
+ isLeftConnected = false
1495
+ Bridge.log("G1: Left GATT connection initiated. isLeftConnected set to false.")
1496
+ } else {
1497
+ Bridge.log("G1: Left Glass GATT already exists")
1498
+ }
1499
+ }
1500
+
1501
+ private fun connectRightDevice(device: BluetoothDevice) {
1502
+ // Only connect right after left is fully connected
1503
+ if (isLeftConnected) {
1504
+ if (rightGlassGatt == null) {
1505
+ Bridge.log("G1: Attempting GATT connection for Right Glass...")
1506
+ rightGlassGatt = device.connectGatt(context, false, rightGattCallback)
1507
+ isRightConnected = false
1508
+ Bridge.log("G1: Right GATT connection initiated. isRightConnected set to false.")
1509
+
1510
+ // Cancel any pending retry attempts since we're now connecting
1511
+ if (rightConnectionRetryRunnable != null) {
1512
+ connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
1513
+ rightConnectionRetryRunnable = null
1514
+ }
1515
+ } else {
1516
+ Bridge.log("G1: Right Glass GATT already exists")
1517
+ }
1518
+ } else {
1519
+ Bridge.log("G1: Waiting for left glass before connecting right. Scheduling retry in "
1520
+ + RIGHT_CONNECTION_RETRY_DELAY + "ms")
1521
+
1522
+ // Cancel any existing retry attempts to avoid duplicate retries
1523
+ if (rightConnectionRetryRunnable != null) {
1524
+ connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
1525
+ }
1526
+
1527
+ // Create new retry runnable
1528
+ rightConnectionRetryRunnable = Runnable {
1529
+ if (!isKilled) {
1530
+ Bridge.log("G1: Retrying right glass connection...")
1531
+ attemptGattConnection(device)
1532
+ } else {
1533
+ Bridge.log("G1: Connection cancelled, stopping retry attempts")
1534
+ }
1535
+ }
1536
+
1537
+ // Schedule retry
1538
+ connectHandler.postDelayed(rightConnectionRetryRunnable!!, RIGHT_CONNECTION_RETRY_DELAY)
1539
+ }
1540
+ }
1541
+
1542
+ private fun createTextPackage(text: String, currentPage: Int, totalPages: Int, screenStatus: Int): ByteArray {
1543
+ val textBytes = text.toByteArray()
1544
+ val buffer = ByteBuffer.allocate(9 + textBytes.size)
1545
+ buffer.put(0x4E.toByte())
1546
+ buffer.put((currentSeq++ and 0xFF).toByte())
1547
+ buffer.put(1.toByte())
1548
+ buffer.put(0.toByte())
1549
+ buffer.put(screenStatus.toByte())
1550
+ buffer.put(0.toByte())
1551
+ buffer.put(0.toByte())
1552
+ buffer.put(currentPage.toByte())
1553
+ buffer.put(totalPages.toByte())
1554
+ buffer.put(textBytes)
1555
+
1556
+ return buffer.array()
1557
+ }
1558
+
1559
+ private fun sendDataSequentially(data: ByteArray) {
1560
+ sendDataSequentially(data, false)
1561
+ }
1562
+
1563
+ private fun sendDataSequentially(data: List<ByteArray>) {
1564
+ sendDataSequentially(data, false)
1565
+ }
1566
+
1567
+ fun sendJson(jsonOriginal: Map<String, Any>, wakeUp: Boolean) {
1568
+
1569
+ }
1570
+
1571
+ override fun requestPhoto(request: PhotoRequest) {
1572
+
1573
+ }
1574
+
1575
+ override fun startStream(message: MutableMap<String, Any>) {
1576
+
1577
+ }
1578
+
1579
+ override fun stopStream() {
1580
+
1581
+ }
1582
+
1583
+ override fun sendStreamKeepAlive(message: MutableMap<String, Any>) {
1584
+
1585
+ }
1586
+
1587
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {
1588
+
1589
+ }
1590
+
1591
+ override fun stopVideoRecording(requestId: String) {
1592
+
1593
+ }
1594
+
1595
+ override fun sendButtonPhotoSettings() {
1596
+
1597
+ }
1598
+
1599
+ override fun sendButtonVideoRecordingSettings() {
1600
+
1601
+ }
1602
+
1603
+ override fun sendCameraFovSetting() {
1604
+ }
1605
+
1606
+ override fun sendButtonMaxRecordingTime() {
1607
+
1608
+ }
1609
+
1610
+ override fun setBrightness(level: Int, autoMode: Boolean) {
1611
+ Bridge.log("G1: setBrightness() - level: " + level + "%, autoMode: " + autoMode)
1612
+ sendBrightnessCommand(level, autoMode)
1613
+ }
1614
+
1615
+ override fun clearDisplay() {
1616
+ Bridge.log("G1: clearDisplay() - sending space")
1617
+ // Bypass the throttle (a clear must always land) and drop any pending caption so it can't
1618
+ // overwrite the clear after the fact.
1619
+ cancelPendingThrottledText()
1620
+ displayTextWall(" ")
1621
+ }
1622
+
1623
+ // G1-specific display throttle (300ms, last-wins) — see G1.swift. G1 firmware can't absorb
1624
+ // rapid text-wall updates; coalesce to the latest within a 300ms window. Belongs in the G1 SGC
1625
+ // (G1 hardware quirk); G2 deliberately does NOT throttle (it must show every caption). The
1626
+ // trailing flush always sends the most recent text, so the final caption is never dropped —
1627
+ // only intermediate frames within a window are coalesced.
1628
+ private val textThrottleHandler = Handler(Looper.getMainLooper())
1629
+ private var textThrottlePending: String? = null
1630
+ private var textThrottleLastSent: Long = 0L
1631
+ private var textThrottleScheduled = false
1632
+ private val TEXT_THROTTLE_WINDOW_MS = 300L
1633
+
1634
+ /** Drop any pending throttled text-wall flush so it can't later overwrite a newer, non-text
1635
+ * display write (a clear, double-text-wall, or bitmap). The posted flush no-ops when
1636
+ * `textThrottlePending` is null. Called from every G1 display path that bypasses the throttle. */
1637
+ private fun cancelPendingThrottledText() {
1638
+ textThrottlePending = null
1639
+ }
1640
+
1641
+ private fun throttledTextWall(text: String) {
1642
+ val now = android.os.SystemClock.uptimeMillis()
1643
+ val sinceLast = now - textThrottleLastSent
1644
+ if (sinceLast >= TEXT_THROTTLE_WINDOW_MS) {
1645
+ // Past the window — send now.
1646
+ textThrottleLastSent = now
1647
+ textThrottlePending = null
1648
+ displayTextWall(text)
1649
+ } else {
1650
+ // Inside the window — keep only the latest and schedule one trailing flush.
1651
+ textThrottlePending = text
1652
+ if (!textThrottleScheduled) {
1653
+ textThrottleScheduled = true
1654
+ textThrottleHandler.postDelayed({
1655
+ textThrottleScheduled = false
1656
+ val pending = textThrottlePending ?: return@postDelayed
1657
+ textThrottlePending = null
1658
+ textThrottleLastSent = android.os.SystemClock.uptimeMillis()
1659
+ displayTextWall(pending)
1660
+ }, TEXT_THROTTLE_WINDOW_MS - sinceLast)
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ override fun sendText(text: String) {
1666
+ Bridge.log("G1: sendText() - text: " + text)
1667
+ throttledTextWall(text)
1668
+ }
1669
+
1670
+ override fun sendTextWall(text: String) {
1671
+ // Bridge.log("G1: sendTextWall() - text: " + text);
1672
+ throttledTextWall(text)
1673
+ }
1674
+
1675
+ override fun sendDoubleTextWall(top: String, bottom: String) {
1676
+ cancelPendingThrottledText() // a newer layout supersedes any pending caption text
1677
+ Bridge.log("G1: sendDoubleTextWall() - top: " + top + ", bottom: " + bottom)
1678
+ displayDoubleTextWall(top, bottom)
1679
+ }
1680
+
1681
+ override fun displayBitmap(base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?): Boolean {
1682
+ cancelPendingThrottledText() // a bitmap supersedes any pending caption text
1683
+ try {
1684
+ // Decode base64 to byte array
1685
+ val bmpData = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)
1686
+
1687
+ if (bmpData == null || bmpData.isEmpty()) {
1688
+ Log.e(TAG, "Failed to decode base64 image data")
1689
+ return false
1690
+ }
1691
+
1692
+ // Call internal implementation
1693
+ displayBitmapImage(bmpData)
1694
+ return true
1695
+ } catch (e: Exception) {
1696
+ Log.e(TAG, "Error displaying bitmap from base64", e)
1697
+ return false
1698
+ }
1699
+ }
1700
+
1701
+ override fun showDashboard() {
1702
+ exit()
1703
+ }
1704
+
1705
+ override fun ping() {
1706
+ Bridge.log("G1: ping()")
1707
+ }
1708
+
1709
+ override fun dbg1() {
1710
+ }
1711
+
1712
+ override fun dbg2() {
1713
+ }
1714
+
1715
+ override fun setDashboardPosition(height: Int, depth: Int) {
1716
+ Bridge.log("G1: setDashboardPosition() - height: " + height + ", depth: " + depth)
1717
+ sendDashboardPositionCommand(height, depth)
1718
+ }
1719
+
1720
+ override fun setHeadUpAngle(angle: Int) {
1721
+ Bridge.log("G1: setHeadUpAngle() - angle: " + angle)
1722
+ sendHeadUpAngleCommand(angle)
1723
+ }
1724
+
1725
+ override fun getBatteryStatus() {
1726
+ Bridge.log("G1: Requesting battery status")
1727
+ queryBatteryStatus()
1728
+ }
1729
+
1730
+ override fun setSilentMode(enabled: Boolean) {
1731
+
1732
+ }
1733
+
1734
+ override fun exit() {
1735
+ sendExitCommand()
1736
+ }
1737
+
1738
+ override fun sendShutdown() {
1739
+ Bridge.log("sendShutdown - not supported on G1")
1740
+ }
1741
+
1742
+ override fun sendReboot() {
1743
+ Bridge.log("sendReboot - not supported on G1")
1744
+ }
1745
+
1746
+ override fun sendRgbLedControl(requestId: String, packageName: String?, action: String, color: String?, onDurationMs: Int, offDurationMs: Int, count: Int) {
1747
+ Bridge.log("sendRgbLedControl - not supported on G1")
1748
+ Bridge.sendRgbLedControlResponse(requestId, false, "device_not_supported")
1749
+ }
1750
+
1751
+ override fun disconnect() {
1752
+ DeviceStore.apply("glasses", "fullyBooted", false)
1753
+ destroy()
1754
+ }
1755
+
1756
+ override fun forget() {
1757
+ DeviceStore.apply("glasses", "fullyBooted", false)
1758
+ destroy()
1759
+ }
1760
+
1761
+ override fun connectById(id: String) {
1762
+ preferredG1DeviceId = id
1763
+ connectToSmartGlasses()
1764
+ }
1765
+
1766
+ override fun getConnectedBluetoothName(): String {
1767
+ // Return left device name if available, otherwise right device name
1768
+ if (leftDevice != null && leftDevice!!.name != null) {
1769
+ return leftDevice!!.name
1770
+ } else if (rightDevice != null && rightDevice!!.name != null) {
1771
+ return rightDevice!!.name
1772
+ }
1773
+ return ""
1774
+ }
1775
+
1776
+ override fun requestWifiScan(scanId: String?) {
1777
+
1778
+ }
1779
+
1780
+ override fun sendWifiCredentials(ssid: String, password: String) {
1781
+
1782
+ }
1783
+
1784
+ override fun forgetWifiNetwork(ssid: String) {
1785
+ // G1 doesn't support WiFi
1786
+ }
1787
+
1788
+ override fun sendHotspotState(enabled: Boolean) {
1789
+ // G1 doesn't support hotspot
1790
+ }
1791
+
1792
+ override fun sendUserEmailToGlasses(email: String) {
1793
+ // G1 doesn't support user email (no ASG client)
1794
+ }
1795
+
1796
+ override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
1797
+ // G1 doesn't support incident reporting (no ASG client)
1798
+ }
1799
+
1800
+ override fun queryGalleryStatus() {
1801
+
1802
+ }
1803
+
1804
+ override fun sendGalleryMode() {
1805
+ // G1 doesn't have a built-in camera/gallery system
1806
+ Bridge.log("G1: sendGalleryModeActive - not supported on G1")
1807
+ }
1808
+
1809
+ override fun requestVersionInfo() {
1810
+ // G1 doesn't support version info requests
1811
+ Bridge.log("G1: requestVersionInfo - not supported on G1")
1812
+ }
1813
+
1814
+ // private void sendDataSequentially(byte[] data, boolean onlyLeft) {
1815
+ // if (stopper) return;
1816
+ // stopper = true;
1817
+ //
1818
+ // new Thread(() -> {
1819
+ // try {
1820
+ // if (leftGlassGatt != null && leftTxChar != null) {
1821
+ // leftTxChar.setValue(data);
1822
+ // leftGlassGatt.writeCharacteristic(leftTxChar);
1823
+ // Thread.sleep(DELAY_BETWEEN_SENDS_MS);
1824
+ // }
1825
+ //
1826
+ // if (!onlyLeft && rightGlassGatt != null && rightTxChar != null) {
1827
+ // rightTxChar.setValue(data);
1828
+ // rightGlassGatt.writeCharacteristic(rightTxChar);
1829
+ // Thread.sleep(DELAY_BETWEEN_SENDS_MS);
1830
+ // }
1831
+ // stopper = false;
1832
+ // } catch (InterruptedException e) {
1833
+ // Bridge.log("G1: Error sending data: " + e.getMessage());
1834
+ // }
1835
+ // }).start();
1836
+ // }
1837
+
1838
+ // Data class to represent a send request
1839
+ private class SendRequest {
1840
+ val data: ByteArray
1841
+ val onlyLeft: Boolean
1842
+ val onlyRight: Boolean
1843
+ var waitTime = -1
1844
+
1845
+ constructor(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean) {
1846
+ this.data = data
1847
+ this.onlyLeft = onlyLeft
1848
+ this.onlyRight = onlyRight
1849
+ }
1850
+
1851
+ constructor(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean, waitTime: Int) {
1852
+ this.data = data
1853
+ this.onlyLeft = onlyLeft
1854
+ this.onlyRight = onlyRight
1855
+ this.waitTime = waitTime
1856
+ }
1857
+ }
1858
+
1859
+ // Queue to hold pending requests
1860
+ private val sendQueue: BlockingQueue<Array<SendRequest?>> = LinkedBlockingQueue()
1861
+
1862
+ @Volatile private var isWorkerRunning = false
1863
+
1864
+ // Non-blocking function to add new send request
1865
+ private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean) {
1866
+ val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, false))
1867
+ sendQueue.offer(chunks)
1868
+ startWorkerIfNeeded()
1869
+ }
1870
+
1871
+ // Non-blocking function to add new send request
1872
+ private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, waitTime: Int) {
1873
+ val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, false, waitTime))
1874
+ sendQueue.offer(chunks)
1875
+ startWorkerIfNeeded()
1876
+ }
1877
+
1878
+ // Overloaded function to handle multiple chunks (List<byte[]>)
1879
+ private fun sendDataSequentially(data: List<ByteArray>, onlyLeft: Boolean) {
1880
+ sendDataSequentially(data, onlyLeft, false)
1881
+ }
1882
+
1883
+ private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean) {
1884
+ val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, onlyRight))
1885
+ sendQueue.offer(chunks)
1886
+ startWorkerIfNeeded()
1887
+ }
1888
+
1889
+ private fun sendDataSequentially(data: ByteArray, onlyLeft: Boolean, onlyRight: Boolean, waitTime: Int) {
1890
+ val chunks = arrayOf<SendRequest?>(SendRequest(data, onlyLeft, onlyRight, waitTime))
1891
+ sendQueue.offer(chunks)
1892
+ startWorkerIfNeeded()
1893
+ }
1894
+
1895
+ private fun sendDataSequentially(data: List<ByteArray>, onlyLeft: Boolean, onlyRight: Boolean) {
1896
+ val chunks = arrayOfNulls<SendRequest>(data.size)
1897
+ for (i in 0 until data.size) {
1898
+ chunks[i] = SendRequest(data[i], onlyLeft, onlyRight)
1899
+ }
1900
+ sendQueue.offer(chunks)
1901
+ startWorkerIfNeeded()
1902
+ }
1903
+
1904
+ // Start the worker thread if it's not already running
1905
+ @Synchronized
1906
+ private fun startWorkerIfNeeded() {
1907
+ if (!isWorkerRunning) {
1908
+ isWorkerRunning = true
1909
+ Thread(this::processQueue, "EvenRealitiesG1SGCProcessQueue").start()
1910
+ }
1911
+ }
1912
+
1913
+ // Fast send method for bitmap chunks - doesn't wait for write confirmation
1914
+ private fun sendBitmapChunkNoWait(data: ByteArray?, sendLeft: Boolean, sendRight: Boolean) {
1915
+ if (data == null || data.isEmpty())
1916
+ return
1917
+
1918
+ // IMPORTANT: Set write type to NO_RESPONSE for fire-and-forget behavior
1919
+ // This prevents waiting for BLE acknowledgments
1920
+
1921
+ // Send to right glass without waiting
1922
+ if (sendRight && rightGlassGatt != null && rightTxChar != null && isRightConnected) {
1923
+ rightTxChar!!.value = data
1924
+ rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
1925
+ rightGlassGatt!!.writeCharacteristic(rightTxChar)
1926
+ // Restore default write type
1927
+ rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
1928
+ }
1929
+
1930
+ // Send to left glass without waiting
1931
+ if (sendLeft && leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
1932
+ leftTxChar!!.value = data
1933
+ leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
1934
+ leftGlassGatt!!.writeCharacteristic(leftTxChar)
1935
+ // Restore default write type
1936
+ leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
1937
+ }
1938
+ }
1939
+
1940
+ class BooleanWaiter {
1941
+ private var flag = true // initially true
1942
+
1943
+ @Synchronized
1944
+ @Throws(InterruptedException::class)
1945
+ fun waitWhileTrue() {
1946
+ while (flag) {
1947
+ (this as java.lang.Object).wait()
1948
+ }
1949
+ }
1950
+
1951
+ @Synchronized
1952
+ fun setTrue() {
1953
+ flag = true
1954
+ }
1955
+
1956
+ @Synchronized
1957
+ fun setFalse() {
1958
+ flag = false
1959
+ (this as java.lang.Object).notifyAll()
1960
+ }
1961
+ }
1962
+
1963
+ private val leftWaiter = BooleanWaiter()
1964
+ private val rightWaiter = BooleanWaiter()
1965
+ private val leftServicesWaiter = BooleanWaiter()
1966
+ private val rightServicesWaiter = BooleanWaiter()
1967
+
1968
+ private fun processQueue() {
1969
+ // First wait until the services are setup and ready to receive data
1970
+ Bridge.log("G1: PROC_QUEUE - waiting on services waiters")
1971
+ try {
1972
+ leftServicesWaiter.waitWhileTrue()
1973
+ rightServicesWaiter.waitWhileTrue()
1974
+ } catch (e: InterruptedException) {
1975
+ Bridge.log("G1: Interrupted waiting for descriptor writes: " + e.toString())
1976
+ }
1977
+ Bridge.log("G1: PROC_QUEUE - DONE waiting on services waiters")
1978
+
1979
+ while (!isKilled) {
1980
+ try {
1981
+ // Make sure services are ready before processing requests
1982
+ leftServicesWaiter.waitWhileTrue()
1983
+ rightServicesWaiter.waitWhileTrue()
1984
+
1985
+ // This will block until data is available - no CPU spinning!
1986
+ val requests = sendQueue.take()
1987
+
1988
+ for (request in requests) {
1989
+ if (request == null) {
1990
+ isWorkerRunning = false
1991
+ break
1992
+ }
1993
+
1994
+ try {
1995
+ // Force an initial delay so BLE gets all setup
1996
+ val timeSinceConnection = System.currentTimeMillis() - lastConnectionTimestamp
1997
+ if (timeSinceConnection < INITIAL_CONNECTION_DELAY_MS) {
1998
+ Thread.sleep(INITIAL_CONNECTION_DELAY_MS - timeSinceConnection)
1999
+ }
2000
+
2001
+ var leftSuccess = true
2002
+ var rightSuccess = true
2003
+
2004
+ // Start both writes without waiting
2005
+ var leftStarted = false
2006
+ var rightStarted = false
2007
+
2008
+ // Start right glass write (non-blocking)
2009
+ if (!request.onlyLeft && rightGlassGatt != null && rightTxChar != null && isRightConnected) {
2010
+ rightWaiter.setTrue()
2011
+ rightTxChar!!.value = request.data
2012
+ rightSuccess = rightGlassGatt!!.writeCharacteristic(rightTxChar)
2013
+ if (rightSuccess) {
2014
+ rightStarted = true
2015
+ lastSendTimestamp = System.currentTimeMillis()
2016
+ }
2017
+ }
2018
+
2019
+ // Start left glass write immediately (non-blocking)
2020
+ if (!request.onlyRight && leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
2021
+ leftWaiter.setTrue()
2022
+ leftTxChar!!.value = request.data
2023
+ leftSuccess = leftGlassGatt!!.writeCharacteristic(leftTxChar)
2024
+ if (leftSuccess) {
2025
+ leftStarted = true
2026
+ lastSendTimestamp = System.currentTimeMillis()
2027
+ }
2028
+ }
2029
+
2030
+ // Now wait for both to complete
2031
+ if (rightStarted) {
2032
+ rightWaiter.waitWhileTrue()
2033
+ }
2034
+ if (leftStarted) {
2035
+ leftWaiter.waitWhileTrue()
2036
+ }
2037
+
2038
+ Thread.sleep(DELAY_BETWEEN_CHUNKS_SEND)
2039
+
2040
+ // If the packet asked us to do a delay, then do it
2041
+ if (request.waitTime != -1) {
2042
+ Thread.sleep(request.waitTime.toLong())
2043
+ }
2044
+ } catch (e: InterruptedException) {
2045
+ Bridge.log("G1: Error sending data: " + e.message)
2046
+ if (isKilled)
2047
+ break
2048
+ }
2049
+ }
2050
+ } catch (e: InterruptedException) {
2051
+ if (isKilled) {
2052
+ Bridge.log("G1: Process queue thread interrupted - shutting down")
2053
+ break
2054
+ }
2055
+ Bridge.log("G1: Error in queue processing: " + e.message)
2056
+ }
2057
+ }
2058
+
2059
+ Bridge.log("G1: Process queue thread exiting")
2060
+ }
2061
+
2062
+ // @Override
2063
+ // public void displayReferenceCardSimple(String title, String body, int
2064
+ // lingerTimeMs) {
2065
+ // displayReferenceCardSimple(title, body, lingerTimeMs);
2066
+ // }
2067
+
2068
+ private fun createNotificationJson(appIdentifier: String, title: String, subtitle: String, message: String): String {
2069
+ val currentTime = System.currentTimeMillis() / 1000L // Unix timestamp in seconds
2070
+ val currentDate = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date()) // Date
2071
+ // format
2072
+ // for
2073
+ // 'date'
2074
+ // field
2075
+
2076
+ val ncsNotification = NCSNotification(
2077
+ notificationNum++, // Increment sequence ID for uniqueness
2078
+ 1, // type (e.g., 1 = notification type)
2079
+ appIdentifier,
2080
+ title,
2081
+ subtitle,
2082
+ message,
2083
+ currentTime.toInt(), // Cast long to int to match Python
2084
+ currentDate, // Add the current date to the notification
2085
+ "AugmentOS" // display_name
2086
+ )
2087
+
2088
+ val notification = Notification(ncsNotification, "Add")
2089
+
2090
+ val gson = Gson()
2091
+ return gson.toJson(notification)
2092
+ }
2093
+
2094
+ internal class Notification {
2095
+ var ncs_notification: NCSNotification? = null
2096
+ var type: String? = null
2097
+
2098
+ constructor() {
2099
+ // Default constructor
2100
+ }
2101
+
2102
+ constructor(ncs_notification: NCSNotification?, type: String?) {
2103
+ this.ncs_notification = ncs_notification
2104
+ this.type = type
2105
+ }
2106
+ }
2107
+
2108
+ internal class NCSNotification(
2109
+ var msg_id: Int,
2110
+ var type: Int,
2111
+ var app_identifier: String,
2112
+ var title: String,
2113
+ var subtitle: String,
2114
+ var message: String,
2115
+ var time_s: Int, // Changed from long to int for consistency
2116
+ var date: String, // Added to match Python's date field
2117
+ var display_name: String
2118
+ )
2119
+
2120
+ private fun createNotificationChunks(json: String): List<ByteArray> {
2121
+ val MAX_CHUNK_SIZE = 176 // 180 - 4 header bytes
2122
+ val jsonBytes = json.toByteArray(StandardCharsets.UTF_8)
2123
+ val totalChunks = Math.ceil(jsonBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
2124
+
2125
+ val chunks = ArrayList<ByteArray>()
2126
+ for (i in 0 until totalChunks) {
2127
+ val start = i * MAX_CHUNK_SIZE
2128
+ val end = Math.min(start + MAX_CHUNK_SIZE, jsonBytes.size)
2129
+ val payloadChunk = Arrays.copyOfRange(jsonBytes, start, end)
2130
+
2131
+ // Create the header
2132
+ val header = byteArrayOf(
2133
+ NOTIFICATION.toByte(),
2134
+ 0x00, // notify_id (can be updated as needed)
2135
+ totalChunks.toByte(),
2136
+ i.toByte()
2137
+ )
2138
+
2139
+ // Combine header and payload
2140
+ val chunk = ByteBuffer.allocate(header.size + payloadChunk.size)
2141
+ chunk.put(header)
2142
+ chunk.put(payloadChunk)
2143
+
2144
+ chunks.add(chunk.array())
2145
+ }
2146
+
2147
+ return chunks
2148
+ }
2149
+
2150
+ fun displayReferenceCardSimple(title: String, body: String) {
2151
+ if (!isConnected()) {
2152
+ Bridge.log("G1: Not connected to glasses")
2153
+ return
2154
+ }
2155
+
2156
+ if (title.trim().isEmpty() && body.trim().isEmpty()) {
2157
+ if (DeviceManager.getInstance().powerSavingMode) {
2158
+ sendExitCommand()
2159
+ return
2160
+ }
2161
+ }
2162
+
2163
+ val chunks = createTextWallChunks(title + "\n\n" + body)
2164
+ for (i in 0 until chunks.size) {
2165
+ val chunk = chunks[i]
2166
+ val isLastChunk = (i == chunks.size - 1)
2167
+
2168
+ if (isLastChunk) {
2169
+ sendDataSequentially(chunk, false)
2170
+ } else {
2171
+ sendDataSequentially(chunk, false, 300)
2172
+ }
2173
+ }
2174
+ Bridge.log("G1: Send simple reference card")
2175
+ }
2176
+
2177
+ fun destroy() {
2178
+ Bridge.log("G1: EvenRealitiesG1SGC ONDESTROY")
2179
+ showHomeScreen()
2180
+ isKilled = true
2181
+ DeviceStore.apply("glasses", "fullyBooted", false)
2182
+
2183
+ // Reset battery levels
2184
+ batteryLeft = -1
2185
+ batteryRight = -1
2186
+ DeviceStore.apply("glasses", "batteryLevel", -1)
2187
+
2188
+ // stop BLE scanning
2189
+ stopScan()
2190
+
2191
+ // Shutdown bitmap executor
2192
+ if (bitmapExecutor != null) {
2193
+ bitmapExecutor!!.shutdown()
2194
+ try {
2195
+ if (!bitmapExecutor!!.awaitTermination(1, TimeUnit.SECONDS)) {
2196
+ bitmapExecutor!!.shutdownNow()
2197
+ }
2198
+ } catch (e: InterruptedException) {
2199
+ bitmapExecutor!!.shutdownNow()
2200
+ }
2201
+ }
2202
+
2203
+ if (bondingReceiver != null && isBondingReceiverRegistered) {
2204
+ context!!.unregisterReceiver(bondingReceiver)
2205
+ isBondingReceiverRegistered = false
2206
+ }
2207
+
2208
+ if (rightConnectionRetryRunnable != null) {
2209
+ connectHandler.removeCallbacks(rightConnectionRetryRunnable!!)
2210
+ rightConnectionRetryRunnable = null
2211
+ }
2212
+
2213
+ // disable the microphone
2214
+ sendSetMicEnabled(false, 0)
2215
+
2216
+ // stop sending heartbeat
2217
+ stopHeartbeat()
2218
+
2219
+ // stop sending micbeat
2220
+ stopMicBeat()
2221
+
2222
+ // Stop periodic notifications
2223
+ stopPeriodicNotifications()
2224
+
2225
+ // Stop periodic text wall
2226
+ // stopPeriodicNotifications();
2227
+
2228
+ if (leftGlassGatt != null) {
2229
+ leftGlassGatt!!.disconnect()
2230
+ leftGlassGatt!!.close()
2231
+ leftGlassGatt = null
2232
+ }
2233
+ if (rightGlassGatt != null) {
2234
+ rightGlassGatt!!.disconnect()
2235
+ rightGlassGatt!!.close()
2236
+ rightGlassGatt = null
2237
+ }
2238
+
2239
+ if (handler != null)
2240
+ handler.removeCallbacksAndMessages(null)
2241
+ if (heartbeatHandler != null && heartbeatRunnable != null)
2242
+ heartbeatHandler.removeCallbacks(heartbeatRunnable!!)
2243
+ if (whiteListHandler != null)
2244
+ whiteListHandler.removeCallbacksAndMessages(null)
2245
+ if (micEnableHandler != null)
2246
+ micEnableHandler.removeCallbacksAndMessages(null)
2247
+ if (notificationHandler != null && notificationRunnable != null)
2248
+ notificationHandler.removeCallbacks(notificationRunnable!!)
2249
+ if (textWallHandler != null && textWallRunnable != null)
2250
+ textWallHandler.removeCallbacks(textWallRunnable!!)
2251
+ // if (goHomeHandler != null)
2252
+ // goHomeHandler.removeCallbacks(goHomeRunnable);
2253
+ if (findCompatibleDevicesHandler != null)
2254
+ findCompatibleDevicesHandler!!.removeCallbacksAndMessages(null)
2255
+ if (connectHandler != null)
2256
+ connectHandler.removeCallbacksAndMessages(null)
2257
+ if (retryBondHandler != null)
2258
+ retryBondHandler!!.removeCallbacksAndMessages(null)
2259
+ if (characteristicHandler != null) {
2260
+ characteristicHandler.removeCallbacksAndMessages(null)
2261
+ }
2262
+ if (reconnectHandler != null) {
2263
+ reconnectHandler.removeCallbacksAndMessages(null)
2264
+ }
2265
+ if (leftConnectionTimeoutHandler != null && leftConnectionTimeoutRunnable != null) {
2266
+ leftConnectionTimeoutHandler.removeCallbacks(leftConnectionTimeoutRunnable!!)
2267
+ }
2268
+ if (rightConnectionTimeoutHandler != null && rightConnectionTimeoutRunnable != null) {
2269
+ rightConnectionTimeoutHandler.removeCallbacks(rightConnectionTimeoutRunnable!!)
2270
+ }
2271
+ if (reconnectHandler != null) {
2272
+ reconnectHandler.removeCallbacksAndMessages(null)
2273
+ }
2274
+ if (queryBatteryStatusHandler != null && queryBatteryStatusHandler != null) {
2275
+ queryBatteryStatusHandler.removeCallbacksAndMessages(null)
2276
+ }
2277
+
2278
+ // free LC3 decoder
2279
+ if (lc3DecoderPtr != 0L) {
2280
+ Lc3Cpp.freeDecoder(lc3DecoderPtr)
2281
+ lc3DecoderPtr = 0
2282
+ }
2283
+
2284
+ sendQueue.clear()
2285
+
2286
+ // Add a dummy element to unblock the take() call if needed
2287
+ sendQueue.offer(arrayOfNulls<SendRequest>(0)) // is this needed?
2288
+
2289
+ isWorkerRunning = false
2290
+
2291
+ isLeftConnected = false
2292
+ isRightConnected = false
2293
+
2294
+ // Clear device references and stored names
2295
+ leftDevice = null
2296
+ rightDevice = null
2297
+ leftDeviceName = null
2298
+ rightDeviceName = null
2299
+
2300
+ Bridge.log("G1: EvenRealitiesG1SGC cleanup complete")
2301
+ }
2302
+
2303
+ fun isConnected(): Boolean {
2304
+ return g1ConnectionState == SmartGlassesConnectionState.CONNECTED
2305
+ }
2306
+
2307
+ // Remaining methods
2308
+ fun showNaturalLanguageCommandScreen(prompt: String, naturalLanguageInput: String) {
2309
+ }
2310
+
2311
+ fun updateNaturalLanguageCommandScreen(naturalLanguageArgs: String) {
2312
+ }
2313
+
2314
+ fun scrollingTextViewIntermediateText(text: String) {
2315
+ }
2316
+
2317
+ fun scrollingTextViewFinalText(text: String) {
2318
+ }
2319
+
2320
+ fun stopScrollingTextViewMode() {
2321
+ }
2322
+
2323
+ fun displayPromptView(title: String, options: Array<String>) {
2324
+ }
2325
+
2326
+ fun displayTextLine(text: String) {
2327
+ }
2328
+
2329
+ fun displayBitmap(bmp: Bitmap) {
2330
+ try {
2331
+ val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
2332
+ displayBitmapImage(bmpBytes)
2333
+ } catch (e: Exception) {
2334
+ Log.e(TAG, e.message!!)
2335
+ }
2336
+ }
2337
+
2338
+ /**
2339
+ * Convert arbitrary image data (PNG/JPEG bytes) to G1-compatible 1-bit BMP format.
2340
+ * Mirrors G2.convertToG2Bmp() for local miniapp bitmap display support.
2341
+ *
2342
+ * @param imageData Raw image bytes (PNG, JPEG, etc.)
2343
+ * @return 1-bit BMP byte array, or null on failure
2344
+ */
2345
+ fun convertToG1Bmp(imageData: ByteArray): ByteArray? {
2346
+ try {
2347
+ val bmp = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
2348
+ if (bmp == null) {
2349
+ Bridge.log("G1: convertToG1Bmp - could not decode image data")
2350
+ return null
2351
+ }
2352
+ val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
2353
+ bmp.recycle()
2354
+ Bridge.log("G1: convertToG1Bmp - produced " + bmpBytes.size + " byte BMP")
2355
+ return bmpBytes
2356
+ } catch (e: Exception) {
2357
+ Log.e(TAG, "G1: convertToG1Bmp error: " + e.message)
2358
+ return null
2359
+ }
2360
+ }
2361
+
2362
+ fun blankScreen() {
2363
+ }
2364
+
2365
+ /**
2366
+ * Display pre-composed double text wall (two columns) on the glasses.
2367
+ *
2368
+ * NOTE: DisplayProcessor now composes double_text_wall into a single text_wall
2369
+ * with pixel-precise column alignment using ColumnComposer. This method may
2370
+ * not be called anymore for new flows, but is kept for backwards compatibility.
2371
+ *
2372
+ * Column composition is handled by DisplayProcessor in React Native.
2373
+ * This method is a "dumb pipe" - it just combines and sends the text.
2374
+ */
2375
+ fun displayDoubleTextWall(textTop: String, textBottom: String) {
2376
+ // Text is already composed by DisplayProcessor's ColumnComposer
2377
+ // Just combine and send as a text wall - no custom wrapping logic needed
2378
+ val combinedText = textTop + "\n" + textBottom
2379
+ displayTextWall(combinedText)
2380
+ }
2381
+
2382
+ fun showHomeScreen() {
2383
+ displayTextWall(" ")
2384
+
2385
+ // if (lastThingDisplayedWasAnImage) {
2386
+ // // clearG1Screen();
2387
+ // lastThingDisplayedWasAnImage = false;
2388
+ // }
2389
+ }
2390
+
2391
+ fun clearG1Screen() {
2392
+ Bridge.log("G1: Clearing G1 screen")
2393
+ val exitCommand = byteArrayOf(0x18.toByte())
2394
+ // sendDataSequentially(exitCommand, false);
2395
+ val theClearBitmapOrSomething = loadEmptyBmpFromAssets()
2396
+ val bmp = BitmapJavaUtils.bytesToBitmap(theClearBitmapOrSomething)
2397
+ try {
2398
+ val bmpBytes = convertBitmapTo1BitBmpBytes(bmp, false)
2399
+ displayBitmapImage(bmpBytes)
2400
+ } catch (e: Exception) {
2401
+ Log.e(TAG, "Error displaying clear bitmap: " + e.message)
2402
+ }
2403
+ }
2404
+
2405
+
2406
+ fun displayRowsCard(rowStrings: Array<String>) {
2407
+ }
2408
+
2409
+ fun displayBulletList(title: String, bullets: Array<String>) {
2410
+ }
2411
+
2412
+ fun displayReferenceCardImage(title: String, body: String, imgUrl: String) {
2413
+ }
2414
+
2415
+ fun displayTextWall(a: String) {
2416
+ val chunks = createTextWallChunks(a)
2417
+ sendChunks(chunks)
2418
+ }
2419
+
2420
+ fun setUpdatingScreen(updatingScreen: Boolean) {
2421
+ this.updatingScreen = updatingScreen
2422
+ }
2423
+
2424
+ fun setFontSizes() {
2425
+ }
2426
+
2427
+ // Heartbeat methods
2428
+ private fun constructHeartbeat(): ByteArray {
2429
+ val buffer = ByteBuffer.allocate(6)
2430
+ buffer.put(0x25.toByte())
2431
+ buffer.put(6.toByte())
2432
+ buffer.put((currentSeq and 0xFF).toByte())
2433
+ buffer.put(0x00.toByte())
2434
+ buffer.put(0x04.toByte())
2435
+ buffer.put((currentSeq++ and 0xFF).toByte())
2436
+ return buffer.array()
2437
+ }
2438
+
2439
+ private fun constructBatteryLevelQuery(): ByteArray {
2440
+ val buffer = ByteBuffer.allocate(2)
2441
+ buffer.put(0x2C.toByte()) // Command
2442
+ buffer.put(0x01.toByte()) // use 0x02 for iOS
2443
+ return buffer.array()
2444
+ }
2445
+
2446
+ private fun startHeartbeat(delay: Int) {
2447
+ Bridge.log("G1: Starting heartbeat")
2448
+ if (heartbeatCount > 0)
2449
+ stopHeartbeat()
2450
+
2451
+ heartbeatRunnable = object : Runnable {
2452
+ override fun run() {
2453
+ sendHeartbeat()
2454
+ // sendLoremIpsum();
2455
+
2456
+ // quickRestartG1();
2457
+
2458
+ heartbeatHandler.postDelayed(this, HEARTBEAT_INTERVAL_MS)
2459
+ }
2460
+ }
2461
+
2462
+ heartbeatHandler.postDelayed(heartbeatRunnable!!, delay.toLong())
2463
+ }
2464
+
2465
+ // periodically send a mic ON request so it never turns off
2466
+ private fun startMicBeat(delay: Int) {
2467
+ Bridge.log("G1: Starting micbeat")
2468
+ if (micBeatCount > 0)
2469
+ stopMicBeat()
2470
+ sendSetMicEnabled(true, 10)
2471
+
2472
+ micBeatRunnable = object : Runnable {
2473
+ override fun run() {
2474
+ Bridge.log("G1: SENDING MIC BEAT")
2475
+ sendSetMicEnabled(shouldUseGlassesMic, 1)
2476
+ micBeatHandler.postDelayed(this, MICBEAT_INTERVAL_MS)
2477
+ }
2478
+ }
2479
+
2480
+ micBeatHandler.postDelayed(micBeatRunnable!!, delay.toLong())
2481
+ }
2482
+
2483
+ override fun findCompatibleDevices() {
2484
+ Bridge.log("G1: findCompatibleDevices called")
2485
+ if (isScanningForCompatibleDevices) {
2486
+ Bridge.log("G1: Scan already in progress, skipping...")
2487
+ return
2488
+ }
2489
+ isScanningForCompatibleDevices = true
2490
+
2491
+ val scanner = bluetoothAdapter!!.bluetoothLeScanner
2492
+ if (scanner == null) {
2493
+ Log.e(TAG, "BluetoothLeScanner not available")
2494
+ isScanningForCompatibleDevices = false
2495
+ return
2496
+ }
2497
+
2498
+ val foundDeviceNames = ArrayList<String>()
2499
+ if (findCompatibleDevicesHandler == null) {
2500
+ findCompatibleDevicesHandler = Handler(Looper.getMainLooper())
2501
+ }
2502
+
2503
+ // Optional: add filters if you want to narrow the scan
2504
+ val filters = ArrayList<ScanFilter>()
2505
+ val settings = ScanSettings.Builder()
2506
+ .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
2507
+ .build()
2508
+
2509
+ // Create a modern ScanCallback instead of the deprecated LeScanCallback
2510
+ val bleScanCallback = object : ScanCallback() {
2511
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
2512
+ val device = result.device
2513
+ val name = device.name
2514
+ if (name != null && name.contains("Even G1_") && name.contains("_L_")) {
2515
+ synchronized(foundDeviceNames) {
2516
+ if (!foundDeviceNames.contains(name)) {
2517
+ foundDeviceNames.add(name)
2518
+ Bridge.log("Found smart glasses: " + name)
2519
+ var adjustedName = parsePairingIdFromDeviceName(name)
2520
+ // If parsing failed, use the full name as fallback
2521
+ if (adjustedName == null) {
2522
+ adjustedName = name
2523
+ Bridge.log("G1: Failed to parse device ID from name: " + name)
2524
+ }
2525
+ Bridge.sendDiscoveredDevice("Even Realities G1", adjustedName)
2526
+ }
2527
+ }
2528
+ }
2529
+ }
2530
+
2531
+ override fun onBatchScanResults(results: List<ScanResult>) {
2532
+ // If needed, handle batch results here
2533
+ }
2534
+
2535
+ override fun onScanFailed(errorCode: Int) {
2536
+ Log.e(TAG, "BLE scan failed with code: " + errorCode)
2537
+ }
2538
+ }
2539
+
2540
+ // Start scanning
2541
+ try {
2542
+ scanner.startScan(filters, settings, bleScanCallback)
2543
+ } catch (e: SecurityException) {
2544
+ Bridge.log("G1: findCompatibleDevices startScan SecurityException — bluetooth permission missing: " + e.message)
2545
+ isScanningForCompatibleDevices = false
2546
+ return
2547
+ } catch (e: Exception) {
2548
+ Bridge.log("G1: findCompatibleDevices startScan failed: " + e.message)
2549
+ isScanningForCompatibleDevices = false
2550
+ return
2551
+ }
2552
+ Bridge.log("G1: Started scanning for smart glasses with BluetoothLeScanner...")
2553
+
2554
+ // Stop scanning after 10 seconds (adjust as needed)
2555
+ findCompatibleDevicesHandler!!.postDelayed({
2556
+ scanner.stopScan(bleScanCallback)
2557
+ isScanningForCompatibleDevices = false
2558
+ Bridge.log("G1: Stopped scanning for smart glasses.")
2559
+ // EventBus.getDefault().post(
2560
+ // new GlassesBluetoothSearchStopEvent(
2561
+ // smartGlassesDevice.deviceModelName
2562
+ // )
2563
+ // );
2564
+ }, 10000L)
2565
+ }
2566
+
2567
+ private fun sendWhiteListCommand(delay: Int) {
2568
+ if (whiteListedAlready) {
2569
+ return
2570
+ }
2571
+ whiteListedAlready = true
2572
+
2573
+ Bridge.log("G1: Sending whitelist command")
2574
+ whiteListHandler.postDelayed({
2575
+ val chunks = getWhitelistChunks()
2576
+ sendDataSequentially(chunks, false)
2577
+ // for (byte[] chunk : chunks) {
2578
+ // Bridge.log("G1: Sending this chunk for white list:" + bytesToUtf8(chunk));
2579
+ // sendDataSequentially(chunk, false);
2580
+ //
2581
+ //// // Sleep for 100 milliseconds between sending each chunk
2582
+ //// try {
2583
+ //// Thread.sleep(150);
2584
+ //// } catch (InterruptedException e) {
2585
+ //// e.printStackTrace();
2586
+ //// }
2587
+ // }
2588
+ }, delay.toLong())
2589
+ }
2590
+
2591
+ private fun stopHeartbeat() {
2592
+ Bridge.log("G1: stopHeartbeat()")
2593
+ if (heartbeatHandler != null) {
2594
+ heartbeatHandler.removeCallbacksAndMessages(null)
2595
+ heartbeatHandler.removeCallbacksAndMessages(heartbeatRunnable)
2596
+ heartbeatCount = 0
2597
+ }
2598
+ }
2599
+
2600
+ private fun stopMicBeat() {
2601
+ Bridge.log("G1: stopMicBeat()")
2602
+ sendSetMicEnabled(false, 10)
2603
+ if (micBeatHandler != null) {
2604
+ micBeatHandler.removeCallbacksAndMessages(null)
2605
+ micBeatHandler.removeCallbacksAndMessages(micBeatRunnable)
2606
+ micBeatRunnable = null
2607
+ micBeatCount = 0
2608
+ }
2609
+ }
2610
+
2611
+ private fun sendHeartbeat() {
2612
+ val heartbeatPacket = constructHeartbeat()
2613
+ // Bridge.log("G1: Sending heartbeat: " + bytesToHex(heartbeatPacket));
2614
+
2615
+ sendDataSequentially(heartbeatPacket, false, 100)
2616
+
2617
+ if (batteryLeft == -1 || batteryRight == -1 || heartbeatCount % 10 == 0) {
2618
+ queryBatteryStatusHandler.postDelayed(this::queryBatteryStatus, 500L)
2619
+ }
2620
+ // queryBatteryStatusHandler.postDelayed(this::queryBatteryStatus, 500);
2621
+
2622
+ heartbeatCount++
2623
+ }
2624
+
2625
+ private fun queryBatteryStatus() {
2626
+ val batteryQueryPacket = constructBatteryLevelQuery()
2627
+ // Bridge.log("G1: Sending battery status query: " +
2628
+ // bytesToHex(batteryQueryPacket));
2629
+
2630
+ sendDataSequentially(batteryQueryPacket, false, 250)
2631
+ }
2632
+
2633
+ fun sendBrightnessCommand(brightness: Int, autoLight: Boolean) {
2634
+ // Validate brightness range
2635
+
2636
+ val validBrightness: Int
2637
+ if (brightness != -1) {
2638
+ validBrightness = (brightness * 63) / 100
2639
+ } else {
2640
+ validBrightness = (30 * 63) / 100
2641
+ }
2642
+
2643
+ // Construct the command
2644
+ val buffer = ByteBuffer.allocate(3)
2645
+ buffer.put(0x01.toByte()) // Command
2646
+ buffer.put(validBrightness.toByte()) // Brightness level (0~63)
2647
+ buffer.put((if (autoLight) 1 else 0).toByte()) // Auto light (0 = close, 1 = open)
2648
+
2649
+ sendDataSequentially(buffer.array(), false, 100)
2650
+
2651
+ Bridge.log("G1: Sent auto light brightness command => Brightness: " + brightness + ", Auto Light: "
2652
+ + (if (autoLight) "Open" else "Close"))
2653
+
2654
+ // send to AugmentOS core
2655
+ // if (autoLight) {
2656
+ // EventBus.getDefault().post(new BrightnessLevelEvent(autoLight));
2657
+ // } else {
2658
+ // EventBus.getDefault().post(new BrightnessLevelEvent(brightness));
2659
+ // }
2660
+ }
2661
+
2662
+ fun sendHeadUpAngleCommand(headUpAngle: Int) {
2663
+ // Validate headUpAngle range (0 ~ 60)
2664
+ var headUpAngleValue = headUpAngle
2665
+ if (headUpAngleValue < 0) {
2666
+ headUpAngleValue = 0
2667
+ } else if (headUpAngleValue > 60) {
2668
+ headUpAngleValue = 60
2669
+ }
2670
+
2671
+ // Construct the command
2672
+ val buffer = ByteBuffer.allocate(3)
2673
+ buffer.put(0x0B.toByte()) // Command for configuring headUp angle
2674
+ buffer.put(headUpAngleValue.toByte()) // Angle value (0~60)
2675
+ buffer.put(0x01.toByte()) // Level (fixed at 0x01)
2676
+
2677
+ sendDataSequentially(buffer.array(), false, 100)
2678
+
2679
+ Bridge.log("G1: Sent headUp angle command => Angle: " + headUpAngleValue)
2680
+ // EventBus.getDefault().post(new HeadUpAngleEvent(headUpAngle));
2681
+ }
2682
+
2683
+ fun sendDashboardPositionCommand(height: Int, depth: Int) {
2684
+ // clamp height and depth to 0-8 and 1-9 respectively:
2685
+ val heightValue = Math.max(0, Math.min(height, 8))
2686
+ val depthValue = Math.max(1, Math.min(depth, 9))
2687
+
2688
+ val globalCounter = 0 // TODO: must be incremented each time this command is sent!
2689
+
2690
+ val buffer = ByteBuffer.allocate(8)
2691
+ buffer.put(0x26.toByte()) // Command for dashboard height
2692
+ buffer.put(0x08.toByte()) // Length
2693
+ buffer.put(0x00.toByte()) // Sequence
2694
+ buffer.put((globalCounter and 0xFF).toByte()) // counter
2695
+ buffer.put(0x02.toByte()) // Fixed value
2696
+ buffer.put(0x01.toByte()) // State ON
2697
+ buffer.put(heightValue.toByte()) // Height value (0-8)
2698
+ buffer.put(depthValue.toByte()) // Depth value (0-9)
2699
+
2700
+ sendDataSequentially(buffer.array(), false, 100)
2701
+
2702
+ Bridge.log("G1: Sent dashboard height/depth command => Height: " + heightValue + ", Depth: " + depthValue)
2703
+ // EventBus.getDefault().post(new DashboardPositionEvent(height, depth));
2704
+ }
2705
+
2706
+ fun updateGlassesBrightness(brightness: Int) {
2707
+ Bridge.log("G1: Updating glasses brightness: " + brightness)
2708
+ sendBrightnessCommand(brightness, false)
2709
+ }
2710
+
2711
+ fun updateGlassesAutoBrightness(autoBrightness: Boolean) {
2712
+ Bridge.log("G1: Updating glasses auto brightness: " + autoBrightness)
2713
+ sendBrightnessCommand(-1, autoBrightness)
2714
+ }
2715
+
2716
+ fun updateGlassesHeadUpAngle(headUpAngle: Int) {
2717
+ sendHeadUpAngleCommand(headUpAngle)
2718
+ }
2719
+
2720
+ fun updateGlassesDepthHeight(depth: Int, height: Int) {
2721
+ sendDashboardPositionCommand(height, depth)
2722
+ }
2723
+
2724
+ fun sendExitCommand() {
2725
+ sendDataSequentially(byteArrayOf(0x18.toByte()), false, 100)
2726
+ }
2727
+
2728
+ // microphone stuff
2729
+ fun sendSetMicEnabled(enable: Boolean, delay: Int) {
2730
+ Bridge.log("G1: sendSetMicEnabled(): " + enable)
2731
+
2732
+ isMicrophoneEnabled = enable // Update the state tracker
2733
+ DeviceStore.apply("glasses", "micEnabled", enable)
2734
+ micEnableHandler.postDelayed({
2735
+ if (!isConnected()) {
2736
+ Bridge.log("G1: Tryna start mic: Not connected to glasses")
2737
+ return@postDelayed
2738
+ }
2739
+
2740
+ val command: Byte = 0x0E // Command for MIC control
2741
+ val enableByte = (if (enable) 1 else 0).toByte() // 1 to enable, 0 to disable
2742
+
2743
+ val buffer = ByteBuffer.allocate(2)
2744
+ buffer.put(command)
2745
+ buffer.put(enableByte)
2746
+
2747
+ sendDataSequentially(buffer.array(), false, true, 300) // wait some time to setup the mic
2748
+ Bridge.log("G1: Sent MIC command: " + bytesToHex(buffer.array()))
2749
+ }, delay.toLong())
2750
+ }
2751
+
2752
+ // notifications
2753
+ private fun startPeriodicNotifications(delay: Int) {
2754
+ if (notifysStarted) {
2755
+ return
2756
+ }
2757
+ notifysStarted = true
2758
+
2759
+ notificationRunnable = object : Runnable {
2760
+ override fun run() {
2761
+ // Send notification
2762
+ sendPeriodicNotification()
2763
+
2764
+ // Schedule the next notification
2765
+ notificationHandler.postDelayed(this, 12000L)
2766
+ }
2767
+ }
2768
+
2769
+ // Start the first notification after 5 seconds
2770
+ notificationHandler.postDelayed(notificationRunnable!!, delay.toLong())
2771
+ }
2772
+
2773
+ private fun sendPeriodicNotification() {
2774
+ if (!isConnected()) {
2775
+ Bridge.log("G1: Cannot send notification: Not connected to glasses")
2776
+ return
2777
+ }
2778
+
2779
+ // Example notification data (replace with your actual data)
2780
+ // String json = createNotificationJson("com.augment.os", "QuestionAnswerer",
2781
+ // "How much caffeine in dark chocolate?", "25 to 50 grams per piece");
2782
+ val json = createNotificationJson("com.augment.os", "QuestionAnswerer",
2783
+ "How much caffeine in dark chocolate?", "25 to 50 grams per piece")
2784
+ Bridge.log("G1: the JSON to send: " + json)
2785
+ val chunks = createNotificationChunks(json)
2786
+ for (chunk in chunks) {
2787
+ Bridge.log("G1: Sent chunk to glasses: " + bytesToUtf8(chunk))
2788
+ }
2789
+
2790
+ // Send each chunk with a short sleep between each send
2791
+ sendDataSequentially(chunks, false)
2792
+
2793
+ Bridge.log("G1: Sent periodic notification")
2794
+ }
2795
+
2796
+ // text wall debug
2797
+ private fun startPeriodicTextWall(delay: Int) {
2798
+ if (textWallsStarted) {
2799
+ return
2800
+ }
2801
+ textWallsStarted = true
2802
+
2803
+ textWallRunnable = object : Runnable {
2804
+ override fun run() {
2805
+ // Send notification
2806
+ sendPeriodicTextWall()
2807
+
2808
+ // Schedule the next notification
2809
+ textWallHandler.postDelayed(this, 12000L)
2810
+ }
2811
+ }
2812
+
2813
+ // Start the first text wall send after 5 seconds
2814
+ textWallHandler.postDelayed(textWallRunnable!!, delay.toLong())
2815
+ }
2816
+
2817
+ private var textSeqNum = 0 // Sequence number for text packets
2818
+
2819
+ /**
2820
+ * Creates BLE chunks for pre-wrapped text.
2821
+ *
2822
+ * IMPORTANT: Text is expected to come pre-wrapped from the DisplayProcessor in React Native.
2823
+ * This function does NOT perform any text wrapping - it only chunks the text for BLE transmission.
2824
+ * The DisplayProcessor handles all pixel-accurate wrapping using @glassly/display-utils.
2825
+ *
2826
+ * @param text Pre-wrapped text with newlines already in place
2827
+ * @return List of BLE chunks ready for transmission
2828
+ */
2829
+ private fun createTextWallChunks(text: String): List<ByteArray> {
2830
+ // Text comes pre-wrapped from DisplayProcessor - just chunk it for transmission
2831
+ return chunkTextForTransmission(text)
2832
+ }
2833
+
2834
+ /**
2835
+ * Chunks text into BLE packets for transmission to glasses.
2836
+ * This is a low-level function that handles BLE protocol framing.
2837
+ *
2838
+ * @param text Text to chunk (should already be formatted/wrapped)
2839
+ * @return List of BLE chunks with headers
2840
+ */
2841
+ private fun chunkTextForTransmission(text: String?): List<ByteArray> {
2842
+ // Handle empty or whitespace-only text by sending at least a space
2843
+ // This ensures the display gets updated/cleared properly
2844
+ val textToSend = if (text == null || text.trim().isEmpty()) " " else sanitizeG1DisplayText(text)
2845
+ val textBytes = textToSend.toByteArray(StandardCharsets.UTF_8)
2846
+ val totalChunks = Math.ceil(textBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
2847
+
2848
+ val allChunks = ArrayList<ByteArray>()
2849
+ for (i in 0 until totalChunks) {
2850
+ val start = i * MAX_CHUNK_SIZE
2851
+ val end = Math.min(start + MAX_CHUNK_SIZE, textBytes.size)
2852
+ val payloadChunk = Arrays.copyOfRange(textBytes, start, end)
2853
+
2854
+ // Create header with protocol specifications
2855
+ val screenStatus: Byte = 0x71 // New content (0x01) + Text Show (0x70)
2856
+ val header = byteArrayOf(
2857
+ TEXT_COMMAND.toByte(), // Command type
2858
+ textSeqNum.toByte(), // Sequence number
2859
+ totalChunks.toByte(), // Total packages
2860
+ i.toByte(), // Current package number
2861
+ screenStatus, // Screen status
2862
+ 0x00.toByte(), // new_char_pos0 (high)
2863
+ 0x00.toByte(), // new_char_pos1 (low)
2864
+ 0x00.toByte(), // Current page number (always 0)
2865
+ 0x01.toByte() // Max page number (always 1)
2866
+ )
2867
+
2868
+ // Combine header and payload
2869
+ val chunk = ByteBuffer.allocate(header.size + payloadChunk.size)
2870
+ chunk.put(header)
2871
+ chunk.put(payloadChunk)
2872
+
2873
+ allChunks.add(chunk.array())
2874
+ }
2875
+
2876
+ // Increment sequence number for next transmission
2877
+ textSeqNum = (textSeqNum + 1) % 256
2878
+
2879
+ return allChunks
2880
+ }
2881
+
2882
+ private fun calculateSpacesForAlignment(currentWidth: Int, targetPosition: Int, spaceWidth: Int): Int {
2883
+ // Calculate space needed in pixels
2884
+ val pixelsNeeded = targetPosition - currentWidth
2885
+
2886
+ // Calculate spaces needed (with minimum of 1 space for separation)
2887
+ if (pixelsNeeded <= 0) {
2888
+ return 1 // Ensure at least one space between columns
2889
+ }
2890
+
2891
+ // Calculate the exact number of spaces needed
2892
+ val spaces = Math.ceil(pixelsNeeded.toDouble() / spaceWidth).toInt()
2893
+
2894
+ // Cap at a reasonable maximum
2895
+ return Math.min(spaces, 100)
2896
+ }
2897
+
2898
+ private fun sendPeriodicTextWall() {
2899
+ if (!isConnected()) {
2900
+ Bridge.log("G1: Cannot send text wall: Not connected to glasses")
2901
+ return
2902
+ }
2903
+
2904
+ Bridge.log("G1: ^^^^^^^^^^^^^ SENDING DEBUG TEXT WALL")
2905
+
2906
+ // Example text wall content - replace with your actual text content
2907
+ val sampleText = "This is an example of a text wall that will be displayed on the glasses. " +
2908
+ "It demonstrates how text can be split into multiple pages and displayed sequentially. " +
2909
+ "Each page contains multiple lines, and each line is carefully formatted to fit the display width. " +
2910
+ "The text continues across multiple pages, showing how longer content can be handled effectively."
2911
+
2912
+ val chunks = createTextWallChunks(sampleText)
2913
+
2914
+ // Send each chunk with a delay between sends
2915
+ for (chunk in chunks) {
2916
+ sendDataSequentially(chunk)
2917
+
2918
+ // try {
2919
+ // Thread.sleep(150); // 150ms delay between chunks
2920
+ // } catch (InterruptedException e) {
2921
+ // e.printStackTrace();
2922
+ // }
2923
+ }
2924
+
2925
+ // Bridge.log("G1: Sent text wall");
2926
+ }
2927
+
2928
+ private fun stopPeriodicNotifications() {
2929
+ if (notificationHandler != null && notificationRunnable != null) {
2930
+ notificationHandler.removeCallbacks(notificationRunnable!!)
2931
+ Bridge.log("G1: Stopped periodic notifications")
2932
+ }
2933
+ }
2934
+
2935
+ fun getWhitelistChunks(): List<ByteArray> {
2936
+ // Define the hardcoded whitelist JSON
2937
+ val apps = ArrayList<AppInfo>()
2938
+ apps.add(AppInfo("com.augment.os", "AugmentOS"))
2939
+ val whitelistJson = createWhitelistJson(apps)
2940
+
2941
+ Bridge.log("G1: Creating chunks for hardcoded whitelist: " + whitelistJson)
2942
+
2943
+ // Convert JSON to bytes and split into chunks
2944
+ return createWhitelistChunks(whitelistJson)
2945
+ }
2946
+
2947
+ private fun createWhitelistJson(apps: List<AppInfo>): String {
2948
+ val appList = JSONArray()
2949
+ try {
2950
+ // Add each app to the list
2951
+ for (app in apps) {
2952
+ val appJson = JSONObject()
2953
+ appJson.put("id", app.id)
2954
+ appJson.put("name", app.name)
2955
+ appList.put(appJson)
2956
+ }
2957
+
2958
+ val whitelistJson = JSONObject()
2959
+ whitelistJson.put("calendar_enable", false)
2960
+ whitelistJson.put("call_enable", false)
2961
+ whitelistJson.put("msg_enable", false)
2962
+ whitelistJson.put("ios_mail_enable", false)
2963
+
2964
+ val appObject = JSONObject()
2965
+ appObject.put("list", appList)
2966
+ appObject.put("enable", true)
2967
+
2968
+ whitelistJson.put("app", appObject)
2969
+
2970
+ return whitelistJson.toString()
2971
+ } catch (e: JSONException) {
2972
+ Log.e(TAG, "Error creating whitelist JSON: " + e.message)
2973
+ return "{}"
2974
+ }
2975
+ }
2976
+
2977
+ // Simple class to hold app info
2978
+ internal class AppInfo(val id: String, val name: String)
2979
+
2980
+ // Helper function to split JSON into chunks
2981
+ private fun createWhitelistChunks(json: String): List<ByteArray> {
2982
+ val MAX_CHUNK_SIZE = 180 - 4 // Reserve space for the header
2983
+ val jsonBytes = json.toByteArray(StandardCharsets.UTF_8)
2984
+ val totalChunks = Math.ceil(jsonBytes.size.toDouble() / MAX_CHUNK_SIZE).toInt()
2985
+
2986
+ val chunks = ArrayList<ByteArray>()
2987
+ for (i in 0 until totalChunks) {
2988
+ val start = i * MAX_CHUNK_SIZE
2989
+ val end = Math.min(start + MAX_CHUNK_SIZE, jsonBytes.size)
2990
+ val payloadChunk = Arrays.copyOfRange(jsonBytes, start, end)
2991
+
2992
+ // Create the header: [WHITELIST_CMD, total_chunks, chunk_index]
2993
+ val header = byteArrayOf(
2994
+ WHITELIST_CMD.toByte(), // Command ID
2995
+ totalChunks.toByte(), // Total number of chunks
2996
+ i.toByte() // Current chunk index
2997
+ )
2998
+
2999
+ // Combine header and payload
3000
+ val buffer = ByteBuffer.allocate(header.size + payloadChunk.size)
3001
+ buffer.put(header)
3002
+ buffer.put(payloadChunk)
3003
+
3004
+ chunks.add(buffer.array())
3005
+ }
3006
+
3007
+ return chunks
3008
+ }
3009
+
3010
+ fun displayCustomContent(content: String) {
3011
+ Bridge.log("G1: DISPLAY CUSTOM CONTENT")
3012
+ }
3013
+
3014
+ private fun sendChunks(chunks: List<ByteArray>) {
3015
+ // Send each chunk with a delay between sends
3016
+ for (chunk in chunks) {
3017
+ // Bridge.log("G1: Sending chunk: " + Arrays.toString(chunk));
3018
+ sendDataSequentially(chunk)
3019
+
3020
+ // try {
3021
+ // Thread.sleep(DELAY_BETWEEN_CHUNKS_SEND); // delay between chunks
3022
+ // } catch (InterruptedException e) {
3023
+ // e.printStackTrace();
3024
+ // }
3025
+ }
3026
+ }
3027
+
3028
+ // public int DEFAULT_CARD_SHOW_TIME = 6;
3029
+ // public void homeScreenInNSeconds(int n){
3030
+ // if (n == -1){
3031
+ // return;
3032
+ // }
3033
+ //
3034
+ // if (n == 0){
3035
+ // n = DEFAULT_CARD_SHOW_TIME;
3036
+ // }
3037
+ //
3038
+ // //disconnect after slight delay, so our above text gets a chance to show up
3039
+ // goHomeHandler.removeCallbacksAndMessages(goHomeRunnable);
3040
+ // goHomeHandler.removeCallbacksAndMessages(null);
3041
+ // goHomeRunnable = new Runnable() {
3042
+ // @Override
3043
+ // public void run() {
3044
+ // showHomeScreen();
3045
+ // }};
3046
+ // goHomeHandler.postDelayed(goHomeRunnable, n * 1000);
3047
+ // }
3048
+
3049
+ @Volatile private var isSendingBitmap = false
3050
+
3051
+ // Progress callback interface
3052
+ interface BmpProgressCallback {
3053
+ fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int)
3054
+
3055
+ fun onSuccess(side: String)
3056
+
3057
+ fun onError(side: String, error: String)
3058
+ }
3059
+
3060
+ /**
3061
+ * Inverts BMP pixel data by flipping all bits after the header.
3062
+ * Matches iOS implementation exactly (G1.swift line 1790-1811)
3063
+ *
3064
+ * @param bmpData The original BMP data
3065
+ * @return Inverted BMP data with pixel bits flipped
3066
+ */
3067
+ private fun invertBmpPixels(bmpData: ByteArray?): ByteArray? {
3068
+ if (bmpData == null || bmpData.size <= 62) {
3069
+ Bridge.log("G1: BMP data too small to contain pixel data")
3070
+ return bmpData
3071
+ }
3072
+
3073
+ // BMP header is 62 bytes (14 byte file header + 40 byte DIB header + 8 byte color table)
3074
+ val headerSize = 62
3075
+ val invertedData = ByteArray(bmpData.size)
3076
+
3077
+ // Copy header unchanged
3078
+ System.arraycopy(bmpData, 0, invertedData, 0, headerSize)
3079
+
3080
+ // Invert the pixel data (everything after the header)
3081
+ for (i in headerSize until bmpData.size) {
3082
+ // Invert each byte (flip all bits)
3083
+ invertedData[i] = bmpData[i].toInt().inv().toByte()
3084
+ }
3085
+
3086
+ Bridge.log("G1: Inverted BMP pixels: " + (bmpData.size - headerSize) + " bytes processed")
3087
+ return invertedData
3088
+ }
3089
+
3090
+ fun displayBitmapImage(bmpData: ByteArray?) {
3091
+ displayBitmapImage(bmpData, null)
3092
+ }
3093
+
3094
+ fun displayBitmapImage(bmpData: ByteArray?, callback: BmpProgressCallback?) {
3095
+ // Invert BMP pixels to match iOS implementation
3096
+ val invertedBmpData = invertBmpPixels(bmpData)
3097
+
3098
+ if (USE_OPTIMIZED_BITMAP_DISPLAY) {
3099
+ displayBitmapImageOptimized(invertedBmpData, callback)
3100
+ } else {
3101
+ displayBitmapImageLegacy(invertedBmpData, callback)
3102
+ }
3103
+ }
3104
+
3105
+ // Optimized bitmap display using iOS-like approach
3106
+ private fun displayBitmapImageOptimized(bmpData: ByteArray?, callback: BmpProgressCallback?) {
3107
+ Bridge.log("G1: Starting OPTIMIZED BMP display process")
3108
+
3109
+ try {
3110
+ if (bmpData == null || bmpData.isEmpty()) {
3111
+ Log.e(TAG, "Invalid BMP data provided")
3112
+ if (callback != null)
3113
+ callback.onError("both", "Invalid BMP data")
3114
+ return
3115
+ }
3116
+
3117
+ isSendingBitmap = true
3118
+ val startTime = System.currentTimeMillis()
3119
+
3120
+ Bridge.log("G1: Processing BMP data, size: " + bmpData.size + " bytes")
3121
+ val chunks = createBmpChunks(bmpData)
3122
+ Bridge.log("G1: Created " + chunks.size + " chunks")
3123
+
3124
+ // Send chunks using optimized method
3125
+ val chunksSuccess = sendBmpChunksOptimized(chunks, callback)
3126
+ if (!chunksSuccess) {
3127
+ Log.e(TAG, "Failed to send BMP chunks")
3128
+ if (callback != null)
3129
+ callback.onError("both", "Failed to send chunks")
3130
+ return
3131
+ }
3132
+
3133
+ // Send end command - this needs confirmation
3134
+ val endSuccess = sendBmpEndCommandOptimized()
3135
+ if (!endSuccess) {
3136
+ Log.e(TAG, "Failed to send BMP end command")
3137
+ if (callback != null)
3138
+ callback.onError("both", "Failed to send end command")
3139
+ return
3140
+ }
3141
+
3142
+ // Send CRC - this needs confirmation
3143
+ val crcSuccess = sendBmpCrcOptimized(bmpData)
3144
+ if (!crcSuccess) {
3145
+ Log.e(TAG, "Failed to send BMP CRC")
3146
+ if (callback != null)
3147
+ callback.onError("both", "CRC check failed")
3148
+ return
3149
+ }
3150
+
3151
+ lastThingDisplayedWasAnImage = true
3152
+
3153
+ val totalTime = System.currentTimeMillis() - startTime
3154
+ Bridge.log("G1: BMP display completed in " + totalTime + "ms")
3155
+
3156
+ if (callback != null) {
3157
+ callback.onSuccess("both")
3158
+ }
3159
+
3160
+ } catch (e: Exception) {
3161
+ Log.e(TAG, "Error in displayBitmapImageOptimized: " + e.message)
3162
+ if (callback != null)
3163
+ callback.onError("both", "Exception: " + e.message)
3164
+ } finally {
3165
+ isSendingBitmap = false
3166
+ }
3167
+ }
3168
+
3169
+ // Legacy method for fallback
3170
+ private fun displayBitmapImageLegacy(bmpData: ByteArray?, callback: BmpProgressCallback?) {
3171
+ Bridge.log("G1: Starting LEGACY BMP display process")
3172
+
3173
+ try {
3174
+ if (bmpData == null || bmpData.isEmpty()) {
3175
+ Log.e(TAG, "Invalid BMP data provided")
3176
+ if (callback != null)
3177
+ callback.onError("both", "Invalid BMP data")
3178
+ return
3179
+ }
3180
+ Bridge.log("G1: Processing BMP data, size: " + bmpData.size + " bytes")
3181
+ // Split into chunks and send
3182
+ val chunks = createBmpChunks(bmpData)
3183
+ Bridge.log("G1: Created " + chunks.size + " chunks")
3184
+
3185
+ // Send all chunks
3186
+ sendBmpChunks(chunks)
3187
+ // Send all chunks with progress
3188
+ val chunksSuccess = sendBmpChunksWithProgress(chunks, callback)
3189
+ if (!chunksSuccess) {
3190
+ Log.e(TAG, "Failed to send BMP chunks")
3191
+ if (callback != null)
3192
+ callback.onError("both", "Failed to send chunks")
3193
+ return
3194
+ }
3195
+
3196
+ // Send end command with retry
3197
+ val endSuccess = sendBmpEndCommandWithRetry()
3198
+ if (!endSuccess) {
3199
+ Log.e(TAG, "Failed to send BMP end command")
3200
+ if (callback != null)
3201
+ callback.onError("both", "Failed to send end command")
3202
+ return
3203
+ }
3204
+
3205
+ // Calculate and send CRC with proper algorithm
3206
+ val crcSuccess = sendBmpCrcWithRetry(bmpData)
3207
+ if (!crcSuccess) {
3208
+ Log.e(TAG, "Failed to send BMP CRC")
3209
+ if (callback != null)
3210
+ callback.onError("both", "CRC check failed")
3211
+ return
3212
+ }
3213
+
3214
+ lastThingDisplayedWasAnImage = true
3215
+
3216
+ if (callback != null) {
3217
+ callback.onSuccess("both")
3218
+ }
3219
+
3220
+ Bridge.log("G1: BMP display process completed successfully")
3221
+
3222
+ } catch (e: Exception) {
3223
+ Log.e(TAG, "Error in displayBitmapImage: " + e.message)
3224
+ if (callback != null)
3225
+ callback.onError("both", "Exception: " + e.message)
3226
+ }
3227
+ }
3228
+
3229
+ private fun createBmpChunks(bmpData: ByteArray): List<ByteArray> {
3230
+ val chunks = ArrayList<ByteArray>()
3231
+ val totalChunks = Math.ceil(bmpData.size.toDouble() / BMP_CHUNK_SIZE).toInt()
3232
+ Bridge.log("G1: Creating " + totalChunks + " chunks from " + bmpData.size + " bytes")
3233
+
3234
+ for (i in 0 until totalChunks) {
3235
+ val start = i * BMP_CHUNK_SIZE
3236
+ val end = Math.min(start + BMP_CHUNK_SIZE, bmpData.size)
3237
+ val chunk = Arrays.copyOfRange(bmpData, start, end)
3238
+
3239
+ // First chunk needs address bytes
3240
+ if (i == 0) {
3241
+ val headerWithAddress = ByteArray(2 + GLASSES_ADDRESS.size + chunk.size)
3242
+ headerWithAddress[0] = 0x15 // Command
3243
+ headerWithAddress[1] = (i and 0xFF).toByte() // Sequence
3244
+ System.arraycopy(GLASSES_ADDRESS, 0, headerWithAddress, 2, GLASSES_ADDRESS.size)
3245
+ System.arraycopy(chunk, 0, headerWithAddress, 6, chunk.size)
3246
+ chunks.add(headerWithAddress)
3247
+ } else {
3248
+ val header = ByteArray(2 + chunk.size)
3249
+ header[0] = 0x15 // Command
3250
+ header[1] = (i and 0xFF).toByte() // Sequence
3251
+ System.arraycopy(chunk, 0, header, 2, chunk.size)
3252
+ chunks.add(header)
3253
+ }
3254
+ }
3255
+ return chunks
3256
+ }
3257
+
3258
+ private fun sendBmpChunksWithProgress(chunks: List<ByteArray>, callback: BmpProgressCallback?): Boolean {
3259
+ if (updatingScreen)
3260
+ return false
3261
+
3262
+ for (i in 0 until chunks.size) {
3263
+ val chunk = chunks[i]
3264
+ Bridge.log("G1: Sending chunk " + i + " of " + chunks.size + ", size: " + chunk.size)
3265
+
3266
+ val success = sendDataSequentiallyWithTimeout(chunk, CHUNK_SEND_TIMEOUT_MS)
3267
+ if (!success) {
3268
+ Log.e(TAG, "Failed to send chunk " + i)
3269
+ return false
3270
+ }
3271
+
3272
+ // Report progress
3273
+ if (callback != null) {
3274
+ val offset = i * BMP_CHUNK_SIZE
3275
+ callback.onProgress("both", offset, i, chunks.size * BMP_CHUNK_SIZE)
3276
+ }
3277
+
3278
+ // Platform-specific delay between chunks (matching Flutter implementation)
3279
+ try {
3280
+ Thread.sleep(ANDROID_CHUNK_DELAY_MS)
3281
+ } catch (e: InterruptedException) {
3282
+ Log.e(TAG, "Sleep interrupted: " + e.message)
3283
+ return false
3284
+ }
3285
+ }
3286
+ return true
3287
+ }
3288
+
3289
+ // Optimized chunk sending - mimics iOS approach
3290
+ private fun sendBmpChunksOptimized(chunks: List<ByteArray>, callback: BmpProgressCallback?): Boolean {
3291
+ if (updatingScreen)
3292
+ return false
3293
+
3294
+ Bridge.log("G1: Sending " + chunks.size + " chunks using OPTIMIZED method")
3295
+
3296
+ // Send all chunks except last without waiting for response
3297
+ for (i in 0 until chunks.size - 1) {
3298
+ val chunk = chunks[i]
3299
+ val chunkIndex = i
3300
+
3301
+ if (USE_PARALLEL_BITMAP_WRITES && bitmapExecutor != null) {
3302
+ // Send to both glasses in parallel using executor
3303
+ val latch = CountDownLatch(2)
3304
+
3305
+ // Send to left glass
3306
+ bitmapExecutor!!.execute {
3307
+ try {
3308
+ if (leftGlassGatt != null && leftTxChar != null && isLeftConnected) {
3309
+ synchronized(leftTxChar!!) {
3310
+ leftTxChar!!.value = chunk
3311
+ leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
3312
+ leftGlassGatt!!.writeCharacteristic(leftTxChar)
3313
+ leftTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
3314
+ }
3315
+ }
3316
+ } finally {
3317
+ latch.countDown()
3318
+ }
3319
+ }
3320
+
3321
+ // Send to right glass
3322
+ bitmapExecutor!!.execute {
3323
+ try {
3324
+ if (rightGlassGatt != null && rightTxChar != null && isRightConnected) {
3325
+ synchronized(rightTxChar!!) {
3326
+ rightTxChar!!.value = chunk
3327
+ rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
3328
+ rightGlassGatt!!.writeCharacteristic(rightTxChar)
3329
+ rightTxChar!!.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
3330
+ }
3331
+ }
3332
+ } finally {
3333
+ latch.countDown()
3334
+ }
3335
+ }
3336
+
3337
+ // Wait for both to complete (but don't wait for BLE response)
3338
+ try {
3339
+ latch.await(50, TimeUnit.MILLISECONDS) // Short timeout
3340
+ } catch (e: InterruptedException) {
3341
+ // Continue anyway
3342
+ }
3343
+ } else {
3344
+ // Fall back to sequential but still no wait
3345
+ sendBitmapChunkNoWait(chunk, true, true)
3346
+ }
3347
+
3348
+ // Report progress
3349
+ if (callback != null) {
3350
+ val offset = chunkIndex * BMP_CHUNK_SIZE
3351
+ callback.onProgress("both", offset, chunkIndex, chunks.size * BMP_CHUNK_SIZE)
3352
+ }
3353
+
3354
+ // Small delay between chunks (iOS uses 8ms)
3355
+ try {
3356
+ Thread.sleep(ANDROID_CHUNK_DELAY_MS)
3357
+ } catch (e: InterruptedException) {
3358
+ Log.e(TAG, "Sleep interrupted: " + e.message)
3359
+ return false
3360
+ }
3361
+ }
3362
+
3363
+ // Last chunk - wait for confirmation (like iOS)
3364
+ if (!chunks.isEmpty()) {
3365
+ val lastChunk = chunks[chunks.size - 1]
3366
+ Bridge.log("G1: Sending last chunk with confirmation")
3367
+
3368
+ val success = sendDataSequentiallyWithTimeout(lastChunk, 2000) // Shorter timeout for last chunk
3369
+ if (!success) {
3370
+ Log.e(TAG, "Failed to send last chunk")
3371
+ return false
3372
+ }
3373
+
3374
+ if (callback != null) {
3375
+ val offset = (chunks.size - 1) * BMP_CHUNK_SIZE
3376
+ callback.onProgress("both", offset, chunks.size - 1, chunks.size * BMP_CHUNK_SIZE)
3377
+ }
3378
+ }
3379
+
3380
+ return true
3381
+ }
3382
+
3383
+ // Optimized end command sending
3384
+ private fun sendBmpEndCommandOptimized(): Boolean {
3385
+ Bridge.log("G1: Sending BMP end command (optimized)")
3386
+
3387
+ // End command needs confirmation, but with shorter timeout
3388
+ val success = sendDataSequentiallyWithTimeout(END_COMMAND, 1000)
3389
+ if (success) {
3390
+ Bridge.log("G1: BMP end command sent successfully")
3391
+
3392
+ // Small delay after end command
3393
+ try {
3394
+ Thread.sleep(10)
3395
+ } catch (e: InterruptedException) {
3396
+ // Ignore
3397
+ }
3398
+ }
3399
+ return success
3400
+ }
3401
+
3402
+ // Optimized CRC sending
3403
+ private fun sendBmpCrcOptimized(bmpData: ByteArray): Boolean {
3404
+ // Create data with address for CRC calculation
3405
+ val dataWithAddress = ByteArray(GLASSES_ADDRESS.size + bmpData.size)
3406
+ System.arraycopy(GLASSES_ADDRESS, 0, dataWithAddress, 0, GLASSES_ADDRESS.size)
3407
+ System.arraycopy(bmpData, 0, dataWithAddress, GLASSES_ADDRESS.size, bmpData.size)
3408
+
3409
+ // Calculate CRC32
3410
+ val crc = CRC32()
3411
+ crc.update(dataWithAddress)
3412
+ val crcValue = crc.value
3413
+
3414
+ // Create CRC command packet
3415
+ val crcCommand = ByteArray(5)
3416
+ crcCommand[0] = 0x16 // CRC command
3417
+ crcCommand[1] = ((crcValue shr 24) and 0xFF).toByte()
3418
+ crcCommand[2] = ((crcValue shr 16) and 0xFF).toByte()
3419
+ crcCommand[3] = ((crcValue shr 8) and 0xFF).toByte()
3420
+ crcCommand[4] = (crcValue and 0xFF).toByte()
3421
+
3422
+ Bridge.log("G1: Sending CRC command (optimized), CRC value: " + java.lang.Long.toHexString(crcValue))
3423
+
3424
+ // CRC needs confirmation, but with shorter timeout
3425
+ val success = sendDataSequentiallyWithTimeout(crcCommand, 1000)
3426
+ if (success) {
3427
+ Bridge.log("G1: CRC command sent successfully")
3428
+ }
3429
+ return success
3430
+ }
3431
+
3432
+ private fun sendDataSequentiallyWithTimeout(data: ByteArray, timeoutMs: Long): Boolean {
3433
+ // Create a future to track the send operation
3434
+ val success = booleanArrayOf(false)
3435
+ val latch = CountDownLatch(1)
3436
+
3437
+ // Send the data asynchronously
3438
+ Thread {
3439
+ try {
3440
+ sendDataSequentially(data)
3441
+ success[0] = true
3442
+ } catch (e: Exception) {
3443
+ Log.e(TAG, "Error sending data: " + e.message)
3444
+ success[0] = false
3445
+ } finally {
3446
+ latch.countDown()
3447
+ }
3448
+ }.start()
3449
+
3450
+ // Wait for completion or timeout
3451
+ try {
3452
+ val completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS)
3453
+ return completed && success[0]
3454
+ } catch (e: InterruptedException) {
3455
+ Log.e(TAG, "Timeout waiting for data send")
3456
+ return false
3457
+ }
3458
+ }
3459
+
3460
+ private fun sendBmpEndCommandWithRetry(): Boolean {
3461
+ if (updatingScreen)
3462
+ return false
3463
+
3464
+ for (attempt in 0 until MAX_BMP_RETRY_ATTEMPTS) {
3465
+ Bridge.log("G1: Sending BMP end command, attempt " + (attempt + 1))
3466
+
3467
+ val success = sendDataSequentiallyWithTimeout(END_COMMAND, END_COMMAND_TIMEOUT_MS)
3468
+ if (success) {
3469
+ Bridge.log("G1: BMP end command sent successfully")
3470
+ return true
3471
+ }
3472
+
3473
+ Log.w(TAG, "BMP end command failed, attempt " + (attempt + 1))
3474
+
3475
+ // Wait before retry
3476
+ try {
3477
+ Thread.sleep(BMP_RETRY_DELAY_MS)
3478
+ } catch (e: InterruptedException) {
3479
+ Log.e(TAG, "Sleep interrupted during retry")
3480
+ return false
3481
+ }
3482
+ }
3483
+
3484
+ Log.e(TAG, "Failed to send BMP end command after " + MAX_BMP_RETRY_ATTEMPTS + " attempts")
3485
+ return false
3486
+ }
3487
+
3488
+ private fun sendBmpCrcWithRetry(bmpData: ByteArray): Boolean {
3489
+ // Create data with address for CRC calculation
3490
+ val dataWithAddress = ByteArray(GLASSES_ADDRESS.size + bmpData.size)
3491
+ System.arraycopy(GLASSES_ADDRESS, 0, dataWithAddress, 0, GLASSES_ADDRESS.size)
3492
+ System.arraycopy(bmpData, 0, dataWithAddress, GLASSES_ADDRESS.size, bmpData.size)
3493
+
3494
+ // Calculate CRC32-XZ (using standard CRC32 for now, but should be Crc32Xz)
3495
+ val crc = CRC32()
3496
+ crc.update(dataWithAddress)
3497
+ val crcValue = crc.value
3498
+
3499
+ // Create CRC command packet
3500
+ val crcCommand = ByteArray(5)
3501
+ crcCommand[0] = 0x16 // CRC command
3502
+ crcCommand[1] = ((crcValue shr 24) and 0xFF).toByte()
3503
+ crcCommand[2] = ((crcValue shr 16) and 0xFF).toByte()
3504
+ crcCommand[3] = ((crcValue shr 8) and 0xFF).toByte()
3505
+ crcCommand[4] = (crcValue and 0xFF).toByte()
3506
+
3507
+ Bridge.log("G1: Sending CRC command, CRC value: " + java.lang.Long.toHexString(crcValue))
3508
+
3509
+ // Send CRC with retry
3510
+ for (attempt in 0 until MAX_BMP_RETRY_ATTEMPTS) {
3511
+ val success = sendDataSequentiallyWithTimeout(crcCommand, CRC_COMMAND_TIMEOUT_MS)
3512
+ if (success) {
3513
+ Bridge.log("G1: CRC command sent successfully")
3514
+ return true
3515
+ }
3516
+
3517
+ Log.w(TAG, "CRC command failed, attempt " + (attempt + 1))
3518
+
3519
+ try {
3520
+ Thread.sleep(BMP_RETRY_DELAY_MS)
3521
+ } catch (e: InterruptedException) {
3522
+ Log.e(TAG, "Sleep interrupted during CRC retry")
3523
+ return false
3524
+ }
3525
+ }
3526
+
3527
+ Log.e(TAG, "Failed to send CRC command after " + MAX_BMP_RETRY_ATTEMPTS + " attempts")
3528
+ return false
3529
+ }
3530
+
3531
+ // Legacy methods for backward compatibility
3532
+ private fun sendBmpChunks(chunks: List<ByteArray>) {
3533
+ sendBmpChunksWithProgress(chunks, null)
3534
+ }
3535
+
3536
+ private fun sendBmpEndCommand() {
3537
+ sendBmpEndCommandWithRetry()
3538
+ }
3539
+
3540
+ private fun sendBmpCRC(bmpData: ByteArray) {
3541
+ sendBmpCrcWithRetry(bmpData)
3542
+ }
3543
+
3544
+ private fun sendBmpToSide(bmpData: ByteArray, side: String) {
3545
+ // For now, send to both sides but could be modified for side-specific sending
3546
+ displayBitmapImage(bmpData, object : BmpProgressCallback {
3547
+ override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
3548
+ Bridge.log("G1: BMP progress for " + side + ": " + offset + "/" + totalSize)
3549
+ }
3550
+
3551
+ override fun onSuccess(side: String) {
3552
+ Bridge.log("G1: BMP sent successfully to " + side)
3553
+ }
3554
+
3555
+ override fun onError(side: String, error: String) {
3556
+ Log.e(TAG, "BMP error for " + side + ": " + error)
3557
+ }
3558
+ })
3559
+ }
3560
+
3561
+ private fun loadBmpFromFile(filePath: String): ByteArray? {
3562
+ try {
3563
+ val file = java.io.File(filePath)
3564
+ if (!file.exists()) {
3565
+ Log.e(TAG, "BMP file does not exist: " + filePath)
3566
+ return null
3567
+ }
3568
+
3569
+ // Read file into byte array
3570
+ val fileData = ByteArray(file.length().toInt())
3571
+ java.io.FileInputStream(file).use { fis ->
3572
+ fis.read(fileData)
3573
+ }
3574
+
3575
+ // Convert to 1-bit BMP format if needed
3576
+ return convertTo1BitBmp(fileData)
3577
+
3578
+ } catch (e: Exception) {
3579
+ Log.e(TAG, "Error loading BMP file: " + e.message)
3580
+ return null
3581
+ }
3582
+ }
3583
+
3584
+ private fun convertTo1BitBmp(originalBmp: ByteArray): ByteArray {
3585
+ // This is a placeholder - in a real implementation, you'd convert the BMP to
3586
+ // 1-bit format
3587
+ // For now, we'll assume the input is already in the correct format
3588
+ return originalBmp
3589
+ }
3590
+
3591
+ private fun loadEmptyBmpFromAssets(): ByteArray? {
3592
+ try {
3593
+ context!!.assets.open("empty_bmp.bmp").use { iss ->
3594
+ return iss.readAllBytes()
3595
+ }
3596
+ } catch (e: IOException) {
3597
+ Log.e(TAG, "Failed to load BMP from assets: " + e.message)
3598
+ return null
3599
+ }
3600
+ }
3601
+
3602
+ fun clearBmpDisplay() {
3603
+ if (updatingScreen)
3604
+ return
3605
+ Bridge.log("G1: Clearing BMP display with EXIT command")
3606
+ val exitCommand = byteArrayOf(0x18)
3607
+ sendDataSequentially(exitCommand)
3608
+ }
3609
+
3610
+ fun exitBmp() {
3611
+ clearBmpDisplay()
3612
+ }
3613
+
3614
+ // Enhanced error handling and validation
3615
+ fun validateBmpFormat(bmpData: ByteArray?): Boolean {
3616
+ if (bmpData == null || bmpData.size < 54) { // Minimum BMP header size
3617
+ Log.e(TAG, "Invalid BMP data: null or too short")
3618
+ return false
3619
+ }
3620
+
3621
+ // Check BMP signature
3622
+ if (bmpData[0] != 'B'.code.toByte() || bmpData[1] != 'M'.code.toByte()) {
3623
+ Log.e(TAG, "Invalid BMP signature")
3624
+ return false
3625
+ }
3626
+
3627
+ // Check if it's 1-bit format (simplified check)
3628
+ val bitsPerPixel = bmpData[28].toInt() and 0xFF
3629
+ if (bitsPerPixel != 1) {
3630
+ Log.w(TAG, "BMP is not 1-bit format (bits per pixel: " + bitsPerPixel + ")")
3631
+ }
3632
+
3633
+ return true
3634
+ }
3635
+
3636
+ // Batch processing with progress
3637
+ fun sendBmpBatch(bmpPaths: List<String>, callback: BmpProgressCallback?) {
3638
+ for (i in 0 until bmpPaths.size) {
3639
+ val path = bmpPaths[i]
3640
+
3641
+ if (callback != null) {
3642
+ callback.onProgress("batch", i, i, bmpPaths.size)
3643
+ }
3644
+
3645
+ try {
3646
+ val bmpData = loadBmpFromFile(path)
3647
+ if (bmpData != null && validateBmpFormat(bmpData)) {
3648
+ displayBitmapImage(bmpData, object : BmpProgressCallback {
3649
+ override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
3650
+ // Individual image progress
3651
+ }
3652
+
3653
+ override fun onSuccess(side: String) {
3654
+ Bridge.log("G1: Successfully sent BMP: " + path)
3655
+ }
3656
+
3657
+ override fun onError(side: String, error: String) {
3658
+ Log.e(TAG, "Failed to send BMP " + path + ": " + error)
3659
+ if (callback != null) {
3660
+ callback.onError("batch", "Failed to send " + path + ": " + error)
3661
+ }
3662
+ }
3663
+ })
3664
+
3665
+ // Delay between images
3666
+ Thread.sleep(2000)
3667
+ } else {
3668
+ Log.e(TAG, "Invalid BMP format: " + path)
3669
+ if (callback != null) {
3670
+ callback.onError("batch", "Invalid BMP format: " + path)
3671
+ }
3672
+ }
3673
+ } catch (e: Exception) {
3674
+ Log.e(TAG, "Error processing BMP " + path + ": " + e.message)
3675
+ if (callback != null) {
3676
+ callback.onError("batch", "Error processing " + path + ": " + e.message)
3677
+ }
3678
+ }
3679
+ }
3680
+
3681
+ if (callback != null) {
3682
+ callback.onSuccess("batch")
3683
+ }
3684
+ }
3685
+
3686
+ private fun sendLoremIpsum() {
3687
+ if (updatingScreen)
3688
+ return
3689
+ val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
3690
+ sendDataSequentially(createTextWallChunks(text))
3691
+ }
3692
+
3693
+ // Enhanced clearing with state management
3694
+ fun clearBmpDisplayWithState() {
3695
+ if (updatingScreen) {
3696
+ Bridge.log("G1: Screen update in progress, queuing clear command")
3697
+ // Queue the clear command for later
3698
+ handler.postDelayed(this::clearBmpDisplay, 1000L)
3699
+ return
3700
+ }
3701
+
3702
+ Bridge.log("G1: Clearing BMP display with EXIT command")
3703
+ updatingScreen = true
3704
+
3705
+ try {
3706
+ val exitCommand = byteArrayOf(0x18)
3707
+ val success = sendDataSequentiallyWithTimeout(exitCommand, 2000)
3708
+ if (success) {
3709
+ lastThingDisplayedWasAnImage = false
3710
+ Bridge.log("G1: BMP display cleared successfully")
3711
+ } else {
3712
+ Log.e(TAG, "Failed to clear BMP display")
3713
+ }
3714
+ } finally {
3715
+ updatingScreen = false
3716
+ }
3717
+ }
3718
+
3719
+ // Optimized batch clearing
3720
+ fun clearAndDisplayBmp(bmpData: ByteArray?, callback: BmpProgressCallback?) {
3721
+ if (updatingScreen) {
3722
+ Bridge.log("G1: Screen update in progress, cannot clear and display")
3723
+ if (callback != null)
3724
+ callback.onError("both", "Screen update in progress")
3725
+ return
3726
+ }
3727
+
3728
+ // Clear first, then display
3729
+ clearBmpDisplayWithState()
3730
+
3731
+ // Small delay to ensure clear completes
3732
+ handler.postDelayed({
3733
+ displayBitmapImage(bmpData, callback)
3734
+ }, 500L)
3735
+ }
3736
+
3737
+ // Efficient multiple image display with clearing
3738
+ fun displayBmpSequence(bmpDataList: List<ByteArray>?, callback: BmpProgressCallback?) {
3739
+ if (bmpDataList == null || bmpDataList.isEmpty()) {
3740
+ Log.e(TAG, "No BMP data provided for sequence")
3741
+ if (callback != null)
3742
+ callback.onError("sequence", "No BMP data provided")
3743
+ return
3744
+ }
3745
+
3746
+ displayBmpSequenceInternal(bmpDataList, 0, callback)
3747
+ }
3748
+
3749
+ private fun displayBmpSequenceInternal(bmpDataList: List<ByteArray>, index: Int, callback: BmpProgressCallback?) {
3750
+ if (index >= bmpDataList.size) {
3751
+ Bridge.log("G1: BMP sequence completed")
3752
+ if (callback != null)
3753
+ callback.onSuccess("sequence")
3754
+ return
3755
+ }
3756
+
3757
+ val bmpData = bmpDataList[index]
3758
+ displayBitmapImage(bmpData, object : BmpProgressCallback {
3759
+ override fun onProgress(side: String, offset: Int, chunkIndex: Int, totalSize: Int) {
3760
+ if (callback != null) {
3761
+ // Calculate overall progress including sequence position
3762
+ val overallProgress = (index * 100) / bmpDataList.size
3763
+ callback.onProgress("sequence", overallProgress, index, bmpDataList.size)
3764
+ }
3765
+ }
3766
+
3767
+ override fun onSuccess(side: String) {
3768
+ Bridge.log("G1: BMP " + (index + 1) + " of " + bmpDataList.size + " displayed successfully")
3769
+
3770
+ // Schedule next image with delay
3771
+ handler.postDelayed({
3772
+ displayBmpSequenceInternal(bmpDataList, index + 1, callback)
3773
+ }, 2000L) // 2 second delay between images
3774
+ }
3775
+
3776
+ override fun onError(side: String, error: String) {
3777
+ Log.e(TAG, "Failed to display BMP " + (index + 1) + ": " + error)
3778
+ if (callback != null) {
3779
+ callback.onError("sequence", "Failed to display BMP " + (index + 1) + ": " + error)
3780
+ }
3781
+ }
3782
+ })
3783
+ }
3784
+
3785
+ private fun quickRestartG1() {
3786
+ Bridge.log("G1: Sending restart 0x23 0x72 Command")
3787
+ sendDataSequentially(byteArrayOf(0x23.toByte(), 0x72.toByte())) // quick restart comand
3788
+ }
3789
+
3790
+ override fun setMicEnabled(enabled: Boolean) {
3791
+ Bridge.log("G1: setMicEnabled(): " + enabled)
3792
+
3793
+ // Update the shouldUseGlassesMic flag to reflect the current state
3794
+ this.shouldUseGlassesMic = enabled
3795
+ Bridge.log("G1: Updated shouldUseGlassesMic to: " + shouldUseGlassesMic)
3796
+
3797
+ if (enabled) {
3798
+ Bridge.log("G1: Microphone enabled, starting audio input handling")
3799
+ sendSetMicEnabled(true, 10)
3800
+ startMicBeat(MICBEAT_INTERVAL_MS.toInt())
3801
+ } else {
3802
+ Bridge.log("G1: Microphone disabled, stopping audio input handling")
3803
+ sendSetMicEnabled(false, 10)
3804
+ stopMicBeat()
3805
+ }
3806
+ }
3807
+
3808
+ override fun sortMicRanking(list: MutableList<String>): MutableList<String> {
3809
+ return list
3810
+ }
3811
+
3812
+ /**
3813
+ * Emits serial number information to React Native (matching iOS implementation)
3814
+ */
3815
+ private fun emitSerialNumberInfo(serialNumber: String, style: String, color: String) {
3816
+ try {
3817
+ val eventBody = JSONObject()
3818
+ eventBody.put("serialNumber", serialNumber)
3819
+ eventBody.put("style", style)
3820
+ eventBody.put("color", color)
3821
+
3822
+ // Bridge.sendTypedMessage("glasses_serial_number", eventBody);
3823
+ Bridge.log("G1: 📱 Emitted serial number info: " + serialNumber + ", Style: " + style + ", Color: " + color)
3824
+ DeviceStore.apply("glasses", "serialNumber", serialNumber)
3825
+ DeviceStore.apply("glasses", "style", style)
3826
+ DeviceStore.apply("glasses", "color", color)
3827
+
3828
+ } catch (e: Exception) {
3829
+ Bridge.log("G1: Error emitting serial number info: " + e.message)
3830
+ }
3831
+ }
3832
+
3833
+ /**
3834
+ * Decodes serial number from manufacturer data bytes
3835
+ *
3836
+ * @param manufacturerData The manufacturer data bytes
3837
+ * @return Decoded serial number string or null if not found
3838
+ */
3839
+ private fun decodeSerialFromManufacturerData(manufacturerData: ByteArray?): String? {
3840
+ if (manufacturerData == null || manufacturerData.size < 10) {
3841
+ return null
3842
+ }
3843
+
3844
+ try {
3845
+ // Convert hex bytes to ASCII string
3846
+ val serialBuilder = StringBuilder()
3847
+ for (i in 0 until manufacturerData.size) {
3848
+ val b = manufacturerData[i]
3849
+ if (b.toInt() == 0x00) {
3850
+ // Stop at null terminator
3851
+ break
3852
+ }
3853
+ if (b.toInt() >= 0x20 && b.toInt() <= 0x7E) {
3854
+ // Only include printable ASCII characters
3855
+ serialBuilder.append(b.toInt().toChar())
3856
+ }
3857
+ }
3858
+
3859
+ val decodedString = serialBuilder.toString().trim()
3860
+
3861
+ // Check if it looks like a valid Even G1 serial number
3862
+ if (decodedString.length >= 12 &&
3863
+ (decodedString.startsWith("S1") || decodedString.startsWith("100")
3864
+ || decodedString.startsWith("110"))) {
3865
+ return decodedString
3866
+ }
3867
+
3868
+ return null
3869
+ } catch (e: Exception) {
3870
+ Log.e(TAG, "Error decoding manufacturer data: " + e.message)
3871
+ return null
3872
+ }
3873
+ }
3874
+
3875
+ override fun cleanup() {
3876
+ // TODO:
3877
+ }
3878
+ }