@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,1241 @@
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
+ /** One firmware-drawn (mode 15) text line at absolute panel coordinates. */
197
+ data class TextDraw(val x: Int, val y: Int, val text: String)
198
+
199
+ /**
200
+ * One incremental encode: the wire payload (null when the frame is
201
+ * visually identical), mode-12 texture uploads that must be queued AHEAD
202
+ * of it, the raster backing the on-glass shadow after this payload
203
+ * applies, and whether the payload re-sends the whole canvas.
204
+ */
205
+ class SceneUpdate(
206
+ val payload: ByteArray?,
207
+ val uploads: List<ByteArray>,
208
+ val raster: ByteArray,
209
+ val usedFullFrame: Boolean,
210
+ )
211
+
212
+ const val MAX_DIRTY_RECTS = 6
213
+ const val REQUIRED_FID_COUNT = MAX_DIRTY_RECTS + 2
214
+
215
+ /** Cap on mode-14 runs per update (mode-8's count byte is the hard bound). */
216
+ const val MAX_GLYPH_RUNS = 180
217
+
218
+ /**
219
+ * Line pitch and default draw options of the glasses' builtin 20 px font
220
+ * chain (mode 15): lineHeight 27 / baseline 22 in firmware 2.2.6.10.
221
+ * 0x1F = top color 15 (white) + bit 4 (source color 0 transparent).
222
+ */
223
+ const val BUILTIN_LINE_HEIGHT = 27
224
+ const val BUILTIN_TEXT_OPTIONS = 0x1F
225
+ private const val DIRTY_ROW_MERGE_GAP = 8
226
+ private const val FULL_FRAME_COVERAGE_PERCENT = 60
227
+ private const val MIN_SCROLL_BAND_ROWS = 16
228
+ private const val SCROLL_SAVINGS_PERCENT = 30
229
+
230
+ /**
231
+ * Rasterize + plan + encode one incremental scene update. Mirrors the iOS
232
+ * renderer: see G2CfwRenderer.swift `encodeUpdate` for the contract.
233
+ */
234
+ fun encodeUpdate(
235
+ frame: SceneFrame,
236
+ boxes: LensBoxes,
237
+ previousRaster: ByteArray?,
238
+ forcedRects: List<CanvasRect>,
239
+ fids: List<Int>,
240
+ textDraws: List<TextDraw>,
241
+ allowRectCopy: Boolean,
242
+ blankBoxes: LensBoxes?,
243
+ omitBuiltinText: Boolean,
244
+ textureGlyphsEnabled: Boolean = false,
245
+ textureImagesEnabled: Boolean = false,
246
+ cacheGeneration: Int = 0,
247
+ ): SceneUpdate? {
248
+ if (fids.size < REQUIRED_FID_COUNT) return null
249
+ val width = boxes.width
250
+ val height = boxes.height
251
+ // Texture draws (modes 13/14) land at absolute panel coordinates with
252
+ // no per-lens variant — only a lens-equal window may use them.
253
+ val lensEqual = boxes.leftX == boxes.rightX && boxes.leftY == boxes.rightY
254
+ val useTexGlyphs = textureGlyphsEnabled && lensEqual
255
+ val useTexImages = textureImagesEnabled && lensEqual
256
+ // A full cache resets between frames (never mid-frame: draws already
257
+ // planned this frame reference the old offsets). Bumping past the
258
+ // caller's generation keeps the caller's own bumps monotonic.
259
+ if ((useTexGlyphs || useTexImages) && (textureCache.generation != cacheGeneration || textureCache.exhausted)) {
260
+ textureCache.reset(cacheGeneration)
261
+ }
262
+
263
+ var grids: List<MonoGrid> = emptyList()
264
+ val raster: ByteArray
265
+ if (useTexGlyphs) {
266
+ val result = rasterizeGridExact(frame, width, height, omitBuiltinText) ?: return null
267
+ raster = quantizeToPanelLevels(result.first)
268
+ grids = result.second
269
+ } else {
270
+ raster = quantizeToPanelLevels(rasterize(frame, width, height, omitBuiltinText) ?: return null)
271
+ }
272
+
273
+ var nextFid = 0
274
+ fun takeFid(): Int = fids[nextFid++]
275
+
276
+ val segments = mutableListOf<ByteArray>()
277
+ if (blankBoxes != null) {
278
+ solidStereoFrame(0, blankBoxes, takeFid())?.let { segments.add(it) }
279
+ }
280
+
281
+ val plan =
282
+ if (previousRaster != null && previousRaster.size == raster.size && blankBoxes == null) {
283
+ diffPlan(previousRaster, raster, width, height, forcedRects, allowRectCopy)
284
+ } else {
285
+ DeltaPlan.FULL_FRAME
286
+ }
287
+
288
+ if (plan.isNoop) {
289
+ return SceneUpdate(
290
+ payload = null, uploads = textureCache.takeUploadPayloads(),
291
+ raster = raster, usedFullFrame = false,
292
+ )
293
+ }
294
+
295
+ if (plan.full) {
296
+ val full = stereoFrame(raster, boxes, takeFid()) ?: return null
297
+ segments.add(full)
298
+ } else {
299
+ plan.copy?.let { segments.add(rectCopyPayload(it, width, boxes)) }
300
+ var deltaRects = plan.rects
301
+ var drawSegments: List<ByteArray> = emptyList()
302
+ if (useTexGlyphs || useTexImages) {
303
+ // Reserve one fid for the bundle-overflow fallback below.
304
+ val fidBudget = REQUIRED_FID_COUNT - nextFid - 1
305
+ val expressed =
306
+ expressTextureRects(
307
+ plan.rects,
308
+ grids = if (useTexGlyphs) grids else emptyList(),
309
+ frame = frame, boxes = boxes, raster = raster,
310
+ width = width, height = height,
311
+ allowImages = useTexImages, fidBudget = fidBudget,
312
+ )
313
+ deltaRects = expressed.first
314
+ drawSegments = expressed.second
315
+ }
316
+ for (rect in deltaRects) {
317
+ val delta = boxDeltaFrame(raster, width, rect, boxes, takeFid()) ?: return null
318
+ segments.add(delta)
319
+ }
320
+ segments.addAll(drawSegments)
321
+ }
322
+
323
+ for (draw in textDraws) {
324
+ builtinStringPayload(draw.x, draw.y, BUILTIN_TEXT_OPTIONS, draw.text)?.let { segments.add(it) }
325
+ }
326
+
327
+ val uploads = textureCache.takeUploadPayloads()
328
+ if (segments.size == 1) return SceneUpdate(segments[0], uploads, raster, plan.full)
329
+ bundle(segments)?.let { return SceneUpdate(it, uploads, raster, plan.full) }
330
+ // A segment overflowed the bundle's 64 KiB cap (a near-incompressible
331
+ // full canvas). Fall back to one bare full frame with any builtin text
332
+ // baked into the bitmap so nothing is lost. The uploads still go out —
333
+ // residency was already recorded in the model.
334
+ val flat = rasterize(frame, width, height, omitBuiltinText = false) ?: return null
335
+ val full = stereoFrame(flat, boxes, takeFid()) ?: return null
336
+ return SceneUpdate(full, uploads, flat, usedFullFrame = true)
337
+ }
338
+
339
+ /**
340
+ * Blit `font: "mono"` elements as opaque glyph grids: the element's box
341
+ * is cleared to black and every non-space cell blits the SAME full-cell
342
+ * raster mode-14 draws replay, so an on-glasses cached draw reproduces
343
+ * the raster exactly. (Opaque cells are also what makes cached draws
344
+ * self-erasing.) Mono grids paint after all other elements — an exotic
345
+ * frame stacking a non-mono element ON TOP of a mono grid would z-order
346
+ * differently here, which no current miniapp does.
347
+ */
348
+ private fun rasterizeGridExact(
349
+ frame: SceneFrame,
350
+ width: Int,
351
+ height: Int,
352
+ omitBuiltinText: Boolean,
353
+ ): Pair<ByteArray, List<MonoGrid>>? {
354
+ val raster =
355
+ rasterize(frame, width, height, omitBuiltinText, omitMonoText = true) ?: return null
356
+
357
+ val grids = mutableListOf<MonoGrid>()
358
+ for ((z, element) in frame.elements.withIndex()) {
359
+ if (element.type != "text" || element.font != "mono") continue
360
+ val bx = element.x
361
+ val by = element.y
362
+ val clipX0 = maxOf(0, bx)
363
+ val clipY0 = maxOf(0, by)
364
+ val clipX1 = minOf(width, bx + element.w)
365
+ val clipY1 = minOf(height, by + element.h)
366
+ if (clipX0 >= clipX1 || clipY0 >= clipY1) continue
367
+ val (cellWidth, lineHeight) = monoCellMetrics(element.fontSize)
368
+ val originX = bx + 2
369
+ val originY = by + 2
370
+ for (y in clipY0 until clipY1) {
371
+ raster.fill(0, y * width + clipX0, y * width + clipX1)
372
+ }
373
+ val lines = (element.text ?: "").split('\n').map { codePoints(it) }
374
+ val blitCols = maxOf(0, (clipX1 - originX + cellWidth - 1) / cellWidth)
375
+ val blitRows = maxOf(0, (clipY1 - originY + lineHeight - 1) / lineHeight)
376
+ for (row in 0 until minOf(blitRows, lines.size)) {
377
+ val line = lines[row]
378
+ for (col in 0 until minOf(blitCols, line.size)) {
379
+ if (line[col] == 32) continue
380
+ val glyph =
381
+ memoizedGlyphRaster(line[col], element.fontSize, cellWidth, lineHeight)
382
+ ?: continue
383
+ blitCell(
384
+ glyph, raster, width,
385
+ atX = originX + col * cellWidth, atY = originY + row * lineHeight,
386
+ cellWidth = cellWidth, cellHeight = lineHeight,
387
+ clipX0 = clipX0, clipY0 = clipY0, clipX1 = clipX1, clipY1 = clipY1,
388
+ )
389
+ }
390
+ }
391
+ grids.add(
392
+ MonoGrid(
393
+ z = z, fontSize = element.fontSize, originX = originX, originY = originY,
394
+ cellWidth = cellWidth, lineHeight = lineHeight,
395
+ fullCols = maxOf(0, (clipX1 - originX) / cellWidth),
396
+ fullRows = maxOf(0, (clipY1 - originY) / lineHeight),
397
+ rows = lines,
398
+ )
399
+ )
400
+ }
401
+ return raster to grids
402
+ }
403
+
404
+ private fun blitCell(
405
+ glyph: ByteArray,
406
+ raster: ByteArray,
407
+ rasterWidth: Int,
408
+ atX: Int,
409
+ atY: Int,
410
+ cellWidth: Int,
411
+ cellHeight: Int,
412
+ clipX0: Int,
413
+ clipY0: Int,
414
+ clipX1: Int,
415
+ clipY1: Int,
416
+ ) {
417
+ val x0 = maxOf(atX, clipX0)
418
+ val y0 = maxOf(atY, clipY0)
419
+ val x1 = minOf(atX + cellWidth, clipX1)
420
+ val y1 = minOf(atY + cellHeight, clipY1)
421
+ if (x0 >= x1 || y0 >= y1) return
422
+ for (y in y0 until y1) {
423
+ System.arraycopy(
424
+ glyph, (y - atY) * cellWidth + (x0 - atX),
425
+ raster, y * rasterWidth + x0,
426
+ x1 - x0,
427
+ )
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Express-or-bake: each dirty rect either becomes cached draws (mode-14
433
+ * glyph runs from the topmost covering mono grid, or one mode-13 draw of
434
+ * an isolated repeated image) and drops its bitmap delta, or stays a
435
+ * plain delta untouched. All-or-nothing per rect keeps the shadow model
436
+ * trivially exact — no partial punch-out. Non-ASCII cells (mode 14 is
437
+ * 32..126 only) become small residual bitmap deltas within an otherwise
438
+ * expressed rect.
439
+ */
440
+ private fun expressTextureRects(
441
+ rects: List<CanvasRect>,
442
+ grids: List<MonoGrid>,
443
+ frame: SceneFrame,
444
+ boxes: LensBoxes,
445
+ raster: ByteArray,
446
+ width: Int,
447
+ height: Int,
448
+ allowImages: Boolean,
449
+ fidBudget: Int,
450
+ ): Pair<List<CanvasRect>, List<ByteArray>> {
451
+ class ElementBox(val z: Int, val x0: Int, val y0: Int, val x1: Int, val y1: Int) {
452
+ fun intersects(r: CanvasRect): Boolean =
453
+ r.x < x1 && r.x + r.w > x0 && r.y < y1 && r.y + r.h > y0
454
+ }
455
+ val elementBoxes =
456
+ frame.elements.mapIndexed { z, el ->
457
+ ElementBox(
458
+ z = z, x0 = maxOf(0, el.x), y0 = maxOf(0, el.y),
459
+ x1 = minOf(width, el.x + el.w), y1 = minOf(height, el.y + el.h),
460
+ )
461
+ }
462
+
463
+ // Isolated image candidates: nothing else overlaps their box, so the
464
+ // frame raster inside the box IS the standalone render and an opaque
465
+ // full-box cached draw reproduces it. Sightings are recorded every
466
+ // frame; only content seen before enters the cache (streaming content
467
+ // like a live minimap never repeats a hash).
468
+ class ImageCandidate(val box: ElementBox, val key: String, val seen: Boolean)
469
+ val imageCandidates = mutableMapOf<Int, ImageCandidate>()
470
+ if (allowImages) {
471
+ for ((z, el) in frame.elements.withIndex()) {
472
+ if (el.type != "image") continue
473
+ val box = elementBoxes[z]
474
+ val w = box.x1 - box.x0
475
+ val h = box.y1 - box.y0
476
+ if (w <= 0 || h <= 0 || w > 255 || h > 255 || el.contentHash.isEmpty()) continue
477
+ val isolated =
478
+ elementBoxes.none { other ->
479
+ other.z != z && other.x0 < box.x1 && other.x1 > box.x0 &&
480
+ other.y0 < box.y1 && other.y1 > box.y0
481
+ }
482
+ if (!isolated) continue
483
+ val key = "${el.contentHash}@${w}x$h"
484
+ imageCandidates[z] = ImageCandidate(box, key, textureCache.noteImageSighting(key))
485
+ }
486
+ }
487
+
488
+ val deltas = mutableListOf<CanvasRect>()
489
+ val imageDraws = sortedMapOf<Int, ByteArray>()
490
+ val runPayloads = mutableListOf<ByteArray>()
491
+ var runCount = 0
492
+
493
+ rectLoop@ for ((index, rect) in rects.withIndex()) {
494
+ val remaining = rects.size - index - 1
495
+
496
+ // Glyph express: topmost mono grid fully covering the rect, with
497
+ // nothing stacked above it in that region.
498
+ val grid = grids.filter { it.covers(rect) }.maxByOrNull { it.z }
499
+ if (grid != null &&
500
+ elementBoxes.none { it.z > grid.z && it.intersects(rect) }
501
+ ) {
502
+ val font = textureCache.font(grid.fontSize, grid.cellWidth, grid.lineHeight)
503
+ if (font != null) {
504
+ val r0 = (rect.y - grid.originY) / grid.lineHeight
505
+ val r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
506
+ val c0 = (rect.x - grid.originX) / grid.cellWidth
507
+ val c1 = (rect.x + rect.w - 1 - grid.originX) / grid.cellWidth
508
+ val newRuns = mutableListOf<Triple<Int, Int, ByteArray>>() // x, y, chars
509
+ val residuals = mutableListOf<CanvasRect>()
510
+ var expressible = true
511
+ rowLoop@ for (row in r0..r1) {
512
+ val rowY = grid.originY + row * grid.lineHeight
513
+ var runChars = mutableListOf<Byte>()
514
+ var runStartCol = c0
515
+ var residualStart: Int? = null
516
+ fun flushRun() {
517
+ if (runChars.isEmpty()) return
518
+ newRuns.add(
519
+ Triple(
520
+ grid.originX + runStartCol * grid.cellWidth, rowY,
521
+ runChars.toByteArray(),
522
+ )
523
+ )
524
+ runChars = mutableListOf()
525
+ }
526
+ fun flushResidual(endCol: Int) {
527
+ val start = residualStart ?: return
528
+ val x0 = maxOf(0, (grid.originX + start * grid.cellWidth) and 3.inv())
529
+ val x1 = minOf(width, (grid.originX + endCol * grid.cellWidth + 3) and 3.inv())
530
+ val y0 = maxOf(0, rowY and 1.inv())
531
+ val y1 = minOf(height, (rowY + grid.lineHeight + 1) and 1.inv())
532
+ residuals.add(CanvasRect(x0, y0, x1 - x0, y1 - y0))
533
+ residualStart = null
534
+ }
535
+ for (col in c0..c1) {
536
+ val scalar = grid.scalar(row, col)
537
+ val resident =
538
+ scalar in 32..126 &&
539
+ textureCache.ensureGlyph(font, scalar) {
540
+ memoizedGlyphRaster(
541
+ scalar, grid.fontSize, grid.cellWidth, grid.lineHeight
542
+ )
543
+ }
544
+ if (resident) {
545
+ flushResidual(col)
546
+ if (runChars.isEmpty()) runStartCol = col
547
+ runChars.add(scalar.toByte())
548
+ if (runChars.size == 255) flushRun()
549
+ } else {
550
+ flushRun()
551
+ if (residualStart == null) residualStart = col
552
+ }
553
+ }
554
+ flushRun()
555
+ flushResidual(c1 + 1)
556
+ if (runCount + newRuns.size > MAX_GLYPH_RUNS) {
557
+ expressible = false
558
+ break@rowLoop
559
+ }
560
+ }
561
+ // Residual deltas + every remaining rect must still fit the
562
+ // fid budget; otherwise keep this rect as one plain delta.
563
+ if (expressible &&
564
+ deltas.size + residuals.size + remaining + 1 <= fidBudget &&
565
+ residuals.size <= 3
566
+ ) {
567
+ var payloads = mutableListOf<ByteArray>()
568
+ for ((runX, runY, chars) in newRuns) {
569
+ val payload =
570
+ G2CfwTextureCache.stringRunPayload(
571
+ fontTableOffset = font.tableOffset,
572
+ x = boxes.leftX + runX, y = boxes.leftY + runY,
573
+ options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS, chars = chars,
574
+ )
575
+ if (payload == null) {
576
+ payloads = mutableListOf()
577
+ break
578
+ }
579
+ payloads.add(payload)
580
+ }
581
+ if (payloads.isNotEmpty() || newRuns.isEmpty()) {
582
+ runPayloads.addAll(payloads)
583
+ runCount += payloads.size
584
+ deltas.addAll(residuals)
585
+ continue@rectLoop
586
+ }
587
+ }
588
+ }
589
+ }
590
+
591
+ // Image express: the rect sits wholly inside an isolated, repeated
592
+ // image's box — one opaque full-box draw covers it (and any other
593
+ // rect in the same box).
594
+ if (allowImages) {
595
+ val entry =
596
+ imageCandidates.entries.firstOrNull { (_, c) ->
597
+ c.seen && rect.x >= c.box.x0 && rect.y >= c.box.y0 &&
598
+ rect.x + rect.w <= c.box.x1 && rect.y + rect.h <= c.box.y1
599
+ }
600
+ if (entry != null) {
601
+ val (z, candidate) = entry
602
+ if (imageDraws.containsKey(z)) continue@rectLoop
603
+ val w = candidate.box.x1 - candidate.box.x0
604
+ val h = candidate.box.y1 - candidate.box.y0
605
+ val gray = ByteArray(w * h)
606
+ for (y in candidate.box.y0 until candidate.box.y1) {
607
+ System.arraycopy(
608
+ raster, y * width + candidate.box.x0,
609
+ gray, (y - candidate.box.y0) * w, w,
610
+ )
611
+ }
612
+ val offset = textureCache.ensureImage(candidate.key, gray, w, h)
613
+ val draw =
614
+ offset?.let {
615
+ G2CfwTextureCache.imageDrawPayload(
616
+ offset = it,
617
+ x = boxes.leftX + candidate.box.x0,
618
+ y = boxes.leftY + candidate.box.y0,
619
+ options = G2CfwTextureCache.OPAQUE_DRAW_OPTIONS,
620
+ )
621
+ }
622
+ if (draw != null) {
623
+ imageDraws[z] = draw
624
+ continue@rectLoop
625
+ }
626
+ }
627
+ }
628
+
629
+ deltas.add(rect)
630
+ }
631
+
632
+ return deltas to (imageDraws.values.toList() + runPayloads)
633
+ }
634
+
635
+ /**
636
+ * Diff two same-size canvas rasters into a scroll copy + aligned dirty
637
+ * rects, or a full-frame decision when the change is too large.
638
+ */
639
+ fun diffPlan(
640
+ previous: ByteArray,
641
+ next: ByteArray,
642
+ width: Int,
643
+ height: Int,
644
+ forced: List<CanvasRect>,
645
+ allowCopy: Boolean,
646
+ ): DeltaPlan {
647
+ if (previous.size != width * height || next.size != width * height || width <= 0 || height <= 0) {
648
+ return DeltaPlan.FULL_FRAME
649
+ }
650
+
651
+ fun rowsEqual(a: ByteArray, aRow: Int, b: ByteArray, bRow: Int): Boolean {
652
+ val aOff = aRow * width
653
+ val bOff = bRow * width
654
+ for (x in 0 until width) {
655
+ if (a[aOff + x] != b[bOff + x]) return false
656
+ }
657
+ return true
658
+ }
659
+
660
+ val changed = BooleanArray(height) { y -> !rowsEqual(previous, y, next, y) }
661
+ var anyChanged = changed.any { it }
662
+ if (!anyChanged && forced.isEmpty()) return DeltaPlan.NOOP
663
+
664
+ // Scroll detection: a vertical band of new rows that byte-match
665
+ // previous rows shifted by dy. Row hashes score candidate shifts;
666
+ // the winning band is byte-verified before use.
667
+ var copy: RowCopy? = null
668
+ var predicted: ByteArray? = null
669
+ if (allowCopy && anyChanged) {
670
+ val prevHash = LongArray(height) { y -> rowHash(previous, y * width, width) }
671
+ val newHash = LongArray(height) { y -> rowHash(next, y * width, width) }
672
+ val changedCount = changed.count { it }
673
+ var bestDy = 0
674
+ var bestScore = 0
675
+ val maxShift = height - MIN_SCROLL_BAND_ROWS
676
+ if (maxShift > 0) {
677
+ for (dy in -maxShift..maxShift) {
678
+ if (dy == 0) continue
679
+ var score = 0
680
+ for (y in maxOf(0, -dy) until minOf(height, height - dy)) {
681
+ if (changed[y] && newHash[y] == prevHash[y + dy]) score += 1
682
+ }
683
+ if (score > bestScore) {
684
+ bestScore = score
685
+ bestDy = dy
686
+ }
687
+ }
688
+ }
689
+ if (bestDy != 0 && bestScore >= MIN_SCROLL_BAND_ROWS &&
690
+ bestScore * 100 >= changedCount * SCROLL_SAVINGS_PERCENT
691
+ ) {
692
+ var runStart = -1
693
+ var bandStart = 0
694
+ var bandLen = 0
695
+ val lo = maxOf(0, -bestDy)
696
+ val hi = minOf(height, height - bestDy)
697
+ for (y in lo..hi) {
698
+ val matches = y < hi && newHash[y] == prevHash[y + bestDy]
699
+ if (matches) {
700
+ if (runStart < 0) runStart = y
701
+ } else if (runStart >= 0) {
702
+ if (y - runStart > bandLen) {
703
+ bandStart = runStart
704
+ bandLen = y - runStart
705
+ }
706
+ runStart = -1
707
+ }
708
+ }
709
+ if (bandLen >= MIN_SCROLL_BAND_ROWS) {
710
+ var verified = true
711
+ for (y in bandStart until bandStart + bandLen) {
712
+ if (!rowsEqual(next, y, previous, y + bestDy)) {
713
+ verified = false
714
+ break
715
+ }
716
+ }
717
+ if (verified) {
718
+ copy = RowCopy(srcY = bandStart + bestDy, dstY = bandStart, height = bandLen)
719
+ predicted = previous.copyOf().also {
720
+ System.arraycopy(
721
+ previous, (bandStart + bestDy) * width, it, bandStart * width, bandLen * width
722
+ )
723
+ }
724
+ for (y in bandStart until bandStart + bandLen) changed[y] = false
725
+ anyChanged = changed.any { it }
726
+ }
727
+ }
728
+ }
729
+ }
730
+
731
+ // Per-row dirty column ranges against the (possibly copied) base.
732
+ val base = predicted ?: previous
733
+ val colLo = IntArray(height) { Int.MAX_VALUE }
734
+ val colHi = IntArray(height) { -1 }
735
+ for (y in 0 until height) {
736
+ if (!changed[y]) continue
737
+ var lo = 0
738
+ while (lo < width && base[y * width + lo] == next[y * width + lo]) lo += 1
739
+ var hi = width - 1
740
+ while (hi > lo && base[y * width + hi] == next[y * width + hi]) hi -= 1
741
+ colLo[y] = lo
742
+ colHi[y] = hi
743
+ }
744
+ val dirtyRows = changed.copyOf()
745
+ for (rect in forced) {
746
+ val y0 = maxOf(0, rect.y)
747
+ val y1 = minOf(height, rect.y + rect.h)
748
+ if (y0 >= y1 || rect.w <= 0) continue
749
+ for (y in y0 until y1) {
750
+ dirtyRows[y] = true
751
+ colLo[y] = minOf(colLo[y], maxOf(0, rect.x))
752
+ colHi[y] = maxOf(colHi[y], minOf(width - 1, rect.x + rect.w - 1))
753
+ }
754
+ }
755
+ if (!dirtyRows.any { it }) {
756
+ return if (copy != null) DeltaPlan(full = false, copy = copy, rects = emptyList()) else DeltaPlan.NOOP
757
+ }
758
+
759
+ // Group dirty rows into bands (small gaps merge), aligned to the grid.
760
+ var rects = mutableListOf<CanvasRect>()
761
+ var y = 0
762
+ while (y < height) {
763
+ if (!dirtyRows[y]) {
764
+ y += 1
765
+ continue
766
+ }
767
+ var end = y
768
+ var gap = 0
769
+ var scan = y
770
+ while (scan < height && gap <= DIRTY_ROW_MERGE_GAP) {
771
+ if (dirtyRows[scan]) {
772
+ end = scan
773
+ gap = 0
774
+ } else {
775
+ gap += 1
776
+ }
777
+ scan += 1
778
+ }
779
+ var lo = Int.MAX_VALUE
780
+ var hi = -1
781
+ for (row in y..end) {
782
+ if (!dirtyRows[row]) continue
783
+ lo = minOf(lo, colLo[row])
784
+ hi = maxOf(hi, colHi[row])
785
+ }
786
+ val x0 = maxOf(0, lo and 3.inv())
787
+ val x1 = minOf(width, (hi + 4) and 3.inv())
788
+ val ry0 = maxOf(0, y and 1.inv())
789
+ val ry1 = minOf(height, (end + 2) and 1.inv())
790
+ rects.add(CanvasRect(x0, ry0, x1 - x0, ry1 - ry0))
791
+ y = end + 1
792
+ }
793
+ if (rects.size > MAX_DIRTY_RECTS) {
794
+ val x0 = rects.minOf { it.x }
795
+ val y0 = rects.minOf { it.y }
796
+ val x1 = rects.maxOf { it.x + it.w }
797
+ val y1 = rects.maxOf { it.y + it.h }
798
+ rects = mutableListOf(CanvasRect(x0, y0, x1 - x0, y1 - y0))
799
+ }
800
+ val coverage = rects.sumOf { it.w * it.h }
801
+ if (copy == null && coverage * 100 >= width * height * FULL_FRAME_COVERAGE_PERCENT) {
802
+ return DeltaPlan.FULL_FRAME
803
+ }
804
+ return DeltaPlan(full = false, copy = copy, rects = rects)
805
+ }
806
+
807
+ private fun rowHash(pixels: ByteArray, offset: Int, count: Int): Long {
808
+ // FNV-1a 64 — collision-safe enough for scoring; bands are byte-verified.
809
+ var hash = -0x340d631b7bdddcdbL
810
+ for (i in offset until offset + count) {
811
+ hash = (hash xor (pixels[i].toLong() and 0xFF)) * 0x100000001b3L
812
+ }
813
+ return hash
814
+ }
815
+
816
+ /**
817
+ * Stereo mode-3 delta of one canvas sub-rect, drawn at each lens's window
818
+ * origin plus the rect offset. Rect must be grid-aligned and in bounds.
819
+ */
820
+ fun boxDeltaFrame(
821
+ raster: ByteArray,
822
+ canvasWidth: Int,
823
+ rect: CanvasRect,
824
+ boxes: LensBoxes,
825
+ frameId: Int,
826
+ ): ByteArray? {
827
+ if (rect.w <= 0 || rect.h <= 0 || rect.x % 4 != 0 || rect.w % 4 != 0 ||
828
+ rect.y % 2 != 0 || rect.h % 2 != 0 || rect.x + rect.w > canvasWidth ||
829
+ (rect.y + rect.h) * canvasWidth > raster.size
830
+ ) {
831
+ return null
832
+ }
833
+ val pixels = ByteArray(rect.w * rect.h)
834
+ for (y in 0 until rect.h) {
835
+ System.arraycopy(raster, (rect.y + y) * canvasWidth + rect.x, pixels, y * rect.w, rect.w)
836
+ }
837
+ return stereoRleFrame(
838
+ rleEncode(pack4Bpp(pixels)),
839
+ lx = boxes.leftX + rect.x, ly = boxes.leftY + rect.y,
840
+ rx = boxes.rightX + rect.x, ry = boxes.rightY + rect.y,
841
+ w = rect.w, h = rect.h, frameId = frameId,
842
+ )
843
+ }
844
+
845
+ /**
846
+ * Mode-9 rect copy with the lenses-differ flag: a full-width canvas band
847
+ * moves srcY→dstY inside each lens's window.
848
+ * `[0x89][L src LTWH dst LTWH][R src LTWH dst LTWH]`, u16 LE each.
849
+ */
850
+ fun rectCopyPayload(copy: RowCopy, width: Int, boxes: LensBoxes): ByteArray {
851
+ val output = ByteArrayOutputStream()
852
+ output.write(0x89)
853
+ fun writeRect(x: Int, y: Int) {
854
+ for (value in intArrayOf(x, y, width, copy.height)) {
855
+ output.write(value and 0xFF)
856
+ output.write((value ushr 8) and 0xFF)
857
+ }
858
+ }
859
+ writeRect(boxes.leftX, boxes.leftY + copy.srcY)
860
+ writeRect(boxes.leftX, boxes.leftY + copy.dstY)
861
+ writeRect(boxes.rightX, boxes.rightY + copy.srcY)
862
+ writeRect(boxes.rightX, boxes.rightY + copy.dstY)
863
+ return output.toByteArray()
864
+ }
865
+
866
+ /**
867
+ * Mode-15 builtin-font string draw:
868
+ * `[15][x:u16][y:u16][options:u8][strlen:u8][UTF-8]`. Bytes 1–31 are
869
+ * firmware x-adjust controls, so control characters are stripped here;
870
+ * the text is truncated to 255 UTF-8 bytes at a codepoint boundary.
871
+ */
872
+ fun builtinStringPayload(x: Int, y: Int, options: Int, text: String): ByteArray? {
873
+ if (x !in 0..0xFFFF || y !in 0..0xFFFF) return null
874
+ val sanitized = text.filter { it.code >= 32 }
875
+ var bytes = sanitized.toByteArray(Charsets.UTF_8)
876
+ if (bytes.size > 255) {
877
+ var cut = 255
878
+ while (cut > 0 && (bytes[cut].toInt() and 0xC0) == 0x80) cut -= 1
879
+ bytes = bytes.copyOfRange(0, cut)
880
+ }
881
+ if (bytes.isEmpty()) return null
882
+ val output = ByteArrayOutputStream()
883
+ output.write(15)
884
+ output.write(x and 0xFF)
885
+ output.write((x ushr 8) and 0xFF)
886
+ output.write(y and 0xFF)
887
+ output.write((y ushr 8) and 0xFF)
888
+ output.write(options and 0xFF)
889
+ output.write(bytes.size)
890
+ output.write(bytes)
891
+ return output.toByteArray()
892
+ }
893
+
894
+ /**
895
+ * The firmware-drawn text lines of a frame, at absolute panel
896
+ * coordinates. Mode 15 has no per-lens variant, so callers must place the
897
+ * window at lens-equal offsets (depth level 1) whenever these are used.
898
+ * The +2 inset matches the rasterizer's text padding.
899
+ */
900
+ fun builtinTextDraws(frame: SceneFrame, boxes: LensBoxes): List<TextDraw> {
901
+ val draws = mutableListOf<TextDraw>()
902
+ for (element in frame.elements) {
903
+ if (element.type != "text" || element.font != "builtin") continue
904
+ val text = element.text ?: continue
905
+ if (text.isEmpty()) continue
906
+ val baseX = boxes.leftX + element.x + 2
907
+ val baseY = boxes.leftY + element.y + 2
908
+ text.split('\n').forEachIndexed { index, line ->
909
+ val y = baseY + index * BUILTIN_LINE_HEIGHT
910
+ if (y >= 0 && y + BUILTIN_LINE_HEIGHT <= FRAME_HEIGHT && line.isNotEmpty()) {
911
+ draws.add(TextDraw(baseX, y, line))
912
+ }
913
+ }
914
+ }
915
+ return draws
916
+ }
917
+
918
+ /**
919
+ * Conservative canvas-space bounds of firmware-drawn text, used to force
920
+ * background re-pushes that erase glyphs the phone raster never
921
+ * contained. Width is overestimated (the phone has no advance tables).
922
+ */
923
+ fun builtinTextBounds(
924
+ draws: List<TextDraw>,
925
+ boxes: LensBoxes,
926
+ canvasWidth: Int,
927
+ canvasHeight: Int,
928
+ ): List<CanvasRect> =
929
+ draws.mapNotNull { draw ->
930
+ val x = maxOf(0, draw.x - boxes.leftX - 4)
931
+ val y = maxOf(0, draw.y - boxes.leftY - 2)
932
+ if (x >= canvasWidth || y >= canvasHeight) return@mapNotNull null
933
+ CanvasRect(
934
+ x = x,
935
+ y = y,
936
+ w = minOf(canvasWidth - x, draw.text.length * 13 + 30),
937
+ h = minOf(canvasHeight - y, BUILTIN_LINE_HEIGHT + 4),
938
+ )
939
+ }
940
+
941
+ /**
942
+ * `omitBuiltinText` skips `font == "builtin"` text elements — the CFW
943
+ * draws those with mode 15, so the bitmap must not also contain them.
944
+ * `omitMonoText` skips `font == "mono"` text elements — the grid-exact
945
+ * texture path blits those from the same glyph rasters it uploads.
946
+ */
947
+ fun rasterize(
948
+ frame: SceneFrame,
949
+ width: Int,
950
+ height: Int,
951
+ omitBuiltinText: Boolean = false,
952
+ omitMonoText: Boolean = false,
953
+ ): ByteArray? {
954
+ if (width <= 0 || height <= 0) return null
955
+ val decodedImages = mutableMapOf<Int, Bitmap>()
956
+ try {
957
+ for ((index, element) in frame.elements.withIndex()) {
958
+ if (element.type != "image") continue
959
+ val encoded = element.data ?: return null
960
+ val data =
961
+ try {
962
+ Base64.decode(encoded, Base64.DEFAULT)
963
+ } catch (_: IllegalArgumentException) {
964
+ return null
965
+ }
966
+ decodedImages[index] = BitmapFactory.decodeByteArray(data, 0, data.size) ?: return null
967
+ }
968
+
969
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
970
+ val canvas = Canvas(bitmap)
971
+ canvas.drawColor(Color.BLACK)
972
+ for ((index, element) in frame.elements.withIndex()) {
973
+ // Mode 15 draws only the glyphs, so an omitted builtin-text
974
+ // element's border must still come from the raster.
975
+ val skipBuiltinInk = omitBuiltinText && element.type == "text" && element.font == "builtin"
976
+ if (skipBuiltinInk && element.border <= 0) continue
977
+ if (omitMonoText && element.type == "text" && element.font == "mono") continue
978
+ val left = element.x.toFloat()
979
+ val top = element.y.toFloat()
980
+ val rect = RectF(left, top, left + element.w, top + element.h)
981
+ val save = canvas.save()
982
+ canvas.clipRect(rect)
983
+ when (element.type) {
984
+ "text" -> if (!skipBuiltinInk) drawText(canvas, element, rect)
985
+ "image" -> decodedImages[index]?.let { drawImage(canvas, it, rect) }
986
+ }
987
+
988
+ val border = maxOf(if (element.type == "rect") 1 else 0, element.border)
989
+ if (border > 0) {
990
+ val paint =
991
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
992
+ color = Color.WHITE
993
+ style = Paint.Style.STROKE
994
+ strokeWidth = border.toFloat()
995
+ }
996
+ val inset = border / 2f
997
+ val borderRect = RectF(rect).apply { inset(inset, inset) }
998
+ val radius =
999
+ element.radius
1000
+ .coerceAtLeast(0)
1001
+ .toFloat()
1002
+ .coerceAtMost(minOf(borderRect.width(), borderRect.height()) / 2f)
1003
+ canvas.drawRoundRect(borderRect, radius, radius, paint)
1004
+ }
1005
+ canvas.restoreToCount(save)
1006
+ }
1007
+
1008
+ val colors = IntArray(width * height)
1009
+ bitmap.getPixels(colors, 0, width, 0, 0, width, height)
1010
+ bitmap.recycle()
1011
+ return ByteArray(colors.size) { index ->
1012
+ val color = colors[index]
1013
+ ((Color.red(color) * 299 + Color.green(color) * 587 + Color.blue(color) * 114) / 1000).toByte()
1014
+ }
1015
+ } finally {
1016
+ decodedImages.values.forEach { it.recycle() }
1017
+ }
1018
+ }
1019
+
1020
+ private fun drawText(canvas: Canvas, element: SceneElement, rect: RectF) {
1021
+ val text = element.text.orEmpty()
1022
+ if (text.isEmpty()) return
1023
+ val availableWidth = (rect.width() - 4f).toInt().coerceAtLeast(1)
1024
+ val save = canvas.save()
1025
+ canvas.translate(rect.left + 2f, rect.top + 2f)
1026
+ if (element.font == "mono") {
1027
+ val size = (if (element.fontSize > 0) element.fontSize else 20).toFloat()
1028
+ val paint =
1029
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
1030
+ color = Color.WHITE
1031
+ textSize = size
1032
+ typeface = Typeface.MONOSPACE
1033
+ }
1034
+ val baseline = -paint.fontMetrics.ascent
1035
+ val pitch = monoLineHeight(size)
1036
+ text.split('\n').forEachIndexed { index, line ->
1037
+ canvas.drawText(line, 0f, baseline + index * pitch, paint)
1038
+ }
1039
+ } else {
1040
+ val paint =
1041
+ TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
1042
+ color = Color.WHITE
1043
+ textSize = 20f
1044
+ typeface = Typeface.DEFAULT
1045
+ }
1046
+ StaticLayout.Builder.obtain(text, 0, text.length, paint, availableWidth)
1047
+ .setAlignment(Layout.Alignment.ALIGN_NORMAL)
1048
+ .setIncludePad(false)
1049
+ .build()
1050
+ .draw(canvas)
1051
+ }
1052
+ canvas.restoreToCount(save)
1053
+ }
1054
+
1055
+ private fun drawImage(canvas: Canvas, bitmap: Bitmap, rect: RectF) {
1056
+ val scale = minOf(rect.width() / bitmap.width.coerceAtLeast(1), rect.height() / bitmap.height.coerceAtLeast(1))
1057
+ val targetWidth = bitmap.width * scale
1058
+ val targetHeight = bitmap.height * scale
1059
+ canvas.drawBitmap(
1060
+ bitmap,
1061
+ Rect(0, 0, bitmap.width, bitmap.height),
1062
+ RectF(
1063
+ rect.centerX() - targetWidth / 2f,
1064
+ rect.centerY() - targetHeight / 2f,
1065
+ rect.centerX() + targetWidth / 2f,
1066
+ rect.centerY() + targetHeight / 2f,
1067
+ ),
1068
+ Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG),
1069
+ )
1070
+ }
1071
+
1072
+ fun stereoFrame(pixels: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? {
1073
+ if (pixels.size != boxes.width * boxes.height) return null
1074
+ return stereoRleFrame(rleEncode(pack4Bpp(pixels)), boxes, frameId)
1075
+ }
1076
+
1077
+ fun solidStereoFrame(color: Int, boxes: LensBoxes, frameId: Int): ByteArray? =
1078
+ stereoRleFrame(rleEncodeSolid(color and 0x0F, boxes.width * boxes.height), boxes, frameId)
1079
+
1080
+ private fun stereoRleFrame(rle: ByteArray, boxes: LensBoxes, frameId: Int): ByteArray? =
1081
+ stereoRleFrame(rle, boxes.leftX, boxes.leftY, boxes.rightX, boxes.rightY, boxes.width, boxes.height, frameId)
1082
+
1083
+ private fun stereoRleFrame(
1084
+ rle: ByteArray,
1085
+ lx: Int,
1086
+ ly: Int,
1087
+ rx: Int,
1088
+ ry: Int,
1089
+ w: Int,
1090
+ h: Int,
1091
+ frameId: Int,
1092
+ ): ByteArray? {
1093
+ val compressed = zlib(rle) ?: return null
1094
+ return byteArrayOf(
1095
+ 0x83.toByte(),
1096
+ (lx / 4).toByte(),
1097
+ (ly / 2).toByte(),
1098
+ (w / 4).toByte(),
1099
+ (h / 2).toByte(),
1100
+ (rx / 4).toByte(),
1101
+ (ry / 2).toByte(),
1102
+ (w / 4).toByte(),
1103
+ (h / 2).toByte(),
1104
+ (frameId and 0xFF).toByte(),
1105
+ ((frameId ushr 8) and 0xFF).toByte(),
1106
+ ) + compressed
1107
+ }
1108
+
1109
+ fun bundle(segments: List<ByteArray>): ByteArray? {
1110
+ if (segments.isEmpty() || segments.size > 255 || segments.any { it.isEmpty() || it.size > 0xFFFF }) {
1111
+ return null
1112
+ }
1113
+ val output = ByteArrayOutputStream()
1114
+ output.write(8)
1115
+ output.write(segments.size)
1116
+ for (segment in segments) {
1117
+ output.write(segment.size and 0xFF)
1118
+ output.write((segment.size ushr 8) and 0xFF)
1119
+ output.write(segment)
1120
+ }
1121
+ return output.toByteArray()
1122
+ }
1123
+
1124
+ /**
1125
+ * Mode-6 full-panel seed with the frame's own pixels composited at the
1126
+ * window position on black — seeds the shadow AND paints the frame in ONE
1127
+ * payload, so a resync shows content immediately instead of flashing a
1128
+ * black panel (the faceclaw reference client's resyncs are mode-6 content
1129
+ * frames and never flash). Mode 6 has no per-lens variant, so this
1130
+ * returns null unless the window is lens-equal — callers fall back to the
1131
+ * black seed there.
1132
+ */
1133
+ fun seededFramePayload(pixels: ByteArray, boxes: LensBoxes): ByteArray? {
1134
+ if (boxes.leftX != boxes.rightX || boxes.leftY != boxes.rightY) return null
1135
+ if (pixels.size != boxes.width * boxes.height) return null
1136
+ val panel = ByteArray(FRAME_WIDTH * FRAME_HEIGHT)
1137
+ for (row in 0 until boxes.height) {
1138
+ System.arraycopy(
1139
+ pixels,
1140
+ row * boxes.width,
1141
+ panel,
1142
+ (boxes.leftY + row) * FRAME_WIDTH + boxes.leftX,
1143
+ boxes.width,
1144
+ )
1145
+ }
1146
+ return encodePixels(panel)
1147
+ }
1148
+
1149
+ fun blackPayload(): ByteArray? {
1150
+ val compressed = zlib(rleEncodeSolid(0, FRAME_WIDTH * FRAME_HEIGHT)) ?: return null
1151
+ return byteArrayOf(6) + compressed
1152
+ }
1153
+
1154
+ fun encodePixels(grayscale: ByteArray): ByteArray? {
1155
+ if (grayscale.size != FRAME_WIDTH * FRAME_HEIGHT) return null
1156
+ val compressed = zlib(rleEncode(pack4Bpp(grayscale))) ?: return null
1157
+ return byteArrayOf(6) + compressed
1158
+ }
1159
+
1160
+ /**
1161
+ * Snap an 8bpp raster to the 16 panel levels (`grayNibble(v) * 16`) before
1162
+ * it enters the diff/shadow pipeline. The reference client diffs packed
1163
+ * 4bpp bytes — exactly what the panel shows; diffing raw 8bpp instead
1164
+ * marked rows dirty for sub-quantization AA noise the panel can never
1165
+ * display (spurious rects, inflated coverage, and a scroll-copy detector
1166
+ * that almost never matched). `grayNibble` round-trips `level * 16`
1167
+ * exactly, so packing a quantized raster is lossless.
1168
+ */
1169
+ fun quantizeToPanelLevels(raster: ByteArray): ByteArray =
1170
+ ByteArray(raster.size) { index -> (grayNibble(raster[index].toInt()) * 16).toByte() }
1171
+
1172
+ /**
1173
+ * 8-bit gray → 4-bit panel level, round-to-nearest (matches the reference
1174
+ * client's GRAY_TO_NIBBLE). 0 stays 0 so the shell's transparent color key
1175
+ * survives; plain truncation instead darkened the upper half of every
1176
+ * 16-value bucket by a full visible step on the 16-level panel.
1177
+ */
1178
+ fun grayNibble(value: Int): Int {
1179
+ val v = value and 0xFF
1180
+ return if (v == 0) 0 else minOf(15, (v + 8) ushr 4)
1181
+ }
1182
+
1183
+ fun pack4Bpp(grayscale: ByteArray): ByteArray =
1184
+ ByteArray((grayscale.size + 1) / 2) { index ->
1185
+ val left = grayNibble(grayscale[index * 2].toInt())
1186
+ val rightIndex = index * 2 + 1
1187
+ val right = if (rightIndex < grayscale.size) grayNibble(grayscale[rightIndex].toInt()) else 0
1188
+ ((left shl 4) or right).toByte()
1189
+ }
1190
+
1191
+ fun rleEncode(packed: ByteArray): ByteArray {
1192
+ val pixelCount = packed.size * 2
1193
+ fun nibble(index: Int): Int =
1194
+ if (index % 2 == 0) (packed[index / 2].toInt() ushr 4) and 0x0F
1195
+ else packed[index / 2].toInt() and 0x0F
1196
+
1197
+ val output = ByteArrayOutputStream()
1198
+ var index = 0
1199
+ while (index < pixelCount) {
1200
+ val color = nibble(index)
1201
+ var end = index + 1
1202
+ while (end < pixelCount && nibble(end) == color) end += 1
1203
+ appendRun(output, color, end - index)
1204
+ index = end
1205
+ }
1206
+ return output.toByteArray()
1207
+ }
1208
+
1209
+ private fun rleEncodeSolid(color: Int, pixelCount: Int): ByteArray =
1210
+ ByteArrayOutputStream().also { appendRun(it, color, pixelCount) }.toByteArray()
1211
+
1212
+ private fun appendRun(output: ByteArrayOutputStream, color: Int, count: Int) {
1213
+ var remaining = count
1214
+ while (remaining > 0) {
1215
+ val chunk = minOf(remaining, 0xFFFF)
1216
+ when {
1217
+ chunk <= 15 -> output.write((chunk shl 4) or color)
1218
+ chunk <= 255 -> {
1219
+ output.write(color)
1220
+ output.write(chunk)
1221
+ }
1222
+ else -> {
1223
+ output.write(color)
1224
+ output.write(0)
1225
+ output.write(chunk and 0xFF)
1226
+ output.write((chunk ushr 8) and 0xFF)
1227
+ }
1228
+ }
1229
+ remaining -= chunk
1230
+ }
1231
+ }
1232
+
1233
+ private fun zlib(data: ByteArray): ByteArray? =
1234
+ try {
1235
+ ByteArrayOutputStream().also { output ->
1236
+ DeflaterOutputStream(output).use { it.write(data) }
1237
+ }.toByteArray()
1238
+ } catch (_: Exception) {
1239
+ null
1240
+ }
1241
+ }