From 2d6e546ba45d56504f80ffc35d97d8727cb9b0f4 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sat, 19 Sep 2026 20:12:37 -0700 Subject: [PATCH] Maximize Zopfli mOTA transport compression --- scripts/zopfli_compress.py | 16 +++++++++++----- tools/ble_mota/ble_mota_seeder.py | 7 +++++-- tools/ble_mota/test_ble_mota_seeder.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/scripts/zopfli_compress.py b/scripts/zopfli_compress.py index 5bdbe3a0a..7fd19d876 100644 --- a/scripts/zopfli_compress.py +++ b/scripts/zopfli_compress.py @@ -32,11 +32,17 @@ if zopfli.__version__ != ZOPFLI_VERSION: # Fifteen iterations is the upstream binding's practical default for assets # of this size and keeps CI/build times reasonable. ITERATIONS = 15 +# Zopfli has no finite algorithmic iteration ceiling. Its ``--i1000`` +# convention is the maximum-effort setting used for host-cached mOTA transfer +# blocks, where the extra host CPU time is preferable to wasting radio airtime. +MAXIMUM_ITERATIONS = 1000 -def _options() -> dict[str, int | bool]: +def _options(numiterations: int = ITERATIONS) -> dict[str, int | bool]: + if numiterations < 1: + raise ValueError("Zopfli requires at least one iteration") return { - "numiterations": ITERATIONS, + "numiterations": numiterations, "blocksplitting": True, "blocksplittinglast": False, "blocksplittingmax": 15, @@ -54,9 +60,9 @@ def gzip_compress(data: bytes) -> bytes: return bytes(encoded) -def raw_deflate_compress(data: bytes) -> bytes: - """Compress *data* to an RFC 1951 raw DEFLATE stream using Google Zopfli.""" - encoded = _zlib.compress(data, **_options()) +def raw_deflate_compress(data: bytes, *, numiterations: int = ITERATIONS) -> bytes: + """Compress *data* to an RFC 1951 raw-DEFLATE stream with Zopfli.""" + encoded = _zlib.compress(data, **_options(numiterations)) # A zlib wrapper is exactly a two-byte header and four-byte Adler-32 # trailer. Removing it preserves the RFC 1951 body expected by tinf. if len(encoded) < 6 or (encoded[0] & 0x0F) != 8: diff --git a/tools/ble_mota/ble_mota_seeder.py b/tools/ble_mota/ble_mota_seeder.py index 21559bc54..931abde3b 100755 --- a/tools/ble_mota/ble_mota_seeder.py +++ b/tools/ble_mota/ble_mota_seeder.py @@ -21,7 +21,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "scripts")) -from zopfli_compress import raw_deflate_compress # noqa: E402 +from zopfli_compress import MAXIMUM_ITERATIONS, raw_deflate_compress # noqa: E402 BLEAK_IMPORT_ERROR: ImportError | None = None try: @@ -62,6 +62,9 @@ STATUS_ERR = 1 MOTA_READ_MAX = 192 MOTA_DEFLATE_CHUNK_MAX = 190 MOTA_DEFLATE_BLOCK_MAX = 2048 +# This work occurs on the host and each result is cached while its chunks are +# served. Use Zopfli's maximum-effort convention to reduce LoRa airtime. +MOTA_ZOPFLI_ITERATIONS = MAXIMUM_ITERATIONS MOTA_DESC_WIRE = 38 MOTA_SOURCE_CAP_DEFLATE_BLOCK = 0x01 MOTA_HEADER_LEN = 8 @@ -101,7 +104,7 @@ class MotaFile: raw = stream.read(raw_length) if len(raw) != raw_length: return None - encoded = raw_deflate_compress(raw) + encoded = raw_deflate_compress(raw, numiterations=MOTA_ZOPFLI_ITERATIONS) return encoded if 0 < len(encoded) < len(raw) else None @staticmethod diff --git a/tools/ble_mota/test_ble_mota_seeder.py b/tools/ble_mota/test_ble_mota_seeder.py index 2ab6a0a8c..6389825c0 100644 --- a/tools/ble_mota/test_ble_mota_seeder.py +++ b/tools/ble_mota/test_ble_mota_seeder.py @@ -6,6 +6,7 @@ import tempfile import unittest import zlib from pathlib import Path +from unittest import mock import ble_mota_seeder as seeder @@ -78,6 +79,19 @@ class TransportDeflateTests(unittest.TestCase): encoded_1k = compressor_1k.compress(self.raw) + compressor_1k.flush() self.assertLess(len(encoded) + 400, len(encoded_1k)) + def test_uses_maximum_zopfli_iterations_for_mota_transport(self): + compressor = zlib.compressobj( + level=9, method=zlib.DEFLATED, wbits=-11 + ) + expected = compressor.compress(self.raw) + compressor.flush() + with mock.patch.object( + seeder, "raw_deflate_compress", return_value=expected + ) as compress: + self.assertEqual(self.catalog.files[0].deflated_block(0), expected) + compress.assert_called_once_with( + self.raw, numiterations=seeder.MOTA_ZOPFLI_ITERATIONS + ) + def test_rejects_bad_block_range_and_oversized_chunk(self): self.assertEqual(self.exchange(1, 0, 0)[0], seeder.STATUS_ERR) self.assertEqual(