@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,2653 @@
1
+ package com.glassly.bluetoothsdk.sgcs;
2
+
3
+ import android.bluetooth.BluetoothAdapter;
4
+ import android.bluetooth.BluetoothDevice;
5
+ import android.bluetooth.BluetoothGatt;
6
+ import android.bluetooth.BluetoothGattCallback;
7
+ import android.bluetooth.BluetoothGattCharacteristic;
8
+ import android.bluetooth.BluetoothGattDescriptor;
9
+ import android.bluetooth.BluetoothGattService;
10
+ import android.bluetooth.BluetoothProfile;
11
+ import android.bluetooth.le.BluetoothLeScanner;
12
+ import android.bluetooth.le.ScanCallback;
13
+ import android.bluetooth.le.ScanResult;
14
+ import android.bluetooth.le.ScanSettings;
15
+ import android.content.Context;
16
+ import android.os.Build;
17
+ import android.os.Handler;
18
+ import android.os.Looper;
19
+ import android.text.format.DateFormat;
20
+ import android.util.Log;
21
+ import com.google.protobuf.CodedInputStream;
22
+ import com.google.protobuf.CodedOutputStream;
23
+ import com.google.protobuf.WireFormat;
24
+ import com.glassly.bluetoothsdk.Bridge;
25
+ import com.glassly.bluetoothsdk.DeviceManager;
26
+ import com.glassly.bluetoothsdk.DeviceStore;
27
+ import com.glassly.bluetoothsdk.PhotoRequest;
28
+ import com.glassly.bluetoothsdk.sgcs.ar99.ota.Ar99OtaManager;
29
+ import com.glassly.bluetoothsdk.sgcs.ar99.ota.OtaGattTransport;
30
+ import com.glassly.bluetoothsdk.utils.ConnTypes;
31
+ import com.glassly.bluetoothsdk.utils.DeviceTypes;
32
+ import com.glassly.bluetoothsdk.utils.audio.Ar99OpusPcmDecoder;
33
+ import java.io.ByteArrayOutputStream;
34
+ import java.io.File;
35
+ import java.io.IOException;
36
+ import java.util.ArrayDeque;
37
+ import java.util.Arrays;
38
+ import java.util.Collections;
39
+ import java.util.Deque;
40
+ import java.util.HashMap;
41
+ import java.util.LinkedHashSet;
42
+ import java.util.TimeZone;
43
+ import java.util.UUID;
44
+ import java.util.concurrent.atomic.AtomicInteger;
45
+
46
+ public class Ar99 extends SGCManager {
47
+ private static final String TAG = "Ar99";
48
+
49
+ /** Use: {@code adb logcat -s Ar99Opus}. */
50
+ private static final String LOG_TAG_OPUS = "Ar99Opus";
51
+
52
+ // NOTE: UUIDs are taken from the reference implementation.
53
+ private static final UUID CTRL_SERVICE_UUID =
54
+ UUID.fromString("A72EC9A8-CF72-8544-AD96-D1481A02CE98");
55
+ private static final UUID CTRL_HOST_TO_SLAVE =
56
+ UUID.fromString("A72EC9A9-CF72-8544-AD96-D1481A02CE98");
57
+ private static final UUID CTRL_SLAVE_TO_HOST =
58
+ UUID.fromString("A72EC9AA-CF72-8544-AD96-D1481A02CE98");
59
+
60
+ private static final UUID OPUS_SERVICE_UUID =
61
+ UUID.fromString("F6B26AAD-41A5-93F5-F640-AC3BA3C73597");
62
+ private static final UUID OPUS_SLAVE_TO_HOST =
63
+ UUID.fromString("F6B26AAF-41A5-93F5-F640-AC3BA3C73597");
64
+
65
+ private static final UUID OTA_SERVICE_UUID =
66
+ UUID.fromString("e49a25f8-f69a-11e8-8eb2-f2801f1b9fd1");
67
+ private static final UUID OTA_WRITE_UUID =
68
+ UUID.fromString("e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1");
69
+ private static final UUID OTA_NOTIFY_UUID =
70
+ UUID.fromString("e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1");
71
+
72
+ private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID =
73
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
74
+
75
+ private static final int MAGIC_NUMBER = 0x1A2B3C4D;
76
+
77
+ private static final int FUNC_COMM = 1;
78
+ private static final int FUNC_TRANS = 3;
79
+ private static final int FUNC_COMM_DISPLAY = 7;
80
+
81
+ private static final int CMD_TRANS_DISPLAY_START = 100;
82
+ private static final int CMD_TRANS_DISPLAY_STOP = 101;
83
+ private static final int CMD_TRANS_DISPLAY_SET_CONTENT = 102;
84
+
85
+ private static final int CMD_DISPLAY_GET_DEVICEINFO = 100;
86
+ private static final int CMD_DISPLAY_SET_LANGUAGE = 131;
87
+ private static final int CMD_DISPLAY_GET_BRIGHT = 102;
88
+ private static final int CMD_DISPLAY_GET_BATTERY = 104;
89
+ private static final int CMD_COMM_SET_TIME = 31;
90
+ private static final int CMD_DISPLAY_SET_BRIGHT = 132;
91
+ private static final int CMD_DISPLAY_SET_AUTO_ADJUST_LIGHT = 138;
92
+
93
+ private static final int CMD_DISPLAY_CTRL_AUDIO_RECORD = 161;
94
+ private static final int CMD_DISPLAY_CTRL_FACTORY_RESET = 162;
95
+ private static final int CMD_DISPLAY_CTRL_SYS_FUNCTION = 164;
96
+ private static final int CMD_DISPLAY_CTRL_MINIMAL_TEXT_START = 181;
97
+ private static final int CMD_DISPLAY_CTRL_MINIMAL_TEXT_SET = 182;
98
+ private static final int CMD_DISPLAY_CTRL_MINIMAL_TEXT_CLEAR = 183;
99
+ private static final int CMD_DISPLAY_CTRL_MINIMAL_TEXT_STOP = 184;
100
+
101
+ private static final int TEXT_STREAM_STATE_BEGIN = 0;
102
+ private static final int TEXT_STREAM_STATE_UPDATE = 1;
103
+ private static final int TEXT_STREAM_STATE_END = 2;
104
+
105
+ private static final int TRANS_DISPLAY_SOURCE_LANGUAGE = 0;
106
+ private static final int TRANS_DISPLAY_TARGET_LANGUAGE = 1;
107
+ private static final int TRANS_DISPLAY_MIC_SOURCE_PHONE = 0;
108
+ private static final int TRANS_DISPLAY_FONT_SIZE = 24;
109
+ private static final int DISPLAY_FUNCTION_TYPE_UI_MODE = 2;
110
+ private static final int DISPLAY_UI_MODE_MINIMAL = 1;
111
+ private static final int DISPLAY_LANGUAGE_ENGLISH = 1;
112
+ private static final int FACTORY_RESET_CONFIRM_CODE = 0xAA;
113
+
114
+ private static final int TARGET_MOBILE_APP = 0;
115
+ private static final int TARGET_GLASS_ANDROID = 1;
116
+ private static final int TARGET_GLASS_MCU_DISPLAY = 3;
117
+
118
+ private static final int CTRL_ACTION_RESPONSE = 0x40;
119
+
120
+ private static final long SCAN_DURATION_MS = 15_000L;
121
+ private static final long INITIAL_BATTERY_RETRY_DELAY_MS = 1_000L;
122
+ private static final int MAX_INITIAL_BATTERY_RETRIES = 5;
123
+ private static final long BATTERY_POLL_INTERVAL_MS = 60_000L;
124
+ private static final int BLE_WRITE_CHUNK_SIZE = 180;
125
+ private static final long BLE_WRITE_CHUNK_DELAY_MS = 10L;
126
+ private static final long READY_MINIMAL_MODE_DELAY_MS = 40L;
127
+ private static final long READY_LANGUAGE_DELAY_MS = 90L;
128
+ private static final long READY_DEVICE_INFO_DELAY_MS = 140L;
129
+ private static final long READY_BRIGHTNESS_DELAY_MS = 280L;
130
+ private static final long READY_BATTERY_DELAY_MS = 420L;
131
+ private static final long DISPLAY_SESSION_IDLE_RESTART_MS = 8_000L;
132
+ static final String[] SUPPORTED_PROJECT_NAMES = new String[] {"AR99"};
133
+
134
+ private static final int CTRL_SENDER_MASK = 0x03;
135
+
136
+ private final Context context;
137
+ private final BluetoothAdapter bluetoothAdapter;
138
+ private final Handler handler;
139
+ private final Handler batteryQueryHandler;
140
+ private final AtomicInteger sequence = new AtomicInteger(1);
141
+
142
+ private final LinkedHashSet<String> discoveredNames = new LinkedHashSet<>();
143
+ private final Deque<byte[]> writeQueue = new ArrayDeque<>();
144
+ private final Deque<byte[]> otaWriteQueue = new ArrayDeque<>();
145
+ private final Deque<BluetoothGattCharacteristic> notifyQueue = new ArrayDeque<>();
146
+ private byte[] controlNotifyBuffer = new byte[0];
147
+
148
+ private BluetoothLeScanner scanner;
149
+ private ScanCallback scanCallback;
150
+ private Runnable scanTimeoutRunnable;
151
+ private String targetIdentifier;
152
+ private String currentProjectName;
153
+ private String currentBroadcastMacAddress;
154
+
155
+ private BluetoothGatt controlGatt;
156
+ private BluetoothGattCharacteristic controlWriteCharacteristic;
157
+ private BluetoothGattCharacteristic controlNotifyCharacteristic;
158
+ private BluetoothGattCharacteristic opusNotifyCharacteristic;
159
+ private BluetoothGattCharacteristic otaWriteCharacteristic;
160
+ private BluetoothGattCharacteristic otaNotifyCharacteristic;
161
+
162
+ /** Lazily created; decodes 48-byte framed Opus from {@link #OPUS_SLAVE_TO_HOST} notifies. */
163
+ private Ar99OpusPcmDecoder opusPcmDecoder;
164
+
165
+ /** Reset in {@link #cleanupGatt}; first notify each session logs INFO with hex preview. */
166
+ private boolean opusLoggedFirstInSession;
167
+
168
+ private boolean isScanning = false;
169
+ @SuppressWarnings("unused")
170
+ private boolean connecting = false;
171
+ private boolean isWriteInFlight = false;
172
+ private boolean isOtaWriteInFlight = false;
173
+ private boolean otaWriteUsesNoResponseMode = false;
174
+ private boolean isOtaNotificationEnabled = false;
175
+ private boolean isNotifyWriteInFlight = false;
176
+ private boolean minimalModeReady = false;
177
+ private boolean displaySessionStarted = false;
178
+ private boolean awaitingMicDisableAck = false;
179
+ private long lastDisplayContentAtMs = 0L;
180
+ private boolean hasPendingDisplayUpdate = false;
181
+ private String pendingDisplayText = "";
182
+ private String lastDisplayText = "";
183
+ private boolean hasReceivedBatteryForCurrentConnection = false;
184
+ private int pendingBatteryRetries = 0;
185
+ private final Runnable writeDrainRunnable =
186
+ new Runnable() {
187
+ @Override
188
+ public void run() {
189
+ isWriteInFlight = false;
190
+ processNextWrite();
191
+ }
192
+ };
193
+
194
+ private final Runnable initialBatteryRetryRunnable =
195
+ new Runnable() {
196
+ @Override
197
+ public void run() {
198
+ if (controlGatt == null || !getConnected()) return;
199
+ if (hasReceivedBatteryForCurrentConnection
200
+ || pendingBatteryRetries >= MAX_INITIAL_BATTERY_RETRIES) {
201
+ return;
202
+ }
203
+
204
+ pendingBatteryRetries++;
205
+ Bridge.log(TAG + ": retrying battery query attempt " + pendingBatteryRetries);
206
+ getBatteryStatus();
207
+ batteryQueryHandler.postDelayed(this, INITIAL_BATTERY_RETRY_DELAY_MS);
208
+ }
209
+ };
210
+
211
+ private final Runnable periodicBatteryPollRunnable =
212
+ new Runnable() {
213
+ @Override
214
+ public void run() {
215
+ if (controlGatt == null || !getConnected()) return;
216
+ getBatteryStatus();
217
+ batteryQueryHandler.postDelayed(this, BATTERY_POLL_INTERVAL_MS);
218
+ }
219
+ };
220
+
221
+ private final Runnable readyDeviceInfoRunnable =
222
+ new Runnable() {
223
+ @Override
224
+ public void run() {
225
+ if (controlGatt == null || !getConnected()) return;
226
+ requestDeviceInfo();
227
+ }
228
+ };
229
+
230
+ private final Runnable readyMinimalModeRunnable =
231
+ new Runnable() {
232
+ @Override
233
+ public void run() {
234
+ if (controlGatt == null || !getConnected()) return;
235
+ setMinimalDisplayMode();
236
+ }
237
+ };
238
+
239
+ private final Runnable readyLanguageRunnable =
240
+ new Runnable() {
241
+ @Override
242
+ public void run() {
243
+ if (controlGatt == null || !getConnected()) return;
244
+ setDisplayLanguage(DISPLAY_LANGUAGE_ENGLISH);
245
+ }
246
+ };
247
+
248
+ private final Runnable readyBrightnessRunnable =
249
+ new Runnable() {
250
+ @Override
251
+ public void run() {
252
+ if (controlGatt == null || !getConnected()) return;
253
+ requestBrightness();
254
+ }
255
+ };
256
+
257
+ private final Runnable readyBatteryRunnable =
258
+ new Runnable() {
259
+ @Override
260
+ public void run() {
261
+ if (controlGatt == null || !getConnected()) return;
262
+ getBatteryStatus();
263
+ }
264
+ };
265
+
266
+ private final BluetoothGattCallback gattCallback =
267
+ new BluetoothGattCallback() {
268
+ @Override
269
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
270
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
271
+ Bridge.log(TAG + ": connected to " + (gatt.getDevice() != null ? gatt.getDevice().getName() : ""));
272
+ controlGatt = gatt;
273
+ gatt.discoverServices();
274
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
275
+ Bridge.log(TAG + ": disconnected status=" + status);
276
+ if (Ar99OtaManager.getInstance().isOTAInProgress()) {
277
+ Ar99OtaManager.getInstance().onBleDisconnected();
278
+ }
279
+ cleanupGatt();
280
+ updateConnectionState(ConnTypes.DISCONNECTED);
281
+ }
282
+ }
283
+
284
+ @Override
285
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
286
+ if (status != BluetoothGatt.GATT_SUCCESS) {
287
+ Bridge.log(TAG + ": service discovery failed status=" + status);
288
+ return;
289
+ }
290
+
291
+ BluetoothGattService controlService = gatt.getService(CTRL_SERVICE_UUID);
292
+ controlWriteCharacteristic = controlService != null ? controlService.getCharacteristic(CTRL_HOST_TO_SLAVE) : null;
293
+ controlNotifyCharacteristic = controlService != null ? controlService.getCharacteristic(CTRL_SLAVE_TO_HOST) : null;
294
+
295
+ BluetoothGattService opusService = gatt.getService(OPUS_SERVICE_UUID);
296
+ opusNotifyCharacteristic = opusService != null ? opusService.getCharacteristic(OPUS_SLAVE_TO_HOST) : null;
297
+
298
+ BluetoothGattService otaService = gatt.getService(OTA_SERVICE_UUID);
299
+ otaWriteCharacteristic = otaService != null ? otaService.getCharacteristic(OTA_WRITE_UUID) : null;
300
+ otaNotifyCharacteristic = otaService != null ? otaService.getCharacteristic(OTA_NOTIFY_UUID) : null;
301
+ isOtaNotificationEnabled = false;
302
+
303
+ notifyQueue.clear();
304
+ if (controlNotifyCharacteristic != null) notifyQueue.add(controlNotifyCharacteristic);
305
+ if (opusNotifyCharacteristic != null) notifyQueue.add(opusNotifyCharacteristic);
306
+
307
+ processNextNotifyDescriptor();
308
+
309
+ if (notifyQueue.isEmpty()) {
310
+ markReady();
311
+ }
312
+ }
313
+
314
+ @Override
315
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
316
+ isNotifyWriteInFlight = false;
317
+ if (status != BluetoothGatt.GATT_SUCCESS) {
318
+ Bridge.log(TAG + ": descriptor write failed status=" + status);
319
+ }
320
+ if (otaNotifyCharacteristic != null
321
+ && descriptor.getCharacteristic() != null
322
+ && OTA_NOTIFY_UUID.equals(descriptor.getCharacteristic().getUuid())) {
323
+ if (status == BluetoothGatt.GATT_SUCCESS) {
324
+ isOtaNotificationEnabled = true;
325
+ Ar99OtaManager.getInstance().onOtaNotifyEnabled();
326
+ }
327
+ }
328
+ processNextNotifyDescriptor();
329
+ }
330
+
331
+ @Override
332
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
333
+ if (characteristic != null && OTA_WRITE_UUID.equals(characteristic.getUuid())) {
334
+ synchronized (otaWriteQueue) {
335
+ if (otaWriteUsesNoResponseMode) {
336
+ return;
337
+ }
338
+ isOtaWriteInFlight = false;
339
+ }
340
+ if (status != BluetoothGatt.GATT_SUCCESS) {
341
+ Bridge.log(TAG + ": OTA characteristic write failed status=" + status);
342
+ }
343
+ processNextOtaWrite();
344
+ return;
345
+ }
346
+
347
+ handler.removeCallbacks(writeDrainRunnable);
348
+ isWriteInFlight = false;
349
+ if (status != BluetoothGatt.GATT_SUCCESS) {
350
+ Bridge.log(TAG + ": characteristic write failed status=" + status);
351
+ }
352
+ processNextWrite();
353
+ }
354
+
355
+ @Override
356
+ public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
357
+ byte[] data = characteristic.getValue();
358
+ if (data == null) return;
359
+
360
+ UUID uuid = characteristic.getUuid();
361
+ if (OTA_NOTIFY_UUID.equals(uuid)) {
362
+ Ar99OtaManager.getInstance().handleOTAResponse(data);
363
+ } else if (CTRL_SLAVE_TO_HOST.equals(uuid)) {
364
+ handleControlNotifyChunk(data);
365
+ } else if (OPUS_SLAVE_TO_HOST.equals(uuid)) {
366
+ handleOpusData(data);
367
+ }
368
+ }
369
+
370
+ @Override
371
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
372
+ if (Ar99OtaManager.getInstance().handleGattMtuChangedForOta(status)) {
373
+ return;
374
+ }
375
+ super.onMtuChanged(gatt, mtu, status);
376
+ }
377
+ };
378
+
379
+ public Ar99() {
380
+ context = Bridge.getContext();
381
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
382
+ handler = new Handler(Looper.getMainLooper());
383
+ batteryQueryHandler = new Handler(Looper.getMainLooper());
384
+
385
+ this.type = DeviceTypes.AR99;
386
+ this.hasMic = true;
387
+
388
+ DeviceStore.INSTANCE.apply("glasses", "micEnabled", false);
389
+ Ar99OtaManager.getInstance()
390
+ .setTransport(
391
+ new OtaGattTransport() {
392
+ @Override
393
+ public boolean enableOtaNotification() {
394
+ return Ar99.this.enableOtaNotification();
395
+ }
396
+
397
+ @Override
398
+ public void sendOtaData(byte[] data) {
399
+ Ar99.this.sendOtaData(data);
400
+ }
401
+
402
+ @Override
403
+ public void requestMtu(int mtu) {
404
+ BluetoothGatt gatt = controlGatt;
405
+ if (gatt != null) {
406
+ gatt.requestMtu(mtu);
407
+ }
408
+ }
409
+
410
+ @Override
411
+ public boolean isBleConnected() {
412
+ return controlGatt != null
413
+ && controlWriteCharacteristic != null
414
+ && otaWriteCharacteristic != null
415
+ && otaNotifyCharacteristic != null;
416
+ }
417
+ });
418
+ }
419
+
420
+ @Override
421
+ public void setMicEnabled(boolean enabled) {
422
+ if (enabled) {
423
+ awaitingMicDisableAck = false;
424
+ DeviceStore.INSTANCE.apply("glasses", "micEnabled", true);
425
+ } else {
426
+ awaitingMicDisableAck = true;
427
+ }
428
+ sendControlDisplayAudioRecord(enabled);
429
+ }
430
+
431
+ @Override
432
+ public java.util.List<String> sortMicRanking(java.util.List<String> list) {
433
+ return list;
434
+ }
435
+
436
+ @Override
437
+ public void requestPhoto(PhotoRequest request) {
438
+ Bridge.log(TAG + ": requestPhoto not implemented for AR99 yet");
439
+ }
440
+
441
+ @Override
442
+ public void startStream(java.util.Map<String, Object> message) {}
443
+
444
+ @Override
445
+ public void stopStream() {}
446
+
447
+ @Override
448
+ public void sendStreamKeepAlive(java.util.Map<String, Object> message) {}
449
+
450
+ @Override
451
+ public void startVideoRecording(String requestId, boolean save, boolean sound) {}
452
+
453
+ @Override
454
+ public void stopVideoRecording(String requestId) {}
455
+
456
+ @Override
457
+ public void sendButtonPhotoSettings() {}
458
+
459
+ public void sendButtonModeSetting() {}
460
+
461
+ @Override
462
+ public void sendButtonVideoRecordingSettings() {}
463
+
464
+ @Override
465
+ public void sendButtonMaxRecordingTime() {}
466
+
467
+ public void sendButtonCameraLedSetting() {}
468
+
469
+ @Override
470
+ public void sendCameraFovSetting() {}
471
+
472
+ @Override
473
+ public void setBrightness(int level, boolean autoMode) {
474
+ int brightness = Math.max(0, Math.min(100, level));
475
+ enqueueMessage(
476
+ FUNC_COMM_DISPLAY,
477
+ CMD_DISPLAY_SET_AUTO_ADJUST_LIGHT,
478
+ buildProtoMessageBool(1, autoMode),
479
+ TARGET_GLASS_MCU_DISPLAY);
480
+ enqueueMessage(
481
+ FUNC_COMM_DISPLAY,
482
+ CMD_DISPLAY_SET_BRIGHT,
483
+ buildProtoMessageUInt32(1, brightness),
484
+ TARGET_GLASS_MCU_DISPLAY);
485
+ }
486
+
487
+ @Override
488
+ public void clearDisplay() {
489
+ clearMinimalText();
490
+ }
491
+
492
+ @Override
493
+ public void sendTextWall(String text) {
494
+ sendMinimalText(text);
495
+ }
496
+
497
+ @Override
498
+ public void sendText(String text) {
499
+ sendTextWall(text);
500
+ }
501
+
502
+ @Override
503
+ public void sendDoubleTextWall(String top, String bottom) {
504
+ sendMinimalText(joinDisplayLines(top, bottom));
505
+ }
506
+
507
+ @Override
508
+ public boolean displayBitmap(
509
+ String base64ImageData, Integer x, Integer y, Integer width, Integer height) {
510
+ return displayBitmap(base64ImageData);
511
+ }
512
+
513
+ public boolean displayBitmap(String base64ImageData) {
514
+ return false;
515
+ }
516
+
517
+ @Override
518
+ public void showDashboard() {}
519
+
520
+ @Override
521
+ public void setDashboardPosition(int height, int depth) {}
522
+
523
+ @Override
524
+ public void setHeadUpAngle(int angle) {}
525
+
526
+ @Override
527
+ public void getBatteryStatus() {
528
+ enqueueMessage(
529
+ FUNC_COMM_DISPLAY,
530
+ CMD_DISPLAY_GET_BATTERY,
531
+ new byte[0],
532
+ TARGET_GLASS_MCU_DISPLAY);
533
+ }
534
+
535
+ @Override
536
+ public void setSilentMode(boolean enabled) {}
537
+
538
+ @Override
539
+ public void exit() {}
540
+
541
+ @Override
542
+ public void sendShutdown() {}
543
+
544
+ @Override
545
+ public void sendReboot() {}
546
+
547
+ public void sendFactoryReset() {
548
+ Bridge.log(TAG + ": sending factory reset");
549
+ enqueueMessage(
550
+ FUNC_COMM_DISPLAY,
551
+ CMD_DISPLAY_CTRL_FACTORY_RESET,
552
+ buildProtoMessageUInt32(1, FACTORY_RESET_CONFIRM_CODE),
553
+ TARGET_GLASS_MCU_DISPLAY);
554
+ }
555
+
556
+ @Override
557
+ public void sendRgbLedControl(
558
+ String requestId,
559
+ String packageName,
560
+ String action,
561
+ String color,
562
+ int ontime,
563
+ int offtime,
564
+ int count) {}
565
+
566
+ @Override
567
+ public void disconnect() {
568
+ stopScan();
569
+ cleanupGatt();
570
+ updateConnectionState(ConnTypes.DISCONNECTED);
571
+ }
572
+
573
+ @Override
574
+ public void forget() {
575
+ targetIdentifier = null;
576
+ disconnect();
577
+ }
578
+
579
+ @Override
580
+ public void findCompatibleDevices() {
581
+ discoveredNames.clear();
582
+ startScan(false);
583
+ }
584
+
585
+ @Override
586
+ public void connectById(String id) {
587
+ targetIdentifier = id != null ? id.trim() : null;
588
+ discoveredNames.clear();
589
+ startScan(true);
590
+ }
591
+
592
+ @Override
593
+ public String getConnectedBluetoothName() {
594
+ if (controlGatt != null && controlGatt.getDevice() != null && controlGatt.getDevice().getName() != null) {
595
+ return controlGatt.getDevice().getName();
596
+ }
597
+ return "";
598
+ }
599
+
600
+ @Override
601
+ public void cleanup() {
602
+ disconnect();
603
+ }
604
+
605
+ @Override
606
+ public void ping() {
607
+ requestDeviceInfo();
608
+ }
609
+
610
+ @Override
611
+ public void dbg1() {}
612
+
613
+ @Override
614
+ public void dbg2() {}
615
+
616
+ @Override
617
+ public void requestWifiScan(String scanId) {}
618
+
619
+ @Override
620
+ public void sendWifiCredentials(String ssid, String password) {}
621
+
622
+ @Override
623
+ public void forgetWifiNetwork(String ssid) {}
624
+
625
+ @Override
626
+ public void sendHotspotState(boolean enabled) {}
627
+
628
+ @Override
629
+ public void sendUserEmailToGlasses(String email) {}
630
+
631
+ @Override
632
+ public void sendIncidentId(String incidentId, String apiBaseUrl) {
633
+ Bridge.log(TAG + ": sendIncidentId not implemented for AR99 yet");
634
+ }
635
+
636
+ @Override
637
+ public void queryGalleryStatus() {}
638
+
639
+ @Override
640
+ public void sendGalleryMode() {}
641
+
642
+ @Override
643
+ public void requestVersionInfo() {
644
+ requestDeviceInfo();
645
+ }
646
+
647
+ private void startScan(boolean forConnection) {
648
+ if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
649
+ Bridge.log(TAG + ": Bluetooth unavailable or disabled");
650
+ return;
651
+ }
652
+
653
+ stopScan();
654
+
655
+ scanner = bluetoothAdapter.getBluetoothLeScanner();
656
+ if (scanner == null) {
657
+ Bridge.log(TAG + ": BLE scanner unavailable");
658
+ return;
659
+ }
660
+
661
+ updateConnectionState(ConnTypes.SCANNING);
662
+ connecting = forConnection;
663
+ isScanning = true;
664
+
665
+ scanCallback =
666
+ new ScanCallback() {
667
+ @Override
668
+ public void onScanResult(int callbackType, ScanResult result) {
669
+ handleScanResult(result, forConnection);
670
+ }
671
+
672
+ @Override
673
+ public void onScanFailed(int errorCode) {
674
+ Bridge.log(TAG + ": scan failed with code=" + errorCode);
675
+ stopScan();
676
+ if (ConnTypes.SCANNING.equals(getConnectionState())) {
677
+ updateConnectionState(ConnTypes.DISCONNECTED);
678
+ }
679
+ }
680
+ };
681
+
682
+ ScanSettings settings =
683
+ new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
684
+
685
+ BluetoothLeScanner localScanner = scanner;
686
+ ScanCallback localCallback = scanCallback;
687
+ if (localScanner == null || localCallback == null) return;
688
+
689
+ localScanner.startScan(Collections.emptyList(), settings, localCallback);
690
+ scanTimeoutRunnable = this::stopScan;
691
+ handler.postDelayed(scanTimeoutRunnable, SCAN_DURATION_MS);
692
+ }
693
+
694
+ @Override
695
+ public void stopScan() {
696
+ if (scanTimeoutRunnable != null) {
697
+ handler.removeCallbacks(scanTimeoutRunnable);
698
+ scanTimeoutRunnable = null;
699
+ }
700
+ if (!isScanning) return;
701
+
702
+ ScanCallback cb = scanCallback;
703
+ if (scanner != null && cb != null) {
704
+ try {
705
+ scanner.stopScan(cb);
706
+ } catch (Throwable t) {
707
+ // ignore
708
+ }
709
+ }
710
+
711
+ isScanning = false;
712
+ scanCallback = null;
713
+ scanner = null;
714
+ }
715
+
716
+ private void handleScanResult(ScanResult result, boolean forConnection) {
717
+ if (result == null || result.getDevice() == null) return;
718
+ BluetoothDevice device = result.getDevice();
719
+
720
+ String name = device.getName();
721
+ ParsedAdvertisement advertisement = parseAdvertisement(result.getScanRecord() != null ? result.getScanRecord().getBytes() : null);
722
+ if (name == null) name = advertisement != null ? advertisement.bleName : null;
723
+ if (name == null) return;
724
+
725
+ if (!matchesAr99(name, advertisement != null ? advertisement.projectName : null)) return;
726
+
727
+ String displayName = buildDisplayName(name, advertisement, device.getAddress());
728
+
729
+ if (!forConnection) {
730
+ if (discoveredNames.add(displayName)) {
731
+ Bridge.sendDiscoveredDevice(
732
+ type,
733
+ displayName,
734
+ advertisement != null && advertisement.bleAddress != null && !advertisement.bleAddress.trim().isEmpty() ? advertisement.bleAddress.trim() : "",
735
+ result.getRssi(),
736
+ advertisement != null ? advertisement.projectName : null);
737
+ }
738
+ return;
739
+ }
740
+ if (advertisement != null && advertisement.projectName != null && !advertisement.projectName.trim().isEmpty()) {
741
+ currentProjectName = advertisement.projectName.trim();
742
+ currentBroadcastMacAddress = advertisement.bleAddress != null && !advertisement.bleAddress.trim().isEmpty()
743
+ ? advertisement.bleAddress.trim()
744
+ : null;
745
+ }
746
+
747
+ String target = targetIdentifier != null ? targetIdentifier : "";
748
+ String normalizedTarget = normalizeDisplayIdentifier(target);
749
+ String targetSerial = extractTargetSerial(target);
750
+ boolean matchesTarget =
751
+ target.isEmpty()
752
+ || name.equalsIgnoreCase(target)
753
+ || displayName.equalsIgnoreCase(target)
754
+ || (device.getAddress() != null
755
+ && (device.getAddress().equalsIgnoreCase(target)
756
+ || device.getAddress().equalsIgnoreCase(normalizedTarget)))
757
+ || (advertisement != null
758
+ && ((advertisement.bleAddress != null
759
+ && (advertisement.bleAddress.equalsIgnoreCase(target)
760
+ || advertisement.bleAddress.equalsIgnoreCase(normalizedTarget)))
761
+ || (advertisement.btAddress != null
762
+ && (advertisement.btAddress.equalsIgnoreCase(target)
763
+ || advertisement.btAddress.equalsIgnoreCase(normalizedTarget)))
764
+ || (advertisement.serialNumber != null
765
+ && (advertisement.serialNumber.equalsIgnoreCase(target)
766
+ || advertisement.serialNumber.equalsIgnoreCase(normalizedTarget)))
767
+ || (advertisement.serialNumber != null && advertisement.serialNumber.equalsIgnoreCase(targetSerial))));
768
+
769
+ if (!matchesTarget) return;
770
+
771
+ String reconnectAddress =
772
+ advertisement != null && advertisement.bleAddress != null && !advertisement.bleAddress.trim().isEmpty()
773
+ ? advertisement.bleAddress.trim()
774
+ : device.getAddress();
775
+ if (reconnectAddress != null && !reconnectAddress.trim().isEmpty()) {
776
+ DeviceManager.getInstance().setDeviceAddress(reconnectAddress);
777
+ }
778
+
779
+ stopScan();
780
+ connectGatt(device);
781
+ }
782
+
783
+ private void connectGatt(BluetoothDevice device) {
784
+ cleanupGatt();
785
+ updateConnectionState(ConnTypes.CONNECTING);
786
+ connecting = true;
787
+
788
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
789
+ controlGatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE);
790
+ } else {
791
+ controlGatt = device.connectGatt(context, false, gattCallback);
792
+ }
793
+ }
794
+
795
+ private void processNextNotifyDescriptor() {
796
+ BluetoothGatt gatt = controlGatt;
797
+ if (gatt == null) return;
798
+ if (isNotifyWriteInFlight) return;
799
+
800
+ BluetoothGattCharacteristic characteristic = notifyQueue.pollFirst();
801
+ if (characteristic == null) {
802
+ markReady();
803
+ Ar99OtaManager.getInstance().tryResumeOtaAfterGattReady();
804
+ return;
805
+ }
806
+
807
+ gatt.setCharacteristicNotification(characteristic, true);
808
+
809
+ BluetoothGattDescriptor descriptor = null;
810
+ if (characteristic.getDescriptors() != null) {
811
+ for (BluetoothGattDescriptor d : characteristic.getDescriptors()) {
812
+ if (d != null && CLIENT_CHARACTERISTIC_CONFIG_UUID.equals(d.getUuid())) {
813
+ descriptor = d;
814
+ break;
815
+ }
816
+ }
817
+ }
818
+
819
+ if (descriptor == null) {
820
+ processNextNotifyDescriptor();
821
+ return;
822
+ }
823
+
824
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
825
+ isNotifyWriteInFlight = gatt.writeDescriptor(descriptor);
826
+
827
+ if (!isNotifyWriteInFlight) {
828
+ Bridge.log(TAG + ": failed to start descriptor write for " + characteristic.getUuid());
829
+ processNextNotifyDescriptor();
830
+ }
831
+ }
832
+
833
+ public boolean startOtaFromFile(String path) {
834
+ if (path == null || path.trim().isEmpty()) {
835
+ sendAr99OtaStatus("failed", 0, 0, 0, "Firmware path is empty");
836
+ return false;
837
+ }
838
+ Ar99OtaManager manager = Ar99OtaManager.getInstance();
839
+ // Match the validated c100_client flow: if the user manually restarts OTA after a
840
+ // BLE disconnect, abandon the paused/in-progress native session first so the fresh
841
+ // start call is not rejected as "already in progress".
842
+ manager.setCallback(null);
843
+ if (manager.isOTAInProgress()) {
844
+ manager.cancelOTA();
845
+ }
846
+ manager.setCallback(
847
+ new Ar99OtaManager.OTACallback() {
848
+ private int lastOffset = 0;
849
+ private int lastTotal = 0;
850
+
851
+ @Override
852
+ public void onProgress(int offset, int total, int progress) {
853
+ lastOffset = offset;
854
+ lastTotal = total;
855
+ sendAr99OtaStatus("transferring", progress, offset, total, null);
856
+ }
857
+
858
+ @Override
859
+ public void onCompleted(boolean needReboot) {
860
+ sendAr99OtaStatus("success", 100, lastTotal, lastTotal, null);
861
+ manager.setCallback(null);
862
+ }
863
+
864
+ @Override
865
+ public void onError(int errorCode, String message) {
866
+ sendAr99OtaStatus("failed", manager.getProgress(), lastOffset, lastTotal, message);
867
+ manager.setCallback(null);
868
+ }
869
+
870
+ @Override
871
+ public void onCancelled() {
872
+ sendAr99OtaStatus("cancelled", manager.getProgress(), lastOffset, lastTotal, null);
873
+ manager.setCallback(null);
874
+ }
875
+
876
+ @Override
877
+ public void onPausedWaitingReconnect() {
878
+ sendAr99OtaStatus(
879
+ "paused", manager.getProgress(), lastOffset, lastTotal, "Waiting for reconnect");
880
+ }
881
+ });
882
+ sendAr99OtaStatus("preparing", 0, 0, 0, null);
883
+ boolean started = manager.startOTA(new File(path));
884
+ if (!started) {
885
+ sendAr99OtaStatus("failed", 0, 0, 0, "Unable to start AR99 OTA");
886
+ manager.setCallback(null);
887
+ }
888
+ return started;
889
+ }
890
+
891
+ public void cancelAr99Ota() {
892
+ Ar99OtaManager.getInstance().cancelOTA();
893
+ }
894
+
895
+ private boolean enableOtaNotification() {
896
+ BluetoothGatt gatt = controlGatt;
897
+ BluetoothGattCharacteristic characteristic = otaNotifyCharacteristic;
898
+ if (gatt == null || characteristic == null) return false;
899
+ if (isOtaNotificationEnabled) {
900
+ Ar99OtaManager.getInstance().onOtaNotifyEnabled();
901
+ return true;
902
+ }
903
+ BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
904
+ if (descriptor == null) return false;
905
+ gatt.setCharacteristicNotification(characteristic, true);
906
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
907
+ return gatt.writeDescriptor(descriptor);
908
+ }
909
+
910
+ private void sendOtaData(byte[] data) {
911
+ if (data == null || data.length == 0) return;
912
+ if (controlGatt == null || otaWriteCharacteristic == null || !isOtaNotificationEnabled) return;
913
+ synchronized (otaWriteQueue) {
914
+ otaWriteQueue.add(data);
915
+ if (!isOtaWriteInFlight) {
916
+ processNextOtaWrite();
917
+ }
918
+ }
919
+ }
920
+
921
+ private void processNextOtaWrite() {
922
+ synchronized (otaWriteQueue) {
923
+ BluetoothGatt gatt = controlGatt;
924
+ BluetoothGattCharacteristic characteristic = otaWriteCharacteristic;
925
+ if (gatt == null || characteristic == null || isOtaWriteInFlight) return;
926
+ byte[] next = otaWriteQueue.pollFirst();
927
+ if (next == null) return;
928
+
929
+ isOtaWriteInFlight = true;
930
+ int props = characteristic.getProperties();
931
+ int writeType =
932
+ (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0
933
+ ? BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
934
+ : BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT;
935
+ characteristic.setWriteType(writeType);
936
+ otaWriteUsesNoResponseMode = writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;
937
+ characteristic.setValue(next);
938
+ boolean started = gatt.writeCharacteristic(characteristic);
939
+ if (!started) {
940
+ isOtaWriteInFlight = false;
941
+ otaWriteUsesNoResponseMode = false;
942
+ otaWriteQueue.addFirst(next);
943
+ handler.postDelayed(this::processNextOtaWrite, 15L);
944
+ } else if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
945
+ isOtaWriteInFlight = false;
946
+ handler.post(this::processNextOtaWrite);
947
+ }
948
+ }
949
+ }
950
+
951
+
952
+ private void sendAr99OtaStatus(String phase, int progress, int offset, int total, String errorMessage) {
953
+ java.util.HashMap<String, Object> body = new java.util.HashMap<>();
954
+ body.put("type", "ar99_ota_status");
955
+ body.put("phase", phase);
956
+ body.put("progress", Math.max(0, Math.min(100, progress)));
957
+ body.put("offset", offset);
958
+ body.put("total", total);
959
+ if (errorMessage != null && !errorMessage.isEmpty()) {
960
+ body.put("errorMessage", errorMessage);
961
+ body.put("error_message", errorMessage);
962
+ }
963
+ Bridge.sendTypedMessage("ar99_ota_status", body);
964
+ }
965
+
966
+ private void enqueueMessage(int function, int cmd, byte[] payload, int receiver) {
967
+ if (payload == null) payload = new byte[0];
968
+ byte[] packet = buildHeader(function, cmd, payload, TARGET_MOBILE_APP, receiver, false);
969
+ MessageHeader header = parseHeader(packet);
970
+ if (header != null) {
971
+ logSend(
972
+ ">>>"
973
+ + bytesToHex(packet)
974
+ + ",len="
975
+ + packet.length
976
+ + " function="
977
+ + header.function
978
+ + " cmd="
979
+ + header.cmd);
980
+ }
981
+
982
+ for (int start = 0; start < packet.length; start += BLE_WRITE_CHUNK_SIZE) {
983
+ int end = Math.min(packet.length, start + BLE_WRITE_CHUNK_SIZE);
984
+ writeQueue.add(Arrays.copyOfRange(packet, start, end));
985
+ }
986
+ processNextWrite();
987
+ }
988
+
989
+ private void processNextWrite() {
990
+ BluetoothGatt gatt = controlGatt;
991
+ if (gatt == null) return;
992
+ BluetoothGattCharacteristic characteristic = controlWriteCharacteristic;
993
+ if (characteristic == null) return;
994
+ if (isWriteInFlight) return;
995
+
996
+ byte[] packet = writeQueue.pollFirst();
997
+ if (packet == null) return;
998
+
999
+ characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
1000
+ characteristic.setValue(packet);
1001
+
1002
+ isWriteInFlight = gatt.writeCharacteristic(characteristic);
1003
+ if (!isWriteInFlight) {
1004
+ writeQueue.addFirst(packet);
1005
+ logSend("writeCharacteristic(false) requeue chunk len=" + packet.length);
1006
+ handler.removeCallbacks(writeDrainRunnable);
1007
+ handler.postDelayed(writeDrainRunnable, BLE_WRITE_CHUNK_DELAY_MS * 2);
1008
+ return;
1009
+ }
1010
+ handler.removeCallbacks(writeDrainRunnable);
1011
+ handler.postDelayed(writeDrainRunnable, BLE_WRITE_CHUNK_DELAY_MS);
1012
+ }
1013
+
1014
+ private byte[] buildHeader(
1015
+ int function,
1016
+ int cmd,
1017
+ byte[] payload,
1018
+ int sender,
1019
+ int receiver,
1020
+ boolean isResponse) {
1021
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1022
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1023
+
1024
+ int ctrl =
1025
+ (sender & CTRL_SENDER_MASK)
1026
+ | ((receiver & CTRL_SENDER_MASK) << 2)
1027
+ | (isResponse ? CTRL_ACTION_RESPONSE : 0);
1028
+ int seq = nextSequence();
1029
+
1030
+ try {
1031
+ coded.writeFixed32(1, MAGIC_NUMBER);
1032
+ coded.writeUInt32(2, function);
1033
+ coded.writeUInt32(3, cmd);
1034
+ if (payload != null && payload.length > 0) {
1035
+ coded.writeUInt32(4, payload.length);
1036
+ }
1037
+ coded.writeUInt32(5, ctrl);
1038
+ coded.writeUInt32(6, seq);
1039
+ if (payload != null && payload.length > 0) {
1040
+ coded.writeUInt32(7, checksum16(payload));
1041
+ coded.writeByteArray(8, payload);
1042
+ }
1043
+ coded.flush();
1044
+ } catch (IOException e) {
1045
+ Bridge.log(TAG + ": buildHeader IO error: " + e.getMessage());
1046
+ }
1047
+ return output.toByteArray();
1048
+ }
1049
+
1050
+ private int nextSequence() {
1051
+ while (true) {
1052
+ int seq = sequence.getAndIncrement() & 0x7F;
1053
+ if (seq != 0) return seq;
1054
+ }
1055
+ }
1056
+
1057
+ private int checksum16(byte[] data) {
1058
+ if (data == null) return 0;
1059
+ int sum = 0;
1060
+ for (byte b : data) {
1061
+ sum += b & 0xFF;
1062
+ }
1063
+ return sum & 0xFFFF;
1064
+ }
1065
+
1066
+ // ---------------- Payload builders (minimal protobuf encoding) ----------------
1067
+
1068
+ private byte[] buildProtoMessageUInt32(int fieldNumber, int value) {
1069
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1070
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1071
+ try {
1072
+ coded.writeUInt32(fieldNumber, value);
1073
+ coded.flush();
1074
+ } catch (IOException e) {
1075
+ Bridge.log(TAG + ": buildProtoMessageUInt32 error: " + e.getMessage());
1076
+ }
1077
+ return output.toByteArray();
1078
+ }
1079
+
1080
+ private byte[] buildProtoMessageBool(int fieldNumber, boolean value) {
1081
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1082
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1083
+ try {
1084
+ coded.writeBool(fieldNumber, value);
1085
+ coded.flush();
1086
+ } catch (IOException e) {
1087
+ Bridge.log(TAG + ": buildProtoMessageBool error: " + e.getMessage());
1088
+ }
1089
+ return output.toByteArray();
1090
+ }
1091
+
1092
+ private byte[] buildSetTimePayload(long timestampMs) {
1093
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1094
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1095
+ try {
1096
+ TimeZone timeZone = TimeZone.getDefault();
1097
+ int timezoneOffsetMinutes = timeZone.getOffset(timestampMs) / (60 * 1000);
1098
+ boolean is24Hour = DateFormat.is24HourFormat(context);
1099
+
1100
+ coded.writeInt64(1, timestampMs);
1101
+ coded.writeUInt32(2, 1);
1102
+ coded.writeBool(3, is24Hour);
1103
+ coded.writeInt32(4, timezoneOffsetMinutes);
1104
+ coded.writeString(5, timeZone.getID());
1105
+ coded.flush();
1106
+ } catch (IOException e) {
1107
+ Bridge.log(TAG + ": buildSetTimePayload error: " + e.getMessage());
1108
+ }
1109
+ return output.toByteArray();
1110
+ }
1111
+
1112
+ private byte[] buildTranslationDisplayStartPayload(
1113
+ int sourceLanguage, int targetLanguage, int micSource) {
1114
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1115
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1116
+ try {
1117
+ coded.writeUInt32(1, sourceLanguage);
1118
+ coded.writeUInt32(2, targetLanguage);
1119
+ coded.writeUInt32(3, micSource);
1120
+ coded.flush();
1121
+ } catch (IOException e) {
1122
+ Bridge.log(TAG + ": buildTranslationDisplayStartPayload error: " + e.getMessage());
1123
+ }
1124
+ return output.toByteArray();
1125
+ }
1126
+
1127
+ private byte[] buildTranslationDisplaySetContentPayload(
1128
+ int state, int fontSize, String sourceText, String targetText) {
1129
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1130
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1131
+ try {
1132
+ coded.writeUInt32(1, state);
1133
+ coded.writeUInt32(2, fontSize);
1134
+ coded.writeUInt32(3, TRANS_DISPLAY_SOURCE_LANGUAGE);
1135
+ coded.writeUInt32(4, TRANS_DISPLAY_TARGET_LANGUAGE);
1136
+ coded.writeString(5, sourceText != null ? sourceText : "");
1137
+ coded.writeString(6, targetText != null ? targetText : "");
1138
+ coded.flush();
1139
+ } catch (IOException e) {
1140
+ Bridge.log(TAG + ": buildTranslationDisplaySetContentPayload error: " + e.getMessage());
1141
+ }
1142
+ return output.toByteArray();
1143
+ }
1144
+
1145
+ private byte[] buildDisplaySystemFunctionPayload(int functionType, int action) {
1146
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1147
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1148
+ try {
1149
+ coded.writeUInt32(1, functionType);
1150
+ coded.writeUInt32(2, action);
1151
+ coded.flush();
1152
+ } catch (IOException e) {
1153
+ Bridge.log(TAG + ": buildDisplaySystemFunctionPayload error: " + e.getMessage());
1154
+ }
1155
+ return output.toByteArray();
1156
+ }
1157
+
1158
+ private byte[] buildControlDisplayAudioRecordPayload(boolean enabled) {
1159
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1160
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1161
+ try {
1162
+ // Old 161 protocol uses start=false to begin recording, start=true to stop.
1163
+ coded.writeBool(1, !enabled);
1164
+ coded.flush();
1165
+ } catch (IOException e) {
1166
+ Bridge.log(TAG + ": buildControlDisplayAudioRecordPayload error: " + e.getMessage());
1167
+ }
1168
+ return output.toByteArray();
1169
+ }
1170
+
1171
+ private byte[] buildMinimalTextSetPayload(String text) {
1172
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
1173
+ CodedOutputStream coded = CodedOutputStream.newInstance(output);
1174
+ try {
1175
+ coded.writeString(1, text != null ? text : "");
1176
+ coded.flush();
1177
+ } catch (IOException e) {
1178
+ Bridge.log(TAG + ": buildMinimalTextSetPayload error: " + e.getMessage());
1179
+ }
1180
+ return output.toByteArray();
1181
+ }
1182
+
1183
+ // ---------------- High-level AR99 messages ----------------
1184
+
1185
+ private void sendMinimalText(String text) {
1186
+ String normalizedText = text != null ? text : "";
1187
+ if (!minimalModeReady) {
1188
+ pendingDisplayText = normalizedText;
1189
+ hasPendingDisplayUpdate = true;
1190
+ logSend(
1191
+ "minimal text deferred until minimal mode ready: len=" + pendingDisplayText.length());
1192
+ return;
1193
+ }
1194
+ long now = System.currentTimeMillis();
1195
+ if (displaySessionStarted
1196
+ && lastDisplayContentAtMs > 0L
1197
+ && now - lastDisplayContentAtMs >= DISPLAY_SESSION_IDLE_RESTART_MS) {
1198
+ displaySessionStarted = false;
1199
+ lastDisplayText = "";
1200
+ }
1201
+ sendMinimalTextNow(normalizedText);
1202
+ }
1203
+
1204
+ private void sendMinimalTextNow(String text) {
1205
+ String normalizedText = text != null ? text : "";
1206
+ if (normalizedText.equals(lastDisplayText)) {
1207
+ return;
1208
+ }
1209
+
1210
+ if (normalizedText.isEmpty()) {
1211
+ clearMinimalTextNow();
1212
+ return;
1213
+ }
1214
+
1215
+ startMinimalTextIfNeeded();
1216
+ logDisplayRequest(normalizedText);
1217
+
1218
+ enqueueMessage(
1219
+ FUNC_COMM_DISPLAY,
1220
+ CMD_DISPLAY_CTRL_MINIMAL_TEXT_SET,
1221
+ buildMinimalTextSetPayload(normalizedText),
1222
+ TARGET_GLASS_MCU_DISPLAY);
1223
+
1224
+ lastDisplayText = normalizedText;
1225
+ lastDisplayContentAtMs = System.currentTimeMillis();
1226
+ }
1227
+
1228
+ private void flushPendingDisplayUpdateIfNeeded() {
1229
+ if (!minimalModeReady || !hasPendingDisplayUpdate) return;
1230
+ String text = pendingDisplayText;
1231
+ hasPendingDisplayUpdate = false;
1232
+ pendingDisplayText = "";
1233
+ logSend("flushing deferred minimal text update: len=" + text.length());
1234
+ sendMinimalTextNow(text);
1235
+ }
1236
+
1237
+ private void startMinimalTextIfNeeded() {
1238
+ if (displaySessionStarted) return;
1239
+ enqueueMessage(
1240
+ FUNC_COMM_DISPLAY,
1241
+ CMD_DISPLAY_CTRL_MINIMAL_TEXT_START,
1242
+ new byte[0],
1243
+ TARGET_GLASS_MCU_DISPLAY);
1244
+ displaySessionStarted = true;
1245
+ lastDisplayContentAtMs = System.currentTimeMillis();
1246
+ }
1247
+
1248
+ private void clearMinimalText() {
1249
+ if (!minimalModeReady) {
1250
+ pendingDisplayText = "";
1251
+ hasPendingDisplayUpdate = false;
1252
+ lastDisplayText = "";
1253
+ return;
1254
+ }
1255
+ clearMinimalTextNow();
1256
+ }
1257
+
1258
+ private void clearMinimalTextNow() {
1259
+ enqueueMessage(
1260
+ FUNC_COMM_DISPLAY,
1261
+ CMD_DISPLAY_CTRL_MINIMAL_TEXT_CLEAR,
1262
+ new byte[0],
1263
+ TARGET_GLASS_MCU_DISPLAY);
1264
+ resetMinimalTextSession();
1265
+ }
1266
+
1267
+ private void resetMinimalTextSession() {
1268
+ displaySessionStarted = false;
1269
+ lastDisplayContentAtMs = 0L;
1270
+ lastDisplayText = "";
1271
+ }
1272
+
1273
+ private String joinDisplayLines(String top, String bottom) {
1274
+ String first = top != null ? top.trim() : "";
1275
+ String second = bottom != null ? bottom.trim() : "";
1276
+ if (first.isEmpty()) return second;
1277
+ if (second.isEmpty()) return first;
1278
+ return first + "\n" + second;
1279
+ }
1280
+
1281
+ private void logDisplayRequest(String text) {
1282
+ logSend(
1283
+ "minimalText len="
1284
+ + (text != null ? text.length() : 0)
1285
+ + " text=\""
1286
+ + formatDisplayTextForLog(text)
1287
+ + "\"");
1288
+ }
1289
+
1290
+ private String formatDisplayTextForLog(String text) {
1291
+ if (text == null) {
1292
+ return "<null>";
1293
+ }
1294
+ return text.replace("\\", "\\\\").replace("\r", "\\r").replace("\n", "\\n");
1295
+ }
1296
+
1297
+ private void sendControlDisplayAudioRecord(boolean enabled) {
1298
+ byte[] payload = buildControlDisplayAudioRecordPayload(enabled);
1299
+ enqueueMessage(
1300
+ FUNC_COMM_DISPLAY, CMD_DISPLAY_CTRL_AUDIO_RECORD, payload, TARGET_GLASS_ANDROID);
1301
+ }
1302
+
1303
+ private void setMinimalDisplayMode() {
1304
+ enqueueMessage(
1305
+ FUNC_COMM_DISPLAY,
1306
+ CMD_DISPLAY_CTRL_SYS_FUNCTION,
1307
+ buildDisplaySystemFunctionPayload(DISPLAY_FUNCTION_TYPE_UI_MODE, DISPLAY_UI_MODE_MINIMAL),
1308
+ TARGET_GLASS_MCU_DISPLAY);
1309
+ }
1310
+
1311
+ private void setDisplayLanguage(int language) {
1312
+ enqueueMessage(
1313
+ FUNC_COMM_DISPLAY,
1314
+ CMD_DISPLAY_SET_LANGUAGE,
1315
+ buildProtoMessageUInt32(1, language),
1316
+ TARGET_GLASS_MCU_DISPLAY);
1317
+ }
1318
+
1319
+ private void requestDeviceInfo() {
1320
+ enqueueMessage(
1321
+ FUNC_COMM_DISPLAY,
1322
+ CMD_DISPLAY_GET_DEVICEINFO,
1323
+ new byte[0],
1324
+ TARGET_GLASS_MCU_DISPLAY);
1325
+ }
1326
+
1327
+ private void requestBrightness() {
1328
+ enqueueMessage(
1329
+ FUNC_COMM_DISPLAY,
1330
+ CMD_DISPLAY_GET_BRIGHT,
1331
+ new byte[0],
1332
+ TARGET_GLASS_MCU_DISPLAY);
1333
+ }
1334
+
1335
+ @Override
1336
+ public void sendSetSystemTime(long timestampMs) {
1337
+ Bridge.log(TAG + ": sending set time timestampMs=" + timestampMs);
1338
+ enqueueMessage(
1339
+ FUNC_COMM,
1340
+ CMD_COMM_SET_TIME,
1341
+ buildSetTimePayload(timestampMs),
1342
+ TARGET_GLASS_MCU_DISPLAY);
1343
+ }
1344
+
1345
+ private void handleControlPacket(byte[] data) {
1346
+ MessageHeader header = parseHeader(data);
1347
+ if (header == null) return;
1348
+ boolean isResponse = (header.ctrl & CTRL_ACTION_RESPONSE) != 0;
1349
+ logRecv(
1350
+ "<<<"
1351
+ + bytesToHex(data)
1352
+ + ",len="
1353
+ + data.length
1354
+ + " function="
1355
+ + header.function
1356
+ + " cmd="
1357
+ + header.cmd
1358
+ + " ctrl="
1359
+ + header.ctrl
1360
+ + " isResponse="
1361
+ + isResponse);
1362
+
1363
+ if (header.function == FUNC_COMM) {
1364
+ handleCommMessage(header.cmd, isResponse, header.payload);
1365
+ } else if (header.function == FUNC_COMM_DISPLAY) {
1366
+ if (header.cmd == CMD_DISPLAY_CTRL_AUDIO_RECORD) {
1367
+ handleLegacyAudioRecordMessage(isResponse, header.payload);
1368
+ } else {
1369
+ handleDisplayMessage(header.cmd, isResponse, header.payload);
1370
+ }
1371
+ } else if (header.function == FUNC_TRANS) {
1372
+ handleTransMessage(header.cmd, isResponse, header.payload);
1373
+ }
1374
+ }
1375
+
1376
+ private void handleTransMessage(int cmd, boolean isResponse, byte[] payload) {
1377
+ if (!isResponse && cmd == CMD_TRANS_DISPLAY_STOP) {
1378
+ setMicEnabled(false);
1379
+ resetMinimalTextSession();
1380
+ logRecv("parsed trans stop request; local display session reset");
1381
+ return;
1382
+ }
1383
+
1384
+ if (isResponse && cmd == CMD_TRANS_DISPLAY_STOP) {
1385
+ Boolean success = parseSuccessResponse(payload);
1386
+ if (success != null && success) {
1387
+ resetMinimalTextSession();
1388
+ }
1389
+ logRecv(
1390
+ "parsed trans stop response success="
1391
+ + success
1392
+ + " localDisplaySessionStarted="
1393
+ + displaySessionStarted);
1394
+ return;
1395
+ }
1396
+
1397
+ if (isResponse && cmd == CMD_TRANS_DISPLAY_START) {
1398
+ Boolean success = parseSuccessResponse(payload);
1399
+ if (success == null || !success) {
1400
+ logRecv(
1401
+ "parsed trans start response success="
1402
+ + success
1403
+ + " localDisplaySessionStarted="
1404
+ + displaySessionStarted);
1405
+ }
1406
+ return;
1407
+ }
1408
+
1409
+ if (isResponse && cmd == CMD_TRANS_DISPLAY_SET_CONTENT) {
1410
+ Boolean success = parseSuccessResponse(payload);
1411
+ if (success == null || !success) {
1412
+ logRecv(
1413
+ "parsed trans set_content response success="
1414
+ + success
1415
+ + " localDisplaySessionStarted="
1416
+ + displaySessionStarted);
1417
+ }
1418
+ }
1419
+ }
1420
+
1421
+ private void handleDisplayMessage(int cmd, boolean isResponse, byte[] payload) {
1422
+ if (cmd == CMD_DISPLAY_GET_BATTERY) {
1423
+ BatteryInfo info = parseBatteryResponse(payload);
1424
+ if (info != null) {
1425
+ logRecv(
1426
+ "parsed cmd="
1427
+ + cmd
1428
+ + " isResponse="
1429
+ + isResponse
1430
+ + " battery="
1431
+ + info.battery
1432
+ + " charging="
1433
+ + info.charging);
1434
+ if (info.battery >= 0 && info.battery <= 100) {
1435
+ DeviceStore.INSTANCE.apply("glasses", "batteryLevel", info.battery);
1436
+ boolean charging =
1437
+ info.charging != null
1438
+ ? info.charging
1439
+ : (DeviceStore.INSTANCE.get("glasses", "charging") instanceof Boolean
1440
+ ? (Boolean) DeviceStore.INSTANCE.get("glasses", "charging")
1441
+ : false);
1442
+ if (info.charging != null) {
1443
+ DeviceStore.INSTANCE.apply("glasses", "charging", charging);
1444
+ }
1445
+ Bridge.sendBatteryStatus(info.battery, charging);
1446
+ hasReceivedBatteryForCurrentConnection = true;
1447
+ pendingBatteryRetries = 0;
1448
+ batteryQueryHandler.removeCallbacks(initialBatteryRetryRunnable);
1449
+ } else {
1450
+ Bridge.log(TAG + ": ignoring out-of-range battery response: " + info.battery);
1451
+ }
1452
+ } else {
1453
+ Bridge.log(
1454
+ TAG + ": failed to parse battery response payload len="
1455
+ + payload.length
1456
+ + " hex="
1457
+ + bytesToHex(payload));
1458
+ }
1459
+ } else if (cmd == CMD_DISPLAY_GET_DEVICEINFO) {
1460
+ DeviceInfo info = parseDeviceInfoResponse(payload);
1461
+ if (info != null) {
1462
+ logRecv(
1463
+ "parsed cmd="
1464
+ + cmd
1465
+ + " isResponse="
1466
+ + isResponse
1467
+ + " firmware="
1468
+ + info.firmwareVersion
1469
+ + " product="
1470
+ + info.productName
1471
+ + " btMac="
1472
+ + info.btMac
1473
+ + " serial="
1474
+ + info.serialNumber
1475
+ + " deviceName="
1476
+ + info.deviceName);
1477
+ if (info.firmwareVersion != null) {
1478
+ DeviceStore.INSTANCE.apply("glasses", "firmwareVersion", info.firmwareVersion);
1479
+ }
1480
+ if (info.productName != null) {
1481
+ DeviceStore.INSTANCE.apply("glasses", "deviceModel", info.productName);
1482
+ }
1483
+ String preferredBtMac = currentBroadcastMacAddress != null && !currentBroadcastMacAddress.trim().isEmpty() ? currentBroadcastMacAddress : info.btMac;
1484
+ if (preferredBtMac != null && !preferredBtMac.trim().isEmpty()) {
1485
+ DeviceStore.INSTANCE.apply("glasses", "bluetoothMacAddress", preferredBtMac);
1486
+ }
1487
+ if (info.serialNumber != null) {
1488
+ DeviceStore.INSTANCE.apply("glasses", "serialNumber", info.serialNumber);
1489
+ }
1490
+ sendVersionInfo(info);
1491
+ }
1492
+ } else if (cmd == CMD_DISPLAY_GET_BRIGHT) {
1493
+ Integer brightness = parseBrightnessResponse(payload);
1494
+ if (brightness != null) {
1495
+ DeviceStore.INSTANCE.apply("glasses", "brightness", brightness);
1496
+ logRecv(
1497
+ "parsed cmd="
1498
+ + cmd
1499
+ + " isResponse="
1500
+ + isResponse
1501
+ + " brightness="
1502
+ + brightness);
1503
+ }
1504
+ } else if (cmd == CMD_DISPLAY_SET_LANGUAGE) {
1505
+ LanguageResponse response = parseLanguageResponse(payload);
1506
+ if (response != null) {
1507
+ logRecv(
1508
+ "parsed cmd="
1509
+ + cmd
1510
+ + " isResponse="
1511
+ + isResponse
1512
+ + " success="
1513
+ + response.success
1514
+ + " language="
1515
+ + response.language);
1516
+ } else {
1517
+ Bridge.log(
1518
+ TAG
1519
+ + ": failed to parse display response cmd="
1520
+ + cmd
1521
+ + " payload="
1522
+ + bytesToHex(payload));
1523
+ }
1524
+ } else if (cmd == CMD_DISPLAY_SET_BRIGHT) {
1525
+ Integer brightness = parseSetBrightnessResponse(payload);
1526
+ if (brightness != null) {
1527
+ DeviceStore.INSTANCE.apply("glasses", "brightness", brightness);
1528
+ logRecv(
1529
+ "parsed cmd="
1530
+ + cmd
1531
+ + " isResponse="
1532
+ + isResponse
1533
+ + " currentBrightness="
1534
+ + brightness);
1535
+ }
1536
+ } else if (cmd == CMD_DISPLAY_CTRL_FACTORY_RESET) {
1537
+ Boolean success = null;
1538
+ Integer confirmCode = null;
1539
+ try {
1540
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1541
+ while (!input.isAtEnd()) {
1542
+ int tag = input.readTag();
1543
+ if (tag == 0) break;
1544
+ switch (WireFormat.getTagFieldNumber(tag)) {
1545
+ case 1:
1546
+ success = input.readBool();
1547
+ break;
1548
+ case 2:
1549
+ confirmCode = input.readUInt32();
1550
+ break;
1551
+ default:
1552
+ input.skipField(tag);
1553
+ break;
1554
+ }
1555
+ }
1556
+ } catch (IOException e) {
1557
+ Bridge.log(TAG + ": failed parsing factory reset response: " + e.getMessage());
1558
+ }
1559
+ logRecv(
1560
+ "parsed cmd="
1561
+ + cmd
1562
+ + " isResponse="
1563
+ + isResponse
1564
+ + " success="
1565
+ + success
1566
+ + " confirmCode="
1567
+ + confirmCode);
1568
+ } else if (cmd == CMD_DISPLAY_CTRL_SYS_FUNCTION) {
1569
+ Boolean success = parseSuccessResponse(payload);
1570
+ if (success != null) {
1571
+ logRecv("parsed cmd=" + cmd + " isResponse=" + isResponse + " success=" + success);
1572
+ if (isResponse && success) {
1573
+ minimalModeReady = true;
1574
+ flushPendingDisplayUpdateIfNeeded();
1575
+ }
1576
+ }
1577
+ } else if (cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_START
1578
+ || cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_SET
1579
+ || cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_CLEAR
1580
+ || cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_STOP) {
1581
+ Boolean success = parseSuccessResponse(payload);
1582
+ logRecv("parsed cmd=" + cmd + " isResponse=" + isResponse + " success=" + success);
1583
+ if (isResponse && Boolean.FALSE.equals(success)) {
1584
+ if (cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_START) {
1585
+ displaySessionStarted = false;
1586
+ } else if (cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_CLEAR
1587
+ || cmd == CMD_DISPLAY_CTRL_MINIMAL_TEXT_STOP) {
1588
+ resetMinimalTextSession();
1589
+ }
1590
+ }
1591
+ }
1592
+ }
1593
+
1594
+ private void handleCommMessage(int cmd, boolean isResponse, byte[] payload) {
1595
+ if (cmd == CMD_COMM_SET_TIME) {
1596
+ SetTimeResponse response = parseSetTimeResponse(payload);
1597
+ if (response != null) {
1598
+ StringBuilder log =
1599
+ new StringBuilder("parsed cmd=")
1600
+ .append(cmd)
1601
+ .append(" isResponse=")
1602
+ .append(isResponse)
1603
+ .append(" success=")
1604
+ .append(response.success);
1605
+ if (response.timeValue != null) {
1606
+ log.append(" timeValue=").append(response.timeValue);
1607
+ }
1608
+ logRecv(log.toString());
1609
+ } else {
1610
+ Bridge.log(
1611
+ TAG + ": failed to parse comm response cmd=" + cmd + " payload=" + bytesToHex(payload));
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ private void handleLegacyAudioRecordMessage(boolean isResponse, byte[] payload) {
1617
+ Boolean currentRecording = parseLegacyAudioRecordResponse(payload);
1618
+ if (currentRecording != null) {
1619
+ if (!currentRecording) {
1620
+ awaitingMicDisableAck = false;
1621
+ if (opusPcmDecoder != null) {
1622
+ opusPcmDecoder.resetBuffer();
1623
+ }
1624
+ }
1625
+ DeviceStore.INSTANCE.apply("glasses", "micEnabled", currentRecording);
1626
+ logRecv(
1627
+ "parsed legacy cmd="
1628
+ + CMD_DISPLAY_CTRL_AUDIO_RECORD
1629
+ + " isResponse="
1630
+ + isResponse
1631
+ + " currentRecording="
1632
+ + currentRecording);
1633
+ }
1634
+ }
1635
+
1636
+ private CommonResult parseCommonResult(CodedInputStream input) throws IOException {
1637
+ byte[] raw = input.readByteArray();
1638
+ return parseCommonResult(raw);
1639
+ }
1640
+
1641
+ private CommonResult parseCommonResult(byte[] raw) throws IOException {
1642
+ CodedInputStream nested = CodedInputStream.newInstance(raw);
1643
+ boolean success = false;
1644
+ int errorCode = 0;
1645
+ String errorMessage = "";
1646
+
1647
+ while (!nested.isAtEnd()) {
1648
+ int tag = nested.readTag();
1649
+ if (tag == 0) break;
1650
+ switch (WireFormat.getTagFieldNumber(tag)) {
1651
+ case 1:
1652
+ success = nested.readBool();
1653
+ break;
1654
+ case 2:
1655
+ errorCode = nested.readUInt32();
1656
+ break;
1657
+ case 3:
1658
+ errorMessage = nested.readString();
1659
+ break;
1660
+ default:
1661
+ nested.skipField(tag);
1662
+ break;
1663
+ }
1664
+ }
1665
+
1666
+ return new CommonResult(success, errorCode, errorMessage);
1667
+ }
1668
+
1669
+ private Boolean readSuccessField(CodedInputStream input, int tag) throws IOException {
1670
+ int wireType = WireFormat.getTagWireType(tag);
1671
+ if (wireType == WireFormat.WIRETYPE_VARINT) {
1672
+ return input.readBool();
1673
+ }
1674
+ if (wireType == WireFormat.WIRETYPE_LENGTH_DELIMITED) {
1675
+ return parseCommonResult(input).success;
1676
+ }
1677
+ input.skipField(tag);
1678
+ return null;
1679
+ }
1680
+
1681
+ private BatteryInfo parseBatteryResponse(byte[] payload) {
1682
+ BatteryInfo nested = parseBatteryResponseNested(payload);
1683
+ if (nested != null) {
1684
+ return nested;
1685
+ }
1686
+ return parseBatteryResponseFlat(payload);
1687
+ }
1688
+
1689
+ private BatteryInfo parseBatteryResponseNested(byte[] payload) {
1690
+ try {
1691
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1692
+ Boolean success = null;
1693
+ int battery = -1;
1694
+ Boolean charging = null;
1695
+
1696
+ while (!input.isAtEnd()) {
1697
+ int tag = input.readTag();
1698
+ if (tag == 0) break;
1699
+ switch (WireFormat.getTagFieldNumber(tag)) {
1700
+ case 1:
1701
+ success = readSuccessField(input, tag);
1702
+ break;
1703
+ case 2:
1704
+ battery = input.readUInt32();
1705
+ break;
1706
+ case 3:
1707
+ charging = input.readBool();
1708
+ break;
1709
+ default:
1710
+ input.skipField(tag);
1711
+ break;
1712
+ }
1713
+ }
1714
+
1715
+ if (success == null || !success) return null;
1716
+ return new BatteryInfo(battery, charging);
1717
+ } catch (Throwable t) {
1718
+ return null;
1719
+ }
1720
+ }
1721
+
1722
+ /**
1723
+ * Some AR99 firmware revisions appear to flatten the battery response instead of nesting a
1724
+ * CommonResult submessage in field 1. Accept both layouts so the UI doesn't stay stuck at -1.
1725
+ */
1726
+ private BatteryInfo parseBatteryResponseFlat(byte[] payload) {
1727
+ try {
1728
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1729
+ Boolean success = null;
1730
+ Integer battery = null;
1731
+ Boolean charging = null;
1732
+
1733
+ while (!input.isAtEnd()) {
1734
+ int tag = input.readTag();
1735
+ if (tag == 0) break;
1736
+
1737
+ int fieldNumber = WireFormat.getTagFieldNumber(tag);
1738
+ int wireType = WireFormat.getTagWireType(tag);
1739
+
1740
+ switch (fieldNumber) {
1741
+ case 1:
1742
+ if (wireType == WireFormat.WIRETYPE_LENGTH_DELIMITED) {
1743
+ CommonResult result = parseCommonResult(input.readByteArray());
1744
+ success = result.success;
1745
+ } else if (wireType == WireFormat.WIRETYPE_VARINT) {
1746
+ long value = input.readUInt64();
1747
+ if (value == 0L || value == 1L) {
1748
+ success = value == 1L;
1749
+ } else if (battery == null && value >= 0L && value <= 100L) {
1750
+ battery = (int) value;
1751
+ }
1752
+ } else {
1753
+ input.skipField(tag);
1754
+ }
1755
+ break;
1756
+ case 2:
1757
+ if (wireType == WireFormat.WIRETYPE_VARINT) {
1758
+ long value = input.readUInt64();
1759
+ if (battery == null && value >= 0L && value <= 100L) {
1760
+ battery = (int) value;
1761
+ } else if (charging == null && (value == 0L || value == 1L)) {
1762
+ charging = value == 1L;
1763
+ }
1764
+ } else {
1765
+ input.skipField(tag);
1766
+ }
1767
+ break;
1768
+ case 3:
1769
+ if (wireType == WireFormat.WIRETYPE_VARINT) {
1770
+ charging = input.readBool();
1771
+ } else {
1772
+ input.skipField(tag);
1773
+ }
1774
+ break;
1775
+ default:
1776
+ input.skipField(tag);
1777
+ break;
1778
+ }
1779
+ }
1780
+
1781
+ if (battery == null) {
1782
+ return null;
1783
+ }
1784
+ if (success != null && !success) {
1785
+ return null;
1786
+ }
1787
+ return new BatteryInfo(battery, charging);
1788
+ } catch (Throwable t) {
1789
+ return null;
1790
+ }
1791
+ }
1792
+
1793
+ private DeviceInfo parseDeviceInfoResponse(byte[] payload) {
1794
+ try {
1795
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1796
+ Boolean success = null;
1797
+ String firmware = null;
1798
+ String version = null;
1799
+ String deviceName = null;
1800
+ String productName = null;
1801
+ String btMac = null;
1802
+ String serial = null;
1803
+
1804
+ while (!input.isAtEnd()) {
1805
+ int tag = input.readTag();
1806
+ if (tag == 0) break;
1807
+ switch (WireFormat.getTagFieldNumber(tag)) {
1808
+ case 1:
1809
+ success = readSuccessField(input, tag);
1810
+ break;
1811
+ case 2:
1812
+ firmware = input.readString();
1813
+ break;
1814
+ case 5:
1815
+ version = input.readString();
1816
+ break;
1817
+ case 6:
1818
+ deviceName = input.readString();
1819
+ break;
1820
+ case 7:
1821
+ productName = input.readString();
1822
+ break;
1823
+ case 8:
1824
+ btMac = input.readString();
1825
+ break;
1826
+ case 9:
1827
+ serial = input.readString();
1828
+ break;
1829
+ default:
1830
+ input.skipField(tag);
1831
+ break;
1832
+ }
1833
+ }
1834
+
1835
+ if (success == null || !success) return null;
1836
+ return new DeviceInfo(
1837
+ version != null && !version.isEmpty() ? version : firmware,
1838
+ deviceName,
1839
+ productName,
1840
+ btMac,
1841
+ serial);
1842
+ } catch (Throwable t) {
1843
+ return null;
1844
+ }
1845
+ }
1846
+
1847
+ private LanguageResponse parseLanguageResponse(byte[] payload) {
1848
+ try {
1849
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1850
+ Boolean success = null;
1851
+ Integer language = null;
1852
+
1853
+ while (!input.isAtEnd()) {
1854
+ int tag = input.readTag();
1855
+ if (tag == 0) break;
1856
+ switch (WireFormat.getTagFieldNumber(tag)) {
1857
+ case 1:
1858
+ success = readSuccessField(input, tag);
1859
+ break;
1860
+ case 2:
1861
+ language = input.readUInt32();
1862
+ break;
1863
+ default:
1864
+ input.skipField(tag);
1865
+ break;
1866
+ }
1867
+ }
1868
+
1869
+ if (success == null) return null;
1870
+ return new LanguageResponse(success, language);
1871
+ } catch (Throwable t) {
1872
+ return null;
1873
+ }
1874
+ }
1875
+
1876
+ private SetTimeResponse parseSetTimeResponse(byte[] payload) {
1877
+ try {
1878
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1879
+ Boolean success = null;
1880
+ Long timeValue = null;
1881
+
1882
+ while (!input.isAtEnd()) {
1883
+ int tag = input.readTag();
1884
+ if (tag == 0) break;
1885
+ int fieldNumber = WireFormat.getTagFieldNumber(tag);
1886
+ int wireType = WireFormat.getTagWireType(tag);
1887
+
1888
+ switch (fieldNumber) {
1889
+ case 1:
1890
+ success = readSuccessField(input, tag);
1891
+ break;
1892
+ case 2:
1893
+ if (wireType == WireFormat.WIRETYPE_VARINT) {
1894
+ timeValue = input.readUInt64();
1895
+ } else {
1896
+ input.skipField(tag);
1897
+ }
1898
+ break;
1899
+ default:
1900
+ input.skipField(tag);
1901
+ break;
1902
+ }
1903
+ }
1904
+
1905
+ if (success == null) return null;
1906
+ return new SetTimeResponse(success, timeValue);
1907
+ } catch (Throwable t) {
1908
+ return null;
1909
+ }
1910
+ }
1911
+
1912
+ private void sendVersionInfo(DeviceInfo info) {
1913
+ HashMap<String, Object> values = new HashMap<>();
1914
+ if (info.firmwareVersion != null) {
1915
+ values.put("firmwareVersion", info.firmwareVersion);
1916
+ }
1917
+ if (info.productName != null) {
1918
+ values.put("deviceModel", info.productName);
1919
+ }
1920
+ if (info.serialNumber != null) {
1921
+ values.put("serialNumber", info.serialNumber);
1922
+ }
1923
+ Bridge.sendVersionInfo(values);
1924
+ }
1925
+
1926
+ private Integer parseBrightnessResponse(byte[] payload) {
1927
+ try {
1928
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1929
+ Boolean success = null;
1930
+ Integer brightness = null;
1931
+
1932
+ while (!input.isAtEnd()) {
1933
+ int tag = input.readTag();
1934
+ if (tag == 0) break;
1935
+ switch (WireFormat.getTagFieldNumber(tag)) {
1936
+ case 1:
1937
+ success = readSuccessField(input, tag);
1938
+ break;
1939
+ case 2:
1940
+ brightness = input.readUInt32();
1941
+ break;
1942
+ default:
1943
+ input.skipField(tag);
1944
+ break;
1945
+ }
1946
+ }
1947
+
1948
+ return success != null && success ? brightness : null;
1949
+ } catch (Throwable t) {
1950
+ return null;
1951
+ }
1952
+ }
1953
+
1954
+ private Integer parseSetBrightnessResponse(byte[] payload) {
1955
+ try {
1956
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1957
+ Boolean success = null;
1958
+ Integer brightness = null;
1959
+
1960
+ while (!input.isAtEnd()) {
1961
+ int tag = input.readTag();
1962
+ if (tag == 0) break;
1963
+ switch (WireFormat.getTagFieldNumber(tag)) {
1964
+ case 1:
1965
+ success = readSuccessField(input, tag);
1966
+ break;
1967
+ case 2:
1968
+ brightness = input.readUInt32();
1969
+ break;
1970
+ default:
1971
+ input.skipField(tag);
1972
+ break;
1973
+ }
1974
+ }
1975
+
1976
+ return success != null && success ? brightness : null;
1977
+ } catch (Throwable t) {
1978
+ return null;
1979
+ }
1980
+ }
1981
+
1982
+ private Boolean parseSuccessResponse(byte[] payload) {
1983
+ try {
1984
+ CodedInputStream input = CodedInputStream.newInstance(payload);
1985
+ Boolean success = null;
1986
+
1987
+ while (!input.isAtEnd()) {
1988
+ int tag = input.readTag();
1989
+ if (tag == 0) break;
1990
+ switch (WireFormat.getTagFieldNumber(tag)) {
1991
+ case 1:
1992
+ success = readSuccessField(input, tag);
1993
+ break;
1994
+ default:
1995
+ input.skipField(tag);
1996
+ break;
1997
+ }
1998
+ }
1999
+
2000
+ return success;
2001
+ } catch (Throwable t) {
2002
+ return null;
2003
+ }
2004
+ }
2005
+
2006
+ private Boolean parseLegacyAudioRecordResponse(byte[] payload) {
2007
+ try {
2008
+ CodedInputStream input = CodedInputStream.newInstance(payload);
2009
+ Boolean success = null;
2010
+ Boolean start = null;
2011
+
2012
+ while (!input.isAtEnd()) {
2013
+ int tag = input.readTag();
2014
+ if (tag == 0) break;
2015
+ switch (WireFormat.getTagFieldNumber(tag)) {
2016
+ case 1:
2017
+ success = input.readBool();
2018
+ break;
2019
+ case 2:
2020
+ start = input.readBool();
2021
+ break;
2022
+ default:
2023
+ input.skipField(tag);
2024
+ break;
2025
+ }
2026
+ }
2027
+
2028
+ if (success == null || !success || start == null) {
2029
+ return null;
2030
+ }
2031
+ return !start;
2032
+ } catch (Throwable t) {
2033
+ return null;
2034
+ }
2035
+ }
2036
+
2037
+ private static final class LanguageResponse {
2038
+ final boolean success;
2039
+ final Integer language;
2040
+
2041
+ LanguageResponse(boolean success, Integer language) {
2042
+ this.success = success;
2043
+ this.language = language;
2044
+ }
2045
+ }
2046
+
2047
+ private static final class SetTimeResponse {
2048
+ final boolean success;
2049
+ final Long timeValue;
2050
+
2051
+ SetTimeResponse(boolean success, Long timeValue) {
2052
+ this.success = success;
2053
+ this.timeValue = timeValue;
2054
+ }
2055
+ }
2056
+
2057
+ private void handleControlNotifyChunk(byte[] data) {
2058
+ if (data == null || data.length == 0) return;
2059
+
2060
+ if (controlNotifyBuffer.length == 0) {
2061
+ controlNotifyBuffer = Arrays.copyOf(data, data.length);
2062
+ } else {
2063
+ byte[] merged = Arrays.copyOf(controlNotifyBuffer, controlNotifyBuffer.length + data.length);
2064
+ System.arraycopy(data, 0, merged, controlNotifyBuffer.length, data.length);
2065
+ controlNotifyBuffer = merged;
2066
+ }
2067
+
2068
+ while (true) {
2069
+ int start = findMessageStart(controlNotifyBuffer);
2070
+ if (start < 0) {
2071
+ trimControlNotifyBuffer();
2072
+ return;
2073
+ }
2074
+ if (start > 0) {
2075
+ controlNotifyBuffer =
2076
+ Arrays.copyOfRange(controlNotifyBuffer, start, controlNotifyBuffer.length);
2077
+ }
2078
+
2079
+ int nextStart = findMessageStart(controlNotifyBuffer, 1);
2080
+ byte[] candidatePacket =
2081
+ nextStart > 0
2082
+ ? Arrays.copyOfRange(controlNotifyBuffer, 0, nextStart)
2083
+ : controlNotifyBuffer;
2084
+
2085
+ MessageHeader header = parseHeader(candidatePacket);
2086
+ if (header == null) {
2087
+ if (nextStart > 0) {
2088
+ controlNotifyBuffer =
2089
+ Arrays.copyOfRange(controlNotifyBuffer, nextStart, controlNotifyBuffer.length);
2090
+ continue;
2091
+ }
2092
+ trimControlNotifyBuffer();
2093
+ return;
2094
+ }
2095
+
2096
+ if (controlNotifyBuffer.length < header.totalBytes) {
2097
+ trimControlNotifyBuffer();
2098
+ return;
2099
+ }
2100
+
2101
+ byte[] packet = Arrays.copyOfRange(controlNotifyBuffer, 0, header.totalBytes);
2102
+ controlNotifyBuffer =
2103
+ Arrays.copyOfRange(controlNotifyBuffer, header.totalBytes, controlNotifyBuffer.length);
2104
+ handleControlPacket(packet);
2105
+ }
2106
+ }
2107
+
2108
+ private void trimControlNotifyBuffer() {
2109
+ if (controlNotifyBuffer.length <= 2048) return;
2110
+
2111
+ int candidateStart = findMessageStart(controlNotifyBuffer);
2112
+ if (candidateStart > 0 && candidateStart < controlNotifyBuffer.length) {
2113
+ controlNotifyBuffer = Arrays.copyOfRange(controlNotifyBuffer, candidateStart, controlNotifyBuffer.length);
2114
+ return;
2115
+ }
2116
+
2117
+ controlNotifyBuffer = new byte[0];
2118
+ }
2119
+
2120
+ private int findMessageStart(byte[] data) {
2121
+ return findMessageStart(data, 0);
2122
+ }
2123
+
2124
+ private int findMessageStart(byte[] data, int fromIndex) {
2125
+ if (data == null || data.length < 5) return -1;
2126
+
2127
+ int start = Math.max(0, fromIndex);
2128
+ for (int i = start; i <= data.length - 5; i++) {
2129
+ if ((data[i] & 0xFF) == 0x0D
2130
+ && (data[i + 1] & 0xFF) == 0x4D
2131
+ && (data[i + 2] & 0xFF) == 0x3C
2132
+ && (data[i + 3] & 0xFF) == 0x2B
2133
+ && (data[i + 4] & 0xFF) == 0x1A) {
2134
+ return i;
2135
+ }
2136
+ }
2137
+
2138
+ return -1;
2139
+ }
2140
+
2141
+
2142
+ private void handleOpusData(byte[] data) {
2143
+ if (data == null || data.length == 0) {
2144
+ return;
2145
+ }
2146
+ // Logcat: adb logcat -s Ar99Opus (DEBUG = per-chunk length; INFO = first chunk + hex)
2147
+ Log.d(LOG_TAG_OPUS, "notify len=" + data.length);
2148
+ if (!opusLoggedFirstInSession) {
2149
+ opusLoggedFirstInSession = true;
2150
+ Log.i(
2151
+ LOG_TAG_OPUS,
2152
+ "Opus BLE (first chunk) len=" + data.length + " head16=" + hexPrefixUpper(data, 16));
2153
+ }
2154
+ if (!getMicEnabled()) {
2155
+ Log.w(LOG_TAG_OPUS, "received OPUS while glasses micEnabled=false; decoding anyway");
2156
+ }
2157
+ ensureOpusDecoder();
2158
+ if (opusPcmDecoder == null) {
2159
+ Log.w(LOG_TAG_OPUS, "decoder unavailable; skipping PCM");
2160
+ return;
2161
+ }
2162
+ opusPcmDecoder.submitBleNotify(data);
2163
+ }
2164
+
2165
+ private static String hexPrefixUpper(byte[] data, int maxBytes) {
2166
+ int n = Math.min(maxBytes, data.length);
2167
+ StringBuilder sb = new StringBuilder(n * 3);
2168
+ for (int i = 0; i < n; i++) {
2169
+ if (i > 0) {
2170
+ sb.append(' ');
2171
+ }
2172
+ sb.append(String.format("%02X", data[i] & 0xFF));
2173
+ }
2174
+ if (data.length > maxBytes) {
2175
+ sb.append(" ...");
2176
+ }
2177
+ return sb.toString();
2178
+ }
2179
+
2180
+ private static String bytesToHex(byte[] data) {
2181
+ if (data == null || data.length == 0) return "";
2182
+ StringBuilder sb = new StringBuilder(data.length * 2);
2183
+ for (byte b : data) {
2184
+ sb.append(String.format("%02X", b & 0xFF));
2185
+ }
2186
+ return sb.toString();
2187
+ }
2188
+
2189
+ private void logSend(String message) {
2190
+ Log.d(TAG, message);
2191
+ }
2192
+
2193
+ private void logRecv(String message) {
2194
+ Log.d(TAG, message);
2195
+ }
2196
+
2197
+ private synchronized void ensureOpusDecoder() {
2198
+ if (opusPcmDecoder != null) {
2199
+ return;
2200
+ }
2201
+ try {
2202
+ opusPcmDecoder =
2203
+ new Ar99OpusPcmDecoder(
2204
+ new Ar99OpusPcmDecoder.Callback() {
2205
+ @Override
2206
+ public void onPcmDecoded(byte[] pcmData) {
2207
+ DeviceManager.getInstance().reportGlassesAudioActivity();
2208
+ DeviceManager.getInstance().handlePcm(pcmData);
2209
+ }
2210
+
2211
+ @Override
2212
+ public void onNotifyProcessed(int rawLen, int pcmFrames, int pcmBytesOut) {
2213
+ if (pcmFrames > 0) {
2214
+ Log.d(
2215
+ LOG_TAG_OPUS,
2216
+ "decoded PCM chunks="
2217
+ + pcmFrames
2218
+ + " bytesOut="
2219
+ + pcmBytesOut
2220
+ + " rawLen="
2221
+ + rawLen);
2222
+ }
2223
+ }
2224
+ });
2225
+ } catch (Throwable e) {
2226
+ Bridge.log(TAG + ": failed to create Opus decoder: " + e.getMessage());
2227
+ }
2228
+ }
2229
+
2230
+ // ---------------- Parsing ----------------
2231
+
2232
+ private MessageHeader parseHeader(byte[] data) {
2233
+ try {
2234
+ CodedInputStream input = CodedInputStream.newInstance(data);
2235
+ int magic = 0;
2236
+ int function = 0;
2237
+ int cmd = 0;
2238
+ int ctrl = 0;
2239
+ int seq = 0;
2240
+ byte[] payload = new byte[0];
2241
+ int totalBytes = -1;
2242
+
2243
+ while (!input.isAtEnd()) {
2244
+ int tag = input.readTag();
2245
+ if (tag == 0) break;
2246
+ int fieldNumber = WireFormat.getTagFieldNumber(tag);
2247
+ switch (fieldNumber) {
2248
+ case 1:
2249
+ magic = input.readFixed32();
2250
+ break;
2251
+ case 2:
2252
+ function = input.readUInt32();
2253
+ break;
2254
+ case 3:
2255
+ cmd = input.readUInt32();
2256
+ break;
2257
+ case 5:
2258
+ ctrl = input.readUInt32();
2259
+ break;
2260
+ case 6:
2261
+ seq = input.readUInt32();
2262
+ break;
2263
+ case 8:
2264
+ payload = input.readByteArray();
2265
+ totalBytes = input.getTotalBytesRead();
2266
+ break;
2267
+ default:
2268
+ input.skipField(tag);
2269
+ break;
2270
+ }
2271
+ }
2272
+
2273
+ if (magic != MAGIC_NUMBER) return null;
2274
+ if (totalBytes <= 0) {
2275
+ totalBytes = data.length;
2276
+ }
2277
+ return new MessageHeader(function, cmd, ctrl, seq, payload, totalBytes);
2278
+ } catch (Throwable t) {
2279
+ return null;
2280
+ }
2281
+ }
2282
+
2283
+
2284
+ // ---------------- Connection state ----------------
2285
+
2286
+ private void markReady() {
2287
+ if (controlWriteCharacteristic == null || controlNotifyCharacteristic == null) return;
2288
+ if (ConnTypes.CONNECTED.equals(getConnectionState()) && getConnected() && getFullyBooted()) return;
2289
+
2290
+ minimalModeReady = false;
2291
+ hasPendingDisplayUpdate = false;
2292
+ pendingDisplayText = "";
2293
+ resetMinimalTextSession();
2294
+ hasReceivedBatteryForCurrentConnection = false;
2295
+ if (currentProjectName != null && !currentProjectName.trim().isEmpty()) {
2296
+ DeviceStore.INSTANCE.apply("bluetooth", "project_name", currentProjectName.trim());
2297
+ }
2298
+ String connectedBluetoothName = getConnectedBluetoothName();
2299
+ if (connectedBluetoothName != null && !connectedBluetoothName.trim().isEmpty()) {
2300
+ DeviceStore.INSTANCE.apply("glasses", "bluetoothName", connectedBluetoothName.trim());
2301
+ }
2302
+ DeviceStore.INSTANCE.apply("glasses", "connected", true);
2303
+ DeviceStore.INSTANCE.apply("glasses", "fullyBooted", true);
2304
+ updateConnectionState(ConnTypes.CONNECTED);
2305
+
2306
+ handler.removeCallbacks(readyMinimalModeRunnable);
2307
+ handler.removeCallbacks(readyLanguageRunnable);
2308
+ handler.removeCallbacks(readyDeviceInfoRunnable);
2309
+ handler.removeCallbacks(readyBrightnessRunnable);
2310
+ handler.removeCallbacks(readyBatteryRunnable);
2311
+ handler.postDelayed(readyMinimalModeRunnable, READY_MINIMAL_MODE_DELAY_MS);
2312
+ handler.postDelayed(readyLanguageRunnable, READY_LANGUAGE_DELAY_MS);
2313
+ handler.postDelayed(readyDeviceInfoRunnable, READY_DEVICE_INFO_DELAY_MS);
2314
+ handler.postDelayed(readyBrightnessRunnable, READY_BRIGHTNESS_DELAY_MS);
2315
+ handler.postDelayed(readyBatteryRunnable, READY_BATTERY_DELAY_MS);
2316
+ scheduleBatteryQueries();
2317
+ }
2318
+
2319
+ private void scheduleBatteryQueries() {
2320
+ pendingBatteryRetries = 0;
2321
+ batteryQueryHandler.removeCallbacks(initialBatteryRetryRunnable);
2322
+ batteryQueryHandler.removeCallbacks(periodicBatteryPollRunnable);
2323
+
2324
+ batteryQueryHandler.postDelayed(initialBatteryRetryRunnable, INITIAL_BATTERY_RETRY_DELAY_MS);
2325
+ batteryQueryHandler.postDelayed(periodicBatteryPollRunnable, BATTERY_POLL_INTERVAL_MS);
2326
+ }
2327
+
2328
+ private void cleanupGatt() {
2329
+ writeQueue.clear();
2330
+ synchronized (otaWriteQueue) {
2331
+ otaWriteQueue.clear();
2332
+ isOtaWriteInFlight = false;
2333
+ otaWriteUsesNoResponseMode = false;
2334
+ }
2335
+ notifyQueue.clear();
2336
+ isWriteInFlight = false;
2337
+ isOtaNotificationEnabled = false;
2338
+ isNotifyWriteInFlight = false;
2339
+ minimalModeReady = false;
2340
+ resetMinimalTextSession();
2341
+ hasPendingDisplayUpdate = false;
2342
+ pendingDisplayText = "";
2343
+ hasReceivedBatteryForCurrentConnection = false;
2344
+ controlNotifyBuffer = new byte[0];
2345
+ pendingBatteryRetries = 0;
2346
+ handler.removeCallbacks(readyMinimalModeRunnable);
2347
+ handler.removeCallbacks(readyLanguageRunnable);
2348
+ handler.removeCallbacks(readyDeviceInfoRunnable);
2349
+ handler.removeCallbacks(readyBrightnessRunnable);
2350
+ handler.removeCallbacks(readyBatteryRunnable);
2351
+ handler.removeCallbacks(writeDrainRunnable);
2352
+ batteryQueryHandler.removeCallbacks(initialBatteryRetryRunnable);
2353
+ batteryQueryHandler.removeCallbacks(periodicBatteryPollRunnable);
2354
+
2355
+ controlWriteCharacteristic = null;
2356
+ controlNotifyCharacteristic = null;
2357
+ opusNotifyCharacteristic = null;
2358
+ otaWriteCharacteristic = null;
2359
+ otaNotifyCharacteristic = null;
2360
+
2361
+ BluetoothGatt gatt = controlGatt;
2362
+ if (gatt != null) {
2363
+ try {
2364
+ gatt.disconnect();
2365
+ } catch (Throwable t) {
2366
+ // ignore
2367
+ }
2368
+ try {
2369
+ gatt.close();
2370
+ } catch (Throwable t) {
2371
+ // ignore
2372
+ }
2373
+ }
2374
+ controlGatt = null;
2375
+
2376
+ opusLoggedFirstInSession = false;
2377
+
2378
+ if (opusPcmDecoder != null) {
2379
+ opusPcmDecoder.resetBuffer();
2380
+ }
2381
+
2382
+ awaitingMicDisableAck = false;
2383
+ DeviceStore.INSTANCE.apply("glasses", "connected", false);
2384
+ DeviceStore.INSTANCE.apply("glasses", "fullyBooted", false);
2385
+ DeviceStore.INSTANCE.apply("glasses", "micEnabled", false);
2386
+ DeviceStore.INSTANCE.apply("glasses", "batteryLevel", -1);
2387
+ DeviceStore.INSTANCE.apply("glasses", "charging", false);
2388
+ }
2389
+
2390
+ private void updateConnectionState(String state) {
2391
+ DeviceStore.INSTANCE.apply("glasses", "connectionState", state);
2392
+ }
2393
+
2394
+ // ---------------- Advertisement parsing (BtUtil-like) ----------------
2395
+
2396
+ private boolean matchesAr99(String name, String projectName) {
2397
+ return isSupportedProjectName(projectName);
2398
+ }
2399
+
2400
+ static boolean isSupportedProjectName(String projectName) {
2401
+ String project = projectName != null ? projectName.trim() : "";
2402
+ for (String supported : SUPPORTED_PROJECT_NAMES) {
2403
+ if (project.equalsIgnoreCase(supported)) return true;
2404
+ }
2405
+ return false;
2406
+ }
2407
+ private ParsedAdvertisement parseAdvertisement(byte[] scanRecord) {
2408
+ if (scanRecord == null || scanRecord.length == 0) return null;
2409
+
2410
+ int pos = 0;
2411
+ String bleName = null;
2412
+ String projectName = null;
2413
+ String bleAddress = null;
2414
+ String btAddress = null;
2415
+ String serialNumber = null;
2416
+
2417
+ while (pos < scanRecord.length - 1) {
2418
+ int size = scanRecord[pos] & 0xFF;
2419
+ if (size == 0 || pos + size >= scanRecord.length) break;
2420
+ int type = scanRecord[pos + 1] & 0xFF;
2421
+
2422
+ switch (type) {
2423
+ case 0x09:
2424
+ bleName = new String(scanRecord, pos + 2, size - 1);
2425
+ break;
2426
+ case 0xFF:
2427
+ int p = pos + 2;
2428
+ if (size >= 21 && p + 19 < scanRecord.length) {
2429
+ projectName = trimAscii4(scanRecord, p);
2430
+ bleAddress = xorMac6ToColon(scanRecord, p + 8);
2431
+ btAddress = xorMac6ToColon(scanRecord, p + 14);
2432
+ serialNumber = parseAr99SerialNumber(scanRecord, pos, size);
2433
+ }
2434
+ break;
2435
+ default:
2436
+ break;
2437
+ }
2438
+
2439
+ pos += size + 1;
2440
+ }
2441
+
2442
+ if (!matchesAr99(bleName, projectName)) return null;
2443
+ return new ParsedAdvertisement(bleName, projectName, bleAddress, btAddress, serialNumber);
2444
+ }
2445
+
2446
+ private String trimAscii4(byte[] data, int offset) {
2447
+ int end = offset + 4;
2448
+ while (end > offset && data[end - 1] == 0) end--;
2449
+ if (end <= offset) return "";
2450
+ try {
2451
+ return new String(data, offset, end - offset, "UTF-8");
2452
+ } catch (Exception e) {
2453
+ return "";
2454
+ }
2455
+ }
2456
+
2457
+ private String xorMac6ToColon(byte[] data, int offset) {
2458
+ StringBuilder sb = new StringBuilder();
2459
+ for (int i = 0; i < 6; i++) {
2460
+ int decoded = (data[offset + i] ^ 0xAD) & 0xFF;
2461
+ if (i > 0) sb.append(":");
2462
+ sb.append(String.format("%02X", decoded));
2463
+ }
2464
+ return sb.toString();
2465
+ }
2466
+
2467
+ private String parseAr99SerialNumber(byte[] scanRecord, int fieldStart, int size) {
2468
+ String project = trimAscii4(scanRecord, fieldStart + 2);
2469
+ if (!matchesAr99(null, project)) return null;
2470
+
2471
+ int lastPayloadIdx = fieldStart + size;
2472
+ int serialOffset = fieldStart + 22;
2473
+
2474
+ if (serialOffset + 12 <= scanRecord.length && serialOffset + 12 - 1 <= lastPayloadIdx) {
2475
+ if (!isAllZeroBytes(scanRecord, serialOffset, 12) && isAsciiDigits(scanRecord, serialOffset, 12)) {
2476
+ return safeAscii(scanRecord, serialOffset, 12);
2477
+ }
2478
+ }
2479
+
2480
+ if (serialOffset + 6 <= scanRecord.length && serialOffset + 6 - 1 <= lastPayloadIdx) {
2481
+ if (!isAllZeroBytes(scanRecord, serialOffset, 6) && isAsciiDigits(scanRecord, serialOffset, 6)) {
2482
+ return safeAscii(scanRecord, serialOffset, 6);
2483
+ }
2484
+ if (!isAllZeroBytes(scanRecord, serialOffset, 6)) {
2485
+ return bytes6ToHex12(scanRecord, serialOffset);
2486
+ }
2487
+ }
2488
+
2489
+ return null;
2490
+ }
2491
+
2492
+ private boolean isAsciiDigits(byte[] data, int offset, int length) {
2493
+ for (int i = 0; i < length; i++) {
2494
+ int b = data[offset + i] & 0xFF;
2495
+ if (b < '0' || b > '9') return false;
2496
+ }
2497
+ return true;
2498
+ }
2499
+
2500
+ private boolean isAllZeroBytes(byte[] data, int offset, int length) {
2501
+ boolean allZero = true;
2502
+ boolean allAsciiZero = true;
2503
+ for (int i = 0; i < length; i++) {
2504
+ int b = data[offset + i] & 0xFF;
2505
+ if (b != 0) {
2506
+ allZero = false;
2507
+ }
2508
+ if (b != '0') {
2509
+ allAsciiZero = false;
2510
+ }
2511
+ }
2512
+ return allZero || allAsciiZero;
2513
+ }
2514
+
2515
+ private String safeAscii(byte[] data, int offset, int length) {
2516
+ try {
2517
+ return new String(data, offset, length, "UTF-8");
2518
+ } catch (Exception e) {
2519
+ return null;
2520
+ }
2521
+ }
2522
+
2523
+ private String bytes6ToHex12(byte[] data, int offset) {
2524
+ StringBuilder sb = new StringBuilder(12);
2525
+ for (int i = 0; i < 6; i++) {
2526
+ sb.append(String.format("%02X", data[offset + i] & 0xFF));
2527
+ }
2528
+ return sb.toString();
2529
+ }
2530
+
2531
+ private String buildDisplayName(
2532
+ String fallbackName, ParsedAdvertisement advertisement, String deviceAddress) {
2533
+ if (advertisement != null && advertisement.serialNumber != null && !advertisement.serialNumber.trim().isEmpty()) {
2534
+ return "SN: " + advertisement.serialNumber.trim();
2535
+ }
2536
+ String bleName = advertisement != null ? advertisement.bleName : fallbackName;
2537
+ String legacyNameSerial = extractLegacyNameSerial(bleName);
2538
+ if (!legacyNameSerial.isEmpty()) {
2539
+ return "SN: " + legacyNameSerial;
2540
+ }
2541
+ if (deviceAddress != null && !deviceAddress.trim().isEmpty()) {
2542
+ return "MAC: " + deviceAddress.trim();
2543
+ }
2544
+ return fallbackName;
2545
+ }
2546
+
2547
+ private String extractTargetSerial(String target) {
2548
+ String trimmed = normalizeDisplayIdentifier(target);
2549
+ if (trimmed.isEmpty()) return "";
2550
+ int underscore = trimmed.lastIndexOf('_');
2551
+ if (underscore >= 0 && underscore + 1 < trimmed.length()) {
2552
+ return trimmed.substring(underscore + 1).trim();
2553
+ }
2554
+ return trimmed;
2555
+ }
2556
+
2557
+ private String normalizeDisplayIdentifier(String value) {
2558
+ if (value == null) return "";
2559
+ String trimmed = value.trim();
2560
+ String upper = trimmed.toUpperCase();
2561
+ if (upper.startsWith("SN:")) {
2562
+ return trimmed.substring(3).trim();
2563
+ }
2564
+ if (upper.startsWith("MAC:")) {
2565
+ return trimmed.substring(4).trim();
2566
+ }
2567
+ return trimmed;
2568
+ }
2569
+
2570
+ private String extractLegacyNameSerial(String bleName) {
2571
+ if (bleName == null) return "";
2572
+ String trimmed = bleName.trim();
2573
+ int underscore = trimmed.indexOf('_');
2574
+ if (underscore < 0 || underscore + 1 >= trimmed.length()) {
2575
+ return "";
2576
+ }
2577
+ return trimmed.substring(underscore + 1).trim();
2578
+ }
2579
+
2580
+ // ---------------- Data holders ----------------
2581
+
2582
+ private static class ParsedAdvertisement {
2583
+ final String bleName;
2584
+ final String projectName;
2585
+ final String bleAddress;
2586
+ final String btAddress;
2587
+ final String serialNumber;
2588
+
2589
+ ParsedAdvertisement(String bleName, String projectName, String bleAddress, String btAddress, String serialNumber) {
2590
+ this.bleName = bleName;
2591
+ this.projectName = projectName;
2592
+ this.bleAddress = bleAddress;
2593
+ this.btAddress = btAddress;
2594
+ this.serialNumber = serialNumber;
2595
+ }
2596
+ }
2597
+
2598
+ private static class MessageHeader {
2599
+ final int function;
2600
+ final int cmd;
2601
+ final int ctrl;
2602
+ final int sequence;
2603
+ final byte[] payload;
2604
+ final int totalBytes;
2605
+
2606
+ MessageHeader(int function, int cmd, int ctrl, int sequence, byte[] payload, int totalBytes) {
2607
+ this.function = function;
2608
+ this.cmd = cmd;
2609
+ this.ctrl = ctrl;
2610
+ this.sequence = sequence;
2611
+ this.payload = payload;
2612
+ this.totalBytes = totalBytes;
2613
+ }
2614
+ }
2615
+
2616
+ private static class DeviceInfo {
2617
+ final String firmwareVersion;
2618
+ final String deviceName;
2619
+ final String productName;
2620
+ final String btMac;
2621
+ final String serialNumber;
2622
+
2623
+ DeviceInfo(String firmwareVersion, String deviceName, String productName, String btMac, String serialNumber) {
2624
+ this.firmwareVersion = firmwareVersion;
2625
+ this.deviceName = deviceName;
2626
+ this.productName = productName;
2627
+ this.btMac = btMac;
2628
+ this.serialNumber = serialNumber;
2629
+ }
2630
+ }
2631
+
2632
+ private static class CommonResult {
2633
+ final boolean success;
2634
+ final int errorCode;
2635
+ final String errorMessage;
2636
+
2637
+ CommonResult(boolean success, int errorCode, String errorMessage) {
2638
+ this.success = success;
2639
+ this.errorCode = errorCode;
2640
+ this.errorMessage = errorMessage;
2641
+ }
2642
+ }
2643
+
2644
+ private static class BatteryInfo {
2645
+ final int battery;
2646
+ final Boolean charging;
2647
+
2648
+ BatteryInfo(int battery, Boolean charging) {
2649
+ this.battery = battery;
2650
+ this.charging = charging;
2651
+ }
2652
+ }
2653
+ }