@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,1295 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ /// Serial background encoder for Glassly CFW's direct-framebuffer mode.
5
+ /// Actor isolation keeps frames ordered without blocking G2's MainActor BLE/UI work.
6
+ ///
7
+ /// Placement mirrors the stock compositor, which blits its 576×288 canvas into
8
+ /// the 640×480 panel 1:1 at a per-lens offset: `y` from the user's height level
9
+ /// and `x` from the depth level, in OPPOSITE directions per eye (that is the
10
+ /// convergence control). The outer margin is never drawn on — it exists so the
11
+ /// window can move, and the optics are only clean where the window sits.
12
+ actor G2CfwRenderer {
13
+ static let frameWidth = 640
14
+ static let frameHeight = 480
15
+
16
+ /// Level → pixel tables from firmware 2.2.6.10 (`DAT_0046c738`, `DAT_0046c730/34`).
17
+ static let heightOffsets: [Int32] = [192, 176, 160, 144, 128, 112, 96, 80, 64, 48, 32, 16, 0]
18
+ static let depthOffsetsLeft: [Int32] = [40, 32, 24]
19
+ static let depthOffsetsRight: [Int32] = [24, 32, 40]
20
+
21
+ /// Where each lens draws the canvas. Mode 3's box header quantizes x/w to
22
+ /// multiples of 4 and y/h to multiples of 2.
23
+ struct LensBoxes: Equatable {
24
+ let left: CGPoint
25
+ let right: CGPoint
26
+ let width: Int32
27
+ let height: Int32
28
+ }
29
+
30
+ static func lensBoxes(
31
+ canvasWidth: Int32, canvasHeight: Int32, heightLevel: Int, depthLevel: Int
32
+ ) -> LensBoxes {
33
+ let h = min(max(heightLevel, 0), heightOffsets.count - 1)
34
+ let d = min(max(depthLevel, 0), depthOffsetsLeft.count - 1)
35
+ let width = min(max(canvasWidth, 4), Int32(frameWidth)) & ~3
36
+ let height = min(max(canvasHeight, 2), Int32(frameHeight)) & ~1
37
+ func place(_ x: Int32, _ y: Int32) -> CGPoint {
38
+ CGPoint(
39
+ x: CGFloat(min(max(x, 0), Int32(frameWidth) - width) & ~3),
40
+ y: CGFloat(min(max(y, 0), Int32(frameHeight) - height) & ~1)
41
+ )
42
+ }
43
+ return LensBoxes(
44
+ left: place(depthOffsetsLeft[d], heightOffsets[h]),
45
+ right: place(depthOffsetsRight[d], heightOffsets[h]),
46
+ width: width, height: height
47
+ )
48
+ }
49
+
50
+ /// Line pitch for `font: "mono"` text — the contract apps size their grids by.
51
+ static func monoLineHeight(fontSize: CGFloat) -> CGFloat {
52
+ (fontSize * 1.2).rounded()
53
+ }
54
+
55
+ // MARK: - Grid-exact mono glyphs (texture-cache path)
56
+
57
+ /// Texture-cache state. Modes 12/13/14 draw at absolute panel coordinates
58
+ /// (no per-lens variant), so the caller only enables this path on frames
59
+ /// whose window is placed lens-equal. The cache model resets whenever the
60
+ /// caller's generation changes (lease cycled / residency unknown); the
61
+ /// glyph raster memo survives resets — rasters don't change.
62
+ private let textureCache = G2CfwTextureCache()
63
+ private var glyphRasterMemo: [String: Data] = [:]
64
+
65
+ /// Fixed cell metrics of the phone's monospaced face at `fontSize`.
66
+ static func monoCellMetrics(fontSize: Int32) -> (cellWidth: Int, lineHeight: Int) {
67
+ let size = CGFloat(fontSize > 0 ? fontSize : 20)
68
+ let font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
69
+ let advance = ("0" as NSString).size(withAttributes: [.font: font]).width
70
+ return (max(1, Int(advance.rounded())), max(1, Int(monoLineHeight(fontSize: size))))
71
+ }
72
+
73
+ /// One full-cell glyph raster (cellWidth × lineHeight, 8-bit gray, white
74
+ /// ink on black). This single function feeds BOTH the frame raster (grid
75
+ /// blits) and the mode-12 uploads, so an on-glasses cached draw reproduces
76
+ /// the phone raster byte-for-byte.
77
+ static func monoGlyphRaster(
78
+ _ scalar: Unicode.Scalar, fontSize: Int32, cellWidth: Int, lineHeight: Int
79
+ ) -> Data? {
80
+ let size = CGFloat(fontSize > 0 ? fontSize : 20)
81
+ let font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
82
+ var gray = Data(count: cellWidth * lineHeight)
83
+ let ok = gray.withUnsafeMutableBytes { buffer -> Bool in
84
+ guard let context = CGContext(
85
+ data: buffer.baseAddress, width: cellWidth, height: lineHeight,
86
+ bitsPerComponent: 8, bytesPerRow: cellWidth,
87
+ space: CGColorSpaceCreateDeviceGray(),
88
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
89
+ ) else { return false }
90
+ context.setFillColor(gray: 0, alpha: 1)
91
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(cellWidth), height: CGFloat(lineHeight)))
92
+ // Flip to UIKit's top-left origin so NSString.draw lands top-down
93
+ // like the frame raster.
94
+ context.translateBy(x: 0, y: CGFloat(lineHeight))
95
+ context.scaleBy(x: 1, y: -1)
96
+ UIGraphicsPushContext(context)
97
+ let yOffset = max(0, (CGFloat(lineHeight) - font.lineHeight) / 2)
98
+ (String(scalar) as NSString).draw(
99
+ at: CGPoint(x: 0, y: yOffset),
100
+ withAttributes: [.font: font, .foregroundColor: UIColor.white]
101
+ )
102
+ UIGraphicsPopContext()
103
+ return true
104
+ }
105
+ return ok ? gray : nil
106
+ }
107
+
108
+ private func memoizedGlyphRaster(
109
+ _ scalar: Unicode.Scalar, fontSize: Int32, cellWidth: Int, lineHeight: Int
110
+ ) -> Data? {
111
+ let key = "\(fontSize)|\(scalar.value)"
112
+ if let cached = glyphRasterMemo[key] { return cached }
113
+ guard let raster = Self.monoGlyphRaster(
114
+ scalar, fontSize: fontSize, cellWidth: cellWidth, lineHeight: lineHeight
115
+ ) else { return nil }
116
+ glyphRasterMemo[key] = raster
117
+ return raster
118
+ }
119
+
120
+ /// Rasterize + encode a scene as a stereo mode-3 frame at `boxes`.
121
+ func encode(_ frame: SceneFrame, boxes: LensBoxes, fid: UInt16) -> Data? {
122
+ guard let pixels = Self.rasterize(frame, width: Int(boxes.width), height: Int(boxes.height))
123
+ else { return nil }
124
+ return Self.stereoFrame(pixels: pixels, boxes: boxes, fid: fid)
125
+ }
126
+
127
+ // MARK: - Incremental updates (dirty rects / rect copy / builtin text)
128
+
129
+ /// A canvas-space rectangle aligned to mode 3's box quantization
130
+ /// (x/w multiples of 4, y/h multiples of 2).
131
+ struct CanvasRect: Equatable {
132
+ let x: Int
133
+ let y: Int
134
+ let w: Int
135
+ let h: Int
136
+ }
137
+
138
+ /// Full-width vertical band copy inside the canvas: rows
139
+ /// [srcY, srcY+height) become rows [dstY, dstY+height).
140
+ struct RowCopy: Equatable {
141
+ let srcY: Int
142
+ let dstY: Int
143
+ let height: Int
144
+ }
145
+
146
+ /// What an incremental frame needs: a full re-send, or a scroll copy plus
147
+ /// the aligned rects whose pixels still differ after it.
148
+ struct DeltaPlan: Equatable {
149
+ let full: Bool
150
+ let copy: RowCopy?
151
+ let rects: [CanvasRect]
152
+
153
+ static let noop = DeltaPlan(full: false, copy: nil, rects: [])
154
+ static let fullFrame = DeltaPlan(full: true, copy: nil, rects: [])
155
+ var isNoop: Bool { !full && copy == nil && rects.isEmpty }
156
+ }
157
+
158
+ /// One firmware-drawn (mode 15) text line at absolute panel coordinates.
159
+ /// `boxWidth` is the source element's canvas-space box width — the erase
160
+ /// bounds floor, since the phone ellipsized the line against it.
161
+ struct TextDraw: Equatable {
162
+ let x: Int
163
+ let y: Int
164
+ let text: String
165
+ let boxWidth: Int
166
+ }
167
+
168
+ /// The result of one incremental encode: the wire payload (nil when the
169
+ /// frame is visually identical to the previous one), mode-12 texture
170
+ /// uploads that must be queued AHEAD of it, the raster backing the
171
+ /// on-glass shadow after this payload applies, and whether the payload
172
+ /// re-sends the whole canvas.
173
+ struct SceneUpdate {
174
+ let payload: Data?
175
+ let uploads: [Data]
176
+ let raster: Data
177
+ let usedFullFrame: Bool
178
+ }
179
+
180
+ /// One mono text element's glyph grid, produced by the grid-exact
181
+ /// rasterizer: cell (0,0) sits at (originX, originY) in canvas coords, and
182
+ /// only the `fullCols`×`fullRows` cells wholly inside the element box (and
183
+ /// canvas) are expressible as cached draws — clipped edge cells stay baked.
184
+ struct MonoGrid {
185
+ let z: Int
186
+ let fontSize: Int32
187
+ let originX: Int
188
+ let originY: Int
189
+ let cellWidth: Int
190
+ let lineHeight: Int
191
+ let fullCols: Int
192
+ let fullRows: Int
193
+ /// Unicode scalar per cell; rows/cols beyond the text are spaces.
194
+ let rows: [[Unicode.Scalar]]
195
+
196
+ func scalar(row: Int, col: Int) -> Unicode.Scalar {
197
+ guard row < rows.count else { return " " }
198
+ let line = rows[row]
199
+ return col < line.count ? line[col] : " "
200
+ }
201
+
202
+ /// The canvas region whose every pixel belongs to a full cell.
203
+ var expressibleRegion: CanvasRect {
204
+ CanvasRect(x: originX, y: originY, w: fullCols * cellWidth, h: fullRows * lineHeight)
205
+ }
206
+
207
+ func covers(_ rect: CanvasRect) -> Bool {
208
+ let region = expressibleRegion
209
+ return rect.x >= region.x && rect.y >= region.y
210
+ && rect.x + rect.w <= region.x + region.w
211
+ && rect.y + rect.h <= region.y + region.h
212
+ }
213
+ }
214
+
215
+ /// Caps for the delta planner. maxDirtyRects bounds fids the caller must
216
+ /// pre-allocate (one per rect, plus full frame, a move blank, and a
217
+ /// right-margin blank).
218
+ static let maxDirtyRects = 6
219
+ static let requiredFidCount = maxDirtyRects + 3
220
+ /// Cap on mode-14 runs per update (mode-8's count byte is the hard bound).
221
+ static let maxGlyphRuns = 180
222
+ /// Line pitch and default draw options of the glasses' builtin 20 px font
223
+ /// chain (mode 15): lineHeight 27 / baseline 22 in firmware 2.2.6.10.
224
+ /// 0x1F = top color 15 (white) + bit 4 (source color 0 transparent).
225
+ static let builtinLineHeight = 27
226
+ static let builtinTextOptions: UInt8 = 0x1F
227
+ private static let dirtyRowMergeGap = 8
228
+ private static let fullFrameCoveragePercent = 60
229
+ private static let minScrollBandRows = 16
230
+ private static let scrollSavingsPercent = 30
231
+
232
+ /// Rasterize + plan + encode one incremental scene update.
233
+ ///
234
+ /// `previousRaster` is the canvas raster matching the on-glass shadow; nil
235
+ /// forces a full frame. `forcedRects` are treated as changed even when the
236
+ /// rasters agree (used to erase firmware-drawn text the raster never
237
+ /// contained). `textDraws` are appended after the pixel segments so glyphs
238
+ /// land on top of freshly pushed background. `blankBoxes` prepends a solid
239
+ /// black frame at a previous window position (window move) in the same
240
+ /// atomic bundle. `marginBlankBoxes` prepends a solid black frame OUTSIDE
241
+ /// the window — mode-15 draws at absolute panel coordinates without
242
+ /// clipping to the window, so stale glyph tails past the window's right
243
+ /// edge can only be erased by a frame aimed at the margin itself.
244
+ /// `omitBuiltinText` skips `font == "builtin"` text during rasterization —
245
+ /// the firmware draws those lines itself.
246
+ func encodeUpdate(
247
+ _ frame: SceneFrame, boxes: LensBoxes, previousRaster: Data?,
248
+ forcedRects: [CanvasRect], fids: [UInt16], textDraws: [TextDraw],
249
+ allowRectCopy: Bool, blankBoxes: LensBoxes?, marginBlankBoxes: LensBoxes? = nil,
250
+ omitBuiltinText: Bool,
251
+ textureGlyphsEnabled: Bool = false, textureImagesEnabled: Bool = false,
252
+ cacheGeneration: Int = 0
253
+ ) -> SceneUpdate? {
254
+ guard fids.count >= Self.requiredFidCount else { return nil }
255
+ let width = Int(boxes.width)
256
+ let height = Int(boxes.height)
257
+ // Texture draws (modes 13/14) land at absolute panel coordinates with
258
+ // no per-lens variant — only a lens-equal window may use them.
259
+ let lensEqual = boxes.left == boxes.right
260
+ let useTexGlyphs = textureGlyphsEnabled && lensEqual
261
+ let useTexImages = textureImagesEnabled && lensEqual
262
+ // A full cache resets between frames (never mid-frame: draws already
263
+ // planned this frame reference the old offsets). Bumping past the
264
+ // caller's generation keeps the caller's own bumps monotonic.
265
+ if useTexGlyphs || useTexImages, textureCache.generation != cacheGeneration || textureCache.exhausted {
266
+ textureCache.reset(generation: cacheGeneration)
267
+ }
268
+
269
+ var grids: [MonoGrid] = []
270
+ let raster: Data
271
+ if useTexGlyphs {
272
+ guard let result = rasterizeGridExact(
273
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText
274
+ ) else { return nil }
275
+ raster = Self.quantizeToPanelLevels(result.raster)
276
+ grids = result.grids
277
+ } else {
278
+ guard let flat = Self.rasterize(
279
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText
280
+ ) else { return nil }
281
+ raster = Self.quantizeToPanelLevels(flat)
282
+ }
283
+
284
+ var nextFid = 0
285
+ func takeFid() -> UInt16 {
286
+ defer { nextFid += 1 }
287
+ return fids[nextFid]
288
+ }
289
+
290
+ var segments: [Data] = []
291
+ if let blankBoxes, let blank = Self.solidStereoFrame(color: 0, boxes: blankBoxes, fid: takeFid()) {
292
+ segments.append(blank)
293
+ }
294
+ if let marginBlankBoxes,
295
+ let blank = Self.solidStereoFrame(color: 0, boxes: marginBlankBoxes, fid: takeFid())
296
+ {
297
+ segments.append(blank)
298
+ }
299
+
300
+ let plan: DeltaPlan
301
+ if let previousRaster, previousRaster.count == raster.count, blankBoxes == nil {
302
+ plan = Self.diffPlan(
303
+ previous: previousRaster, next: raster, width: width, height: height,
304
+ forced: forcedRects, allowCopy: allowRectCopy
305
+ )
306
+ } else {
307
+ plan = .fullFrame
308
+ }
309
+
310
+ if plan.isNoop {
311
+ let uploads = textureCache.takeUploadPayloads()
312
+ return SceneUpdate(payload: nil, uploads: uploads, raster: raster, usedFullFrame: false)
313
+ }
314
+
315
+ if plan.full {
316
+ guard let full = Self.stereoFrame(pixels: raster, boxes: boxes, fid: takeFid())
317
+ else { return nil }
318
+ segments.append(full)
319
+ } else {
320
+ if let copy = plan.copy {
321
+ segments.append(Self.rectCopyPayload(copy: copy, width: width, boxes: boxes))
322
+ }
323
+ var deltaRects = plan.rects
324
+ var drawSegments: [Data] = []
325
+ if useTexGlyphs || useTexImages {
326
+ // Reserve one fid for the bundle-overflow fallback below.
327
+ let fidBudget = Self.requiredFidCount - nextFid - 1
328
+ let expressed = expressTextureRects(
329
+ plan.rects, grids: useTexGlyphs ? grids : [], frame: frame,
330
+ boxes: boxes, raster: raster, width: width, height: height,
331
+ allowImages: useTexImages, fidBudget: fidBudget
332
+ )
333
+ deltaRects = expressed.deltas
334
+ drawSegments = expressed.draws
335
+ }
336
+ for rect in deltaRects {
337
+ guard let delta = Self.boxDeltaFrame(
338
+ raster: raster, canvasWidth: width, rect: rect, boxes: boxes, fid: takeFid()
339
+ ) else { return nil }
340
+ segments.append(delta)
341
+ }
342
+ segments.append(contentsOf: drawSegments)
343
+ }
344
+
345
+ for draw in textDraws {
346
+ if let text = Self.builtinStringPayload(
347
+ x: draw.x, y: draw.y, options: Self.builtinTextOptions, text: draw.text
348
+ ) {
349
+ segments.append(text)
350
+ }
351
+ }
352
+
353
+ let uploads = textureCache.takeUploadPayloads()
354
+ if segments.count == 1 {
355
+ return SceneUpdate(payload: segments[0], uploads: uploads, raster: raster, usedFullFrame: plan.full)
356
+ }
357
+ if let bundled = Self.bundle(segments) {
358
+ return SceneUpdate(payload: bundled, uploads: uploads, raster: raster, usedFullFrame: plan.full)
359
+ }
360
+ // A segment overflowed the bundle's 64 KiB cap (a near-incompressible
361
+ // full canvas). Fall back to one bare full frame with any builtin text
362
+ // baked into the bitmap so nothing is lost. The uploads still go out —
363
+ // residency was already recorded in the model.
364
+ guard let flat = Self.rasterize(frame, width: width, height: height, omitBuiltinText: false),
365
+ let full = Self.stereoFrame(pixels: flat, boxes: boxes, fid: takeFid())
366
+ else { return nil }
367
+ return SceneUpdate(payload: full, uploads: uploads, raster: flat, usedFullFrame: true)
368
+ }
369
+
370
+ /// Blit `font: "mono"` elements as opaque glyph grids: the element's box
371
+ /// is cleared to black and every non-space cell blits the SAME full-cell
372
+ /// raster mode-14 draws replay, so an on-glasses cached draw reproduces
373
+ /// the raster exactly. (Opaque cells are also what makes cached draws
374
+ /// self-erasing.) Mono grids paint after all other elements — an exotic
375
+ /// frame stacking a non-mono element ON TOP of a mono grid would z-order
376
+ /// differently here, which no current miniapp does.
377
+ private func rasterizeGridExact(
378
+ _ frame: SceneFrame, width: Int, height: Int, omitBuiltinText: Bool
379
+ ) -> (raster: Data, grids: [MonoGrid])? {
380
+ guard var raster = Self.rasterize(
381
+ frame, width: width, height: height, omitBuiltinText: omitBuiltinText, omitMonoText: true
382
+ ) else { return nil }
383
+
384
+ var grids: [MonoGrid] = []
385
+ for (z, element) in frame.elements.enumerated()
386
+ where element.type == "text" && element.font == "mono"
387
+ {
388
+ let bx = Int(element.x)
389
+ let by = Int(element.y)
390
+ let clipX0 = max(0, bx)
391
+ let clipY0 = max(0, by)
392
+ let clipX1 = min(width, bx + Int(element.w))
393
+ let clipY1 = min(height, by + Int(element.h))
394
+ guard clipX0 < clipX1, clipY0 < clipY1 else { continue }
395
+ let metrics = Self.monoCellMetrics(fontSize: element.fontSize)
396
+ let originX = bx + 2
397
+ let originY = by + 2
398
+ raster.withUnsafeMutableBytes { buf in
399
+ let base = buf.bindMemory(to: UInt8.self).baseAddress!
400
+ for y in clipY0 ..< clipY1 {
401
+ memset(base + y * width + clipX0, 0, clipX1 - clipX0)
402
+ }
403
+ }
404
+ let lines: [[Unicode.Scalar]] = (element.text ?? "")
405
+ .split(separator: "\n", omittingEmptySubsequences: false)
406
+ .map { Array($0.unicodeScalars) }
407
+ let blitCols = max(0, (clipX1 - originX + metrics.cellWidth - 1) / metrics.cellWidth)
408
+ let blitRows = max(0, (clipY1 - originY + metrics.lineHeight - 1) / metrics.lineHeight)
409
+ for row in 0 ..< min(blitRows, lines.count) {
410
+ let line = lines[row]
411
+ for col in 0 ..< min(blitCols, line.count) where line[col].value != 32 {
412
+ guard let glyph = memoizedGlyphRaster(
413
+ line[col], fontSize: element.fontSize,
414
+ cellWidth: metrics.cellWidth, lineHeight: metrics.lineHeight
415
+ ) else { continue }
416
+ Self.blitCell(
417
+ glyph, into: &raster, rasterWidth: width,
418
+ atX: originX + col * metrics.cellWidth, atY: originY + row * metrics.lineHeight,
419
+ cellWidth: metrics.cellWidth, cellHeight: metrics.lineHeight,
420
+ clipX0: clipX0, clipY0: clipY0, clipX1: clipX1, clipY1: clipY1
421
+ )
422
+ }
423
+ }
424
+ grids.append(MonoGrid(
425
+ z: z, fontSize: element.fontSize, originX: originX, originY: originY,
426
+ cellWidth: metrics.cellWidth, lineHeight: metrics.lineHeight,
427
+ fullCols: max(0, (clipX1 - originX) / metrics.cellWidth),
428
+ fullRows: max(0, (clipY1 - originY) / metrics.lineHeight),
429
+ rows: lines
430
+ ))
431
+ }
432
+ return (raster, grids)
433
+ }
434
+
435
+ private static func blitCell(
436
+ _ glyph: Data, into raster: inout Data, rasterWidth: Int,
437
+ atX: Int, atY: Int, cellWidth: Int, cellHeight: Int,
438
+ clipX0: Int, clipY0: Int, clipX1: Int, clipY1: Int
439
+ ) {
440
+ let x0 = max(atX, clipX0)
441
+ let y0 = max(atY, clipY0)
442
+ let x1 = min(atX + cellWidth, clipX1)
443
+ let y1 = min(atY + cellHeight, clipY1)
444
+ guard x0 < x1, y0 < y1 else { return }
445
+ raster.withUnsafeMutableBytes { dstRaw in
446
+ glyph.withUnsafeBytes { srcRaw in
447
+ let dst = dstRaw.bindMemory(to: UInt8.self).baseAddress!
448
+ let src = srcRaw.bindMemory(to: UInt8.self).baseAddress!
449
+ for y in y0 ..< y1 {
450
+ memcpy(
451
+ dst + y * rasterWidth + x0,
452
+ src + (y - atY) * cellWidth + (x0 - atX),
453
+ x1 - x0
454
+ )
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ /// Express-or-bake: each dirty rect either becomes cached draws (mode-14
461
+ /// glyph runs from the topmost covering mono grid, or one mode-13 draw of
462
+ /// an isolated repeated image) and drops its bitmap delta, or stays a
463
+ /// plain delta untouched. All-or-nothing per rect keeps the shadow model
464
+ /// trivially exact — no partial punch-out. Non-ASCII cells (mode 14 is
465
+ /// 32..126 only) become small residual bitmap deltas within an otherwise
466
+ /// expressed rect.
467
+ private func expressTextureRects(
468
+ _ rects: [CanvasRect], grids: [MonoGrid], frame: SceneFrame,
469
+ boxes: LensBoxes, raster: Data, width: Int, height: Int,
470
+ allowImages: Bool, fidBudget: Int
471
+ ) -> (deltas: [CanvasRect], draws: [Data]) {
472
+ struct ElementBox {
473
+ let z: Int
474
+ let x0: Int
475
+ let y0: Int
476
+ let x1: Int
477
+ let y1: Int
478
+ func intersects(_ r: CanvasRect) -> Bool {
479
+ r.x < x1 && r.x + r.w > x0 && r.y < y1 && r.y + r.h > y0
480
+ }
481
+ }
482
+ let elementBoxes: [ElementBox] = frame.elements.enumerated().map { z, el in
483
+ ElementBox(
484
+ z: z, x0: max(0, Int(el.x)), y0: max(0, Int(el.y)),
485
+ x1: min(width, Int(el.x) + Int(el.w)), y1: min(height, Int(el.y) + Int(el.h))
486
+ )
487
+ }
488
+
489
+ // Isolated image candidates: nothing else overlaps their box, so the
490
+ // frame raster inside the box IS the standalone render and an opaque
491
+ // full-box cached draw reproduces it. Sightings are recorded every
492
+ // frame; only content seen before enters the cache (streaming content
493
+ // like a live minimap never repeats a hash).
494
+ struct ImageCandidate {
495
+ let box: ElementBox
496
+ let key: String
497
+ let seen: Bool
498
+ }
499
+ var imageCandidates: [Int: ImageCandidate] = [:]
500
+ if allowImages {
501
+ for (z, el) in frame.elements.enumerated() where el.type == "image" {
502
+ let box = elementBoxes[z]
503
+ let w = box.x1 - box.x0
504
+ let h = box.y1 - box.y0
505
+ guard w > 0, h > 0, w <= 255, h <= 255, !el.contentHash.isEmpty else { continue }
506
+ let isolated = !elementBoxes.contains { other in
507
+ other.z != z && other.x0 < box.x1 && other.x1 > box.x0
508
+ && other.y0 < box.y1 && other.y1 > box.y0
509
+ }
510
+ guard isolated else { continue }
511
+ let key = "\(el.contentHash)@\(w)x\(h)"
512
+ imageCandidates[z] = ImageCandidate(
513
+ box: box, key: key, seen: textureCache.noteImageSighting(key)
514
+ )
515
+ }
516
+ }
517
+
518
+ var deltas: [CanvasRect] = []
519
+ var imageDraws: [Int: Data] = [:]
520
+ var runPayloads: [Data] = []
521
+ var runCount = 0
522
+
523
+ rectLoop: for (index, rect) in rects.enumerated() {
524
+ let remaining = rects.count - index - 1
525
+
526
+ // Glyph express: topmost mono grid fully covering the rect, with
527
+ // nothing stacked above it in that region.
528
+ if let grid = grids.filter({ $0.covers(rect) }).max(by: { $0.z < $1.z }),
529
+ !elementBoxes.contains(where: { $0.z > grid.z && $0.intersects(rect) }),
530
+ let font = textureCache.font(
531
+ fontSize: grid.fontSize, cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
532
+ )
533
+ {
534
+ let r0 = (rect.y - grid.originY) / grid.lineHeight
535
+ let r1 = (rect.y + rect.h - 1 - grid.originY) / grid.lineHeight
536
+ let c0 = (rect.x - grid.originX) / grid.cellWidth
537
+ let c1 = (rect.x + rect.w - 1 - grid.originX) / grid.cellWidth
538
+ var newRuns: [(x: Int, y: Int, chars: [UInt8])] = []
539
+ var residuals: [CanvasRect] = []
540
+ var expressible = true
541
+ for row in r0 ... r1 {
542
+ let rowY = grid.originY + row * grid.lineHeight
543
+ var runChars: [UInt8] = []
544
+ var runStartCol = c0
545
+ var residualStart: Int?
546
+ func flushRun(endCol: Int) {
547
+ guard !runChars.isEmpty else { return }
548
+ newRuns.append((
549
+ x: grid.originX + runStartCol * grid.cellWidth, y: rowY, chars: runChars
550
+ ))
551
+ runChars = []
552
+ _ = endCol
553
+ }
554
+ func flushResidual(endCol: Int) {
555
+ guard let start = residualStart else { return }
556
+ let x0 = max(0, (grid.originX + start * grid.cellWidth) & ~3)
557
+ let x1 = min(width, (grid.originX + endCol * grid.cellWidth + 3) & ~3)
558
+ let y0 = max(0, rowY & ~1)
559
+ let y1 = min(height, (rowY + grid.lineHeight + 1) & ~1)
560
+ residuals.append(CanvasRect(x: x0, y: y0, w: x1 - x0, h: y1 - y0))
561
+ residualStart = nil
562
+ }
563
+ for col in c0 ... c1 {
564
+ let scalar = grid.scalar(row: row, col: col)
565
+ if scalar.value >= 32, scalar.value <= 126,
566
+ textureCache.ensureGlyph(font: font, char: UInt8(scalar.value), rasterize: {
567
+ self.memoizedGlyphRaster(
568
+ scalar, fontSize: grid.fontSize,
569
+ cellWidth: grid.cellWidth, lineHeight: grid.lineHeight
570
+ )
571
+ })
572
+ {
573
+ flushResidual(endCol: col)
574
+ if runChars.isEmpty { runStartCol = col }
575
+ runChars.append(UInt8(scalar.value))
576
+ if runChars.count == 255 { flushRun(endCol: col + 1) }
577
+ } else {
578
+ flushRun(endCol: col)
579
+ if residualStart == nil { residualStart = col }
580
+ }
581
+ }
582
+ flushRun(endCol: c1 + 1)
583
+ flushResidual(endCol: c1 + 1)
584
+ if runCount + newRuns.count > Self.maxGlyphRuns { expressible = false; break }
585
+ }
586
+ // Residual deltas + every remaining rect must still fit the
587
+ // fid budget; otherwise keep this rect as one plain delta.
588
+ if expressible, deltas.count + residuals.count + remaining + 1 <= fidBudget,
589
+ residuals.count <= 3
590
+ {
591
+ var payloads: [Data] = []
592
+ for run in newRuns {
593
+ guard let payload = G2CfwTextureCache.stringRunPayload(
594
+ fontTableOffset: font.tableOffset,
595
+ x: Int(boxes.left.x) + run.x, y: Int(boxes.left.y) + run.y,
596
+ options: G2CfwTextureCache.opaqueDrawOptions, chars: run.chars
597
+ ) else { payloads = []; break }
598
+ payloads.append(payload)
599
+ }
600
+ if !payloads.isEmpty || newRuns.isEmpty {
601
+ runPayloads.append(contentsOf: payloads)
602
+ runCount += payloads.count
603
+ deltas.append(contentsOf: residuals)
604
+ continue rectLoop
605
+ }
606
+ }
607
+ }
608
+
609
+ // Image express: the rect sits wholly inside an isolated, repeated
610
+ // image's box — one opaque full-box draw covers it (and any other
611
+ // rect in the same box).
612
+ if allowImages,
613
+ let (z, candidate) = imageCandidates.first(where: { _, c in
614
+ c.seen && rect.x >= c.box.x0 && rect.y >= c.box.y0
615
+ && rect.x + rect.w <= c.box.x1 && rect.y + rect.h <= c.box.y1
616
+ })
617
+ {
618
+ if imageDraws[z] != nil { continue rectLoop }
619
+ let w = candidate.box.x1 - candidate.box.x0
620
+ let h = candidate.box.y1 - candidate.box.y0
621
+ var gray = Data(capacity: w * h)
622
+ raster.withUnsafeBytes { raw in
623
+ let src = raw.bindMemory(to: UInt8.self)
624
+ for y in candidate.box.y0 ..< candidate.box.y1 {
625
+ gray.append(contentsOf: UnsafeBufferPointer(
626
+ start: src.baseAddress! + y * width + candidate.box.x0, count: w
627
+ ))
628
+ }
629
+ }
630
+ if let offset = textureCache.ensureImage(hash: candidate.key, gray: gray, width: w, height: h),
631
+ let draw = G2CfwTextureCache.imageDrawPayload(
632
+ offset: offset,
633
+ x: Int(boxes.left.x) + candidate.box.x0, y: Int(boxes.left.y) + candidate.box.y0,
634
+ options: G2CfwTextureCache.opaqueDrawOptions
635
+ )
636
+ {
637
+ imageDraws[z] = draw
638
+ continue rectLoop
639
+ }
640
+ }
641
+
642
+ deltas.append(rect)
643
+ }
644
+
645
+ let draws = imageDraws.sorted { $0.key < $1.key }.map(\.value) + runPayloads
646
+ return (deltas, draws)
647
+ }
648
+
649
+ /// Diff two same-size canvas rasters into a scroll copy + aligned dirty
650
+ /// rects, or a full-frame decision when the change is too large to be
651
+ /// worth splitting.
652
+ static func diffPlan(
653
+ previous: Data, next: Data, width: Int, height: Int,
654
+ forced: [CanvasRect], allowCopy: Bool
655
+ ) -> DeltaPlan {
656
+ guard previous.count == width * height, next.count == width * height,
657
+ width > 0, height > 0
658
+ else { return .fullFrame }
659
+
660
+ return previous.withUnsafeBytes { prevRaw in
661
+ next.withUnsafeBytes { nextRaw in
662
+ let prev = prevRaw.bindMemory(to: UInt8.self)
663
+ let new = nextRaw.bindMemory(to: UInt8.self)
664
+
665
+ var changed = [Bool](repeating: false, count: height)
666
+ var anyChanged = false
667
+ for y in 0 ..< height {
668
+ let row = y * width
669
+ if memcmp(prev.baseAddress! + row, new.baseAddress! + row, width) != 0 {
670
+ changed[y] = true
671
+ anyChanged = true
672
+ }
673
+ }
674
+ if !anyChanged, forced.isEmpty { return .noop }
675
+
676
+ // Scroll detection: a vertical band of new rows that byte-match
677
+ // previous rows shifted by dy. Row hashes score candidate
678
+ // shifts; the winning band is byte-verified before use.
679
+ var copy: RowCopy?
680
+ var predicted: [UInt8]?
681
+ if allowCopy, anyChanged {
682
+ var prevHash = [UInt64](repeating: 0, count: height)
683
+ var newHash = [UInt64](repeating: 0, count: height)
684
+ for y in 0 ..< height {
685
+ prevHash[y] = rowHash(prev, offset: y * width, count: width)
686
+ newHash[y] = rowHash(new, offset: y * width, count: width)
687
+ }
688
+ let changedCount = changed.lazy.filter { $0 }.count
689
+ var bestDy = 0
690
+ var bestScore = 0
691
+ let maxShift = height - minScrollBandRows
692
+ if maxShift > 0 {
693
+ for dy in -maxShift ... maxShift where dy != 0 {
694
+ var score = 0
695
+ let lo = max(0, -dy)
696
+ let hi = min(height, height - dy)
697
+ for y in lo ..< hi where changed[y] && newHash[y] == prevHash[y + dy] {
698
+ score += 1
699
+ }
700
+ if score > bestScore {
701
+ bestScore = score
702
+ bestDy = dy
703
+ }
704
+ }
705
+ }
706
+ if bestDy != 0, bestScore >= minScrollBandRows,
707
+ bestScore * 100 >= changedCount * scrollSavingsPercent
708
+ {
709
+ // Longest contiguous hash-matching run, then byte-verify.
710
+ var runStart = -1
711
+ var bandStart = 0
712
+ var bandLen = 0
713
+ let lo = max(0, -bestDy)
714
+ let hi = min(height, height - bestDy)
715
+ for y in lo ... hi {
716
+ let matches = y < hi && newHash[y] == prevHash[y + bestDy]
717
+ if matches {
718
+ if runStart < 0 { runStart = y }
719
+ } else if runStart >= 0 {
720
+ if y - runStart > bandLen {
721
+ bandStart = runStart
722
+ bandLen = y - runStart
723
+ }
724
+ runStart = -1
725
+ }
726
+ }
727
+ if bandLen >= minScrollBandRows {
728
+ var verified = true
729
+ for y in bandStart ..< bandStart + bandLen where verified {
730
+ verified = memcmp(
731
+ new.baseAddress! + y * width,
732
+ prev.baseAddress! + (y + bestDy) * width, width
733
+ ) == 0
734
+ }
735
+ if verified {
736
+ copy = RowCopy(srcY: bandStart + bestDy, dstY: bandStart, height: bandLen)
737
+ var scratch = [UInt8](repeating: 0, count: width * height)
738
+ scratch.withUnsafeMutableBufferPointer { buf in
739
+ memcpy(buf.baseAddress!, prev.baseAddress!, width * height)
740
+ memmove(
741
+ buf.baseAddress! + bandStart * width,
742
+ prev.baseAddress! + (bandStart + bestDy) * width,
743
+ bandLen * width
744
+ )
745
+ }
746
+ predicted = scratch
747
+ for y in bandStart ..< bandStart + bandLen {
748
+ changed[y] = false
749
+ }
750
+ anyChanged = changed.contains(true)
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ // Per-row dirty column ranges against the (possibly copied) base.
757
+ var colLo = [Int](repeating: Int.max, count: height)
758
+ var colHi = [Int](repeating: -1, count: height)
759
+ func base(_ y: Int, _ x: Int) -> UInt8 {
760
+ if let predicted { return predicted[y * width + x] }
761
+ return prev[y * width + x]
762
+ }
763
+ for y in 0 ..< height where changed[y] {
764
+ var lo = 0
765
+ while lo < width, base(y, lo) == new[y * width + lo] { lo += 1 }
766
+ var hi = width - 1
767
+ while hi > lo, base(y, hi) == new[y * width + hi] { hi -= 1 }
768
+ colLo[y] = lo
769
+ colHi[y] = hi
770
+ }
771
+ var dirtyRows = changed
772
+ for rect in forced {
773
+ let y0 = max(0, rect.y)
774
+ let y1 = min(height, rect.y + rect.h)
775
+ guard y0 < y1, rect.w > 0 else { continue }
776
+ for y in y0 ..< y1 {
777
+ dirtyRows[y] = true
778
+ colLo[y] = min(colLo[y], max(0, rect.x))
779
+ colHi[y] = max(colHi[y], min(width - 1, rect.x + rect.w - 1))
780
+ }
781
+ }
782
+ guard dirtyRows.contains(true) else {
783
+ if let copy { return DeltaPlan(full: false, copy: copy, rects: []) }
784
+ return .noop
785
+ }
786
+
787
+ // Group dirty rows into bands (small gaps merge), then align
788
+ // each band to the mode-3 quantization grid.
789
+ var rects: [CanvasRect] = []
790
+ var y = 0
791
+ while y < height {
792
+ guard dirtyRows[y] else {
793
+ y += 1
794
+ continue
795
+ }
796
+ var end = y
797
+ var gap = 0
798
+ var scan = y
799
+ while scan < height, gap <= dirtyRowMergeGap {
800
+ if dirtyRows[scan] {
801
+ end = scan
802
+ gap = 0
803
+ } else {
804
+ gap += 1
805
+ }
806
+ scan += 1
807
+ }
808
+ var lo = Int.max
809
+ var hi = -1
810
+ for row in y ... end where dirtyRows[row] {
811
+ lo = min(lo, colLo[row])
812
+ hi = max(hi, colHi[row])
813
+ }
814
+ let x0 = max(0, lo & ~3)
815
+ let x1 = min(width, (hi + 4) & ~3)
816
+ let ry0 = max(0, y & ~1)
817
+ let ry1 = min(height, (end + 2) & ~1)
818
+ rects.append(CanvasRect(x: x0, y: ry0, w: x1 - x0, h: ry1 - ry0))
819
+ y = end + 1
820
+ }
821
+ if rects.count > maxDirtyRects {
822
+ var x0 = Int.max
823
+ var y0 = Int.max
824
+ var x1 = 0
825
+ var y1 = 0
826
+ for rect in rects {
827
+ x0 = min(x0, rect.x)
828
+ y0 = min(y0, rect.y)
829
+ x1 = max(x1, rect.x + rect.w)
830
+ y1 = max(y1, rect.y + rect.h)
831
+ }
832
+ rects = [CanvasRect(x: x0, y: y0, w: x1 - x0, h: y1 - y0)]
833
+ }
834
+ let coverage = rects.reduce(0) { $0 + $1.w * $1.h }
835
+ if copy == nil, coverage * 100 >= width * height * fullFrameCoveragePercent {
836
+ return .fullFrame
837
+ }
838
+ return DeltaPlan(full: false, copy: copy, rects: rects)
839
+ }
840
+ }
841
+ }
842
+
843
+ private static func rowHash(_ pixels: UnsafeBufferPointer<UInt8>, offset: Int, count: Int) -> UInt64 {
844
+ // FNV-1a 64 — collision-safe enough for scoring; bands are byte-verified.
845
+ var hash: UInt64 = 0xCBF2_9CE4_8422_2325
846
+ for i in offset ..< offset + count {
847
+ hash = (hash ^ UInt64(pixels[i])) &* 0x0000_0100_0000_01B3
848
+ }
849
+ return hash
850
+ }
851
+
852
+ /// Stereo mode-3 delta of one canvas sub-rect, drawn at each lens's window
853
+ /// origin plus the rect offset. Rect must be grid-aligned and in bounds.
854
+ static func boxDeltaFrame(
855
+ raster: Data, canvasWidth: Int, rect: CanvasRect, boxes: LensBoxes, fid: UInt16
856
+ ) -> Data? {
857
+ guard rect.w > 0, rect.h > 0, rect.x % 4 == 0, rect.w % 4 == 0,
858
+ rect.y % 2 == 0, rect.h % 2 == 0,
859
+ rect.x + rect.w <= canvasWidth,
860
+ (rect.y + rect.h) * canvasWidth <= raster.count
861
+ else { return nil }
862
+ var pixels = Data(capacity: rect.w * rect.h)
863
+ raster.withUnsafeBytes { raw in
864
+ let src = raw.bindMemory(to: UInt8.self)
865
+ for y in rect.y ..< rect.y + rect.h {
866
+ pixels.append(contentsOf: UnsafeBufferPointer(
867
+ start: src.baseAddress! + y * canvasWidth + rect.x, count: rect.w
868
+ ))
869
+ }
870
+ }
871
+ let rle = rleEncode(pack4Bpp(pixels))
872
+ return stereoRleFrame(
873
+ rle: rle,
874
+ lx: Int(boxes.left.x) + rect.x, ly: Int(boxes.left.y) + rect.y,
875
+ rx: Int(boxes.right.x) + rect.x, ry: Int(boxes.right.y) + rect.y,
876
+ w: Int32(rect.w), h: Int32(rect.h), fid: fid
877
+ )
878
+ }
879
+
880
+ /// Mode-9 rect copy with the lenses-differ flag: a full-width canvas band
881
+ /// moves srcY→dstY inside each lens's window.
882
+ /// `[0x89][L src LTWH dst LTWH][R src LTWH dst LTWH]`, u16 LE each.
883
+ static func rectCopyPayload(copy: RowCopy, width: Int, boxes: LensBoxes) -> Data {
884
+ var payload = Data([0x89])
885
+ func appendRect(_ x: Int, _ y: Int) {
886
+ for value in [x, y, width, copy.height] {
887
+ payload.append(UInt8(value & 0xFF))
888
+ payload.append(UInt8((value >> 8) & 0xFF))
889
+ }
890
+ }
891
+ appendRect(Int(boxes.left.x), Int(boxes.left.y) + copy.srcY)
892
+ appendRect(Int(boxes.left.x), Int(boxes.left.y) + copy.dstY)
893
+ appendRect(Int(boxes.right.x), Int(boxes.right.y) + copy.srcY)
894
+ appendRect(Int(boxes.right.x), Int(boxes.right.y) + copy.dstY)
895
+ return payload
896
+ }
897
+
898
+ /// Mode-15 builtin-font string draw:
899
+ /// `[15][x:u16][y:u16][options:u8][strlen:u8][UTF-8]`. Bytes 1–31 are
900
+ /// firmware x-adjust controls, so control characters are stripped here;
901
+ /// the text is truncated to 255 UTF-8 bytes at a codepoint boundary.
902
+ static func builtinStringPayload(x: Int, y: Int, options: UInt8, text: String) -> Data? {
903
+ guard x >= 0, x <= 0xFFFF, y >= 0, y <= 0xFFFF else { return nil }
904
+ let sanitized = String(text.unicodeScalars.filter { $0.value >= 32 })
905
+ var bytes = Array(sanitized.utf8)
906
+ if bytes.count > 255 {
907
+ var cut = 255
908
+ while cut > 0, bytes[cut] & 0xC0 == 0x80 { cut -= 1 }
909
+ bytes = Array(bytes[..<cut])
910
+ }
911
+ guard !bytes.isEmpty else { return nil }
912
+ var payload = Data([15])
913
+ payload.append(UInt8(x & 0xFF))
914
+ payload.append(UInt8((x >> 8) & 0xFF))
915
+ payload.append(UInt8(y & 0xFF))
916
+ payload.append(UInt8((y >> 8) & 0xFF))
917
+ payload.append(options)
918
+ payload.append(UInt8(bytes.count))
919
+ payload.append(contentsOf: bytes)
920
+ return payload
921
+ }
922
+
923
+ /// The firmware-drawn text lines of a frame, at absolute panel
924
+ /// coordinates. Mode 15 has no per-lens variant, so callers must place the
925
+ /// window at lens-equal offsets (depth level 1) whenever these are used.
926
+ /// The +2 inset matches the rasterizer's text padding.
927
+ static func builtinTextDraws(_ frame: SceneFrame, boxes: LensBoxes) -> [TextDraw] {
928
+ var draws: [TextDraw] = []
929
+ for element in frame.elements where element.type == "text" && element.font == "builtin" {
930
+ guard let text = element.text, !text.isEmpty else { continue }
931
+ let baseX = Int(boxes.left.x) + Int(element.x) + 2
932
+ let baseY = Int(boxes.left.y) + Int(element.y) + 2
933
+ for (index, line) in text.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
934
+ let y = baseY + index * builtinLineHeight
935
+ guard y >= 0, y + builtinLineHeight <= frameHeight, !line.isEmpty else { continue }
936
+ draws.append(TextDraw(x: baseX, y: y, text: String(line), boxWidth: Int(element.w)))
937
+ }
938
+ }
939
+ return draws
940
+ }
941
+
942
+ /// Conservative canvas-space bounds of firmware-drawn text, used to force
943
+ /// background re-pushes that erase glyphs the phone raster never
944
+ /// contained. The phone has no advance tables, so width is the char-count
945
+ /// estimate floored at the element's box width — wide glyphs overshoot
946
+ /// 13px/char, and an undershot erase leaves glyph tails on the panel.
947
+ static func builtinTextBounds(
948
+ _ draws: [TextDraw], boxes: LensBoxes, canvasWidth: Int, canvasHeight: Int
949
+ ) -> [CanvasRect] {
950
+ draws.compactMap { draw in
951
+ let x = max(0, draw.x - Int(boxes.left.x) - 4)
952
+ let y = max(0, draw.y - Int(boxes.left.y) - 2)
953
+ guard x < canvasWidth, y < canvasHeight else { return nil }
954
+ let width = min(canvasWidth - x, max(draw.text.count * 13 + 30, draw.boxWidth + 8))
955
+ let height = min(canvasHeight - y, builtinLineHeight + 4)
956
+ return CanvasRect(x: x, y: y, w: width, h: height)
957
+ }
958
+ }
959
+
960
+ /// Rasterize a scene into a width×height 8-bit grayscale buffer (row-major,
961
+ /// one byte per pixel), or nil when an image element fails to decode.
962
+ /// `omitBuiltinText` skips `font == "builtin"` text elements — the CFW
963
+ /// draws those with mode 15, so the bitmap must not also contain them.
964
+ /// `omitMonoText` skips `font == "mono"` text elements — the grid-exact
965
+ /// texture path blits those from the same glyph rasters it uploads.
966
+ static func rasterize(
967
+ _ frame: SceneFrame, width: Int, height: Int, omitBuiltinText: Bool = false,
968
+ omitMonoText: Bool = false
969
+ ) -> Data? {
970
+ var decodedImages: [Int: UIImage] = [:]
971
+ for (index, element) in frame.elements.enumerated() where element.type == "image" {
972
+ guard let encoded = element.data,
973
+ let data = Data(base64Encoded: encoded),
974
+ let image = UIImage(data: data)
975
+ else { return nil }
976
+ decodedImages[index] = image
977
+ }
978
+
979
+ let format = UIGraphicsImageRendererFormat()
980
+ format.scale = 1
981
+ format.opaque = true
982
+ let renderer = UIGraphicsImageRenderer(
983
+ size: CGSize(width: CGFloat(width), height: CGFloat(height)),
984
+ format: format
985
+ )
986
+ let image = renderer.image { rendererContext in
987
+ let context = rendererContext.cgContext
988
+ context.setFillColor(UIColor.black.cgColor)
989
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
990
+
991
+ context.saveGState()
992
+ for (index, element) in frame.elements.enumerated() {
993
+ // Mode 15 draws only the glyphs, so an omitted builtin-text
994
+ // element's border must still come from the raster.
995
+ let skipBuiltinInk = omitBuiltinText && element.type == "text"
996
+ && element.font == "builtin"
997
+ if skipBuiltinInk, element.border <= 0 {
998
+ continue
999
+ }
1000
+ if omitMonoText, element.type == "text", element.font == "mono" {
1001
+ continue
1002
+ }
1003
+ let rect = CGRect(
1004
+ x: CGFloat(element.x), y: CGFloat(element.y),
1005
+ width: CGFloat(element.w), height: CGFloat(element.h)
1006
+ )
1007
+ context.saveGState()
1008
+ context.clip(to: rect)
1009
+
1010
+ switch element.type {
1011
+ case "text" where skipBuiltinInk:
1012
+ break
1013
+
1014
+ case "text":
1015
+ let paragraph = NSMutableParagraphStyle()
1016
+ let font: UIFont
1017
+ if element.font == "mono" {
1018
+ // Grid text: fixed pitch, fixed line height, no re-wrap —
1019
+ // the app laid out cols × rows against these metrics.
1020
+ let size = CGFloat(element.fontSize > 0 ? element.fontSize : 20)
1021
+ font = UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
1022
+ paragraph.lineBreakMode = .byClipping
1023
+ paragraph.minimumLineHeight = Self.monoLineHeight(fontSize: size)
1024
+ paragraph.maximumLineHeight = paragraph.minimumLineHeight
1025
+ } else {
1026
+ font = UIFont.systemFont(ofSize: 20)
1027
+ paragraph.lineBreakMode = .byWordWrapping
1028
+ }
1029
+ let attributes: [NSAttributedString.Key: Any] = [
1030
+ .font: font,
1031
+ .foregroundColor: UIColor.white,
1032
+ .paragraphStyle: paragraph,
1033
+ ]
1034
+ ((element.text ?? "") as NSString).draw(
1035
+ in: rect.insetBy(dx: 2, dy: 2), withAttributes: attributes
1036
+ )
1037
+
1038
+ case "image":
1039
+ if let source = decodedImages[index] {
1040
+ let sourceSize = source.size
1041
+ let imageScale = min(
1042
+ rect.width / max(sourceSize.width, 1),
1043
+ rect.height / max(sourceSize.height, 1)
1044
+ )
1045
+ let target = CGSize(
1046
+ width: sourceSize.width * imageScale,
1047
+ height: sourceSize.height * imageScale
1048
+ )
1049
+ source.draw(
1050
+ in: CGRect(
1051
+ x: rect.midX - target.width / 2,
1052
+ y: rect.midY - target.height / 2,
1053
+ width: target.width, height: target.height
1054
+ )
1055
+ )
1056
+ }
1057
+
1058
+ case "rect":
1059
+ break
1060
+
1061
+ default:
1062
+ break
1063
+ }
1064
+
1065
+ let logicalBorder = max(element.type == "rect" ? Int32(1) : 0, element.border)
1066
+ if logicalBorder > 0 {
1067
+ context.setStrokeColor(UIColor.white.cgColor)
1068
+ context.setLineWidth(CGFloat(logicalBorder))
1069
+ let inset = CGFloat(logicalBorder) / 2
1070
+ let borderRect = rect.insetBy(dx: inset, dy: inset)
1071
+ let radius = min(
1072
+ CGFloat(max(0, element.radius)),
1073
+ min(borderRect.width, borderRect.height) / 2
1074
+ )
1075
+ context.addPath(
1076
+ UIBezierPath(roundedRect: borderRect, cornerRadius: radius).cgPath
1077
+ )
1078
+ context.strokePath()
1079
+ }
1080
+ context.restoreGState()
1081
+ }
1082
+ context.restoreGState()
1083
+ }
1084
+
1085
+ guard let cgImage = image.cgImage else { return nil }
1086
+ var grayscale = Data(count: width * height)
1087
+ let rendered = grayscale.withUnsafeMutableBytes { buffer -> Bool in
1088
+ guard let context = CGContext(
1089
+ data: buffer.baseAddress,
1090
+ width: width,
1091
+ height: height,
1092
+ bitsPerComponent: 8,
1093
+ bytesPerRow: width,
1094
+ space: CGColorSpaceCreateDeviceGray(),
1095
+ bitmapInfo: CGImageAlphaInfo.none.rawValue
1096
+ ) else { return false }
1097
+ context.setFillColor(gray: 0, alpha: 1)
1098
+ context.fill(CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
1099
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))
1100
+ return true
1101
+ }
1102
+ return rendered ? grayscale : nil
1103
+ }
1104
+
1105
+ /// Mode 3 with the lenses-differ bit: one zlib(RLE(4bpp)) payload of the
1106
+ /// box pixels, drawn by each lens at its own box.
1107
+ /// `[0x83][lx/4][ly/2][w/4][h/2][rx/4][ry/2][w/4][h/2][fid lo][fid hi][zlib]`
1108
+ static func stereoFrame(pixels: Data, boxes: LensBoxes, fid: UInt16) -> Data? {
1109
+ guard pixels.count == Int(boxes.width) * Int(boxes.height) else { return nil }
1110
+ let rle = rleEncode(pack4Bpp(pixels))
1111
+ return stereoFrame(rle: rle, boxes: boxes, fid: fid)
1112
+ }
1113
+
1114
+ /// Solid-color mode-3 frame at `boxes` — blanks a previous window position.
1115
+ static func solidStereoFrame(color: UInt8, boxes: LensBoxes, fid: UInt16) -> Data? {
1116
+ let rle = rleEncodeSolid(color: color, pixelCount: Int(boxes.width) * Int(boxes.height))
1117
+ return stereoFrame(rle: rle, boxes: boxes, fid: fid)
1118
+ }
1119
+
1120
+ private static func stereoFrame(rle: Data, boxes: LensBoxes, fid: UInt16) -> Data? {
1121
+ stereoRleFrame(
1122
+ rle: rle,
1123
+ lx: Int(boxes.left.x), ly: Int(boxes.left.y),
1124
+ rx: Int(boxes.right.x), ry: Int(boxes.right.y),
1125
+ w: boxes.width, h: boxes.height, fid: fid
1126
+ )
1127
+ }
1128
+
1129
+ private static func stereoRleFrame(
1130
+ rle: Data, lx: Int, ly: Int, rx: Int, ry: Int, w: Int32, h: Int32, fid: UInt16
1131
+ ) -> Data? {
1132
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1133
+ var payload = Data([0x83])
1134
+ for (x, y) in [(lx, ly), (rx, ry)] {
1135
+ payload.append(UInt8(x / 4))
1136
+ payload.append(UInt8(y / 2))
1137
+ payload.append(UInt8(w / 4))
1138
+ payload.append(UInt8(h / 2))
1139
+ }
1140
+ payload.append(UInt8(fid & 0xFF))
1141
+ payload.append(UInt8(fid >> 8))
1142
+ payload.append(compressed)
1143
+ return payload
1144
+ }
1145
+
1146
+ /// Mode 8: apply every segment to the shadow, then present once.
1147
+ /// `[8][count][len lo][len hi][segment]...`
1148
+ static func bundle(_ segments: [Data]) -> Data? {
1149
+ guard !segments.isEmpty, segments.count <= 255 else { return nil }
1150
+ for segment in segments where segment.isEmpty || segment.count > 0xFFFF {
1151
+ return nil
1152
+ }
1153
+ var payload = Data([8, UInt8(segments.count)])
1154
+ for segment in segments {
1155
+ payload.append(UInt8(segment.count & 0xFF))
1156
+ payload.append(UInt8(segment.count >> 8))
1157
+ payload.append(segment)
1158
+ }
1159
+ return payload
1160
+ }
1161
+
1162
+ /// Mode-6 full-panel seed with the frame's own pixels composited at the
1163
+ /// window position on black — seeds the shadow AND paints the frame in ONE
1164
+ /// payload, so a resync shows content immediately instead of flashing a
1165
+ /// black panel (the faceclaw reference client's resyncs are mode-6 content
1166
+ /// frames and never flash). Mode 6 has no per-lens variant, so this
1167
+ /// returns nil unless the window is lens-equal — callers fall back to the
1168
+ /// black seed there.
1169
+ static func seededFramePayload(pixels: Data, boxes: LensBoxes) -> Data? {
1170
+ guard boxes.left == boxes.right,
1171
+ pixels.count == Int(boxes.width) * Int(boxes.height)
1172
+ else { return nil }
1173
+ var panel = Data(count: frameWidth * frameHeight)
1174
+ let x0 = Int(boxes.left.x)
1175
+ let y0 = Int(boxes.left.y)
1176
+ let w = Int(boxes.width)
1177
+ let h = Int(boxes.height)
1178
+ panel.withUnsafeMutableBytes { dstRaw in
1179
+ pixels.withUnsafeBytes { srcRaw in
1180
+ let dst = dstRaw.bindMemory(to: UInt8.self).baseAddress!
1181
+ let src = srcRaw.bindMemory(to: UInt8.self).baseAddress!
1182
+ for row in 0 ..< h {
1183
+ memcpy(dst + (y0 + row) * frameWidth + x0, src + row * w, w)
1184
+ }
1185
+ }
1186
+ }
1187
+ return encodePixels(panel)
1188
+ }
1189
+
1190
+ /// Mode 6 full-panel black: seeds the shadow mode-3 frames composite onto.
1191
+ static func blackPayload() -> Data? {
1192
+ let pixelCount = frameWidth * frameHeight
1193
+ let rle = rleEncodeSolid(color: 0, pixelCount: pixelCount)
1194
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1195
+ var payload = Data([6])
1196
+ payload.append(compressed)
1197
+ return payload
1198
+ }
1199
+
1200
+ static func encodePixels(_ grayscale: Data) -> Data? {
1201
+ guard grayscale.count == frameWidth * frameHeight else { return nil }
1202
+ let packed = pack4Bpp(grayscale)
1203
+ let rle = rleEncode(packed)
1204
+ guard let compressed = BinaryCodec.zlibRFC1950(rle) else { return nil }
1205
+ var payload = Data([6])
1206
+ payload.append(compressed)
1207
+ return payload
1208
+ }
1209
+
1210
+ /// 8-bit gray → 4-bit panel level, round-to-nearest (matches the reference
1211
+ /// client's GRAY_TO_NIBBLE). 0 stays 0 so the shell's transparent color key
1212
+ /// survives; plain truncation instead darkened the upper half of every
1213
+ /// 16-value bucket by a full visible step on the 16-level panel.
1214
+ static func grayNibble(_ value: UInt8) -> UInt8 {
1215
+ value == 0 ? 0 : UInt8(min(15, (Int(value) + 8) >> 4))
1216
+ }
1217
+
1218
+ /// Snap an 8bpp raster to the 16 panel levels (`grayNibble(v) * 16`) before
1219
+ /// it enters the diff/shadow pipeline. The reference client diffs packed
1220
+ /// 4bpp bytes — exactly what the panel shows; diffing raw 8bpp instead
1221
+ /// marked rows dirty for sub-quantization AA noise the panel can never
1222
+ /// display (spurious rects, inflated coverage, and a scroll-copy detector
1223
+ /// that almost never matched). `grayNibble` round-trips `level * 16`
1224
+ /// exactly, so packing a quantized raster is lossless.
1225
+ static func quantizeToPanelLevels(_ raster: Data) -> Data {
1226
+ var out = raster
1227
+ out.withUnsafeMutableBytes { buffer in
1228
+ guard let base = buffer.bindMemory(to: UInt8.self).baseAddress else { return }
1229
+ for index in 0 ..< buffer.count {
1230
+ base[index] = grayNibble(base[index]) &* 16
1231
+ }
1232
+ }
1233
+ return out
1234
+ }
1235
+
1236
+ static func pack4Bpp(_ grayscale: Data) -> Data {
1237
+ let source = [UInt8](grayscale)
1238
+ var packed = Data(count: (source.count + 1) / 2)
1239
+ for index in packed.indices {
1240
+ let left = grayNibble(source[index * 2])
1241
+ let rightIndex = index * 2 + 1
1242
+ let right = rightIndex < source.count ? grayNibble(source[rightIndex]) : 0
1243
+ packed[index] = (left << 4) | right
1244
+ }
1245
+ return packed
1246
+ }
1247
+
1248
+ static func rleEncode(_ packed: Data) -> Data {
1249
+ let bytes = [UInt8](packed)
1250
+ let pixelCount = bytes.count * 2
1251
+ var output = Data()
1252
+ output.reserveCapacity(pixelCount / 4)
1253
+
1254
+ func nibble(_ index: Int) -> UInt8 {
1255
+ index.isMultiple(of: 2) ? bytes[index / 2] >> 4 : bytes[index / 2] & 0x0F
1256
+ }
1257
+
1258
+ var index = 0
1259
+ while index < pixelCount {
1260
+ let value = nibble(index)
1261
+ var end = index + 1
1262
+ while end < pixelCount, nibble(end) == value {
1263
+ end += 1
1264
+ }
1265
+ appendRun(color: value, count: end - index, to: &output)
1266
+ index = end
1267
+ }
1268
+ return output
1269
+ }
1270
+
1271
+ private static func rleEncodeSolid(color: UInt8, pixelCount: Int) -> Data {
1272
+ var output = Data()
1273
+ appendRun(color: color & 0x0F, count: pixelCount, to: &output)
1274
+ return output
1275
+ }
1276
+
1277
+ private static func appendRun(color: UInt8, count: Int, to output: inout Data) {
1278
+ var remaining = count
1279
+ while remaining > 0 {
1280
+ let chunk = min(remaining, 0xFFFF)
1281
+ if chunk <= 15 {
1282
+ output.append(UInt8(chunk << 4) | color)
1283
+ } else if chunk <= 255 {
1284
+ output.append(color)
1285
+ output.append(UInt8(chunk))
1286
+ } else {
1287
+ output.append(color)
1288
+ output.append(0)
1289
+ output.append(UInt8(chunk & 0xFF))
1290
+ output.append(UInt8((chunk >> 8) & 0xFF))
1291
+ }
1292
+ remaining -= chunk
1293
+ }
1294
+ }
1295
+ }