@glassly/bluetooth-sdk 0.1.0-dev.100

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 (553) 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/assets/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
  139. package/android/src/main/assets/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
  140. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2321 -0
  141. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
  142. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
  143. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
  144. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1087 -0
  145. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
  146. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2472 -0
  147. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +359 -0
  148. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +9 -0
  149. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  150. package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
  151. package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
  152. package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
  153. package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
  154. package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  155. package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
  156. package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
  157. package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  158. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  159. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +1057 -0
  160. package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
  161. package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
  162. package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
  163. package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
  164. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  165. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
  166. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  167. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
  168. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
  169. package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
  170. package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
  171. package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
  172. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
  173. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  174. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  175. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3837 -0
  176. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  177. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6457 -0
  178. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
  179. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1253 -0
  180. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
  181. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
  182. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
  183. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
  184. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1267 -0
  185. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10742 -0
  186. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  187. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  188. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2008 -0
  189. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  190. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
  191. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
  192. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  193. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  194. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  195. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  196. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  197. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  198. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
  199. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
  200. package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
  201. package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
  202. package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
  203. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
  204. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
  205. package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +227 -0
  206. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
  207. package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
  208. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
  209. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  210. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  211. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  212. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
  213. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
  214. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  215. package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
  216. package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
  217. package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  218. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  219. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
  220. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  221. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
  222. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
  223. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
  224. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
  225. package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
  226. package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  227. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  228. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  229. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  230. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  231. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  232. package/android/src/main/res/values/strings.xml +4 -0
  233. package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  234. package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  235. package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  236. package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  237. package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  238. package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  239. package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
  240. package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  241. package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  242. package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  243. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  244. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  245. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  246. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
  247. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  248. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
  249. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
  250. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
  251. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
  252. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  253. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  254. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  255. package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
  256. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  257. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
  258. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
  259. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  260. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  261. package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  262. package/android/src/test/resources/avif_with_exif.avif +0 -0
  263. package/app.plugin.js +3 -0
  264. package/build/BluetoothSdk.types.d.ts +1390 -0
  265. package/build/BluetoothSdk.types.d.ts.map +1 -0
  266. package/build/BluetoothSdk.types.js +41 -0
  267. package/build/BluetoothSdk.types.js.map +1 -0
  268. package/build/_internal.d.ts +17 -0
  269. package/build/_internal.d.ts.map +1 -0
  270. package/build/_internal.js +16 -0
  271. package/build/_internal.js.map +1 -0
  272. package/build/_private/BluetoothSdkModule.d.ts +171 -0
  273. package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
  274. package/build/_private/BluetoothSdkModule.js +300 -0
  275. package/build/_private/BluetoothSdkModule.js.map +1 -0
  276. package/build/_private/LocalNetworkModule.d.ts +38 -0
  277. package/build/_private/LocalNetworkModule.d.ts.map +1 -0
  278. package/build/_private/LocalNetworkModule.js +3 -0
  279. package/build/_private/LocalNetworkModule.js.map +1 -0
  280. package/build/_private/cameraRequestPayload.d.ts +4 -0
  281. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  282. package/build/_private/cameraRequestPayload.js +32 -0
  283. package/build/_private/cameraRequestPayload.js.map +1 -0
  284. package/build/_private/photoRequestPayload.d.ts +6 -0
  285. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  286. package/build/_private/photoRequestPayload.js +86 -0
  287. package/build/_private/photoRequestPayload.js.map +1 -0
  288. package/build/_private/sdkOtaManifest.d.ts +11 -0
  289. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  290. package/build/_private/sdkOtaManifest.js +15 -0
  291. package/build/_private/sdkOtaManifest.js.map +1 -0
  292. package/build/changelogs.d.ts +7 -0
  293. package/build/changelogs.d.ts.map +1 -0
  294. package/build/changelogs.js +46 -0
  295. package/build/changelogs.js.map +1 -0
  296. package/build/debug.d.ts +3 -0
  297. package/build/debug.d.ts.map +1 -0
  298. package/build/debug.js +8 -0
  299. package/build/debug.js.map +1 -0
  300. package/build/generated/changelogCatalog.d.ts +12 -0
  301. package/build/generated/changelogCatalog.d.ts.map +1 -0
  302. package/build/generated/changelogCatalog.js +16 -0
  303. package/build/generated/changelogCatalog.js.map +1 -0
  304. package/build/generated/releaseMetadata.d.ts +12 -0
  305. package/build/generated/releaseMetadata.d.ts.map +1 -0
  306. package/build/generated/releaseMetadata.js +10 -0
  307. package/build/generated/releaseMetadata.js.map +1 -0
  308. package/build/index.d.ts +6 -0
  309. package/build/index.d.ts.map +1 -0
  310. package/build/index.js +170 -0
  311. package/build/index.js.map +1 -0
  312. package/build/ota-server/OtaServer.types.d.ts +14 -0
  313. package/build/ota-server/OtaServer.types.d.ts.map +1 -0
  314. package/build/ota-server/OtaServer.types.js +2 -0
  315. package/build/ota-server/OtaServer.types.js.map +1 -0
  316. package/build/ota-server/OtaServerModule.d.ts +21 -0
  317. package/build/ota-server/OtaServerModule.d.ts.map +1 -0
  318. package/build/ota-server/OtaServerModule.js +3 -0
  319. package/build/ota-server/OtaServerModule.js.map +1 -0
  320. package/build/ota-server/OtaServerModule.web.d.ts +10 -0
  321. package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
  322. package/build/ota-server/OtaServerModule.web.js +12 -0
  323. package/build/ota-server/OtaServerModule.web.js.map +1 -0
  324. package/build/ota-server/index.d.ts +3 -0
  325. package/build/ota-server/index.d.ts.map +1 -0
  326. package/build/ota-server/index.js +3 -0
  327. package/build/ota-server/index.js.map +1 -0
  328. package/build/ota-transport/index.d.ts +67 -0
  329. package/build/ota-transport/index.d.ts.map +1 -0
  330. package/build/ota-transport/index.js +58 -0
  331. package/build/ota-transport/index.js.map +1 -0
  332. package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
  333. package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
  334. package/build/photo-receiver/PhotoReceiver.types.js +2 -0
  335. package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
  336. package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
  337. package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
  338. package/build/photo-receiver/PhotoReceiverModule.js +3 -0
  339. package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
  340. package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
  341. package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
  342. package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
  343. package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
  344. package/build/photo-receiver/index.d.ts +3 -0
  345. package/build/photo-receiver/index.d.ts.map +1 -0
  346. package/build/photo-receiver/index.js +3 -0
  347. package/build/photo-receiver/index.js.map +1 -0
  348. package/build/react/index.d.ts +5 -0
  349. package/build/react/index.d.ts.map +1 -0
  350. package/build/react/index.js +4 -0
  351. package/build/react/index.js.map +1 -0
  352. package/build/react/useBluetooth.d.ts +100 -0
  353. package/build/react/useBluetooth.d.ts.map +1 -0
  354. package/build/react/useBluetooth.js +167 -0
  355. package/build/react/useBluetooth.js.map +1 -0
  356. package/build/react/useBluetoothEvent.d.ts +6 -0
  357. package/build/react/useBluetoothEvent.d.ts.map +1 -0
  358. package/build/react/useBluetoothEvent.js +21 -0
  359. package/build/react/useBluetoothEvent.js.map +1 -0
  360. package/build/react/useBluetoothScan.d.ts +22 -0
  361. package/build/react/useBluetoothScan.d.ts.map +1 -0
  362. package/build/react/useBluetoothScan.js +135 -0
  363. package/build/react/useBluetoothScan.js.map +1 -0
  364. package/build/react/useBluetoothStatus.d.ts +16 -0
  365. package/build/react/useBluetoothStatus.d.ts.map +1 -0
  366. package/build/react/useBluetoothStatus.js +137 -0
  367. package/build/react/useBluetoothStatus.js.map +1 -0
  368. package/build/react/useGlassesConnection.d.ts +29 -0
  369. package/build/react/useGlassesConnection.d.ts.map +1 -0
  370. package/build/react/useGlassesConnection.js +153 -0
  371. package/build/react/useGlassesConnection.js.map +1 -0
  372. package/build/types/index.d.ts +3 -0
  373. package/build/types/index.d.ts.map +1 -0
  374. package/build/types/index.js +2 -0
  375. package/build/types/index.js.map +1 -0
  376. package/expo-module.config.json +14 -0
  377. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  378. package/ios/BluetoothSdkModule.swift +1028 -0
  379. package/ios/GlasslyBluetoothSDK.podspec +106 -0
  380. package/ios/LocalIPv4.swift +150 -0
  381. package/ios/LocalOtaServer.swift +309 -0
  382. package/ios/LocalPhotoUploadServer.swift +754 -0
  383. package/ios/OtaServerModule.swift +147 -0
  384. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  385. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  386. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  387. package/ios/Packages/CoreObjC/attdet.c +92 -0
  388. package/ios/Packages/CoreObjC/attdet.h +44 -0
  389. package/ios/Packages/CoreObjC/bits.c +375 -0
  390. package/ios/Packages/CoreObjC/bits.h +315 -0
  391. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  392. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  393. package/ios/Packages/CoreObjC/common.h +151 -0
  394. package/ios/Packages/CoreObjC/energy.c +70 -0
  395. package/ios/Packages/CoreObjC/energy.h +43 -0
  396. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  397. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  398. package/ios/Packages/CoreObjC/lc3.c +704 -0
  399. package/ios/Packages/CoreObjC/lc3.h +313 -0
  400. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  401. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  402. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  403. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  404. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  405. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  406. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  407. package/ios/Packages/CoreObjC/mdct.c +469 -0
  408. package/ios/Packages/CoreObjC/mdct.h +57 -0
  409. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  410. package/ios/Packages/CoreObjC/meson.build +61 -0
  411. package/ios/Packages/CoreObjC/plc.c +61 -0
  412. package/ios/Packages/CoreObjC/plc.h +57 -0
  413. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  414. package/ios/Packages/CoreObjC/sns.c +880 -0
  415. package/ios/Packages/CoreObjC/sns.h +103 -0
  416. package/ios/Packages/CoreObjC/spec.c +907 -0
  417. package/ios/Packages/CoreObjC/spec.h +119 -0
  418. package/ios/Packages/CoreObjC/tables.c +3457 -0
  419. package/ios/Packages/CoreObjC/tables.h +94 -0
  420. package/ios/Packages/CoreObjC/tns.c +457 -0
  421. package/ios/Packages/CoreObjC/tns.h +99 -0
  422. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  423. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  424. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
  425. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
  426. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  427. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  428. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  429. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  430. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  431. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  432. package/ios/Packages/libbz2/module.modulemap +5 -0
  433. package/ios/Packages/libbz2/shim.h +1 -0
  434. package/ios/PhotoReceiverModule.swift +129 -0
  435. package/ios/Source/BluetoothSDK.swift +2299 -0
  436. package/ios/Source/BluetoothSDKDebug.swift +12 -0
  437. package/ios/Source/BluetoothSdkDefaults.swift +29 -0
  438. package/ios/Source/Bridge.swift +708 -0
  439. package/ios/Source/Bridging-Header.h +16 -0
  440. package/ios/Source/DeviceManager.swift +2054 -0
  441. package/ios/Source/DeviceStore.swift +350 -0
  442. package/ios/Source/GeneratedChangelogCatalog.swift +8 -0
  443. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  444. package/ios/Source/ObservableStore.swift +118 -0
  445. package/ios/Source/OtaManifest.swift +211 -0
  446. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  447. package/ios/Source/ReleaseChangelog.swift +65 -0
  448. package/ios/Source/Resources/G2Firmware/README.md +10 -0
  449. package/ios/Source/Resources/G2Firmware/g2-glassly-cfw-2.2.9.22.evenota.bin +0 -0
  450. package/ios/Source/Resources/G2Firmware/g2-stock-2.2.9.22.evenota.bin +0 -0
  451. package/ios/Source/audio/AudioModels.swift +178 -0
  452. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  453. package/ios/Source/camera/CameraModels.swift +866 -0
  454. package/ios/Source/connection/ScanSession.swift +27 -0
  455. package/ios/Source/controllers/ControllerManager.swift +184 -0
  456. package/ios/Source/controllers/R1.swift +925 -0
  457. package/ios/Source/errors/BluetoothSdkError.swift +19 -0
  458. package/ios/Source/events/BluetoothEvents.swift +342 -0
  459. package/ios/Source/internal/BleTraceLogger.swift +192 -0
  460. package/ios/Source/internal/BluetoothAvailability.swift +78 -0
  461. package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
  462. package/ios/Source/internal/ValueParsing.swift +99 -0
  463. package/ios/Source/requests/DisplayRequests.swift +82 -0
  464. package/ios/Source/services/PhoneMic.swift +662 -0
  465. package/ios/Source/sgcs/Ar99.swift +2451 -0
  466. package/ios/Source/sgcs/G1.swift +2554 -0
  467. package/ios/Source/sgcs/G2.swift +7142 -0
  468. package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
  469. package/ios/Source/sgcs/G2CfwRenderer.swift +1295 -0
  470. package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
  471. package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
  472. package/ios/Source/sgcs/G2Flasher.swift +773 -0
  473. package/ios/Source/sgcs/Mach1.swift +541 -0
  474. package/ios/Source/sgcs/MentraLive.swift +7011 -0
  475. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  476. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  477. package/ios/Source/sgcs/MentraNex.swift +3220 -0
  478. package/ios/Source/sgcs/Nimo.swift +1886 -0
  479. package/ios/Source/sgcs/SGCManager.swift +621 -0
  480. package/ios/Source/sgcs/Simulated.swift +274 -0
  481. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  482. package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
  483. package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
  484. package/ios/Source/status/DeviceStatus.swift +981 -0
  485. package/ios/Source/status/RuntimeState.swift +359 -0
  486. package/ios/Source/status/WifiHotspotStatus.swift +345 -0
  487. package/ios/Source/streaming/StreamModels.swift +693 -0
  488. package/ios/Source/stt/STTTools.swift +191 -0
  489. package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
  490. package/ios/Source/tts/TTSTools.swift +167 -0
  491. package/ios/Source/types/DeviceModels.swift +177 -0
  492. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  493. package/ios/Source/utils/BinaryCodec.swift +61 -0
  494. package/ios/Source/utils/BleJsonCompact.swift +405 -0
  495. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  496. package/ios/Source/utils/Constants.swift +78 -0
  497. package/ios/Source/utils/Enums.swift +95 -0
  498. package/ios/Source/utils/G1Text.swift +2096 -0
  499. package/ios/Source/utils/G2NotifyFile.swift +116 -0
  500. package/ios/Source/utils/JSCExperiment.swift +241 -0
  501. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  502. package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
  503. package/ios/Source/utils/MessageChunker.swift +294 -0
  504. package/ios/Source/utils/Models.swift +135 -0
  505. package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
  506. package/ios/Source/utils/TarBz2Extractor.swift +257 -0
  507. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
  508. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
  509. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
  510. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
  511. package/ios/Tests/BluetoothSDKTests/G2NotifyFileTests.swift +91 -0
  512. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  513. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  514. package/package.json +129 -0
  515. package/plugin/build/index.d.ts +10 -0
  516. package/plugin/build/index.js +12 -0
  517. package/plugin/build/withAndroid.d.ts +3 -0
  518. package/plugin/build/withAndroid.js +190 -0
  519. package/plugin/build/withIos.d.ts +5 -0
  520. package/plugin/build/withIos.js +108 -0
  521. package/scripts/inject-ios-sdk-version.mjs +65 -0
  522. package/scripts/public-ota-api.test.mjs +37 -0
  523. package/scripts/verify-release-package.mjs +46 -0
  524. package/scripts/write-release-metadata.mjs +154 -0
  525. package/scripts/write-release-metadata.test.mjs +37 -0
  526. package/src/BluetoothSdk.types.ts +1659 -0
  527. package/src/_internal.ts +16 -0
  528. package/src/_private/BluetoothSdkModule.ts +683 -0
  529. package/src/_private/LocalNetworkModule.ts +50 -0
  530. package/src/_private/cameraRequestPayload.ts +34 -0
  531. package/src/_private/photoRequestPayload.ts +90 -0
  532. package/src/_private/sdkOtaManifest.ts +18 -0
  533. package/src/changelogs.ts +46 -0
  534. package/src/debug.ts +9 -0
  535. package/src/generated/changelogCatalog.ts +17 -0
  536. package/src/generated/releaseMetadata.ts +20 -0
  537. package/src/index.ts +320 -0
  538. package/src/ota-server/OtaServer.types.ts +10 -0
  539. package/src/ota-server/OtaServerModule.ts +23 -0
  540. package/src/ota-server/OtaServerModule.web.ts +17 -0
  541. package/src/ota-server/index.ts +2 -0
  542. package/src/ota-transport/index.ts +116 -0
  543. package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
  544. package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
  545. package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
  546. package/src/photo-receiver/index.ts +2 -0
  547. package/src/react/index.ts +21 -0
  548. package/src/react/useBluetooth.ts +309 -0
  549. package/src/react/useBluetoothEvent.ts +35 -0
  550. package/src/react/useBluetoothScan.ts +173 -0
  551. package/src/react/useBluetoothStatus.ts +180 -0
  552. package/src/react/useGlassesConnection.ts +202 -0
  553. package/src/types/index.ts +12 -0
