@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,2011 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ import com.glassly.bluetoothsdk.BluetoothSdkDefaults
4
+ import com.glassly.bluetoothsdk.DeviceManager
5
+ import com.glassly.bluetoothsdk.PhotoRequest
6
+ import com.glassly.bluetoothsdk.DeviceStore
7
+
8
+ import android.graphics.Bitmap
9
+ import android.graphics.BitmapFactory
10
+ import android.graphics.Canvas
11
+ import android.graphics.ColorMatrix
12
+ import android.graphics.ColorMatrixColorFilter
13
+ import android.graphics.Paint
14
+ import android.bluetooth.BluetoothAdapter
15
+ import android.bluetooth.BluetoothDevice
16
+ import android.bluetooth.BluetoothGatt
17
+ import android.bluetooth.BluetoothGattCallback
18
+ import android.bluetooth.BluetoothGattDescriptor
19
+ import android.bluetooth.BluetoothGattService
20
+ import android.bluetooth.BluetoothGattCharacteristic
21
+ import android.bluetooth.BluetoothProfile
22
+ import android.bluetooth.le.BluetoothLeScanner
23
+ import android.bluetooth.le.ScanCallback
24
+ import android.bluetooth.le.ScanFilter
25
+ import android.bluetooth.le.ScanResult
26
+ import android.bluetooth.le.ScanSettings
27
+ import android.os.Message
28
+ import android.os.Handler
29
+ import android.os.Looper
30
+ import android.util.Log
31
+ import android.content.Context
32
+
33
+ import glassly.ble.GlasslyBle.GlassesToPhone
34
+ import glassly.ble.GlasslyBle.PhoneToGlasses
35
+ import glassly.ble.GlasslyBle.ChargingState
36
+ import glassly.ble.GlasslyBle.BatteryStatus
37
+ import glassly.ble.GlasslyBle.HeadGesture
38
+ import glassly.ble.GlasslyBle.ButtonEvent
39
+ import glassly.ble.GlasslyBle.ImuData
40
+ import glassly.ble.GlasslyBle.ImageTransferComplete
41
+ import glassly.ble.GlasslyBle.HeadUpAngleResponse
42
+ import glassly.ble.GlasslyBle.HeadPosition
43
+ import glassly.ble.GlasslyBle.DeviceInfo
44
+
45
+ import com.glassly.bluetoothsdk.sgcs.SGCManager
46
+ import com.glassly.bluetoothsdk.Bridge
47
+ import com.glassly.bluetoothsdk.utils.DeviceTypes
48
+ import com.glassly.bluetoothsdk.utils.NexProtobufUtils
49
+ import com.glassly.bluetoothsdk.utils.NexEventUtils
50
+ import com.glassly.bluetoothsdk.utils.NexBluetoothConstants
51
+ import com.glassly.bluetoothsdk.utils.NexDisplayConstants
52
+ import com.glassly.bluetoothsdk.utils.NexBluetoothPacketTypes
53
+ import com.glassly.bluetoothsdk.utils.BitmapJavaUtils
54
+ import com.glassly.bluetoothsdk.utils.G1FontLoaderKt
55
+ import com.glassly.bluetoothsdk.utils.G1Text
56
+ import com.glassly.bluetoothsdk.utils.ConnTypes
57
+ import com.glassly.lc3Lib.Lc3Cpp
58
+ import com.glassly.bluetoothsdk.utils.audio.Lc3Player
59
+
60
+ import java.util.UUID
61
+ import java.util.concurrent.LinkedBlockingQueue
62
+ import java.nio.charset.Charset
63
+ import java.util.Random
64
+
65
+
66
+ import org.json.JSONObject
67
+
68
+ import com.google.protobuf.InvalidProtocolBufferException
69
+
70
+ class MentraNex : SGCManager() {
71
+ companion object {
72
+ private const val TAG = "MentraNex";
73
+
74
+ private const val MTU_517: Int = 517
75
+ private const val MTU_DEFAULT: Int = 23
76
+
77
+ private const val MAX_CHUNK_SIZE_DEFAULT: Int = 176 // Maximum chunk size for BLE packets
78
+ private const val DELAY_BETWEEN_CHUNKS_SEND: Long = 5 // Adjust this value as needed
79
+ // Upper bound on waiting for onCharacteristicWrite. A no-response write's
80
+ // callback is normally near-instant; this only guards against a callback that
81
+ // never arrives, so a dropped write can't wedge the send queue forever.
82
+ private const val WRITE_CALLBACK_TIMEOUT_MS: Long = 10
83
+ // Upper bound for an acked (write-with-response) fragment. The callback only
84
+ // fires after the firmware's ATT Write Response — at least one connection
85
+ // interval — so 10ms is far too short here: timing out early would let the
86
+ // worker issue the next write while this one is still outstanding, which the
87
+ // stack rejects (one GATT op at a time) and the fragment is dropped. This is a
88
+ // safety bound only; the ack normally arrives within an interval or two.
89
+ private const val WRITE_ACK_TIMEOUT_MS: Long = 400
90
+
91
+ private const val INITIAL_CONNECTION_DELAY_MS = 350L // Adjust this value as needed
92
+ // Window to let a queued DisconnectRequest flush to the glasses before we close GATT.
93
+ private const val DISCONNECT_FLUSH_DELAY_MS = 250L
94
+ private const val MICBEAT_INTERVAL_MS: Long = (1000 * 60) * 30; // micbeat every 30 minutes
95
+
96
+ private const val MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED: Int = 110
97
+ private const val MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES: Int = 120
98
+ private const val MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED: Int = 210
99
+
100
+ // actions of device or gatt
101
+ private const val MAIN_TASK_HANDLER_CODE_CONNECT_DEVICE: Int = 310
102
+ private const val MAIN_TASK_HANDLER_CODE_DISCONNECT_DEVICE: Int = 320
103
+ private const val MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE: Int = 350
104
+ private const val MAIN_TASK_HANDLER_CODE_CANCEL_RECONNECT_DEVICE: Int = 360
105
+ private const val MAIN_TASK_HANDLER_CODE_RECONNECT_GATT: Int = 370
106
+ private const val MAIN_TASK_HANDLER_CODE_SCAN_START: Int = 410
107
+ private const val MAIN_TASK_HANDLER_CODE_SCAN_END: Int = 420
108
+
109
+ // actions of NEX Glasses
110
+ private const val MAIN_TASK_HANDLER_CODE_BATTERY_QUERY: Int = 620
111
+ private const val MAIN_TASK_HANDLER_CODE_HEART_BEAT: Int = 630
112
+
113
+ // Fixed rect for the live-captions box (G2-style): a single full-canvas text container
114
+ // reused across caption updates, so each caption is an in-place text change that coexists
115
+ // with any bitmap/other components. Matches the 500x220 Glassly display canvas.
116
+ private const val CAPTION_BOX_X: Int = 0
117
+ private const val CAPTION_BOX_Y: Int = 0
118
+ private const val CAPTION_BOX_W: Int = 500
119
+ private const val CAPTION_BOX_H: Int = 220
120
+ }
121
+
122
+ private var heartbeatCount: Int = 0;
123
+ private var micBeatCount: Int = 0;
124
+
125
+ private var context: Context? = null
126
+ // private var isDebug: Boolean = true
127
+
128
+ // Off by default; toggled from Nex Developer Settings via the nex_lc3_audio_playback flag.
129
+ private val isLc3AudioEnabled: Boolean
130
+ get() = DeviceStore.get("bluetooth", "nex_lc3_audio_playback") as? Boolean ?: false
131
+ private var lc3AudioPlayer: Lc3Player? = null
132
+
133
+ private var lc3DecoderPtr: Long = 0
134
+
135
+ @Volatile private var isImageSendProgressing: Boolean = false
136
+
137
+ private var mainDevice: BluetoothDevice? = null
138
+ private var bluetoothAdapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
139
+
140
+ private var isScanningForCompatibleDevices: Boolean = false
141
+
142
+ private var isMainConnected = false
143
+ private var isKilled: Boolean = false
144
+ private var lastConnectionTimestamp: Long = 0
145
+ private var lastSendTimestamp: Long = 0
146
+ private var mainReconnectAttempts: Int = 0
147
+
148
+ private var lastReceivedLc3Sequence: Int = -1
149
+
150
+ private val fontLoader: G1FontLoaderKt = G1FontLoaderKt()
151
+ private val textRenderer: G1Text = G1Text()
152
+ private var preferredMainDeviceId: String? = null
153
+ private var savedNexMainName: String? = null
154
+ private var savedNexMainAddress: String? = null
155
+
156
+ private var mainGlassGatt: BluetoothGatt? = null
157
+ private var mainWriteChar: BluetoothGattCharacteristic? = null
158
+ private var mainNotifyChar: BluetoothGattCharacteristic? = null
159
+ private var currentMTU: Int = 0
160
+ private var deviceMaxMTU: Int = 0
161
+
162
+ private var maxChunkSize: Int = MAX_CHUNK_SIZE_DEFAULT // Maximum chunk size for BLE packets
163
+ private var bmpChunkSize: Int = 194 // BMP chunk size
164
+ private var protobufSeq: Int = 0 // Rolling sequence for fragmented control messages
165
+
166
+ @Volatile private var isWorkerRunning = false
167
+ // Queue to hold pending requests
168
+ private val sendQueue = LinkedBlockingQueue<Array<SendRequest>>()
169
+ private val mainWaiter = BooleanWaiter()
170
+ private val mainServicesWaiter = BooleanWaiter()
171
+
172
+ private var shouldUseGlassesMic: Boolean = false
173
+ private var microphoneStateBeforeDisconnection: Boolean = false
174
+
175
+ private var whiteListedAlready: Boolean = false
176
+ private var isMicrophoneEnabled: Boolean = true
177
+ private var lastThingDisplayedWasAnImage: Boolean = false
178
+ private var isScanning: Boolean = false
179
+
180
+ private var protobufVersionPosted: Boolean = false
181
+
182
+ private var bleScanCallback: ScanCallback? = null
183
+
184
+ private val modernScanCallback = object : ScanCallback() {
185
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
186
+ val device = result.getDevice()
187
+ val name = device.getName()
188
+
189
+ // Now you can reference the bluetoothAdapter field if needed:
190
+ if (!bluetoothAdapter.isEnabled) {
191
+ Log.e(TAG, "Bluetooth is disabled")
192
+ return
193
+ }
194
+ Bridge.log("=== New Device is Found ===$name ${device.address}")
195
+ Bridge.log("=== New Glasses Device Information ===")
196
+
197
+ // Log all available device information for debugging
198
+ Bridge.log("=== Device Information ===")
199
+ Bridge.log("Device Name: $name")
200
+ Bridge.log("Device Address: ${device.address}")
201
+ Bridge.log("Device Type: ${device.type}")
202
+ Bridge.log("Device Class: ${device.bluetoothClass}")
203
+ Bridge.log("Bond State: ${device.bondState}")
204
+
205
+ if (name == null || (!name.contains("Nex1-") && !name.contains("MENTRA_DISPLAY_"))) {
206
+ return
207
+ }
208
+
209
+ // If we already have saved device names for main...
210
+ if (name != null && preferredMainDeviceId != null) {
211
+ if (!name.contains(preferredMainDeviceId!!)) {
212
+ return // Not a matching device
213
+ }
214
+ }
215
+
216
+ // Identify which side (main)
217
+ stopScan()
218
+ mainDevice = device
219
+ mainTaskHandler.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0) // 1 second delay
220
+ }
221
+
222
+ override fun onScanFailed(errorCode: Int) {
223
+ Log.e(TAG,"Scan failed with error: $errorCode")
224
+ }
225
+ }
226
+
227
+ init {
228
+ context = Bridge.getContext()
229
+ // isDebug = isDebug(context)
230
+ type = DeviceTypes.NEX
231
+ hasMic = true
232
+ DeviceStore.apply("glasses", "micEnabled", false)
233
+ preferredMainDeviceId = DeviceManager.getInstance().deviceName
234
+
235
+ // Initialize LC3 audio player
236
+ lc3AudioPlayer = Lc3Player(context)
237
+ lc3AudioPlayer?.init()
238
+ if (isLc3AudioEnabled) {
239
+ lc3AudioPlayer?.startPlay()
240
+ }
241
+ }
242
+
243
+ /** Start/stop the LC3 player when the nex_lc3_audio_playback flag changes. */
244
+ override fun applyNexAudioPlaybackSetting() {
245
+ if (isLc3AudioEnabled) {
246
+ Bridge.log("Nex: LC3 audio playback enabled - starting player")
247
+ lc3AudioPlayer?.startPlay()
248
+ } else {
249
+ Bridge.log("Nex: LC3 audio playback disabled - stopping player")
250
+ lc3AudioPlayer?.stopPlay()
251
+ }
252
+ }
253
+
254
+ private val random: Random = Random()
255
+
256
+ private val mainGattCallback: BluetoothGattCallback = createGattCallback()
257
+
258
+ private var mainTaskHandler: Handler = Handler(Looper.getMainLooper(), Handler.Callback(::handleMainTaskMessage))
259
+ private var whiteListHandler: Handler = Handler()
260
+ private var micEnableHandler: Handler = Handler()
261
+ private var micBeatHandler: Handler = Handler()
262
+ private var micBeatRunnable: Runnable? = null
263
+ private var notificationHandler: Handler = Handler()
264
+ private var notificationRunnable: Runnable? = null
265
+ private var textWallHandler: Handler = Handler()
266
+ private val textWallRunnable: Runnable? = null
267
+ private var findCompatibleDevicesHandler: Handler? = null
268
+
269
+ private var lastHeartbeatSentTime: Long = 0
270
+ private var lastHeartbeatReceivedTime: Long = 0
271
+
272
+ private var currentImageChunks: MutableList<ByteArray> = mutableListOf()
273
+
274
+ // Data class to represent a send request
275
+ // ack=true issues the fragment as a write-WITH-response (WRITE_TYPE_DEFAULT): the
276
+ // firmware ATT-acks each write, so the worker knows it landed. ack=false is a
277
+ // no-response write (WRITE_TYPE_NO_RESPONSE) — fire-and-forget, used for captions
278
+ // where the newest frame supersedes the last and throughput matters more than
279
+ // per-packet delivery. See processQueue() for how the write type is applied.
280
+ private data class SendRequest( val data: ByteArray, var waitTime: Int = -1, val ack: Boolean = true) {
281
+ override fun equals(other: Any?): Boolean {
282
+ if (this === other) return true
283
+ if (javaClass != other?.javaClass) return false
284
+
285
+ other as SendRequest
286
+
287
+ if (!data.contentEquals(other.data)) return false
288
+ if (waitTime != other.waitTime) return false
289
+ if (ack != other.ack) return false
290
+
291
+ return true
292
+ }
293
+
294
+ override fun hashCode(): Int {
295
+ var result = data.contentHashCode()
296
+ result = 31 * result + waitTime
297
+ result = 31 * result + ack.hashCode()
298
+ return result
299
+ }
300
+ }
301
+
302
+ private class BooleanWaiter {
303
+ @Volatile
304
+ private var flag = true // initially true
305
+
306
+ @Synchronized
307
+ fun waitWhileTrue() {
308
+ while (flag) {
309
+ (this as Object).wait()
310
+ }
311
+ }
312
+
313
+ /** Bounded wait: returns early if the flag is still set after [timeoutMs]. */
314
+ @Synchronized
315
+ fun waitWhileTrue(timeoutMs: Long) {
316
+ val deadline = System.currentTimeMillis() + timeoutMs
317
+ while (flag) {
318
+ val remaining = deadline - System.currentTimeMillis()
319
+ if (remaining <= 0) break
320
+ (this as Object).wait(remaining)
321
+ }
322
+ }
323
+
324
+ @Synchronized
325
+ fun setTrue() {
326
+ flag = true
327
+ }
328
+
329
+ @Synchronized
330
+ fun setFalse() {
331
+ flag = false
332
+ (this as Object).notifyAll()
333
+ }
334
+ }
335
+
336
+ // Network Management
337
+ override fun requestWifiScan (scanId: String?) { Bridge.log("Nex: requestWifiScan operation not supported") }
338
+ override fun sendWifiCredentials(ssid: String, password: String) { Bridge.log("Nex: sendWifiCredentials operation not supported") }
339
+ override fun forgetWifiNetwork(ssid: String) { }
340
+ override fun sendHotspotState(enabled: Boolean) { Bridge.log("Nex: sendHotspotState operation not supported") }
341
+
342
+ // Gallery: Not supported on Nex (No camera)
343
+ override fun queryGalleryStatus() { Bridge.log("Nex: queryGalleryStatus operation not supported") }
344
+ override fun sendGalleryMode() { Bridge.log("Nex: sendGalleryMode operation not supported") }
345
+
346
+ override fun sendVoiceActivityDetectionSetting() {
347
+ val enabled =
348
+ DeviceStore.get("bluetooth", "voice_activity_detection_enabled") as? Boolean
349
+ ?: BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED
350
+ Bridge.log("Nex: 🎤 Sending Voice Activity Detection setting to glasses: $enabled")
351
+
352
+ if (connectionState != ConnTypes.CONNECTED) {
353
+ Bridge.log("Nex: Cannot send VAD setting - not connected")
354
+ return
355
+ }
356
+
357
+ val bytes = NexProtobufUtils.generateVadEnabledRequestCommandBytes(enabled)
358
+ sendDataSequentially(bytes)
359
+ Bridge.sendVoiceActivityDetectionStatus(enabled)
360
+ }
361
+
362
+ // Version info: Not supported on Nex (uses protobuf for version info)
363
+ override fun requestVersionInfo() { Bridge.log("Nex: requestVersionInfo operation not supported") }
364
+
365
+ // Camera & Media: Not supported on Nex (No camera)
366
+ override fun requestPhoto(request: PhotoRequest) {
367
+ Bridge.log("Nex: requestPhoto operation not supported")
368
+ }
369
+ override fun startStream(message: MutableMap<String, Any>) { Bridge.log("Nex: startStream operation not supported") }
370
+ override fun stopStream() { Bridge.log("Nex: stopStream operation not supported") }
371
+ override fun sendStreamKeepAlive(message: MutableMap<String, Any>) { Bridge.log("Nex: sendStreamKeepAlive operation not supported") }
372
+ override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) { Bridge.log("Nex: startVideoRecording operation not supported") }
373
+ override fun stopVideoRecording(requestId: String) { Bridge.log("Nex: stopVideoRecording operation not supported") }
374
+
375
+ // Button Settings: Not supported on Nex
376
+ override fun sendButtonPhotoSettings() { Bridge.log("Nex: sendButtonPhotoSettings operation not supported") }
377
+ override fun sendButtonVideoRecordingSettings() { Bridge.log("Nex: sendButtonVideoRecordingSettings operation not supported") }
378
+ override fun sendButtonMaxRecordingTime() { Bridge.log("Nex: sendButtonMaxRecordingTime operation not supported") }
379
+
380
+ override fun sendCameraFovSetting() { Bridge.log("Nex: sendCameraFovSetting operation not supported") }
381
+
382
+ override fun sendUserEmailToGlasses(email: String) { Bridge.log("Nex: sendUserEmailToGlasses operation not supported") }
383
+
384
+ override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {
385
+ Bridge.log("Nex: sendIncidentId operation not supported")
386
+ }
387
+
388
+ // Connection
389
+ override fun findCompatibleDevices() {
390
+ if (isScanningForCompatibleDevices) {
391
+ Bridge.log("Scan already in progress, skipping...")
392
+ return
393
+ }
394
+
395
+ isScanningForCompatibleDevices = true
396
+ val scanner = bluetoothAdapter.bluetoothLeScanner ?: run {
397
+ Log.e(TAG, "BluetoothLeScanner not available")
398
+ isScanningForCompatibleDevices = false
399
+ return
400
+ }
401
+
402
+ val foundDeviceNames = mutableListOf<String>()
403
+ if (findCompatibleDevicesHandler == null) {
404
+ findCompatibleDevicesHandler = Handler(Looper.getMainLooper())
405
+ }
406
+
407
+ // Optional: add filters if you want to narrow the scan
408
+ val filters = mutableListOf<ScanFilter>()
409
+ val settings = ScanSettings.Builder()
410
+ .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
411
+ .build()
412
+
413
+ // Create a modern ScanCallback instead of the deprecated LeScanCallback
414
+ bleScanCallback = object : ScanCallback() {
415
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
416
+ val device = result.device
417
+ val name = device.name
418
+ val address = device.address
419
+
420
+ name?.let {
421
+ if (it.startsWith("Nex1-") || it.startsWith("MENTRA_DISPLAY_")) {
422
+ Bridge.log("bleScanCallback onScanResult: $name address $address")
423
+ synchronized(foundDeviceNames) {
424
+ if (!foundDeviceNames.contains(it)) {
425
+ foundDeviceNames.add(it)
426
+ Bridge.log("Found smart glasses: $name")
427
+ Bridge.sendDiscoveredDevice(DeviceTypes.NEX, it);
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+
434
+ override fun onBatchScanResults(results: List<ScanResult>) {
435
+ // If needed, handle batch results here
436
+ }
437
+
438
+ override fun onScanFailed(errorCode: Int) {
439
+ Log.e(TAG, "BLE scan failed with code: $errorCode")
440
+ }
441
+ }
442
+
443
+ // Start scanning
444
+ scanner.startScan(filters, settings, bleScanCallback)
445
+ Bridge.log("Started scanning for smart glasses with BluetoothLeScanner...")
446
+ scanner.flushPendingScanResults(bleScanCallback)
447
+
448
+ // Stop scanning after 10 seconds (adjust as needed)
449
+ findCompatibleDevicesHandler?.postDelayed({
450
+ bleScanCallback?.let { scanner.stopScan(it) }
451
+ isScanningForCompatibleDevices = false
452
+ bleScanCallback = null
453
+ Bridge.log("Stopped scanning for smart glasses.")
454
+ }, 10000)
455
+ }
456
+
457
+ override fun connectById(id: String) {
458
+ preferredMainDeviceId = id
459
+ connectToSmartGlasses()
460
+ }
461
+
462
+ override fun getConnectedBluetoothName(): String {
463
+ return mainDevice?.getName() ?: ""
464
+ }
465
+
466
+ override fun disconnect() {
467
+ DeviceStore.apply("glasses", "fullyBooted", false)
468
+ destroy()
469
+ }
470
+
471
+ override fun forget() {
472
+ DeviceStore.apply("glasses", "fullyBooted", false)
473
+ destroy()
474
+ }
475
+
476
+ override fun cleanup() {
477
+ // TODO: Later
478
+ Bridge.log("To be implemented")
479
+ }
480
+
481
+ // Device
482
+ override fun setHeadUpAngle(angle: Int) {
483
+ // Validate headUpAngle range (0 ~ 60)
484
+ val validatedAngle = angle.coerceIn(0, 60)
485
+ Bridge.log("Nex: === SENDING HEAD UP ANGLE COMMAND TO GLASSES ===")
486
+ Bridge.log("Nex: Head Up Angle: $validatedAngle degrees (validated range: 0-60)")
487
+ val cmdBytes = NexProtobufUtils.generateHeadUpAngleConfigCommandBytes(angle)
488
+ sendProtobuf(cmdBytes, 10)
489
+ }
490
+
491
+ override fun getBatteryStatus() {
492
+ queryBatteryStatus()
493
+ }
494
+
495
+ override fun exit() {
496
+ sendDataSequentially(byteArrayOf(0x18.toByte()), 100)
497
+ }
498
+
499
+ override fun sendShutdown() {
500
+ Bridge.log("sendShutdown - not supported on Nex")
501
+ }
502
+
503
+ override fun sendReboot() {
504
+ Bridge.log("sendReboot - not supported on Nex")
505
+ }
506
+
507
+ override fun sendRgbLedControl(requestId: String, packageName: String?, action: String, color: String?, onDurationMs: Int, offDurationMs: Int, count: Int) {
508
+ Bridge.log("sendRgbLedControl - not supported on Nex");
509
+ Bridge.sendRgbLedControlResponse(requestId, false, "device_not_supported");
510
+ }
511
+
512
+ // Not supported in Nex
513
+ override fun setSilentMode(enabled: Boolean) { Bridge.log("Nex: setSilentMode operation not supported") }
514
+
515
+ // Display
516
+ override fun setBrightness(level: Int, autoMode: Boolean) {
517
+ // TODO: test this logic. Is it correct? Should we send both or just one?
518
+ Bridge.log("Nex: setBrightness() - level: " + level + "%, autoMode: " + autoMode);
519
+ val brightnessCmdBytes = NexProtobufUtils.generateBrightnessConfigCommandBytes(level)
520
+ sendProtobuf(brightnessCmdBytes, 10)
521
+
522
+ val autoBrightnessCmdBytes = NexProtobufUtils.generateAutoBrightnessConfigCommandBytes(autoMode)
523
+ sendProtobuf(autoBrightnessCmdBytes, 10)
524
+ }
525
+
526
+ override fun clearDisplay() {
527
+ Bridge.log("Nex: clearDisplay() - clearing canvas + caption");
528
+ // Tear down any canvas components (bitmap + positioned text) and forget the element mapping
529
+ // + active layout, then clear the caption text path too so either kind of screen is wiped.
530
+ canvasElements.clear()
531
+ currentLayoutId = null
532
+ sendProtobuf(NexProtobufUtils.generateCanvasClearCommandBytes(), 10)
533
+ val clearDisplayPackets = NexProtobufUtils.generateClearDisplayRequestCommandBytes()
534
+ sendProtobuf(clearDisplayPackets, 10)
535
+ Bridge.log("Nex: clearDisplay() - sent");
536
+ }
537
+
538
+
539
+ override fun sendText(text: String) {
540
+ Bridge.log("Nex: sendText() - text: " + text);
541
+ showCaptionText(text)
542
+ }
543
+
544
+ override fun sendTextWall(text: String) {
545
+ Bridge.log("Nex: sendTextWall() - text: " + text);
546
+ showCaptionText(text)
547
+ }
548
+
549
+ override fun sendDoubleTextWall(top: String, bottom: String) {
550
+ Bridge.log("Nex: sendDoubleTextWall() - top: " + top + ", bottom: " + bottom);
551
+ val finalText: String = listOf(top, bottom).filter { it.isNotEmpty() }.joinToString("\n")
552
+ showCaptionText(finalText)
553
+ }
554
+
555
+ // Live captions / text walls render into a single reused canvas text container (the caption
556
+ // box), like Even G2: the first call creates it, each subsequent call is an in-place text
557
+ // update on the same component (reused by rect via sendPositionedText), so captions can sit
558
+ // alongside a navigation minimap or other canvas components instead of owning the screen.
559
+ private fun showCaptionText(text: String) {
560
+ // Captions are high-frequency, replace-in-place updates where the newest frame
561
+ // supersedes the last, so they go out WITHOUT ATT acks (write-no-response) for
562
+ // throughput — every other command uses acked writes. Route through upsertText
563
+ // directly (rather than the public sendPositionedText override, which can't take
564
+ // an ack flag) using the same rect-synthesized element id so reuse still works.
565
+ val elementId = "text@$CAPTION_BOX_X,$CAPTION_BOX_Y,${CAPTION_BOX_W}x$CAPTION_BOX_H,0,0"
566
+ upsertText(elementId, null, text, CAPTION_BOX_X, CAPTION_BOX_Y, CAPTION_BOX_W, CAPTION_BOX_H, 0, 0, ack = false)
567
+ }
568
+
569
+ // ---- Canvas text containers (G2-style) -------------------------------------------------
570
+ // Positioned text is drawn as canvas TEXTBOX components so it coexists with the bitmap
571
+ // component (a navigation screen = minimap + several text boxes shown together). Components
572
+ // are keyed by their rect+border and reused so repeated updates don't recreate the LVGL
573
+ // object; ids come from the firmware's text pool (1..6) with oldest-out eviction.
574
+ // Retained-mode canvas registry: elementId -> firmware component. The layout path (nav HUD)
575
+ // passes explicit element ids; legacy callers (captions, one-off bitmaps) synthesize an id from
576
+ // their rect, so reuse/update works for them through the same path. Text ids come from the
577
+ // firmware pool 1..6, bitmaps from 10..13.
578
+ private data class CanvasElement(val firmwareId: Int, val isBitmap: Boolean, val rect: String)
579
+ private val canvasElements = LinkedHashMap<String, CanvasElement>()
580
+ private val canvasTextIdPool: List<Int> = listOf(1, 2, 3, 4, 5, 6)
581
+ private val canvasBitmapIdPool: List<Int> = listOf(10, 11, 12, 13)
582
+ // Active layout/scene id. A display call carrying a DIFFERENT layoutId clears the previous scene
583
+ // first (retained-mode contract); legacy calls (null layoutId) leave it untouched.
584
+ private var currentLayoutId: String? = null
585
+
586
+ /**
587
+ * Handle a scene/layout change. DeviceManager sweeps the previous app's
588
+ * elements before a cross-app frame arrives, so the registry is usually
589
+ * empty here and switching costs nothing. Only when stale components remain
590
+ * (e.g. legacy callers with no sweep) do we delete them — individually, NOT
591
+ * via CanvasClear: clear also exits the canvas VIEW (the first create
592
+ * re-activates it), which reads as a full-screen flash.
593
+ */
594
+ private fun ensureLayout(layoutId: String?) {
595
+ if (layoutId != null && layoutId != currentLayoutId) {
596
+ if (canvasElements.isNotEmpty()) {
597
+ for (el in canvasElements.values) {
598
+ sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(el.firmwareId))
599
+ }
600
+ canvasElements.clear()
601
+ }
602
+ currentLayoutId = layoutId
603
+ }
604
+ }
605
+
606
+ /**
607
+ * A replay frame (reconnect / firmware recovery / re-dispatch) repaints from
608
+ * scratch: forget the registry so every element takes the CREATE path.
609
+ * Create-on-existing-id is replace in firmware, so repainting over a live
610
+ * canvas is safe, and updates-to-dead-ids (which firmware drops silently)
611
+ * can never happen.
612
+ */
613
+ override fun onSceneReplay(appId: String) {
614
+ canvasElements.clear()
615
+ currentLayoutId = appId
616
+ }
617
+
618
+ /**
619
+ * Free firmware id from the type's pool. When the pool is exhausted the
620
+ * OLDEST registered element of that type is evicted for real: its firmware
621
+ * component is deleted and its id reused (LinkedHashMap preserves insertion
622
+ * order).
623
+ */
624
+ private fun allocFirmwareId(isBitmap: Boolean): Int? {
625
+ val pool = if (isBitmap) canvasBitmapIdPool else canvasTextIdPool
626
+ val used = canvasElements.values.filter { it.isBitmap == isBitmap }.map { it.firmwareId }.toSet()
627
+ pool.firstOrNull { it !in used }?.let {
628
+ return it
629
+ }
630
+ // Pool full — evict the oldest same-type element.
631
+ val oldest = canvasElements.entries.firstOrNull { it.value.isBitmap == isBitmap } ?: return null
632
+ Bridge.log("Nex: pool full — evicting oldest ${if (isBitmap) "bitmap" else "text"} element '${oldest.key}' (fw id ${oldest.value.firmwareId})")
633
+ sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(oldest.value.firmwareId))
634
+ canvasElements.remove(oldest.key)
635
+ return oldest.value.firmwareId
636
+ }
637
+
638
+ // Legacy positioned text: identity is the rect, so repeated updates reuse the component.
639
+ override fun sendPositionedText(
640
+ text: String, x: Int, y: Int, width: Int, height: Int, borderWidth: Int, borderRadius: Int
641
+ ) {
642
+ upsertText("text@$x,$y,${width}x$height,$borderWidth,$borderRadius", null,
643
+ text, x, y, width, height, borderWidth, borderRadius)
644
+ }
645
+
646
+ override fun drawLayoutText(
647
+ text: String, x: Int, y: Int, width: Int, height: Int,
648
+ borderWidth: Int, borderRadius: Int, elementId: String, layoutId: String?
649
+ ) {
650
+ upsertText(elementId, layoutId, text, x, y, width, height, borderWidth, borderRadius)
651
+ }
652
+
653
+ private fun upsertText(
654
+ elementId: String, layoutId: String?, text: String,
655
+ x: Int, y: Int, width: Int, height: Int, borderWidth: Int, borderRadius: Int,
656
+ ack: Boolean = true
657
+ ) {
658
+ ensureLayout(layoutId)
659
+ val rect = "$x,$y,${width}x$height,$borderWidth,$borderRadius"
660
+ val existing = canvasElements[elementId]
661
+ if (existing != null && !existing.isBitmap) {
662
+ if (existing.rect != rect) {
663
+ // Geometry moved/resized — recreate the box at the SAME firmware id, then set text.
664
+ sendProtobuf(NexProtobufUtils.generateCanvasCreateTextboxCommandBytes(
665
+ existing.firmwareId, x, y, width, height, borderWidth, borderRadius), ack = ack)
666
+ canvasElements[elementId] = existing.copy(rect = rect)
667
+ }
668
+ sendProtobuf(NexProtobufUtils.generateCanvasUpdateTextCommandBytes(existing.firmwareId, text), ack = ack)
669
+ return
670
+ }
671
+ val fid = allocFirmwareId(false)
672
+ if (fid == null) {
673
+ Bridge.log("Nex: text pool full (>6) — dropping element '$elementId'")
674
+ return
675
+ }
676
+ canvasElements[elementId] = CanvasElement(fid, false, rect)
677
+ sendProtobuf(NexProtobufUtils.generateCanvasCreateTextboxCommandBytes(
678
+ fid, x, y, width, height, borderWidth, borderRadius), ack = ack)
679
+ sendProtobuf(NexProtobufUtils.generateCanvasUpdateTextCommandBytes(fid, text), ack = ack)
680
+ }
681
+
682
+ override fun removeLayoutElement(elementId: String, layoutId: String?) {
683
+ val slot = canvasElements.remove(elementId) ?: return
684
+ sendProtobuf(NexProtobufUtils.generateCanvasDeleteComponentCommandBytes(slot.firmwareId))
685
+ }
686
+
687
+ private data class NexBmp(val data: ByteArray, val w: Int, val h: Int)
688
+
689
+ /**
690
+ * Decode a base64 image to the glasses-native 1-bit BMP at the requested size. The panel is
691
+ * 1-bpp and renders white-on-black, so we scale to the rect, invert, then Floyd–Steinberg dither
692
+ * (preserving gradients as dot patterns instead of a hard 50% threshold) before the 1-bit encode.
693
+ */
694
+ private fun decodeBitmapForNex(base64ImageData: String, width: Int?, height: Int?): NexBmp? {
695
+ return try {
696
+ val bmpData = android.util.Base64.decode(base64ImageData, android.util.Base64.DEFAULT)
697
+ if (bmpData == null || bmpData.isEmpty()) {
698
+ Log.e(TAG, "Failed to decode base64 image data")
699
+ return null
700
+ }
701
+ val bmp = BitmapFactory.decodeByteArray(bmpData, 0, bmpData.size)
702
+ val destW = width ?: bmp.width
703
+ val destH = height ?: bmp.height
704
+ val scaled = Bitmap.createScaledBitmap(bmp, destW, destH, true)
705
+ val inverted = invertBitmap(scaled)
706
+ val dithered = floydSteinbergDither(inverted)
707
+ NexBmp(BitmapJavaUtils.convertBitmapTo1BitBmpBytes(dithered, false), destW, destH)
708
+ } catch (e: Exception) {
709
+ Log.e(TAG, "Error decoding bitmap: " + e.message)
710
+ null
711
+ }
712
+ }
713
+
714
+ // Legacy one-off bitmap: identity is the rect (no layout scene).
715
+ override fun displayBitmap(
716
+ base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?
717
+ ): Boolean {
718
+ val nb = decodeBitmapForNex(base64ImageData, width, height) ?: return false
719
+ upsertBitmap("bmp@${x ?: 0},${y ?: 0},${nb.w}x${nb.h}", null, nb.data, x ?: 0, y ?: 0, nb.w, nb.h)
720
+ return true
721
+ }
722
+
723
+ override fun drawLayoutBitmap(
724
+ base64ImageData: String, x: Int, y: Int, width: Int, height: Int,
725
+ elementId: String, layoutId: String?
726
+ ): Boolean {
727
+ val nb = decodeBitmapForNex(base64ImageData, width, height) ?: return false
728
+ upsertBitmap(elementId, layoutId, nb.data, x, y, nb.w, nb.h)
729
+ return true
730
+ }
731
+
732
+ private fun upsertBitmap(
733
+ elementId: String, layoutId: String?, bmpData: ByteArray, x: Int, y: Int, w: Int, h: Int
734
+ ) {
735
+ ensureLayout(layoutId)
736
+ val rect = "$x,$y,${w}x$h"
737
+ val existing = canvasElements[elementId]
738
+ val fid: Int
739
+ if (existing != null && existing.isBitmap) {
740
+ fid = existing.firmwareId
741
+ if (existing.rect != rect) {
742
+ sendProtobuf(NexProtobufUtils.generateCanvasCreateBitmapCommandBytes(fid, x, y, w, h))
743
+ canvasElements[elementId] = existing.copy(rect = rect)
744
+ }
745
+ } else {
746
+ val alloc = allocFirmwareId(true)
747
+ if (alloc == null) {
748
+ Bridge.log("Nex: bitmap pool full (>4) — dropping element '$elementId'")
749
+ return
750
+ }
751
+ fid = alloc
752
+ canvasElements[elementId] = CanvasElement(fid, true, rect)
753
+ sendProtobuf(NexProtobufUtils.generateCanvasCreateBitmapCommandBytes(fid, x, y, w, h))
754
+ }
755
+ streamBitmapPixels(fid, bmpData)
756
+ }
757
+ override fun showDashboard() {
758
+ exit()
759
+ }
760
+
761
+ override fun setDashboardPosition(height: Int, depth: Int) {
762
+ Bridge.log("Nex: setDashboardPosition() - height: " + height + ", depth: " + depth)
763
+ // Send display_height then display_distance; adjust order if Nex firmware requires otherwise.
764
+ val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
765
+ sendProtobuf(heightBytes, 10)
766
+ val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
767
+ val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
768
+ sendProtobuf(distanceBytes, 10)
769
+ }
770
+
771
+ override fun setDashboardHeightOnly(height: Int) {
772
+ Bridge.log("Nex: setDashboardHeightOnly() - height: $height")
773
+ val heightBytes = NexProtobufUtils.generateDisplayHeightCommandBytes(height)
774
+ sendProtobuf(heightBytes, 10)
775
+ }
776
+
777
+ override fun setDashboardDepthOnly(depth: Int) {
778
+ Bridge.log("Nex: setDashboardDepthOnly() - depth: $depth")
779
+ val distanceCm = NexProtobufUtils.dashboardDepthToDistanceCm(depth)
780
+ val distanceBytes = NexProtobufUtils.generateDisplayDistanceCommandBytes(distanceCm)
781
+ sendProtobuf(distanceBytes, 10)
782
+ }
783
+
784
+ override fun ping() {
785
+ Bridge.log("Nex: ping()");
786
+ }
787
+
788
+ override fun dbg1() {}
789
+ override fun dbg2() {}
790
+
791
+ // Audio Control
792
+ // TODO: Validate this logic. looks weird.
793
+ override fun setMicEnabled(enabled: Boolean) {
794
+ Bridge.log("Nex: setMicEnabled(): $enabled")
795
+ // Update the shouldUseGlassesMic flag to reflect the current state
796
+ shouldUseGlassesMic = enabled
797
+ Bridge.log("Nex: Updated shouldUseGlassesMic to: $shouldUseGlassesMic")
798
+ if (enabled) {
799
+ Bridge.log("Nex: Microphone enabled, starting audio input handling")
800
+ startMicBeat(MICBEAT_INTERVAL_MS.toInt())
801
+ } else {
802
+ Bridge.log("Nex: Microphone disabled, stopping audio input handling")
803
+ stopMicBeat()
804
+ }
805
+ }
806
+
807
+ override fun sortMicRanking(list: MutableList<String>): MutableList<String> {
808
+ return list
809
+ }
810
+
811
+ //==================== HELPER FUNCTIONS =================================================
812
+ private fun createGattCallback(): BluetoothGattCallback {
813
+ return object : BluetoothGattCallback() {
814
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
815
+ Bridge.log("onConnectionStateChange status State ${status == BluetoothGatt.GATT_SUCCESS}")
816
+ Bridge.log("onConnectionStateChange connected State ${newState == BluetoothProfile.STATE_CONNECTED}")
817
+
818
+ if (status == BluetoothGatt.GATT_SUCCESS) {
819
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
820
+ Bridge.log("glass connected, discovering services...")
821
+ gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
822
+ gatt.setPreferredPhy(
823
+ BluetoothDevice.PHY_LE_2M_MASK,
824
+ BluetoothDevice.PHY_LE_2M_MASK,
825
+ BluetoothDevice.PHY_OPTION_NO_PREFERRED
826
+ )
827
+
828
+ isMainConnected = true
829
+ mainReconnectAttempts = 0
830
+ Bridge.log("Both glasses connected. Stopping BLE scan.")
831
+ stopScan()
832
+
833
+ if (!isWorkerRunning) {
834
+ Bridge.log("Worker thread is not running. Starting it.")
835
+ startWorkerIfNeeded()
836
+ }
837
+
838
+ Bridge.log("Discover services calling...")
839
+ gatt.discoverServices()
840
+ updateConnectionState()
841
+ mainDevice?.let { device ->
842
+ savedNexMainName = device.name
843
+ savedNexMainAddress = device.address
844
+ // savePairedDeviceNames()
845
+ // savePairedDeviceAddress()
846
+ }
847
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
848
+ handleDisconnection(gatt)
849
+ }
850
+ } else {
851
+ handleConnectionFailure(gatt, status)
852
+ }
853
+ }
854
+
855
+ private fun handleDisconnection(gatt: BluetoothGatt) {
856
+ Bridge.log("glass disconnected, stopping heartbeats")
857
+ Bridge.log("Entering STATE_DISCONNECTED branch")
858
+
859
+ // The glasses lose their canvas on disconnect — forget the
860
+ // component registry so post-reconnect frames take the CREATE
861
+ // path (firmware silently drops updates to dead component ids).
862
+ // The host replays the current scene after reconnect.
863
+ canvasElements.clear()
864
+ currentLayoutId = null
865
+
866
+ // Save current microphone state before disconnection
867
+ microphoneStateBeforeDisconnection = isMicrophoneEnabled
868
+ Bridge.log("Saved microphone state before disconnection: $microphoneStateBeforeDisconnection")
869
+
870
+ // Reset protobuf version posted flag for next connection
871
+ protobufVersionPosted = false
872
+
873
+ // Reset chunk sizes to defaults
874
+ maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
875
+ bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
876
+ mainServicesWaiter.setTrue()
877
+ mainWaiter.setFalse()
878
+ Bridge.log("[BLE] Disconnect: reset mainServicesWaiter=true, mainWaiter=false (unblock any in-flight write). isWorkerRunning=$isWorkerRunning")
879
+ forceSideDisconnection()
880
+ Bridge.log("Called forceSideDisconnection()")
881
+ currentMTU = 0
882
+
883
+ // Stop any periodic transmissions
884
+ stopMicBeat()
885
+ sendQueue.clear()
886
+ Bridge.log("Stopped heartbeat monitoring and mic beat; cleared sendQueue")
887
+ updateConnectionState()
888
+ Bridge.log("Updated connection state after disconnection")
889
+
890
+ // gatt.device?.let {
891
+ // Bridge.log("Closing GATT connection for device: ${it.address}")
892
+ // gatt.disconnect()
893
+ // gatt.close()
894
+ // Bridge.log("GATT connection closed")
895
+ // } ?: run {
896
+ // Bridge.log("No GATT device available to disconnect")
897
+ // }
898
+
899
+ mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 2000)
900
+ }
901
+
902
+ private fun handleConnectionFailure(gatt: BluetoothGatt, status: Int) {
903
+ // Same as handleDisconnection: the glasses lose their canvas —
904
+ // forget the component registry so post-reconnect frames take
905
+ // the CREATE path (fw silently drops updates to dead ids).
906
+ canvasElements.clear()
907
+ currentLayoutId = null
908
+
909
+ // Save current microphone state before connection failure
910
+ microphoneStateBeforeDisconnection = isMicrophoneEnabled
911
+ Bridge.log("Saved microphone state before connection failure: $microphoneStateBeforeDisconnection")
912
+
913
+ currentMTU = 0
914
+ maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
915
+ bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
916
+ Bridge.log("Unexpected connection state encountered for glass: $status")
917
+ stopMicBeat()
918
+ sendQueue.clear()
919
+
920
+ // Mark as not ready
921
+ mainServicesWaiter.setTrue()
922
+ mainWaiter.setFalse()
923
+ Bridge.log("[BLE] ConnectionFailure: reset mainServicesWaiter=true, mainWaiter=false (unblock any in-flight write). isWorkerRunning=$isWorkerRunning")
924
+ Bridge.log("Stopped heartbeat monitoring and mic beat; cleared sendQueue due to connection failure")
925
+ Bridge.log("glass connection failed with status: $status")
926
+
927
+ isMainConnected = false
928
+ mainReconnectAttempts++
929
+
930
+ mainGlassGatt?.let {
931
+ it.disconnect()
932
+ it.close()
933
+ mainGlassGatt = null
934
+ }
935
+
936
+ forceSideDisconnection()
937
+ Bridge.log("Called forceSideDisconnection() after connection failure")
938
+ Bridge.log("GATT connection disconnected and closed due to failure")
939
+ updateConnectionState()
940
+
941
+ mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 2000)
942
+ }
943
+
944
+ private fun forceSideDisconnection() {
945
+ Bridge.log("forceSideDisconnection() called")
946
+ isMainConnected = false
947
+ mainReconnectAttempts++
948
+ Bridge.log("Main glass: Marked as disconnected and incremented mainReconnectAttempts to $mainReconnectAttempts")
949
+
950
+ mainGlassGatt?.let {
951
+ Bridge.log("Main glass GATT exists. Disconnecting and closing mainGlassGatt")
952
+ it.disconnect()
953
+ it.close()
954
+ mainGlassGatt = null
955
+ } ?: run {
956
+ Bridge.log("Main glass GATT is already null")
957
+ }
958
+ }
959
+
960
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
961
+ val statusBool = status == BluetoothGatt.GATT_SUCCESS
962
+ mainTaskHandler?.obtainMessage(
963
+ MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES,
964
+ statusBool
965
+ )?.sendToTarget()
966
+ }
967
+
968
+ override fun onCharacteristicWrite(
969
+ gatt: BluetoothGatt,
970
+ characteristic: BluetoothGattCharacteristic,
971
+ status: Int
972
+ ) {
973
+ mainWaiter.setFalse()
974
+ }
975
+
976
+ override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
977
+ val statusStr = if (status == BluetoothGatt.GATT_SUCCESS) "SUCCESS" else "FAILED($status)"
978
+ Bridge.log("[BLE] onDescriptorWrite: $statusStr — releasing mainServicesWaiter, worker can now send")
979
+ mainServicesWaiter.setFalse()
980
+ }
981
+
982
+ override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
983
+ val statusBool = status == BluetoothGatt.GATT_SUCCESS
984
+ Bridge.log("🔄 MTU Negotiation Result: Success=$statusBool, Device MTU=$mtu, Status=$status")
985
+
986
+ if (statusBool) {
987
+ // Store device capability and calculate actual negotiated MTU
988
+ deviceMaxMTU = mtu // Record what device actually supports
989
+ // The actual negotiated MTU is the minimum of our request and device capability
990
+ currentMTU = MTU_517.coerceAtMost(mtu)
991
+
992
+ Bridge.log("🎯 MTU Negotiation Complete:")
993
+ Bridge.log(" 📱 App Requested: $MTU_517 bytes")
994
+ Bridge.log(" 📡 Device Supports: $deviceMaxMTU bytes")
995
+ Bridge.log(" 🤝 Negotiated MTU: $currentMTU bytes")
996
+
997
+ // Calculate optimal chunk sizes based on negotiated MTU
998
+ maxChunkSize = currentMTU - 10
999
+ bmpChunkSize = currentMTU - 20 // BMP has more config bytes
1000
+
1001
+ Bridge.log("✅ MTU Configuration Complete:")
1002
+ Bridge.log(" 📊 Final MTU: $currentMTU bytes")
1003
+ Bridge.log(" 📦 Data Chunk Size: $maxChunkSize bytes")
1004
+ Bridge.log(" 🖼️ Image Chunk Size: $bmpChunkSize bytes")
1005
+ Bridge.log(" 🔧 Device Maximum: $deviceMaxMTU bytes")
1006
+ } else {
1007
+ Bridge.log("❌ MTU Request Failed - Status: $status, Requested: $mtu")
1008
+
1009
+ // Simple fallback strategy: 247 → 23
1010
+ if (mtu == MTU_517) {
1011
+ Bridge.log("🔄 247 bytes failed, trying default: $MTU_DEFAULT bytes...")
1012
+ Bridge.log("📤 Requesting MTU: $MTU_DEFAULT bytes (fallback)")
1013
+ gatt.requestMtu(MTU_DEFAULT)
1014
+ } else {
1015
+ Bridge.log("⚠️ All MTU requests failed, using defaults")
1016
+ currentMTU = MTU_DEFAULT
1017
+ deviceMaxMTU = MTU_DEFAULT
1018
+ maxChunkSize = MAX_CHUNK_SIZE_DEFAULT
1019
+ bmpChunkSize = MAX_CHUNK_SIZE_DEFAULT
1020
+
1021
+ Bridge.log("📋 Fallback Configuration:")
1022
+ Bridge.log(" 📊 Default MTU: $MTU_DEFAULT bytes")
1023
+ Bridge.log(" 📦 Data Chunk Size: $maxChunkSize bytes")
1024
+ Bridge.log(" 🖼️ Image Chunk Size: $bmpChunkSize bytes")
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
1030
+ val msg = Message.obtain().apply {
1031
+ what = MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED
1032
+ obj = characteristic // Attach any object you want
1033
+ }
1034
+ mainTaskHandler.sendMessage(msg)
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ private fun handleMainTaskMessage(msg: Message): Boolean {
1040
+ val msgCode = msg.what
1041
+ // NOTE: do not log here. Every incoming BLE notification (including
1042
+ // ~50 audio packets/sec) is dispatched through this handler on the
1043
+ // main looper, and Bridge.log marshals an event across the RN bridge
1044
+ // to JS. Per-message logging here floods the main thread and backs up
1045
+ // the audio queue when the CPU downclocks at screen-off. (Matches G1,
1046
+ // which does not log per audio packet.)
1047
+ when (msgCode) {
1048
+ MAIN_TASK_HANDLER_CODE_GATT_STATUS_CHANGED -> {}
1049
+ MAIN_TASK_HANDLER_CODE_DISCOVER_SERVICES -> {
1050
+ val statusBool = msg.obj as? Boolean ?: false
1051
+ if (statusBool) {
1052
+ mainGlassGatt?.let { initNexGlasses(it) }
1053
+ }
1054
+ }
1055
+ MAIN_TASK_HANDLER_CODE_CHARACTERISTIC_VALUE_NOTIFIED -> {
1056
+ val characteristic = msg.obj as? BluetoothGattCharacteristic
1057
+ characteristic?.let { onCharacteristicChangedHandler(it) }
1058
+ }
1059
+ MAIN_TASK_HANDLER_CODE_CONNECT_DEVICE -> {}
1060
+ MAIN_TASK_HANDLER_CODE_DISCONNECT_DEVICE -> {}
1061
+ MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE -> {
1062
+ mainDevice?.let { attemptGattConnection(it) }
1063
+ }
1064
+ MAIN_TASK_HANDLER_CODE_CANCEL_RECONNECT_DEVICE -> {}
1065
+ MAIN_TASK_HANDLER_CODE_RECONNECT_GATT -> {}
1066
+ MAIN_TASK_HANDLER_CODE_SCAN_START -> {}
1067
+ MAIN_TASK_HANDLER_CODE_SCAN_END -> {}
1068
+ MAIN_TASK_HANDLER_CODE_BATTERY_QUERY -> {
1069
+ queryBatteryStatus()
1070
+ }
1071
+ MAIN_TASK_HANDLER_CODE_HEART_BEAT -> {
1072
+ // Note: Heartbeat is now handled by receiving ping from glasses
1073
+ // This case is kept for backward compatibility but no longer used
1074
+ }
1075
+ else -> { }
1076
+ }
1077
+
1078
+ return true
1079
+ }
1080
+
1081
+ private fun initNexGlasses(gatt: BluetoothGatt) {
1082
+ // Start MTU discovery with our maximum target
1083
+ Bridge.log("Nex: 🔍 MTU Discovery: Requesting maximum MTU size: $MTU_517")
1084
+ Bridge.log("Nex: 🎯 Target: Use $MTU_517 bytes max, or $MTU_DEFAULT bytes default")
1085
+ Bridge.log("Nex: 📤 Requesting MTU: $MTU_517 bytes")
1086
+ gatt.requestMtu(MTU_517) // Request our maximum MTU
1087
+
1088
+ val uartService: BluetoothGattService? = gatt.getService(NexBluetoothConstants.MAIN_SERVICE_UUID)
1089
+
1090
+ if (uartService != null) {
1091
+ val writeChar = uartService.getCharacteristic(NexBluetoothConstants.WRITE_CHAR_UUID)
1092
+ val notifyChar = uartService.getCharacteristic(NexBluetoothConstants.NOTIFY_CHAR_UUID)
1093
+
1094
+ writeChar?.let {
1095
+ mainWriteChar = it
1096
+ // Write type is now chosen PER FRAGMENT in processQueue (acked for
1097
+ // everything except captions), so it's set there right before each
1098
+ // write rather than once here. Captions still use write-without-
1099
+ // response — which unblocks TX from the ~1-packet-per-connection-
1100
+ // interval ATT round-trip when the phone sleeps and the interval is
1101
+ // relaxed — while every other command is acked for reliable delivery.
1102
+ Bridge.log("Nex: glass TX characteristic found (write type set per-fragment)")
1103
+ }
1104
+
1105
+ notifyChar?.let {
1106
+ mainNotifyChar = it
1107
+ enableNotification(gatt, it)
1108
+ Bridge.log("Nex: glass RX characteristic found")
1109
+ }
1110
+
1111
+ // Mark as connected but wait for setup below to update connection state
1112
+ isMainConnected = true
1113
+ Bridge.log("Nex: PROC_QUEUE - left side setup complete")
1114
+
1115
+ if (isMainConnected) {
1116
+ // Do first battery status query
1117
+ mainTaskHandler.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_BATTERY_QUERY, 10)
1118
+
1119
+ // Restore previous microphone state or disable if this is the first connection
1120
+ val shouldRestoreMic = microphoneStateBeforeDisconnection
1121
+ Bridge.log("Nex: `Restoring microphone state to: $shouldRestoreMic (previous state: $microphoneStateBeforeDisconnection)")
1122
+
1123
+ if (shouldRestoreMic) {
1124
+ startMicBeat(MICBEAT_INTERVAL_MS.toInt())
1125
+ }
1126
+
1127
+ // Enable our AugmentOS notification key
1128
+ sendWhiteListCommand(10)
1129
+
1130
+ showHomeScreen() // Turn on the NexGlasses display
1131
+ updateConnectionState()
1132
+
1133
+ // Post protobuf schema version information (only once)
1134
+ if (!protobufVersionPosted) {
1135
+ postProtobufSchemaVersionInfo()
1136
+ protobufVersionPosted = true
1137
+ }
1138
+
1139
+ // Query glasses protobuf version from firmware
1140
+ Bridge.log("=== SENDING GLASSES PROTOBUF VERSION REQUEST ===")
1141
+ val versionQueryPacket = NexProtobufUtils.generateVersionRequestCommandBytes()
1142
+ if (versionQueryPacket.isNotEmpty()) {
1143
+ sendProtobuf(versionQueryPacket, 100)
1144
+ Bridge.log("Sent glasses protobuf version request")
1145
+ } else {
1146
+ Bridge.log("Skipping version request: schema removed VersionRequest")
1147
+ }
1148
+
1149
+ // Push current glasses-side Voice Activity Detection setting
1150
+ sendVoiceActivityDetectionSetting()
1151
+ }
1152
+ } else {
1153
+ Log.e(TAG, " glass UART service not found")
1154
+ }
1155
+ }
1156
+
1157
+ private fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
1158
+ Bridge.log("Nex: PROC_QUEUE - Starting notification setup")
1159
+
1160
+ // Enable notifications
1161
+ val result = gatt.setCharacteristicNotification(characteristic, true)
1162
+ Bridge.log("Nex: PROC_QUEUE - setCharacteristicNotification result: $result")
1163
+
1164
+ // NOTE: write type is NOT set here. This receives the *notify*
1165
+ // characteristic, which the phone never writes to — setting writeType
1166
+ // on it had no effect. The write characteristic (mainWriteChar) is
1167
+ // configured for WRITE_TYPE_NO_RESPONSE at its assignment instead.
1168
+
1169
+ // Add delay
1170
+ Bridge.log("Nex: PROC_QUEUE - waiting to enable notification...")
1171
+ try {
1172
+ Thread.sleep(100)
1173
+ } catch (e: InterruptedException) {
1174
+ Log.e(TAG, "Error sending data: ${e.message}")
1175
+ }
1176
+
1177
+ // Get and configure descriptor
1178
+ val descriptor = characteristic.getDescriptor(NexBluetoothConstants.CLIENT_CHARACTERISTIC_CONFIG_UUID)
1179
+ descriptor?.let {
1180
+ it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
1181
+ val writeResult = gatt.writeDescriptor(it)
1182
+ Bridge.log("Nex: PROC_QUEUE - set descriptor with result: $writeResult")
1183
+ }
1184
+ }
1185
+
1186
+ private fun onCharacteristicChangedHandler(characteristic: BluetoothGattCharacteristic) {
1187
+ if (characteristic.uuid == NexBluetoothConstants.NOTIFY_CHAR_UUID) {
1188
+ val data = characteristic.value
1189
+ val deviceName = mainGlassGatt?.device?.name ?: return
1190
+ // NOTE: this runs on the main looper for every notification, audio
1191
+ // included (~50/sec). Do not log or stringify the whole packet to
1192
+ // hex on this hot path — Bridge.log crosses the RN bridge to JS and
1193
+ // packetHex is O(n) per packet. Both back up the audio queue at
1194
+ // screen-off. packetHex is only needed by the protobuf branch, so
1195
+ // compute it lazily there.
1196
+ if (data.isEmpty()) return
1197
+
1198
+ val packetType = data[0]
1199
+
1200
+ when (packetType) {
1201
+ NexBluetoothPacketTypes.PACKET_TYPE_JSON -> {
1202
+ val jsonData = data.copyOfRange(1, data.size)
1203
+ decodeJsons(jsonData)
1204
+ }
1205
+ NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF -> {
1206
+ val protobufData = data.copyOfRange(1, data.size)
1207
+ val packetHex = data.joinToString("") { "%02X".format(it) }
1208
+ decodeProtobufs(protobufData, packetHex)
1209
+ }
1210
+ NexBluetoothPacketTypes.PACKET_TYPE_AUDIO -> {
1211
+ // Check for audio packet header
1212
+ if (data[0] == 0xA0.toByte()) {
1213
+ val sequenceNumber = data[1]
1214
+ val receiveTime = System.currentTimeMillis()
1215
+
1216
+ // Basic sequence validation
1217
+ if (lastReceivedLc3Sequence != -1 && (lastReceivedLc3Sequence + 1).toByte() != sequenceNumber) {
1218
+ Bridge.log("LC3 packet sequence mismatch. Expected: ${lastReceivedLc3Sequence + 1}, Got: $sequenceNumber")
1219
+ }
1220
+ lastReceivedLc3Sequence = sequenceNumber.toInt()
1221
+
1222
+ val lc3Data = data.copyOfRange(2, data.size)
1223
+
1224
+ // No per-packet log here: this fires ~50x/sec and each
1225
+ // Bridge.log crosses the RN bridge to JS. (Matches G1.)
1226
+
1227
+ // Play LC3 audio directly through LC3 player.
1228
+ // No logging in any branch: this runs per audio packet
1229
+ // (~50/sec) and Bridge.log crosses the RN bridge to JS.
1230
+ // With isLc3AudioEnabled hardcoded off, the disabled
1231
+ // branch would otherwise log on every single packet.
1232
+ if (lc3AudioPlayer != null && isLc3AudioEnabled) {
1233
+ // Use the original packet format that LC3 player expects
1234
+ lc3AudioPlayer?.write(data, 0, data.size)
1235
+ }
1236
+
1237
+ DeviceManager.getInstance().handleGlassesMicData(lc3Data);
1238
+
1239
+ // Still decode for callback compatibility
1240
+ // TODO: (Verify) Commenting because commented in G1
1241
+ // if (lc3DecoderPtr != 0L) {
1242
+ // val pcmData = Lc3Cpp.decodeLC3(lc3DecoderPtr, lc3Data)
1243
+ // Bridge.log("pcmData size:${pcmData?.size ?: 0}")
1244
+ // audioProcessingCallback?.let { callback ->
1245
+ // if (!pcmData.isNullOrEmpty()) {
1246
+ // callback.onAudioDataAvailable(pcmData)
1247
+ // }
1248
+ // } ?: run {
1249
+ // // If we get here, it means the callback wasn't properly registered
1250
+ // Log.e(TAG,"Audio processing callback is null - callback registration failed!")
1251
+ // }
1252
+ // }
1253
+ }
1254
+ }
1255
+ NexBluetoothPacketTypes.PACKET_TYPE_IMAGE -> {
1256
+ // Handle image packet if needed
1257
+ }
1258
+ else -> {
1259
+ Bridge.log("unknown packetType: ${String.format("%02X ", packetType)}")
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ private fun attemptGattConnection(device: BluetoothDevice) {
1266
+ if (device == null) {
1267
+ Bridge.log("Cannot connect to GATT: Device is null")
1268
+ return
1269
+ }
1270
+
1271
+ val deviceName = device.name
1272
+ if (deviceName.isNullOrEmpty()) {
1273
+ Log.e(TAG,"Skipping null/empty device name: ${device.address}... this means something horrific has occurred. Look into this.")
1274
+ return
1275
+ }
1276
+
1277
+ Bridge.log("attemptGattConnection called for device: $deviceName (${device.address})")
1278
+
1279
+ DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTING)
1280
+ Bridge.log("Setting connectionState to CONNECTING. Notifying connectionEvent.")
1281
+ // connectionEvent(connectionState)
1282
+
1283
+ if (mainGlassGatt == null) {
1284
+ Bridge.log("Attempting GATT connection for Main Glass...")
1285
+ mainGlassGatt = device.connectGatt(context, false, mainGattCallback)
1286
+ isMainConnected = false
1287
+ Bridge.log("Main GATT connection initiated. isMainConnected set to false.")
1288
+ } else {
1289
+ Bridge.log("Main Glass GATT already exists")
1290
+ }
1291
+ }
1292
+
1293
+ private fun postProtobufSchemaVersionInfo() {
1294
+ try {
1295
+ // Call the version method only once
1296
+ val schemaVersion = NexProtobufUtils.getProtobufSchemaVersion(context!!)
1297
+
1298
+ // Build the info string directly instead of calling getProtobufBuildInfo()
1299
+ val fileDescriptorName = glassly.ble.GlasslyBle.getDescriptor().file.name
1300
+ val buildInfo = "Schema v$schemaVersion | $fileDescriptorName"
1301
+
1302
+ // protobufSchemaVersion = schemaVersion.toString()
1303
+
1304
+ // val event = ProtobufSchemaVersionEvent(
1305
+ // schemaVersion,
1306
+ // buildInfo,
1307
+ // smartGlassesDevice?.deviceModelName ?: "Unknown"
1308
+ // )
1309
+
1310
+ // EventBus.getDefault().post(event)
1311
+ Bridge.log("Posted protobuf schema version event: $buildInfo")
1312
+ } catch (e: Exception) {
1313
+ Log.e(TAG,"Error posting protobuf schema version event: ${e.message}", e)
1314
+ }
1315
+ }
1316
+
1317
+ private fun connectToSmartGlasses() {
1318
+ val deviceModelName = DeviceManager.getInstance().deviceName
1319
+ val deviceAddress = DeviceManager.getInstance().deviceAddress
1320
+
1321
+ // Register bonding receiver
1322
+ Bridge.log("connectToSmartGlasses start")
1323
+ Bridge.log("try to ConnectToSmartGlassesing deviceModelName: ${deviceModelName} deviceAddress: ${deviceAddress}")
1324
+ preferredMainDeviceId = DeviceManager.getInstance().deviceName
1325
+ if (!bluetoothAdapter.isEnabled) {
1326
+ return
1327
+ }
1328
+ when {
1329
+ !deviceModelName.isNullOrEmpty() && !deviceAddress.isNullOrEmpty() -> {
1330
+ stopScan()
1331
+ mainDevice = bluetoothAdapter.getRemoteDevice(deviceAddress)
1332
+ mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
1333
+ }
1334
+ savedNexMainAddress != null -> {
1335
+ mainDevice = bluetoothAdapter.getRemoteDevice(savedNexMainAddress)
1336
+ mainTaskHandler?.sendEmptyMessageDelayed(MAIN_TASK_HANDLER_CODE_RECONNECT_DEVICE, 0)
1337
+ }
1338
+ else -> {
1339
+ // Start scanning for devices
1340
+ stopScan()
1341
+ DeviceStore.apply("glasses", "connectionState", ConnTypes.SCANNING)
1342
+ // connectionEvent(connectionState) // TODO: Figure out where is connection event defined????
1343
+ startScan()
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ private fun destroy() {
1349
+ // Tell the glasses this teardown is intentional so they return to the welcome
1350
+ // screen immediately instead of holding the last frame through the firmware's
1351
+ // unexpected-disconnect grace period. Best-effort: if the write can't be sent
1352
+ // or doesn't flush in time, the firmware grace period is the safety net.
1353
+ val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
1354
+ if (!canSend) {
1355
+ performDestroy()
1356
+ return
1357
+ }
1358
+ Bridge.log("Nex: Sending DisconnectRequest before teardown")
1359
+ sendProtobuf(NexProtobufUtils.generateDisconnectRequestCommandBytes(), 100)
1360
+ mainTaskHandler.postDelayed({ performDestroy() }, DISCONNECT_FLUSH_DELAY_MS)
1361
+ }
1362
+
1363
+ private fun performDestroy() {
1364
+ Bridge.log("Nex: MentraNexSGC ONDESTROY")
1365
+ showHomeScreen()
1366
+ isKilled = true
1367
+ // stop BLE scanning
1368
+ stopScan()
1369
+ // Save current microphone state before destroying
1370
+ microphoneStateBeforeDisconnection = isMicrophoneEnabled
1371
+ Bridge.log("Nex: Saved microphone state during destroy: $microphoneStateBeforeDisconnection")
1372
+ // disable the microphone and stop sending micbeat
1373
+ stopMicBeat()
1374
+ // Stop periodic notifications
1375
+ stopPeriodicNotifications()
1376
+ mainGlassGatt?.let { gatt ->
1377
+ gatt.disconnect()
1378
+ gatt.close()
1379
+ mainGlassGatt = null
1380
+ }
1381
+ lc3AudioPlayer?.let { player ->
1382
+ try {
1383
+ player.stopPlay()
1384
+ Bridge.log("Nex: LC3 audio player stopped and cleaned up")
1385
+ } catch (e: Exception) {
1386
+ Log.e(TAG,"Nex: Error stopping LC3 audio player during destroy: ${e.message}")
1387
+ } finally {
1388
+ lc3AudioPlayer = null
1389
+ }
1390
+ }
1391
+ // Clean up handlers
1392
+ mainTaskHandler?.removeCallbacksAndMessages(null)
1393
+ whiteListHandler?.removeCallbacksAndMessages(null)
1394
+ micEnableHandler?.removeCallbacksAndMessages(null)
1395
+ notificationRunnable?.let {
1396
+ notificationHandler?.removeCallbacks(it)
1397
+ }
1398
+
1399
+ textWallRunnable?.let {
1400
+ textWallHandler?.removeCallbacks(it)
1401
+ }
1402
+ findCompatibleDevicesHandler?.removeCallbacksAndMessages(null)
1403
+ // Free LC3 decoder
1404
+ if (lc3DecoderPtr != 0L) {
1405
+ Lc3Cpp.freeDecoder(lc3DecoderPtr)
1406
+ lc3DecoderPtr = 0L
1407
+ }
1408
+ currentImageChunks.clear()
1409
+ isImageSendProgressing = false
1410
+ sendQueue.clear()
1411
+ // Add a dummy element to unblock the take() call if needed
1412
+ sendQueue.offer(emptyArray()) // is this needed?
1413
+ isWorkerRunning = false
1414
+ isMainConnected = false
1415
+ Bridge.log("Nex: MentraNexSGC cleanup complete")
1416
+ }
1417
+
1418
+ private fun startScan() {
1419
+ val scanner: BluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
1420
+ if (scanner == null) {
1421
+ Log.e(TAG,"BluetoothLeScanner not available.")
1422
+ return
1423
+ }
1424
+
1425
+ // Optionally, define filters if needed
1426
+ val filters = mutableListOf<ScanFilter>()
1427
+ // For example, to filter by device name:
1428
+ // filters.add(ScanFilter.Builder().setDeviceName("Even G1_").build())
1429
+
1430
+ // Set desired scan settings
1431
+ val settings = ScanSettings.Builder()
1432
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
1433
+ .build()
1434
+
1435
+ // Start scanning
1436
+ isScanning = true
1437
+ scanner.startScan(filters, settings, modernScanCallback)
1438
+ scanner.flushPendingScanResults(modernScanCallback)
1439
+ Bridge.log("CALL START SCAN - Started scanning for devices...")
1440
+
1441
+ // Ensure scanning state is immediately communicated to UI
1442
+ DeviceStore.apply("glasses", "connectionState", ConnTypes.SCANNING)
1443
+ // connectionEvent(connectionState)
1444
+
1445
+ // Stop the scan after some time (e.g., 10-15s instead of 60 to avoid
1446
+ // throttling)
1447
+ // handler.postDelayed({ stopScan() }, 10000)
1448
+ }
1449
+
1450
+ override fun stopScan() {
1451
+ val scanner = bluetoothAdapter.bluetoothLeScanner
1452
+ scanner?.stopScan(modernScanCallback)
1453
+ isScanning = false
1454
+ Bridge.log("Stopped scanning for devices")
1455
+
1456
+ if (bleScanCallback != null && isScanningForCompatibleDevices) {
1457
+ scanner?.stopScan(bleScanCallback)
1458
+ isScanningForCompatibleDevices = false
1459
+ }
1460
+ }
1461
+
1462
+ /**
1463
+ * Returns a colour-inverted copy of [src] (white <-> black). The firmware decoder normalises
1464
+ * BMP palette polarity, so the only way to flip the on-glass result from the SGC is to invert
1465
+ * the actual pixel content here.
1466
+ */
1467
+ private fun invertBitmap(src: Bitmap): Bitmap {
1468
+ val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
1469
+ val paint = Paint().apply {
1470
+ colorFilter = ColorMatrixColorFilter(
1471
+ ColorMatrix(
1472
+ floatArrayOf(
1473
+ -1f, 0f, 0f, 0f, 255f,
1474
+ 0f, -1f, 0f, 0f, 255f,
1475
+ 0f, 0f, -1f, 0f, 255f,
1476
+ 0f, 0f, 0f, 1f, 0f
1477
+ )
1478
+ )
1479
+ )
1480
+ }
1481
+ Canvas(out).drawBitmap(src, 0f, 0f, paint)
1482
+ return out
1483
+ }
1484
+
1485
+ /**
1486
+ * Floyd-Steinberg error-diffusion dithering to monochrome. Returns a copy of [src] whose pixels
1487
+ * are pure black or white but whose dot density approximates the original luminance, preserving
1488
+ * far more perceived detail on a 1-bpp panel than a flat threshold. Luminance uses Rec. 601
1489
+ * weights; quantization error is pushed to the right/below neighbours (7/3/5/1 over 16).
1490
+ */
1491
+ private fun floydSteinbergDither(src: Bitmap): Bitmap {
1492
+ val w = src.width
1493
+ val h = src.height
1494
+ val px = IntArray(w * h)
1495
+ src.getPixels(px, 0, w, 0, 0, w, h)
1496
+
1497
+ // Working luminance buffer (float so diffused error can over/undershoot 0..255).
1498
+ val lum = FloatArray(w * h)
1499
+ for (i in 0 until w * h) {
1500
+ val c = px[i]
1501
+ val r = (c shr 16) and 0xFF
1502
+ val g = (c shr 8) and 0xFF
1503
+ val b = c and 0xFF
1504
+ lum[i] = 0.299f * r + 0.587f * g + 0.114f * b
1505
+ }
1506
+
1507
+ for (y in 0 until h) {
1508
+ for (x in 0 until w) {
1509
+ val idx = y * w + x
1510
+ val old = lum[idx]
1511
+ val newVal = if (old < 128f) 0f else 255f
1512
+ val err = old - newVal
1513
+ px[idx] = if (newVal >= 128f) 0xFFFFFFFF.toInt() else 0xFF000000.toInt()
1514
+
1515
+ if (x + 1 < w) lum[idx + 1] += err * 7f / 16f
1516
+ if (y + 1 < h) {
1517
+ if (x - 1 >= 0) lum[idx + w - 1] += err * 3f / 16f
1518
+ lum[idx + w] += err * 5f / 16f
1519
+ if (x + 1 < w) lum[idx + w + 1] += err * 1f / 16f
1520
+ }
1521
+ }
1522
+ }
1523
+
1524
+ val out = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
1525
+ out.setPixels(px, 0, w, 0, 0, w, h)
1526
+ return out
1527
+ }
1528
+
1529
+ /** Stream 1-bit BMP pixels into an already-created canvas bitmap component [componentId]. */
1530
+ private fun streamBitmapPixels(componentId: Int, bmpData: ByteArray) {
1531
+ try {
1532
+ if (bmpData.isEmpty()) {
1533
+ Log.e(TAG, "Invalid BMP data provided")
1534
+ return
1535
+ }
1536
+ // 4-digit hex stream id per the canvas image protocol.
1537
+ val totalChunks = (bmpData.size + bmpChunkSize - 1) / bmpChunkSize
1538
+ val streamId = "%04X".format(random.nextInt(0x10000))
1539
+
1540
+ sendProtobuf(NexProtobufUtils.generateCanvasUpdateImageCommandBytes(
1541
+ componentId, streamId, totalChunks))
1542
+
1543
+ val chunks = NexProtobufUtils.createBmpChunksForNexGlasses(streamId, bmpData, totalChunks, bmpChunkSize)
1544
+ currentImageChunks.clear()
1545
+ currentImageChunks.addAll(chunks)
1546
+ sendDataSequentially(chunks)
1547
+ } catch (e: Exception) {
1548
+ Log.e(TAG, "Error streaming bitmap pixels: ${e.message}")
1549
+ }
1550
+ }
1551
+
1552
+ private fun stopPeriodicNotifications() {
1553
+ notificationRunnable?.let { runnable ->
1554
+ notificationHandler?.removeCallbacks(runnable)
1555
+ Bridge.log("Stopped periodic notifications")
1556
+ }
1557
+ }
1558
+ private fun startMicBeat(delay: Int) {
1559
+ Bridge.log("Nex: Starting micbeat")
1560
+ if (micBeatCount > 0) {
1561
+ stopMicBeat()
1562
+ }
1563
+ sendSetMicEnabled(true, 10)
1564
+ micBeatRunnable = Runnable {
1565
+ sendSetMicEnabled(shouldUseGlassesMic, 1)
1566
+ micBeatHandler.postDelayed(micBeatRunnable!!, MICBEAT_INTERVAL_MS)
1567
+ }
1568
+ micBeatHandler.postDelayed(micBeatRunnable!!, delay.toLong())
1569
+ }
1570
+
1571
+ private fun stopMicBeat() {
1572
+ sendSetMicEnabled(false, 10)
1573
+ if (micBeatHandler != null) {
1574
+ micBeatHandler.removeCallbacksAndMessages(null);
1575
+ micBeatHandler.removeCallbacksAndMessages(micBeatRunnable);
1576
+ micBeatRunnable = null;
1577
+ micBeatCount = 0;
1578
+ }
1579
+ }
1580
+
1581
+ private fun sendWhiteListCommand(delay: Int) {
1582
+ if (whiteListedAlready) return
1583
+
1584
+ whiteListedAlready = true
1585
+ Bridge.log("Nex: Sending whitelist command")
1586
+
1587
+ whiteListHandler.postDelayed({
1588
+ val chunks = NexProtobufUtils.getWhitelistChunks(maxChunkSize)
1589
+ sendDataSequentially(chunks)
1590
+
1591
+ // Uncomment if needed for debugging:
1592
+ // chunks.forEach { chunk ->
1593
+ // Bridge.log("Nex: Sending this chunk for white list: ${bytesToUtf8(chunk)}")
1594
+ // sendDataSequentially(chunk)
1595
+ // // Thread.sleep(150) // Uncomment if delay between chunks is needed
1596
+ // }
1597
+ }, delay.toLong())
1598
+ }
1599
+
1600
+ private fun showHomeScreen() {
1601
+ Bridge.log("Nex: showHomeScreen")
1602
+
1603
+ if (lastThingDisplayedWasAnImage) {
1604
+ // clearNexScreen()
1605
+ lastThingDisplayedWasAnImage = false
1606
+ }
1607
+ }
1608
+
1609
+ private fun updateConnectionState() {
1610
+ if (isMainConnected) {
1611
+ DeviceStore.apply("glasses", "connectionState", ConnTypes.CONNECTED)
1612
+ Bridge.log("Nex: Main glasses connected")
1613
+ lastConnectionTimestamp = System.currentTimeMillis()
1614
+ DeviceStore.apply("glasses", "fullyBooted", true)
1615
+ DeviceStore.apply("glasses", "connected", true)
1616
+ // Removed commented sleep code as it's not needed
1617
+ // connectionEvent(it)
1618
+ } else {
1619
+ DeviceStore.apply("glasses", "connectionState", ConnTypes.DISCONNECTED)
1620
+ Bridge.log("Nex: No Main glasses connected")
1621
+ DeviceStore.apply("glasses", "fullyBooted", false)
1622
+ DeviceStore.apply("glasses", "connected", false)
1623
+ // connectionEvent(it)
1624
+ }
1625
+ }
1626
+
1627
+ private fun decodeJsons(jsonBytes: ByteArray) {
1628
+ val jsonString = String(jsonBytes, Charset.defaultCharset())
1629
+ try {
1630
+ val commandObject = JSONObject(jsonString)
1631
+ when (val type = commandObject.getString("type")) {
1632
+ "image_transfer_complete" -> {
1633
+ // Handle image transfer complete
1634
+ }
1635
+ "disconnect" -> {
1636
+ // Handle disconnect
1637
+ }
1638
+ "request_battery_state" -> {
1639
+ // Handle battery state request
1640
+ }
1641
+ "charging_state" -> {
1642
+ // Handle charging state
1643
+ }
1644
+ "device_info" -> {
1645
+ // val deviceInfo = gson.fromJson(jsonString, DeviceInfo::class.java)
1646
+ }
1647
+ "enter_pairing_mode" -> {
1648
+ // Handle pairing mode
1649
+ }
1650
+ "request_head_position" -> {
1651
+ // Handle head position request
1652
+ }
1653
+ "set_head_up_angle" -> {
1654
+ // Handle head up angle
1655
+ }
1656
+ "ping" -> {
1657
+ // Handle ping
1658
+ }
1659
+ "vad_event" -> {
1660
+ // Handle VAD event
1661
+ }
1662
+ "imu_data" -> {
1663
+ // Handle IMU data
1664
+ }
1665
+ "button_event" -> {
1666
+ // Handle button event
1667
+ }
1668
+ "head_gesture" -> {
1669
+ // Handle head gesture
1670
+ }
1671
+ }
1672
+ } catch (e: Exception) {
1673
+ Log.e(TAG,"Error decoding JSON: ${e.message}")
1674
+ }
1675
+ }
1676
+
1677
+ private fun decodeProtobufs(protobufBytes: ByteArray, packetHex: String) {
1678
+ try {
1679
+ val glassesToPhone = GlassesToPhone.parseFrom(protobufBytes)
1680
+ val payloadCase = glassesToPhone.payloadCase.toString()
1681
+
1682
+ Bridge.log("decodeProtobufs glassesToPhone: $glassesToPhone")
1683
+ Bridge.log("decodeProtobufs glassesToPhone payloadCase: $payloadCase")
1684
+
1685
+ NexEventUtils.sendBleCommandReceivedEvent(payloadCase, packetHex, System.currentTimeMillis())
1686
+ // if (isDebugMode) {
1687
+ // EventBus.getDefault().post(BleCommandReceiver(payloadCase, packetHex))
1688
+ // }
1689
+
1690
+ when (glassesToPhone.payloadCase) {
1691
+ GlassesToPhone.PayloadCase.BATTERY_STATUS -> {
1692
+ val batteryStatus: BatteryStatus = glassesToPhone.batteryStatus
1693
+ DeviceStore.apply("glasses", "batteryLevel", batteryStatus.level)
1694
+ DeviceStore.apply("glasses", "charging", batteryStatus.charging)
1695
+ }
1696
+ GlassesToPhone.PayloadCase.CHARGING_STATE -> {
1697
+ val chargingState: ChargingState = glassesToPhone.chargingState
1698
+ DeviceStore.apply(
1699
+ "glasses",
1700
+ "charging",
1701
+ chargingState.state == ChargingState.State.CHARGING
1702
+ )
1703
+ }
1704
+ GlassesToPhone.PayloadCase.DEVICE_INFO -> {
1705
+ val deviceInfo: DeviceInfo = glassesToPhone.deviceInfo
1706
+ Bridge.log("deviceInfo: $deviceInfo")
1707
+ DeviceStore.apply("glasses", "firmwareVersion", deviceInfo.fwVersion)
1708
+ }
1709
+ GlassesToPhone.PayloadCase.HEAD_POSITION -> {
1710
+ val headPosition: HeadPosition = glassesToPhone.headPosition
1711
+ // EventBus.getDefault().post(HeadUpAngleEvent(headPosition.angle))
1712
+ Bridge.log("headPosition: $headPosition")
1713
+ }
1714
+ GlassesToPhone.PayloadCase.HEAD_UP_ANGLE_SET -> {
1715
+ val headUpAngleResponse: HeadUpAngleResponse = glassesToPhone.headUpAngleSet
1716
+ Bridge.log("headUpAngleResponse: $headUpAngleResponse")
1717
+ }
1718
+ GlassesToPhone.PayloadCase.VAD_EVENT -> {
1719
+ // val vadEvent = glassesToPhone.vadEvent
1720
+ // EventBus.getDefault().post(VadEvent(vadEvent.vad))
1721
+ }
1722
+ GlassesToPhone.PayloadCase.IMAGE_TRANSFER_COMPLETE -> {
1723
+ val transferComplete: ImageTransferComplete = glassesToPhone.imageTransferComplete
1724
+ Bridge.log("transferComplete: $transferComplete")
1725
+
1726
+ when (transferComplete.status) {
1727
+ ImageTransferComplete.Status.OK -> {
1728
+ currentImageChunks.clear()
1729
+ isImageSendProgressing = false
1730
+ }
1731
+ ImageTransferComplete.Status.INCOMPLETE -> {
1732
+ val missingChunksList = transferComplete.missingChunksList
1733
+ reSendImageMissingChunks(missingChunksList)
1734
+ }
1735
+ else -> {}
1736
+ }
1737
+ }
1738
+ GlassesToPhone.PayloadCase.IMU_DATA -> {
1739
+ val imuData: ImuData = glassesToPhone.imuData
1740
+ Bridge.log("imuData: $imuData")
1741
+ }
1742
+ GlassesToPhone.PayloadCase.BUTTON_EVENT -> {
1743
+ val buttonEvent: ButtonEvent = glassesToPhone.buttonEvent
1744
+ Bridge.log("buttonEvent: $buttonEvent")
1745
+ // buttonEvent.button.number
1746
+ // EventBus.getDefault().post(ButtonPressEvent(
1747
+ // smartGlassesDevice.deviceModelName,
1748
+ // buttonId,
1749
+ // pressType,
1750
+ // timestamp
1751
+ // ))
1752
+ }
1753
+ GlassesToPhone.PayloadCase.HEAD_GESTURE -> {
1754
+ val headGesture: HeadGesture = glassesToPhone.headGesture
1755
+ Bridge.log("headGesture: $headGesture")
1756
+ // Matches iOS: only the head-up gesture reaches the shared
1757
+ // store (iOS has no clearing path either).
1758
+ if (headGesture.gesture == HeadGesture.Gesture.HEAD_UP) {
1759
+ DeviceStore.apply("glasses", "headUp", true)
1760
+ }
1761
+ }
1762
+ GlassesToPhone.PayloadCase.CANVAS_RESULT -> {
1763
+ // Ack for CanvasCreateComponent / CanvasClear (updates are
1764
+ // unacked). Firmware codes: OK / INVALID (bad id for type) /
1765
+ // OVERSIZE (bitmap > 200x200) / OOM (no backing storage).
1766
+ // Consumed for observability — a silent blank screen becomes
1767
+ // a diagnosable log line. Non-OK codes also invalidate the
1768
+ // registry entry so the next frame recreates the component.
1769
+ val canvasResult = glassesToPhone.canvasResult
1770
+ if (canvasResult.code != glassly.ble.GlasslyBle.CanvasResult.ResultCode.OK) {
1771
+ Bridge.log("Nex: CANVAS_RESULT id=${canvasResult.id} code=${canvasResult.code} — dropping registry entry for recreate")
1772
+ canvasElements.entries.removeAll { it.value.firmwareId == canvasResult.id.toInt() }
1773
+ }
1774
+ }
1775
+ GlassesToPhone.PayloadCase.PAYLOAD_NOT_SET,
1776
+ null -> {
1777
+ // Do nothing
1778
+ }
1779
+ else -> {
1780
+
1781
+ }
1782
+ }
1783
+ } catch (e: InvalidProtocolBufferException) {
1784
+ Log.e(TAG,"Error decoding protobuf: ${e.message}")
1785
+ }
1786
+ }
1787
+
1788
+ private fun decodeProtobufsByWrite(protobufBytes: ByteArray, packetHex: String) {
1789
+ try {
1790
+ val phoneToGlasses = PhoneToGlasses.parseFrom(protobufBytes)
1791
+ Bridge.log("decodeProtobufsByWrite phoneToGlasses: $phoneToGlasses")
1792
+ Bridge.log("decodeProtobufsByWrite phoneToGlasses payloadCase: ${phoneToGlasses.payloadCase}")
1793
+ val payloadCase = phoneToGlasses.payloadCase.toString()
1794
+ NexEventUtils.sendBleCommandSentEvent(payloadCase, packetHex, System.currentTimeMillis())
1795
+ // if (isDebugMode) {
1796
+ // EventBus.getDefault().post(BleCommandSender(payloadCase, packetHex))
1797
+ // }
1798
+ } catch (e: Exception) {
1799
+ Log.e(TAG,"Error in decodeProtobufsByWrite: ${e.message}")
1800
+ }
1801
+ }
1802
+
1803
+ private fun reSendImageMissingChunks(missingChunksIndexList: List<Int>) {
1804
+ if (!isImageSendProgressing || currentImageChunks.isEmpty() || missingChunksIndexList.isEmpty()) {
1805
+ return
1806
+ }
1807
+ val missingChunks = missingChunksIndexList.map { index ->
1808
+ currentImageChunks[index]
1809
+ }
1810
+
1811
+ sendDataSequentially(missingChunks)
1812
+ }
1813
+
1814
+ private fun createTextWallChunksForNex(text: String): ByteArray {
1815
+ // Create the PhoneToGlasses using its builder and set the DisplayText
1816
+ return NexProtobufUtils.generateDisplayTextCommandBytes(text)
1817
+ }
1818
+
1819
+ private fun sendSetMicEnabled(enable: Boolean, delay: Long) {
1820
+ Bridge.log("Nex: setMicEnabled called with enable: $enable and delay: $delay")
1821
+ Bridge.log("Nex: Running set mic enabled: $enable")
1822
+ isMicrophoneEnabled = enable // Update the state tracker
1823
+ DeviceStore.apply("glasses", "micEnabled", enable)
1824
+
1825
+ micEnableHandler?.postDelayed({
1826
+ if (connectionState != ConnTypes.CONNECTED) {
1827
+ Bridge.log("Nex: Tryna start mic: Not connected to glasses")
1828
+ return@postDelayed
1829
+ }
1830
+ Bridge.log("Nex: === SENDING MICROPHONE STATE COMMAND TO GLASSES ===")
1831
+ val micConfigBytes = NexProtobufUtils.generateMicStateConfigCommandBytes(enable)
1832
+ sendProtobuf(micConfigBytes, 10) // wait some time to setup the mic
1833
+ Bridge.log("Nex: Sent MIC command: ${micConfigBytes.joinToString("") { "%02x".format(it) }}")
1834
+ }, delay)
1835
+ }
1836
+
1837
+ private fun queryBatteryStatus() {
1838
+ val batteryQueryPacket = NexProtobufUtils.generateBatteryStateRequestCommandBytes()
1839
+ sendProtobuf(batteryQueryPacket, 250)
1840
+ }
1841
+
1842
+ ///// PROCESSING THREAD /////////////////
1843
+
1844
+ // Start the worker thread if it's not already running
1845
+ @Synchronized
1846
+ private fun startWorkerIfNeeded() {
1847
+ if (!isWorkerRunning) {
1848
+ isWorkerRunning = true
1849
+ Thread({ processQueue() }, "MentraNexSGCProcessQueue").start()
1850
+ }
1851
+ }
1852
+
1853
+ // Non-blocking function to add new send request
1854
+ private fun sendDataSequentially(data: ByteArray) {
1855
+ val chunks = arrayOf(SendRequest(data))
1856
+ sendQueue.offer(chunks)
1857
+ startWorkerIfNeeded()
1858
+ }
1859
+
1860
+ // Non-blocking function to add new send request with wait time
1861
+ private fun sendDataSequentially(data: ByteArray, waitTime: Int) {
1862
+ val chunks = arrayOf(SendRequest(data, waitTime))
1863
+ sendQueue.offer(chunks)
1864
+ startWorkerIfNeeded()
1865
+ }
1866
+
1867
+ private fun sendDataSequentially(data: List<ByteArray>) {
1868
+ val chunks = Array(data.size) { i -> SendRequest(data[i]) }
1869
+ sendQueue.offer(chunks)
1870
+ startWorkerIfNeeded()
1871
+ }
1872
+
1873
+ /**
1874
+ * Frames a serialized protobuf message onto the 0x02 control channel and sends it.
1875
+ *
1876
+ * The message is split into fragments that each fit the negotiated MTU, prefixed with a
1877
+ * 4-byte transport header: [0x02][seq][totalChunks][chunkIndex]. The firmware reassembles
1878
+ * fragments by seq/index before decoding. Single-fragment messages (totalChunks == 1) are
1879
+ * decoded directly by the firmware fast path.
1880
+ *
1881
+ * @param protobufBytes raw serialized PhoneToGlasses bytes (no framing)
1882
+ * @param waitTime optional post-message delay in ms applied after the last fragment (-1 = none)
1883
+ * @param ack when true (default) each fragment is a write-with-response; captions pass false
1884
+ */
1885
+ private fun sendProtobuf(protobufBytes: ByteArray, waitTime: Int = -1, ack: Boolean = true) {
1886
+ val headerSize = 4 // [0x02][seq][totalChunks][chunkIndex]
1887
+ val maxFragment = (maxChunkSize - headerSize).coerceAtLeast(1)
1888
+ val totalChunks =
1889
+ if (protobufBytes.isEmpty()) 1 else (protobufBytes.size + maxFragment - 1) / maxFragment
1890
+
1891
+ if (totalChunks > 255) {
1892
+ Log.e(TAG, "Nex: Protobuf message too large to fragment ($totalChunks chunks) - dropping")
1893
+ return
1894
+ }
1895
+
1896
+ val seq = protobufSeq
1897
+ protobufSeq = (protobufSeq + 1) and 0xFF
1898
+
1899
+ val frames = Array(totalChunks) { index ->
1900
+ val start = index * maxFragment
1901
+ val end = minOf(start + maxFragment, protobufBytes.size)
1902
+ val fragLen = end - start
1903
+ val frame = ByteArray(headerSize + fragLen)
1904
+ frame[0] = NexBluetoothPacketTypes.PACKET_TYPE_PROTOBUF
1905
+ frame[1] = seq.toByte()
1906
+ frame[2] = totalChunks.toByte()
1907
+ frame[3] = index.toByte()
1908
+ if (fragLen > 0) {
1909
+ System.arraycopy(protobufBytes, start, frame, headerSize, fragLen)
1910
+ }
1911
+ // Carry the post-message delay on the final fragment only.
1912
+ SendRequest(frame, if (index == totalChunks - 1) waitTime else -1, ack)
1913
+ }
1914
+
1915
+ sendQueue.offer(frames)
1916
+ startWorkerIfNeeded()
1917
+ }
1918
+
1919
+ private fun processQueue() {
1920
+ // First wait until the services are ready to receive data
1921
+ Bridge.log("[BLE] PROC_QUEUE started — waiting for descriptor write (mainServicesWaiter)")
1922
+ try {
1923
+ mainServicesWaiter.waitWhileTrue()
1924
+ } catch (e: InterruptedException) {
1925
+ Log.e(TAG,"Nex: Interrupted waiting for descriptor writes: $e")
1926
+ }
1927
+ Bridge.log("[BLE] PROC_QUEUE ready — descriptor write done, entering send loop")
1928
+
1929
+ while (!isKilled) {
1930
+ try {
1931
+ // Make sure services are ready before processing requests
1932
+ mainServicesWaiter.waitWhileTrue()
1933
+
1934
+ // This will block until data is available
1935
+ val requests = sendQueue.take()
1936
+
1937
+ for (request in requests) {
1938
+ if (isKilled) {
1939
+ isWorkerRunning = false
1940
+ break
1941
+ }
1942
+
1943
+ try {
1944
+ // Force an initial delay so BLE gets all setup
1945
+ val timeSinceConnection = System.currentTimeMillis() - lastConnectionTimestamp
1946
+ if (timeSinceConnection < INITIAL_CONNECTION_DELAY_MS) {
1947
+ Thread.sleep(INITIAL_CONNECTION_DELAY_MS - timeSinceConnection)
1948
+ }
1949
+
1950
+ // Send to main glass
1951
+ val canSend = mainGlassGatt != null && mainWriteChar != null && isMainConnected
1952
+ var writeStarted = false
1953
+ if (canSend) {
1954
+ mainWaiter.setTrue()
1955
+ // Per-request write type: acked fragments go out WITH response
1956
+ // (firmware ATT-acks each), captions go out WITHOUT response for
1957
+ // throughput. Set on the shared char just before the write — the
1958
+ // worker is single-threaded so there's no concurrent write to race.
1959
+ mainWriteChar?.writeType = if (request.ack) {
1960
+ BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
1961
+ } else {
1962
+ BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
1963
+ }
1964
+ mainWriteChar?.value = request.data
1965
+ // Honor the return value: writeCharacteristic returns false when the
1966
+ // stack is busy / out of buffers, and in that case no callback fires.
1967
+ // Only wait for the callback if the write actually started, otherwise
1968
+ // the waiter would block until the next disconnect.
1969
+ val issued = mainGlassGatt?.writeCharacteristic(mainWriteChar) ?: false
1970
+ if (issued) {
1971
+ writeStarted = true
1972
+ lastSendTimestamp = System.currentTimeMillis()
1973
+ } else {
1974
+ mainWaiter.setFalse()
1975
+ Log.e(TAG, "[BLE] PROC_QUEUE writeCharacteristic returned false — stack busy, dropping fragment")
1976
+ }
1977
+ } else {
1978
+ Bridge.log("[BLE] PROC_QUEUE skipping write — not ready, packet dropped")
1979
+ }
1980
+
1981
+ if (writeStarted) {
1982
+ // Acked writes need to wait for the real ATT response (a
1983
+ // connection interval or two); no-response writes get the
1984
+ // near-instant fast-path bound.
1985
+ val timeout = if (request.ack) WRITE_ACK_TIMEOUT_MS else WRITE_CALLBACK_TIMEOUT_MS
1986
+ mainWaiter.waitWhileTrue(timeout)
1987
+ }
1988
+
1989
+
1990
+ // If the packet asked us to do a delay, then do it
1991
+ if (request.waitTime != -1) {
1992
+ Thread.sleep(request.waitTime.toLong())
1993
+ }
1994
+ } catch (e: InterruptedException) {
1995
+ Log.e(TAG,"Nex: Error sending data: ${e.message}")
1996
+ if (isKilled) break
1997
+ }
1998
+ }
1999
+ } catch (e: InterruptedException) {
2000
+ if (isKilled) {
2001
+ Bridge.log("Nex: Process queue thread interrupted - shutting down")
2002
+ break
2003
+ }
2004
+ Log.e(TAG,"Nex: Error in queue processing: ${e.message}")
2005
+ }
2006
+ }
2007
+
2008
+ Bridge.log("Nex: Process queue thread exiting")
2009
+ isWorkerRunning = false
2010
+ }
2011
+ }