@@ -0,0 +1,1253 @@
1
+ package com.glassly.bluetoothsdk.sgcs
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Paint
8
+ import android.graphics.Rect
9
+ import android.graphics.RectF
10
+ import android.graphics.Typeface
11
+ import android.text.Layout
12
+ import android.text.StaticLayout
13
+ import android.text.TextPaint
14
+ import android.util.Base64
15
+ import java.io.ByteArrayOutputStream
16
+ import java.util.zip.DeflaterOutputStream
17
+ import kotlin.math.roundToInt
18
+
19
+ /** Phone-side full-scene rasterizer for Glassly CFW's direct-framebuffer protocol. */
20
+ internal object G2CfwRenderer {
21
+ const val FRAME_WIDTH = 640
22
+ const val FRAME_HEIGHT = 480
23
+
24
+ private val heightOffsets = intArrayOf(192, 176, 160, 144, 128, 112, 96, 80, 64, 48, 32, 16, 0)
25
+ private val depthOffsetsLeft = intArrayOf(40, 32, 24)
26
+ private val depthOffsetsRight = intArrayOf(24, 32, 40)
27
+
28
+ data class LensBoxes(
29
+ val leftX: Int,
30
+ val leftY: Int,
31
+ val rightX: Int,
32
+ val rightY: Int,
33
+ val width: Int,
34
+ val height: Int,
35
+ )
36
+
37
+ fun lensBoxes(
38
+ canvasWidth: Int,
39
+ canvasHeight: Int,
40
+ heightLevel: Int,
41
+ depthLevel: Int,
42
+ ): LensBoxes {
43
+ val heightIndex = heightLevel.coerceIn(heightOffsets.indices)
44
+ val depthIndex = depthLevel.coerceIn(depthOffsetsLeft.indices)
45
+ val width = canvasWidth.coerceIn(4, FRAME_WIDTH) and 3.inv()
46
+ val height = canvasHeight.coerceIn(2, FRAME_HEIGHT) and 1.inv()
47
+
48
+ fun x(value: Int) = value.coerceIn(0, FRAME_WIDTH - width) and 3.inv()
49
+ fun y(value: Int) = value.coerceIn(0, FRAME_HEIGHT - height) and 1.inv()
50
+ return LensBoxes(
51
+ leftX = x(depthOffsetsLeft[depthIndex]),
52
+ leftY = y(heightOffsets[heightIndex]),
53
+ rightX = x(depthOffsetsRight[depthIndex]),
54
+ rightY = y(heightOffsets[heightIndex]),
55
+ width = width,
56
+ height = height,
57
+ )
58
+ }
59
+
60
+ fun monoLineHeight(fontSize: Float): Float = (fontSize * 1.2f).roundToInt().toFloat()
61
+
62
+ fun encode(frame: SceneFrame, boxes: LensBoxes, frameId: Int): ByteArray? {
63
+ val pixels = rasterize(frame, boxes.width, boxes.height) ?: return null
64
+ return stereoFrame(pixels, boxes, frameId)
65
+ }
66
+
67
+ // ── Grid-exact mono glyphs (texture-cache path) ─────────────────────────
68
+
69
+ /**
70
+ * Texture-cache state. Modes 12/13/14 draw at absolute panel coordinates
71
+ * (no per-lens variant), so the caller only enables this path on frames
72
+ * whose window is placed lens-equal. The cache model resets whenever the
73
+ * caller's generation changes (lease cycled / residency unknown); the
74
+ * glyph raster memo survives resets — rasters don't change.
75
+ */
76
+ private val textureCache = G2CfwTextureCache()
77
+ private val glyphRasterMemo = mutableMapOf<String, ByteArray>()
78
+
79
+ private fun monoPaint(fontSize: Float): Paint =
80
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
81
+ color = Color.WHITE
82
+ textSize = fontSize
83
+ typeface = Typeface.MONOSPACE
84
+ }
85
+
86
+ /** Fixed cell metrics of the phone's monospaced face at `fontSize`: (cellWidth, lineHeight). */
87
+ fun monoCellMetrics(fontSize: Int): Pair<Int, Int> {
88
+ val size = (if (fontSize > 0) fontSize else 20).toFloat()
89
+ val advance = monoPaint(size).measureText("0")
90
+ return maxOf(1, advance.roundToInt()) to maxOf(1, monoLineHeight(size).toInt())
91
+ }
92
+
93
+ /**
94
+ * One full-cell glyph raster (cellWidth × lineHeight, 8-bit gray, white
95
+ * ink on black). This single function feeds BOTH the frame raster (grid
96
+ * blits) and the mode-12 uploads, so an on-glasses cached draw reproduces
97
+ * the phone raster byte-for-byte. Baseline placement matches the flat
98
+ * rasterizer's mono path (-ascent from the cell top).
99
+ */
100
+ fun monoGlyphRaster(codePoint: Int, fontSize: Int, cellWidth: Int, lineHeight: Int): ByteArray? {
101
+ val size = (if (fontSize > 0) fontSize else 20).toFloat()
102
+ val paint = monoPaint(size)
103
+ val bitmap = Bitmap.createBitmap(cellWidth, lineHeight, Bitmap.Config.ARGB_8888)
104
+ try {
105
+ val canvas = Canvas(bitmap)
106
+ canvas.drawColor(Color.BLACK)
107
+ canvas.drawText(String(Character.toChars(codePoint)), 0f, -paint.fontMetrics.ascent, paint)
108
+ val colors = IntArray(cellWidth * lineHeight)
109
+ bitmap.getPixels(colors, 0, cellWidth, 0, 0, cellWidth, lineHeight)
110
+ return ByteArray(colors.size) { index ->
111
+ val color = colors[index]
112
+ ((Color.red(color) * 299 + Color.green(color) * 587 + Color.blue(color) * 114) / 1000).toByte()
113
+ }
114
+ } finally {
115
+ bitmap.recycle()
116
+ }
117
+ }
118
+
119
+ private fun memoizedGlyphRaster(codePoint: Int, fontSize: Int, cellWidth: Int, lineHeight: Int): ByteArray? {
120
+ val key = "$fontSize|$codePoint"
121
+ glyphRasterMemo[key]?.let { return it }
122
+ val raster = monoGlyphRaster(codePoint, fontSize, cellWidth, lineHeight) ?: return null
123
+ glyphRasterMemo[key] = raster
124
+ return raster
125
+ }
126
+
127
+ /** A line's Unicode code points (mode 14 addresses cells per code point). */
128
+ private fun codePoints(line: String): IntArray {
129
+ val out = mutableListOf<Int>()
130
+ var i = 0
131
+ while (i < line.length) {
132
+ val cp = line.codePointAt(i)
133
+ out.add(cp)
134
+ i += Character.charCount(cp)
135
+ }
136
+ return out.toIntArray()
137
+ }
138
+
139
+ /**
140
+ * One mono text element's glyph grid, produced by the grid-exact
141
+ * rasterizer: cell (0,0) sits at (originX, originY) in canvas coords, and
142
+ * only the `fullCols`×`fullRows` cells wholly inside the element box (and
143
+ * canvas) are expressible as cached draws — clipped edge cells stay baked.
144
+ */
145
+ data class MonoGrid(
146
+ val z: Int,
147
+ val fontSize: Int,
148
+ val originX: Int,
149
+ val originY: Int,
150
+ val cellWidth: Int,
151
+ val lineHeight: Int,
152
+ val fullCols: Int,
153
+ val fullRows: Int,
154
+ /** Code point per cell; rows/cols beyond the text are spaces. */
155
+ val rows: List<IntArray>,
156
+ ) {
157
+ fun scalar(row: Int, col: Int): Int {
158
+ if (row >= rows.size) return 32
159
+ val line = rows[row]
160
+ return if (col < line.size) line[col] else 32
161
+ }
162
+
163
+ /** The canvas region whose every pixel belongs to a full cell. */
164
+ val expressibleRegion: CanvasRect
165
+ get() = CanvasRect(originX, originY, fullCols * cellWidth, fullRows * lineHeight)
166
+
167
+ fun covers(rect: CanvasRect): Boolean {
168
+ val region = expressibleRegion
169
+ return rect.x >= region.x && rect.y >= region.y &&
170
+ rect.x + rect.w <= region.x + region.w &&
171
+ rect.y + rect.h <= region.y + region.h
172
+ }
173
+ }
174
+
175
+ // ── Incremental updates (dirty rects / rect copy / builtin text) ────────
176
+
177
+ /** Canvas-space rect aligned to mode 3's quantization (x/w %4, y/h %2). */
178
+ data class CanvasRect(val x: Int, val y: Int, val w: Int, val h: Int)
179
+
180
+ /** Full-width vertical band copy: rows [srcY, srcY+height) → [dstY, ...). */
181
+ data class RowCopy(val srcY: Int, val dstY: Int, val height: Int)
182
+
183
+ /**
184
+ * What an incremental frame needs: a full re-send, or a scroll copy plus
185
+ * the aligned rects whose pixels still differ after it.
186
+ */
187
+ data class DeltaPlan(val full: Boolean, val copy: RowCopy?, val rects: List<CanvasRect>) {
188
+ val isNoop: Boolean get() = !full && copy == null && rects.isEmpty()
189
+
190
+ companion object {
191
+ val NOOP = DeltaPlan(full = false, copy = null, rects = emptyList())
192
+ val FULL_FRAME = DeltaPlan(full = true, copy = null, rects = emptyList())
193
+ }
194
+ }
195
+
196
+ /**
197
+ * One firmware-drawn (mode 15) text line at absolute panel coordinates.
198
+ * `boxWidth` is the source element's canvas-space box width — the erase
199
+ * bounds floor, since the phone ellipsized the line against it.
200
+ */
201
+ data class TextDraw(val x: Int, val y: Int, val text: String, val boxWidth: Int)
202
+
203
+ /**
204
+ * One incremental encode: the wire payload (null when the frame is
205
+ * visually identical), mode-12 texture uploads that must be queued AHEAD
206
+ * of it, the raster backing the on-glass shadow after this payload
207
+ * applies, and whether the payload re-sends the whole canvas.
208
+ */
209
+ class SceneUpdate(
210
+ val payload: ByteArray?,
211
+ val uploads: List<ByteArray>,
212
+ val raster: ByteArray,
213
+ val usedFullFrame: Boolean,
214
+ )
215
+
216
+ const val MAX_DIRTY_RECTS = 6
217
+ const val REQUIRED_FID_COUNT = MAX_DIRTY_RECTS + 3
218
+
219
+ /** Cap on mode-14 runs per update (mode-8's count byte is the hard bound). */
220
+ const val MAX_GLYPH_RUNS = 180
221
+
222
+ /**
223
+ * Line pitch and default draw options of the glasses' builtin 20 px font
224
+ * chain (mode 15): lineHeight 27 / baseline 22 in firmware 2.2.6.10.
225
+ * 0x1F = top color 15 (white) + bit 4 (source color 0 transparent).
226
+ */
227
+ const val BUILTIN_LINE_HEIGHT = 27
228
+ const val BUILTIN_TEXT_OPTIONS = 0x1F
229
+ private const val DIRTY_ROW_MERGE_GAP = 8
230
+ private const val FULL_FRAME_COVERAGE_PERCENT = 60
231
+ private const val MIN_SCROLL_BAND_ROWS = 16
232
+ private const val SCROLL_SAVINGS_PERCENT = 30
233
+
234
+ /**
235
+ * Rasterize + plan + encode one incremental scene update. Mirrors the iOS
236
+ * renderer: see G2CfwRenderer.swift `encodeUpdate` for the contract.
237
+ */
238
+ fun encodeUpdate(
239
+ frame: SceneFrame,
240
+ boxes: LensBoxes,
241
+ previousRaster: ByteArray?,
242
+ forcedRects: List<CanvasRect>,
243
+ fids: List<Int>,
244
+ textDraws: List<TextDraw>,
245
+ allowRectCopy: Boolean,
246
+ blankBoxes: LensBoxes?,
247
+ marginBlankBoxes: LensBoxes? = null,
248
+ omitBuiltinText: Boolean,
249
+ textureGlyphsEnabled: Boolean = false,
250
+ textureImagesEnabled: Boolean = false,
251
+ cacheGeneration: Int = 0,
252
+ ): SceneUpdate? {
253
+ if (fids.size < REQUIRED_FID_COUNT) return null
254
+ val width = boxes.width
255
+ val height = boxes.height
256
+ // Texture draws (modes 13/14) land at absolute panel coordinates with
257
+ // no per-lens variant — only a lens-equal window may use them.
258
+ val lensEqual = boxes.leftX == boxes.rightX && boxes.leftY == boxes.rightY
259
+ val useTexGlyphs = textureGlyphsEnabled && lensEqual
260
+ val useTexImages = textureImagesEnabled && lensEqual
261
+ // A full cache resets between frames (never mid-frame: draws already
262
+ // planned this frame reference the old offsets). Bumping past the
263
+ // caller's generation keeps the caller's own bumps monotonic.
264
+ if ((useTexGlyphs || useTexImages) && (textureCache.generation != cacheGeneration || textureCache.exhausted)) {
265
+ textureCache.reset(cacheGeneration)
266
+ }
267
+
268
+ var grids: List<MonoGrid> = emptyList()
269
+ val raster: ByteArray
270
+ if (useTexGlyphs) {
271
+ val result = rasterizeGridExact(frame, width, height, omitBuiltinText) ?: return null
272
+ raster = quantizeToPanelLevels(result.first)
273
+ grids = result.second
274
+ } else {
275
+ raster = quantizeToPanelLevels(rasterize(frame, width, height, omitBuiltinText) ?: return null)
276
+ }
277
+
278
+ var nextFid = 0
279
+ fun takeFid(): Int = fids[nextFid++]
280
+
281
+ val segments = mutableListOf<ByteArray>()
282
+ if (blankBoxes != null) {
283
+ solidStereoFrame(0, blankBoxes, takeFid())?.let { segments.add(it) }
284
+ }
285
+ // Mode-15 glyphs aren't clipped to the window; tails past its right
286
+ // edge live in the outer panel margin only this frame can erase.
287
+ if (marginBlankBoxes != null) {
288
+ solidStereoFrame(0, marginBlankBoxes, takeFid())?.let { segments.add(it) }
289
+ }
290
+
291
+ val plan =
292
+ if (previousRaster != null && previousRaster.size == raster.size && blankBoxes == null) {
293
+ diffPlan(previousRaster, raster, width, height, forcedRects, allowRectCopy)
294
+ } else {
295
+ DeltaPlan.FULL_FRAME
296
+ }
297
+
298
+ if (plan.isNoop) {
299
+ return SceneUpdate(
300
+ payload = null, uploads = textureCache.takeUploadPayloads(),
301
+ raster = raster, usedFullFrame = false,
302
+ )
303
+ }
304
+
305
+ if (plan.full) {
306
+ val full = stereoFrame(raster, boxes, takeFid()) ?: return null
307
+ segments.add(full)
308
+ } else {
309
+ plan.copy?.let { segments.add(rectCopyPayload(it, width, boxes)) }
310
+ var deltaRects = plan.rects
311
+ var drawSegments: List<ByteArray> = emptyList()
312
+ if (useTexGlyphs || useTexImages) {
313
+ // Reserve one fid for the bundle-overflow fallback below.
314
+ val fidBudget = REQUIRED_FID_COUNT - nextFid - 1
315
+ val expressed =
316
+ expressTextureRects(
317
+ plan.rects,
318
+ grids = if (useTexGlyphs) grids else emptyList(),
319
+ frame = frame, boxes = boxes, raster = raster,
320
+ width = width, height = height,
321
+ allowImages = useTexImages, fidBudget = fidBudget,
322
+ )
323
+ deltaRects = expressed.first
324
+ drawSegments = expressed.second
325
+ }
326
+ for (rect in deltaRects) {
327
+ val delta = boxDeltaFrame(raster, width, rect, boxes, takeFid()) ?: return null
328
+ segments.add(delta)
329
+ }
330
+ segments.addAll(drawSegments)
331
+ }
332
+
333
+ for (draw in textDraws) {
334
+ builtinStringPayload(draw.x, draw.y, BUILTIN_TEXT_OPTIONS, draw.text)?.let { segments.add(it) }
335
+ }
336
+
337
+ val uploads = textureCache.takeUploadPayloads()
338
+ if (segments.size == 1) return SceneUpdate(segments[0], uploads, raster, plan.full)
339
+ bundle(segments)?.let { return SceneUpdate(it, uploads, raster, plan.full) }
340
+ // A segment overflowed the bundle's 64 KiB cap (a near-incompressible
341
+ // full canvas). Fall back to one bare full frame with any builtin text
342
+ // baked into the bitmap so nothing is lost. The uploads still go out —
343
+ // residency was already recorded in the model.
344
+ val flat = rasterize(frame, width, height, omitBuiltinText = false) ?: return null
345
+ val full = stereoFrame(flat, boxes, takeFid()) ?: return null
346
+ return SceneUpdate(full, uploads, flat, usedFullFrame = true)
347
+ }
348
+
349
+ /**
350
+ * Blit `font: "mono"` elements as opaque glyph grids: the element's box
351
+ * is cleared to black and every non-space cell blits the SAME full-cell
352
+ * raster mode-14 draws replay, so an on-glasses cached draw reproduces
353
+ * the raster exactly. (Opaque cells are also what makes cached draws
354
+ * self-erasing.) Mono grids paint after all other elements — an exotic
355
+ * frame stacking a non-mono element ON TOP of a mono grid would z-order
356
+ * differently here, which no current miniapp does.
357
+ */
358
+ private fun rasterizeGridExact(
359
+ frame: SceneFrame,
360
+ width: Int,
361
+ height: Int,
362
+ omitBuiltinText: Boolean,
363
+ ): Pair<ByteArray, List<MonoGrid>>? {
364
+ val raster =
365
+ rasterize(frame, width, height, omitBuiltinText, omitMonoText = true) ?: return null
366
+
367
+ val grids = mutableListOf<MonoGrid>()
368
+ for ((z, element) in frame.elements.withIndex()) {
369
+ if (element.type != "text" || element.font != "mono") continue
370
+ val bx = element.x
371
+ val by = element.y
372
+ val clipX0 = maxOf(0, bx)
373
+ val clipY0 = maxOf(0, by)
374
+ val clipX1 = minOf(width, bx + element.w)
375
+ val clipY1 = minOf(height, by + element.h)
376
+ if (clipX0 >= clipX1 || clipY0 >= clipY1) continue
377
+ val (cellWidth, lineHeight) = monoCellMetrics(element.fontSize)
378
+ val originX = bx + 2
379
+ val originY = by + 2
380
+ for (y in clipY0 until clipY1) {
381
+ raster.fill(0, y * width + clipX0, y * width + clipX1)
382
+ }
383
+ val lines = (element.text ?: "").split('\n').map { codePoints(it) }
384
+ val blitCols = maxOf(0, (clipX1 - originX + cellWidth - 1) / cellWidth)
385
+ val blitRows = maxOf(0, (clipY1 - originY + lineHeight - 1) / lineHeight)
386
+ for (row in 0 until minOf(blitRows, lines.size)) {
387
+ val line = lines[row]
388
+ for (col in 0 until minOf(blitCols, line.size)) {
389
+ if (line[col] == 32) continue
390
+ val glyph =
391
+ memoizedGlyphRaster(line[col], element.fontSize, cellWidth, lineHeight)
392
+ ?: continue
393
+ blitCell(
394
+ glyph, raster, width,
395
+ atX = originX + col * cellWidth, atY = originY + row * lineHeight,
396
+ cellWidth = cellWidth, cellHeight = lineHeight,
397
+ clipX0 = clipX0, clipY0 = clipY0, clipX1 = clipX1, clipY1 = clipY1,
398
+ )
399
+ }
400
+ }
401
+ grids.add(
402
+ MonoGrid(
403
+ z = z, fontSize = element.fontSize, originX = originX, originY = originY,
404
+ cellWidth = cellWidth, lineHeight = lineHeight,
405
+ fullCols = maxOf(0, (clipX1 - originX) / cellWidth),
406
+ fullRows = maxOf(0, (clipY1 - originY) / lineHeight),
407
+ rows = lines,
408
+ )
409
+ )
410
+ }
411
+ return raster to grids
412
+ }
413
+
414
+ private fun blitCell(
415
+ glyph: ByteArray,
416
+ raster: ByteArray,
417
+ rasterWidth: Int,
418
+ atX: Int,
419
+ atY: Int,
420
+ cellWidth: Int,
421
+ cellHeight: Int,
422
+ clipX0: Int,
423
+ clipY0: Int,
424
+ clipX1: Int,
425
+ clipY1: Int,
426
+ ) {
427
+ val x0 = maxOf(atX, clipX0)
428
+ val y0 = maxOf(atY, clipY0)
429
+ val x1 = minOf(atX + cellWidth, clipX1)
430
+ val y1 = minOf(atY + cellHeight, clipY1)
431
+ if (x0 >= x1 || y0 >= y1) return
432
+ for (y in y0 until y1) {
433
+ System.arraycopy(
434
+ glyph, (y - atY) * cellWidth + (x0 - atX),
435
+ raster, y * rasterWidth + x0,
436
+ x1 - x0,
437
+ )
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Express-or-bake: each dirty rect either becomes cached draws (mode-14
443
+ * glyph runs from the topmost covering mono grid, or one mode-13 draw of
444
+ * an isolated repeated image) and drops its bitmap delta, or stays a
445
+ * plain delta untouched. All-or-nothing per rect keeps the shadow model
446
+ * trivially exact — no partial punch-out. Non-ASCII cells (mode 14 is
447
+ * 32..126 only) become small residual bitmap deltas within an otherwise
448
+ * expressed rect.
449
+ */
450
+ private fun expressTextureRects(
451
+ rects: List<CanvasRect>,
452
+ grids: List<MonoGrid>,
453
+ frame: SceneFrame,
454
+ boxes: LensBoxes,
455
+ raster: ByteArray,
456
+ width: Int,
457
+ height: Int,
458
+ allowImages: Boolean,
459
+ fidBudget: Int,
460
+ ): Pair<List<CanvasRect>, List<ByteArray>> {
461
+ class ElementBox(val z: Int, val x0: Int, val y0: Int, val x1: Int, val y1: Int) {
462
+ fun intersects(r: CanvasRect): Boolean =
463
+ r.x < x1 && r.x + r.w > x0 && r.y < y1 && r.y + r.h > y0
464
+ }
465
+ val elementBoxes =
466
+ frame.elements.mapIndexed { z, el ->
467
+ ElementBox(
468
+ z = z, x0 = maxOf(0, el.x), y0 = maxOf(0, el.y),
469
+ x1 = minOf(width, el.x + el.w), y1 = minOf(height, el.y + el.h),
470
+ )
471
+ }
472
+
473
+ // Isolated image candidates: nothing else overlaps their box, so the
474
+ // frame raster inside the box IS the standalone render and an opaque
475
+ // full-box cached draw reproduces it. Sightings are recorded every
476
+ // frame; only content seen before enters the cache (streaming content
477
+ // like a live minimap never repeats a hash).
478
+ class ImageCandidate(val box: ElementBox, val key: String, val seen: Boolean)
479
+ val imageCandidates = mutableMapOf<Int, ImageCandidate>()
480
+ if (allowImages) {
481
+ for ((z, el) in frame.elements.withIndex()) {
482
+ if (el.type != "image") continue
483
+ val box = elementBoxes[z]
484
+ val w = box.x1 - box.x0
485
+ val h = box.y1 - box.y0
486
+ if (w <= 0 || h <= 0 || w > 255 || h > 255 || el.contentHash.isEmpty()) continue
487
+ val isolated =
488
+ elementBoxes.none { other ->
489
+ other.z != z && other.x0 < box.x1 && other.x1 > box.x0 &&
490
+ other.y0 < box.y1 && other.y1 > box.y0
491
+ }
492
+ if (!isolated) continue
493
+ val key = "${el.contentHash}@${w}x$h"
494
+ imageCandidates[z] = ImageCandidate(box, key, textureCache.noteImageSighting(key))
495
+ }
496
+ }
497
+
498
+ val deltas = mutableListOf<CanvasRect>()
499
+ val imageDraws = sortedMapOf<Int, ByteArray>()
500
+ val runPayloads = mutableListOf<ByteArray>()
501
+ var runCount = 0
502
+
503
+ rectLoop@ for ((index, rect) in rects.withIndex()) {
504
+ val remaining = rects.size - index - 1
505
+
506
+ // Glyph express: topmost mono grid fully covering the rect, with
507
+ // nothing stacked above it in that region.
508
+ val grid = grids.filter { it.covers(rect) }.maxByOrNull { it.z }
509
+ if (grid != null &&
510
+ elementBoxes.none { it.z > grid.z && it.intersects(rect) }
511
+ ) {
512
+ val font = textureCache.font(grid.fontSize, grid.cellWidth, grid.lineHeight)
513
+ if (font != null) {
514
+ val r0 = (rect.y - grid.originY) / grid.lineHeight
515
+ val r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
516
+ val c0 = (rect.x - grid.originX) / grid.cellWidth
517
+ val c1 = (rect.x + rect.w - 1 - grid.originX) / grid.cellWidth
518
+ val newRuns = mutableListOf<Triple<Int, Int, ByteArray>>() // x, y, chars
519
+ val residuals = mutableListOf<CanvasRect>()
520
+ var expressible = true
521
+ rowLoop@ for (row in r0..r1) {
522
+ val rowY = grid.originY + row * grid.lineHeight
523
+ var runChars = mutableListOf<Byte>()
524
+ var runStartCol = c0
525
+ var residualStart: Int? = null
526
+ fun flushRun() {
527
+ if (runChars.isEmpty()) return
528
+ newRuns.add(
529
+ Triple(
530
+ grid.originX + runStartCol * grid.cellWidth, rowY,
531
+ runChars.toByteArray(),
532
+ )
533
+ )
534
+ runChars = mutableListOf()
535
+ }
536
+ fun flushResidual(endCol: Int) {
537
+ val start = residualStart ?: return
538
+ val x0 = maxOf(0, (grid.originX + start * grid.cellWidth) and 3.inv())
539
+ val x1 = minOf(width, (grid.originX + endCol * grid.cellWidth + 3) and 3.inv())
540
+ val y0 = maxOf(0, rowY and 1.inv())
541
+ val y1 = minOf(height, (rowY + grid.lineHeight + 1) and 1.inv())
542
+ residuals.add(CanvasRect(x0, y0, x1 - x0, y1 - y0))
543
+ residualStart = null
544
+ }
545
+ for (col in c0..c1) {
546
+ val scalar = grid.scalar(row, col)
547
+ val resident =
548
+ scalar in 32..126 &&
549
+ textureCache.ensureGlyph(font, scalar) {
550
+ memoizedGlyphRaster(
551
+ scalar, grid.fontSize, grid.cellWidth, grid.lineHeight
552
+ )
553
+ }
554
+ if (resident) {
555
+ flushResidual(col)
556
+ if (runChars.isEmpty()) runStartCol = col
557
+ runChars.add(scalar.toByte())
558
+ if (runChars.size == 255) flushRun()
559
+ } else {
560
+ flushRun()
561
+ if (residualStart == null) residualStart = col
562
+ }
563
+ }
564
+ flushRun()
565
+ flushResidual(c1 + 1)
566
+ if (runCount + newRuns.size > MAX_GLYPH_RUNS) {
567
+ expressible = false
568
+ break@rowLoop
569
+ }
570
+ }
571
+ // Residual deltas + every remaining rect must still fit the
572
+ // fid budget; otherwise keep this rect as one plain delta.
573
+ if (expressible &&
574
+ deltas.size + residuals.size + remaining + 1 <= fidBudget &&
575
+ residuals.size <= 3
576
+ ) {
577
+ var payloads = mutableListOf<ByteArray>()
578
+ for ((runX, runY, chars) in newRuns) {
579
+ val payload =
580
+ G2CfwTextureCache.stringRunPayload(
581
+ fontTableOffset = font.tableOffset,
582
+ x = boxes.leftX + runX, y = boxes.leftY + runY,
583
+ options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS, chars = chars,
584
+ )
585
+ if (payload == null) {
586
+ payloads = mutableListOf()
587
+ break
588
+ }
589
+ payloads.add(payload)
590
+ }
591
+ if (payloads.isNotEmpty() || newRuns.isEmpty()) {
592
+ runPayloads.addAll(payloads)
593
+ runCount += payloads.size
594
+ deltas.addAll(residuals)
595
+ continue@rectLoop
596
+ }
597
+ }
598
+ }
599
+ }
600
+
601
+ // Image express: the rect sits wholly inside an isolated, repeated
602
+ // image's box — one opaque full-box draw covers it (and any other
603
+ // rect in the same box).
604
+ if (allowImages) {
605
+ val entry =
606
+ imageCandidates.entries.firstOrNull { (_, c) ->
607
+ c.seen && rect.x >= c.box.x0 && rect.y >= c.box.y0 &&
608
+ rect.x + rect.w <= c.box.x1 && rect.y + rect.h <= c.box.y1
609
+ }
610
+ if (entry != null) {
611
+ val (z, candidate) = entry
612
+ if (imageDraws.containsKey(z)) continue@rectLoop
613
+ val w = candidate.box.x1 - candidate.box.x0
614
+ val h = candidate.box.y1 - candidate.box.y0
615
+ val gray = ByteArray(w * h)
616
+ for (y in candidate.box.y0 until candidate.box.y1) {
617
+ System.arraycopy(
618
+ raster, y * width + candidate.box.x0,
619
+ gray, (y - candidate.box.y0) * w, w,
620
+ )
621
+ }
622
+ val offset = textureCache.ensureImage(candidate.key, gray, w, h)
623
+ val draw =
624
+ offset?.let {
625
+ G2CfwTextureCache.imageDrawPayload(
626
+ offset = it,
627
+ x = boxes.leftX + candidate.box.x0,
628
+ y = boxes.leftY + candidate.box.y0,
629
+ options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS,
630
+ )
631
+ }
632
+ if (draw != null) {
633
+ imageDraws[z] = draw
634
+ continue@rectLoop
635
+ }
636
+ }
637
+ }
638
+
639
+ deltas.add(rect)
640
+ }
641
+
642
+ return deltas to (imageDraws.values.toList() + runPayloads)
643
+ }
644
+
645
+ /**
646
+ * Diff two same-size canvas rasters into a scroll copy + aligned dirty
647
+ * rects, or a full-frame decision when the change is too large.
648
+ */
649
+ fun diffPlan(
650
+ previous: ByteArray,
651
+ next: ByteArray,
652
+ width: Int,
653
+ height: Int,
654
+ forced: List<CanvasRect>,
655
+ allowCopy: Boolean,
656
+ ): DeltaPlan {
657
+ if (previous.size != width * height || next.size != width * height || width <= 0 || height <= 0) {
658
+ return DeltaPlan.FULL_FRAME
659
+ }
660
+
661
+ fun rowsEqual(a: ByteArray, aRow: Int, b: ByteArray, bRow: Int): Boolean {
662
+ val aOff = aRow * width
663
+ val bOff = bRow * width
664
+ for (x in 0 until width) {
665
+ if (a[aOff + x] != b[bOff + x]) return false
666
+ }
667
+ return true
668
+ }
669
+
670
+ val changed = BooleanArray(height) { y -> !rowsEqual(previous, y, next, y) }
671
+ var anyChanged = changed.any { it }
672
+ if (!anyChanged && forced.isEmpty()) return DeltaPlan.NOOP
673
+
674
+ // Scroll detection: a vertical band of new rows that byte-match
675
+ // previous rows shifted by dy. Row hashes score candidate shifts;
676
+ // the winning band is byte-verified before use.
677
+ var copy: RowCopy? = null
678
+ var predicted: ByteArray? = null
679
+ if (allowCopy && anyChanged) {
680
+ val prevHash = LongArray(height) { y -> rowHash(previous, y * width, width) }
681
+ val newHash = LongArray(height) { y -> rowHash(next, y * width, width) }
682
+ val changedCount = changed.count { it }
683
+ var bestDy = 0
684
+ var bestScore = 0
685
+ val maxShift = height - MIN_SCROLL_BAND_ROWS
686
+ if (maxShift > 0) {
687
+ for (dy in -maxShift..maxShift) {
688
+ if (dy == 0) continue
689
+ var score = 0
690
+ for (y in maxOf(0, -dy) until minOf(height, height - dy)) {
691
+ if (changed[y] && newHash[y] == prevHash[y + dy]) score += 1
692
+ }
693
+ if (score > bestScore) {
694
+ bestScore = score
695
+ bestDy = dy
696
+ }
697
+ }
698
+ }
699
+ if (bestDy != 0 && bestScore >= MIN_SCROLL_BAND_ROWS &&
700
+ bestScore * 100 >= changedCount * SCROLL_SAVINGS_PERCENT
701
+ ) {
702
+ var runStart = -1
703
+ var bandStart = 0
704
+ var bandLen = 0
705
+ val lo = maxOf(0, -bestDy)
706
+ val hi = minOf(height, height - bestDy)
707
+ for (y in lo..hi) {
708
+ val matches = y < hi && newHash[y] == prevHash[y + bestDy]
709
+ if (matches) {
710
+ if (runStart < 0) runStart = y
711
+ } else if (runStart >= 0) {
712
+ if (y - runStart > bandLen) {
713
+ bandStart = runStart
714
+ bandLen = y - runStart
715
+ }
716
+ runStart = -1
717
+ }
718
+ }
719
+ if (bandLen >= MIN_SCROLL_BAND_ROWS) {
720
+ var verified = true
721
+ for (y in bandStart until bandStart + bandLen) {
722
+ if (!rowsEqual(next, y, previous, y + bestDy)) {
723
+ verified = false
724
+ break
725
+ }
726
+ }
727
+ if (verified) {
728
+ copy = RowCopy(srcY = bandStart + bestDy, dstY = bandStart, height = bandLen)
729
+ predicted = previous.copyOf().also {
730
+ System.arraycopy(
731
+ previous, (bandStart + bestDy) * width, it, bandStart * width, bandLen * width
732
+ )
733
+ }
734
+ for (y in bandStart until bandStart + bandLen) changed[y] = false
735
+ anyChanged = changed.any { it }
736
+ }
737
+ }
738
+ }
739
+ }
740
+
741
+ // Per-row dirty column ranges against the (possibly copied) base.
742
+ val base = predicted ?: previous
743
+ val colLo = IntArray(height) { Int.MAX_VALUE }
744
+ val colHi = IntArray(height) { -1 }
745
+ for (y in 0 until height) {
746
+ if (!changed[y]) continue
747
+ var lo = 0
748
+ while (lo < width && base[y * width + lo] == next[y * width + lo]) lo += 1
749
+ var hi = width - 1
750
+ while (hi > lo && base[y * width + hi] == next[y * width + hi]) hi -= 1
751
+ colLo[y] = lo
752
+ colHi[y] = hi
753
+ }
754
+ val dirtyRows = changed.copyOf()
755
+ for (rect in forced) {
756
+ val y0 = maxOf(0, rect.y)
757
+ val y1 = minOf(height, rect.y + rect.h)
758
+ if (y0 >= y1 || rect.w <= 0) continue
759
+ for (y in y0 until y1) {
760
+ dirtyRows[y] = true
761
+ colLo[y] = minOf(colLo[y], maxOf(0, rect.x))
762
+ colHi[y] = maxOf(colHi[y], minOf(width - 1, rect.x + rect.w - 1))
763
+ }
764
+ }
765
+ if (!dirtyRows.any { it }) {
766
+ return if (copy != null) DeltaPlan(full = false, copy = copy, rects = emptyList()) else DeltaPlan.NOOP
767
+ }
768
+
769
+ // Group dirty rows into bands (small gaps merge), aligned to the grid.
770
+ var rects = mutableListOf<CanvasRect>()
771
+ var y = 0
772
+ while (y < height) {
773
+ if (!dirtyRows[y]) {
774
+ y += 1
775
+ continue
776
+ }
777
+ var end = y
778
+ var gap = 0
779
+ var scan = y
780
+ while (scan < height && gap <= DIRTY_ROW_MERGE_GAP) {
781
+ if (dirtyRows[scan]) {
782
+ end = scan
783
+ gap = 0
784
+ } else {
785
+ gap += 1
786
+ }
787
+ scan += 1
788
+ }
789
+ var lo = Int.MAX_VALUE
790
+ var hi = -1
791
+ for (row in y..end) {
792
+ if (!dirtyRows[row]) continue
793
+ lo = minOf(lo, colLo[row])
794
+ hi = maxOf(hi, colHi[row])
795
+ }
796
+ val x0 = maxOf(0, lo and 3.inv())
797
+ val x1 = minOf(width, (hi + 4) and 3.inv())
798
+ val ry0 = maxOf(0, y and 1.inv())
799
+ val ry1 = minOf(height, (end + 2) and 1.inv())
800
+ rects.add(CanvasRect(x0, ry0, x1 - x0, ry1 - ry0))
801
+ y = end + 1
802
+ }
803
+ if (rects.size > MAX_DIRTY_RECTS) {
804
+ val x0 = rects.minOf { it.x }
805
+ val y0 = rects.minOf { it.y }
806
+ val x1 = rects.maxOf { it.x + it.w }
807
+ val y1 = rects.maxOf { it.y + it.h }
808
+ rects = mutableListOf(CanvasRect(x0, y0, x1 - x0, y1 - y0))
809
+ }
810
+ val coverage = rects.sumOf { it.w * it.h }
811
+ if (copy == null && coverage * 100 >= width * height * FULL_FRAME_COVERAGE_PERCENT) {
812
+ return DeltaPlan.FULL_FRAME
813
+ }
814
+ return DeltaPlan(full = false, copy = copy, rects = rects)
815
+ }
816
+
817
+ private fun rowHash(pixels: ByteArray, offset: Int, count: Int): Long {
818
+ // FNV-1a 64 — collision-safe enough for scoring; bands are byte-verified.
819
+ var hash = -0x340d631b7bdddcdbL
820
+ for (i in offset until offset + count) {
821
+ hash = (hash xor (pixels[i].toLong() and 0xFF)) * 0x100000001b3L
822
+ }
823
+ return hash
824
+ }
825
+
826
+ /**
827
+ * Stereo mode-3 delta of one canvas sub-rect, drawn at each lens's window
828
+ * origin plus the rect offset. Rect must be grid-aligned and in bounds.
829
+ */
830
+ fun boxDeltaFrame(
831
+ raster: ByteArray,
832
+ canvasWidth: Int,
833
+ rect: CanvasRect,
834
+ boxes: LensBoxes,
835
+ frameId: Int,
836
+ ): ByteArray? {
837
+ if (rect.w <= 0 || rect.h <= 0 || rect.x % 4 != 0 || rect.w % 4 != 0 ||
838
+ rect.y % 2 != 0 || rect.h % 2 != 0 || rect.x + rect.w > canvasWidth ||
839
+ (rect.y + rect.h) * canvasWidth > raster.size
840
+ ) {
841
+ return null
842
+ }
843
+ val pixels = ByteArray(rect.w * rect.h)
844
+ for (y in 0 until rect.h) {
845
+ System.arraycopy(raster, (rect.y + y) * canvasWidth + rect.x, pixels, y * rect.w, rect.w)
846
+ }
847
+ return stereoRleFrame(
848
+ rleEncode(pack4Bpp(pixels)),
849
+ lx = boxes.leftX + rect.x, ly = boxes.leftY + rect.y,
850
+ rx = boxes.rightX + rect.x, ry = boxes.rightY + rect.y,
851
+ w = rect.w, h = rect.h, frameId = frameId,
852
+ )
853
+ }
854
+
855
+ /**
856
+ * Mode-9 rect copy with the lenses-differ flag: a full-width canvas band
857
+ * moves srcY→dstY inside each lens's window.
858
+ * `[0x89][L src LTWH dst LTWH][R src LTWH dst LTWH]`, u16 LE each.
859
+ */
860
+ fun rectCopyPayload(copy: RowCopy, width: Int, boxes: LensBoxes): ByteArray {
861
+ val output = ByteArrayOutputStream()
862
+ output.write(0x89)
863
+ fun writeRect(x: Int, y: Int) {
864
+ for (value in intArrayOf(x, y, width, copy.height)) {
865
+ output.write(value and 0xFF)
866
+ output.write((value ushr 8) and 0xFF)
867
+ }
868
+ }
869
+ writeRect(boxes.leftX, boxes.leftY + copy.srcY)
870
+ writeRect(boxes.leftX, boxes.leftY + copy.dstY)
871
+ writeRect(boxes.rightX, boxes.rightY + copy.srcY)
872
+ writeRect(boxes.rightX, boxes.rightY + copy.dstY)
873
+ return output.toByteArray()
874
+ }
875
+
876
+ /**
877
+ * Mode-15 builtin-font string draw:
878
+ * `[15][x:u16][y:u16][options:u8][strlen:u8][UTF-8]`. Bytes 1–31 are
879
+ * firmware x-adjust controls, so control characters are stripped here;
880
+ * the text is truncated to 255 UTF-8 bytes at a codepoint boundary.
881
+ */
882
+ fun builtinStringPayload(x: Int, y: Int, options: Int, text: String): ByteArray? {
883
+ if (x !in 0..0xFFFF || y !in 0..0xFFFF) return null
884
+ val sanitized = text.filter { it.code >= 32 }
885
+ var bytes = sanitized.toByteArray(Charsets.UTF_8)
886
+ if (bytes.size > 255) {
887
+ var cut = 255
888
+ while (cut > 0 && (bytes[cut].toInt() and 0xC0) == 0x80) cut -= 1
889
+ bytes = bytes.copyOfRange(0, cut)
890
+ }
891
+ if (bytes.isEmpty()) return null
892
+ val output = ByteArrayOutputStream()
893
+ output.write(15)
894
+ output.write(x and 0xFF)
895
+ output.write((x ushr 8) and 0xFF)
896
+ output.write(y and 0xFF)
897
+ output.write((y ushr 8) and 0xFF)
898
+ output.write(options and 0xFF)
899
+ output.write(bytes.size)
900
+ output.write(bytes)
901
+ return output.toByteArray()
902
+ }
903
+
904
+ /**
905
+ * The firmware-drawn text lines of a frame, at absolute panel
906
+ * coordinates. Mode 15 has no per-lens variant, so callers must place the
907
+ * window at lens-equal offsets (depth level 1) whenever these are used.
908
+ * The +2 inset matches the rasterizer's text padding.
909
+ */
910
+ fun builtinTextDraws(frame: SceneFrame, boxes: LensBoxes): List<TextDraw> {
911
+ val draws = mutableListOf<TextDraw>()
912
+ for (element in frame.elements) {
913
+ if (element.type != "text" || element.font != "builtin") continue
914
+ val text = element.text ?: continue
915
+ if (text.isEmpty()) continue
916
+ val baseX = boxes.leftX + element.x + 2
917
+ val baseY = boxes.leftY + element.y + 2
918
+ text.split('\n').forEachIndexed { index, line ->
919
+ val y = baseY + index * BUILTIN_LINE_HEIGHT
920
+ if (y >= 0 && y + BUILTIN_LINE_HEIGHT <= FRAME_HEIGHT && line.isNotEmpty()) {
921
+ draws.add(TextDraw(baseX, y, line, element.w))
922
+ }
923
+ }
924
+ }
925
+ return draws
926
+ }
927
+
928
+ /**
929
+ * Conservative canvas-space bounds of firmware-drawn text, used to force
930
+ * background re-pushes that erase glyphs the phone raster never
931
+ * contained. The phone has no advance tables, so width is the char-count
932
+ * estimate floored at the element's box width — wide glyphs overshoot
933
+ * 13px/char, and an undershot erase leaves glyph tails on the panel.
934
+ */
935
+ fun builtinTextBounds(
936
+ draws: List<TextDraw>,
937
+ boxes: LensBoxes,
938
+ canvasWidth: Int,
939
+ canvasHeight: Int,
940
+ ): List<CanvasRect> =
941
+ draws.mapNotNull { draw ->
942
+ val x = maxOf(0, draw.x - boxes.leftX - 4)
943
+ val y = maxOf(0, draw.y - boxes.leftY - 2)
944
+ if (x >= canvasWidth || y >= canvasHeight) return@mapNotNull null
945
+ CanvasRect(
946
+ x = x,
947
+ y = y,
948
+ w = minOf(canvasWidth - x, maxOf(draw.text.length * 13 + 30, draw.boxWidth + 8)),
949
+ h = minOf(canvasHeight - y, BUILTIN_LINE_HEIGHT + 4),
950
+ )
951
+ }
952
+
953
+ /**
954
+ * `omitBuiltinText` skips `font == "builtin"` text elements — the CFW
955
+ * draws those with mode 15, so the bitmap must not also contain them.
956
+ * `omitMonoText` skips `font == "mono"` text elements — the grid-exact
957
+ * texture path blits those from the same glyph rasters it uploads.
958
+ */
959
+ fun rasterize(
960
+ frame: SceneFrame,
961
+ width: Int,
962
+ height: Int,
963
+ omitBuiltinText: Boolean = false,
964
+ omitMonoText: Boolean = false,
965
+ ): ByteArray? {
966
+ if (width <= 0 || height <= 0) return null
967
+ val decodedImages = mutableMapOf<Int, Bitmap>()
968
+ try {
969
+ for ((index, element) in frame.elements.withIndex()) {
970
+ if (element.type != "image") continue
971
+ val encoded = element.data ?: return null
972
+ val data =
973
+ try {
974
+ Base64.decode(encoded, Base64.DEFAULT)
975
+ } catch (_: IllegalArgumentException) {
976
+ return null
977
+ }
978
+ decodedImages[index] = BitmapFactory.decodeByteArray(data, 0, data.size) ?: return null
979
+ }
980
+
981
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
982
+ val canvas = Canvas(bitmap)
983
+ canvas.drawColor(Color.BLACK)
984
+ for ((index, element) in frame.elements.withIndex()) {
985
+ // Mode 15 draws only the glyphs, so an omitted builtin-text
986
+ // element's border must still come from the raster.
987
+ val skipBuiltinInk = omitBuiltinText && element.type == "text" && element.font == "builtin"
988
+ if (skipBuiltinInk && element.border <= 0) continue
989
+ if (omitMonoText && element.type == "text" && element.font == "mono") continue
990
+ val left = element.x.toFloat()
991
+ val top = element.y.toFloat()
992
+ val rect = RectF(left, top, left + element.w, top + element.h)
993
+ val save = canvas.save()
994
+ canvas.clipRect(rect)
995
+ when (element.type) {
996
+ "text" -> if (!skipBuiltinInk) drawText(canvas, element, rect)
997
+ "image" -> decodedImages[index]?.let { drawImage(canvas, it, rect) }
998
+ }
999
+
1000
+ val border = maxOf(if (element.type == "rect") 1 else 0, element.border)
1001
+ if (border > 0) {
1002
+ val paint =
1003
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
1004
+ color = Color.WHITE
1005
+ style = Paint.Style.STROKE
1006
+ strokeWidth = border.toFloat()
1007
+ }
1008
+ val inset = border / 2f
1009
+ val borderRect = RectF(rect).apply { inset(inset, inset) }
1010
+ val radius =
1011
+ element.radius
1012
+ .coerceAtLeast(0)
1013
+ .toFloat()
1014
+ .coerceAtMost(minOf(borderRect.width(), borderRect.height()) / 2f)
1015
+ canvas.drawRoundRect(borderRect, radius, radius, paint)
1016
+ }
1017
+ canvas.restoreToCount(save)
1018
+ }
1019
+
1020
+ val colors = IntArray(width * height)
1021
+ bitmap.getPixels(colors, 0, width, 0, 0, width, height)
1022
+ bitmap.recycle()
1023
+ return ByteArray(colors.size) { index ->
1024
+ val color = colors[index]
1025
+ ((Color.red(color) * 299 + Color.green(color) * 587 + Color.blue(color) * 114) / 1000).toByte()
1026
+ }
1027
+ } finally {
1028
+ decodedImages.values.forEach { it.recycle() }
1029
+ }
1030
+ }
1031
+
1032
+ private fun drawText(canvas: Canvas, element: SceneElement, rect: RectF) {
1033
+ val text = element.text.orEmpty()
1034
+ if (text.isEmpty()) return
1035
+ val availableWidth = (rect.width() - 4f).toInt().coerceAtLeast(1)
1036
+ val save = canvas.save()
1037
+ canvas.translate(rect.left + 2f, rect.top + 2f)
1038
+ if (element.font == "mono") {
1039
+ val size = (if (element.fontSize > 0) element.fontSize else 20).toFloat()
1040
+ val paint =
1041
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
1042
+ color = Color.WHITE
1043
+ textSize = size
1044
+ typeface = Typeface.MONOSPACE
1045
+ }
1046
+ val baseline = -paint.fontMetrics.ascent
1047
+ val pitch = monoLineHeight(size)
1048
+ text.split('\n').forEachIndexed { index, line ->
1049
+ canvas.drawText(line, 0f, baseline + index * pitch, paint)
1050
+ }
1051
+ } else {
1052
+ val paint =
1053
+ TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
1054
+ color = Color.WHITE
1055
+ textSize = 20f
1056
+ typeface = Typeface.DEFAULT
1057
+ }
1058
+ StaticLayout.Builder.obtain(text, 0, text.length, paint, availableWidth)
1059
+ .setAlignment(Layout.Alignment.ALIGN_NORMAL)
1060
+ .setIncludePad(false)
1061
+ .build()
1062
+ .draw(canvas)
1063
+ }
1064
+ canvas.restoreToCount(save)
1065
+ }
1066
+
1067
+ private fun drawImage(canvas: Canvas, bitmap: Bitmap, rect: RectF) {
1068
+ val scale = minOf(rect.width() / bitmap.width.coerceAtLeast(1), rect.height() / bitmap.height.coerceAtLeast(1))
1069
+ val targetWidth = bitmap.width * scale
1070
+ val targetHeight = bitmap.height * scale
1071
+ canvas.drawBitmap(
1072
+ bitmap,
1073
+ Rect(0, 0, bitmap.width, bitmap.height),
1074
+ RectF(
1075
+ rect.centerX() - targetWidth / 2f,
1076
+ rect.centerY() - targetHeight / 2f,
1077
+ rect.centerX() + targetWidth / 2f,
1078
+ rect.centerY() + targetHeight / 2f,
1079
+ ),
1080
+ Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG),
1081
+ )
1082
+ }
1083
+
1084
+ fun stereoFrame(pixels: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? {
1085
+ if (pixels.size != boxes.width * boxes.height) return null
1086
+ return stereoRleFrame(rleEncode(pack4Bpp(pixels)), boxes, frameId)
1087
+ }
1088
+
1089
+ fun solidStereoFrame(color: Int, boxes: LensBoxes, frameId: Int): ByteArray? =
1090
+ stereoRleFrame(rleEncodeSolid(color and 0x0F, boxes.width * boxes.height), boxes, frameId)
1091
+
1092
+ private fun stereoRleFrame(rle: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? =
1093
+ stereoRleFrame(rle, boxes.leftX, boxes.leftY, boxes.rightX, boxes.rightY, boxes.width, boxes.height, frameId)
1094
+
1095
+ private fun stereoRleFrame(
1096
+ rle: ByteArray,
1097
+ lx: Int,
1098
+ ly: Int,
1099
+ rx: Int,
1100
+ ry: Int,
1101
+ w: Int,
1102
+ h: Int,
1103
+ frameId: Int,
1104
+ ): ByteArray? {
1105
+ val compressed = zlib(rle) ?: return null
1106
+ return byteArrayOf(
1107
+ 0x83.toByte(),
1108
+ (lx / 4).toByte(),
1109
+ (ly / 2).toByte(),
1110
+ (w / 4).toByte(),
1111
+ (h / 2).toByte(),
1112
+ (rx / 4).toByte(),
1113
+ (ry / 2).toByte(),
1114
+ (w / 4).toByte(),
1115
+ (h / 2).toByte(),
1116
+ (frameId and 0xFF).toByte(),
1117
+ ((frameId ushr 8) and 0xFF).toByte(),
1118
+ ) + compressed
1119
+ }
1120
+
1121
+ fun bundle(segments: List<ByteArray>): ByteArray? {
1122
+ if (segments.isEmpty() || segments.size > 255 || segments.any { it.isEmpty() || it.size > 0xFFFF }) {
1123
+ return null
1124
+ }
1125
+ val output = ByteArrayOutputStream()
1126
+ output.write(8)
1127
+ output.write(segments.size)
1128
+ for (segment in segments) {
1129
+ output.write(segment.size and 0xFF)
1130
+ output.write((segment.size ushr 8) and 0xFF)
1131
+ output.write(segment)
1132
+ }
1133
+ return output.toByteArray()
1134
+ }
1135
+
1136
+ /**
1137
+ * Mode-6 full-panel seed with the frame's own pixels composited at the
1138
+ * window position on black — seeds the shadow AND paints the frame in ONE
1139
+ * payload, so a resync shows content immediately instead of flashing a
1140
+ * black panel (the faceclaw reference client's resyncs are mode-6 content
1141
+ * frames and never flash). Mode 6 has no per-lens variant, so this
1142
+ * returns null unless the window is lens-equal — callers fall back to the
1143
+ * black seed there.
1144
+ */
1145
+ fun seededFramePayload(pixels: ByteArray, boxes: LensBoxes): ByteArray? {
1146
+ if (boxes.leftX != boxes.rightX || boxes.leftY != boxes.rightY) return null
1147
+ if (pixels.size != boxes.width * boxes.height) return null
1148
+ val panel = ByteArray(FRAME_WIDTH * FRAME_HEIGHT)
1149
+ for (row in 0 until boxes.height) {
1150
+ System.arraycopy(
1151
+ pixels,
1152
+ row * boxes.width,
1153
+ panel,
1154
+ (boxes.leftY + row) * FRAME_WIDTH + boxes.leftX,
1155
+ boxes.width,
1156
+ )
1157
+ }
1158
+ return encodePixels(panel)
1159
+ }
1160
+
1161
+ fun blackPayload(): ByteArray? {
1162
+ val compressed = zlib(rleEncodeSolid(0, FRAME_WIDTH * FRAME_HEIGHT)) ?: return null
1163
+ return byteArrayOf(6) + compressed
1164
+ }
1165
+
1166
+ fun encodePixels(grayscale: ByteArray): ByteArray? {
1167
+ if (grayscale.size != FRAME_WIDTH * FRAME_HEIGHT) return null
1168
+ val compressed = zlib(rleEncode(pack4Bpp(grayscale))) ?: return null
1169
+ return byteArrayOf(6) + compressed
1170
+ }
1171
+
1172
+ /**
1173
+ * Snap an 8bpp raster to the 16 panel levels (`grayNibble(v) * 16`) before
1174
+ * it enters the diff/shadow pipeline. The reference client diffs packed
1175
+ * 4bpp bytes — exactly what the panel shows; diffing raw 8bpp instead
1176
+ * marked rows dirty for sub-quantization AA noise the panel can never
1177
+ * display (spurious rects, inflated coverage, and a scroll-copy detector
1178
+ * that almost never matched). `grayNibble` round-trips `level * 16`
1179
+ * exactly, so packing a quantized raster is lossless.
1180
+ */
1181
+ fun quantizeToPanelLevels(raster: ByteArray): ByteArray =
1182
+ ByteArray(raster.size) { index -> (grayNibble(raster[index].toInt()) * 16).toByte() }
1183
+
1184
+ /**
1185
+ * 8-bit gray → 4-bit panel level, round-to-nearest (matches the reference
1186
+ * client's GRAY_TO_NIBBLE). 0 stays 0 so the shell's transparent color key
1187
+ * survives; plain truncation instead darkened the upper half of every
1188
+ * 16-value bucket by a full visible step on the 16-level panel.
1189
+ */
1190
+ fun grayNibble(value: Int): Int {
1191
+ val v = value and 0xFF
1192
+ return if (v == 0) 0 else minOf(15, (v + 8) ushr 4)
1193
+ }
1194
+
1195
+ fun pack4Bpp(grayscale: ByteArray): ByteArray =
1196
+ ByteArray((grayscale.size + 1) / 2) { index ->
1197
+ val left = grayNibble(grayscale[index * 2].toInt())
1198
+ val rightIndex = index * 2 + 1
1199
+ val right = if (rightIndex < grayscale.size) grayNibble(grayscale[rightIndex].toInt()) else 0
1200
+ ((left shl 4) or right).toByte()
1201
+ }
1202
+
1203
+ fun rleEncode(packed: ByteArray): ByteArray {
1204
+ val pixelCount = packed.size * 2
1205
+ fun nibble(index: Int): Int =
1206
+ if (index % 2 == 0) (packed[index / 2].toInt() ushr 4) and 0x0F
1207
+ else packed[index / 2].toInt() and 0x0F
1208
+
1209
+ val output = ByteArrayOutputStream()
1210
+ var index = 0
1211
+ while (index < pixelCount) {
1212
+ val color = nibble(index)
1213
+ var end = index + 1
1214
+ while (end < pixelCount && nibble(end) == color) end += 1
1215
+ appendRun(output, color, end - index)
1216
+ index = end
1217
+ }
1218
+ return output.toByteArray()
1219
+ }
1220
+
1221
+ private fun rleEncodeSolid(color: Int, pixelCount: Int): ByteArray =
1222
+ ByteArrayOutputStream().also { appendRun(it, color, pixelCount) }.toByteArray()
1223
+
1224
+ private fun appendRun(output: ByteArrayOutputStream, color: Int, count: Int) {
1225
+ var remaining = count
1226
+ while (remaining > 0) {
1227
+ val chunk = minOf(remaining, 0xFFFF)
1228
+ when {
1229
+ chunk <= 15 -> output.write((chunk shl 4) or color)
1230
+ chunk <= 255 -> {
1231
+ output.write(color)
1232
+ output.write(chunk)
1233
+ }
1234
+ else -> {
1235
+ output.write(color)
1236
+ output.write(0)
1237
+ output.write(chunk and 0xFF)
1238
+ output.write((chunk ushr 8) and 0xFF)
1239
+ }
1240
+ }
1241
+ remaining -= chunk
1242
+ }
1243
+ }
1244
+
1245
+ private fun zlib(data: ByteArray): ByteArray? =
1246
+ try {
1247
+ ByteArrayOutputStream().also { output ->
1248
+ DeflaterOutputStream(output).use { it.write(data) }
1249
+ }.toByteArray()
1250
+ } catch (_: Exception) {
1251
+ null
1252
+ }
1253
+ }