#!/usr/bin/env python # # Pre-build script: Brotli-compress webui/index.html into a PROGMEM C header so the # webconfig portal can serve the page straight from flash with # Content-Encoding: br. The generated header is tracked so ESP32 targets # that replace extra_scripts still compile from a clean checkout; this script # rewrites it only when the emitted firmware bytes actually change. # # Comments and indentation are stripped before compressing (see strip_source). # The source page is heavily commented by house style and none of it is worth # flash, so the page ships smaller than it reads. # # Output: src/helpers/esp32/WebConfigHtml.h # WEBCONFIG_HTML_LOADER[] - small bootstrap page (PROGMEM) # WEBCONFIG_HTML_BR[] - complete Brotli-compressed page (PROGMEM) # WEBCONFIG_HTML_ETAG - quoted strong ETag for the loader page # # Runnable outside SCons to inspect exactly what gets shipped: # python3 scripts/generate_webconfig_html.py --emit /tmp/shipped.html # or served directly by the mock backend with its --minify flag. import hashlib import os import sys try: Import("env") # noqa: F821 except NameError: pass # running standalone (--emit), not as a PIO extra_script SOURCE = os.path.join("webui", "index.html") OUTPUT = os.path.join("src", "helpers", "esp32", "WebConfigHtml.h") # __file__ is not defined inside PIO/SCons-executed extra_scripts HASH_MARKER = "// shipped-page-sha256: " sys.path.insert(0, os.path.join(os.getcwd(), "scripts")) from webconfig_minify import check_stripped, strip_source # noqa: E402 from brotli_compress import BROTLI_VERSION, LGWIN, QUALITY, compress_html # noqa: E402 def status(msg): sys.stderr.write("WebConfig HTML: %s\n" % msg) def content_hash(raw): """Identify the bytes that the device actually serves. Do not hash this generator or the minifier source: a comment-only tooling edit must not dirty the checked-in firmware header. Generator changes are still safe because main() renders the candidate header and compares its full byte sequence before deciding whether to write. """ return hashlib.sha256(raw).hexdigest() def shipped_page(): """The exact bytes the device serves: the source page, stripped.""" with open(SOURCE, "r", encoding="utf-8") as f: raw = f.read() stripped = strip_source(raw) problem = check_stripped(raw, stripped) if problem: status("ERROR: comment stripping corrupted the page (%s)" % problem) sys.exit(2) return raw, stripped def append_byte_array(lines, name, data): lines.append("const uint8_t %s[] PROGMEM = {" % name) for i in range(0, len(data), 16): chunk = data[i:i + 16] lines.append(" " + "".join("0x%02x," % b for b in chunk)) lines.append("};") def main(): if not os.path.isfile(SOURCE): status("ERROR: %s not found" % SOURCE) sys.exit(2) if "--emit" in sys.argv: dest = sys.argv[sys.argv.index("--emit") + 1] src, stripped = shipped_page() with open(dest, "w", encoding="utf-8") as f: f.write(stripped) status("%s -> %s (%d -> %d bytes)" % (SOURCE, dest, len(src), len(stripped))) return src, stripped = shipped_page() raw = stripped.encode("utf-8") src_hash = content_hash(raw) br = compress_html(raw) version = hashlib.sha256(br).hexdigest()[:16] loader = ("MeshCore " "WebConfigLoading MeshCore WebConfig…" % version) loader_bytes = loader.encode("utf-8") etag = hashlib.sha256(loader_bytes).hexdigest()[:16] lines = [] lines.append("// Auto-generated by scripts/generate_webconfig_html.py from %s" % SOURCE.replace(os.sep, "/")) lines.append("// DO NOT EDIT - edit webui/index.html instead.") lines.append("// Compressed with Google Brotli %s (quality %d, lgwin %d)." % (BROTLI_VERSION, QUALITY, LGWIN)) lines.append("%s%s" % (HASH_MARKER, src_hash)) lines.append("#pragma once") lines.append("#include ") lines.append("#include ") lines.append("") lines.append("const uint32_t WEBCONFIG_HTML_RAW_LEN = %d;" % len(raw)) lines.append("const uint32_t WEBCONFIG_HTML_BR_LEN = %d;" % len(br)) lines.append('const char WEBCONFIG_HTML_ETAG[] = "\\"%s\\"";' % etag) lines.append('const char WEBCONFIG_HTML_VERSION[] = "%s";' % version) lines.append("const uint16_t WEBCONFIG_HTML_LOADER_LEN = %d;" % len(loader_bytes)) append_byte_array(lines, "WEBCONFIG_HTML_LOADER", loader_bytes) lines.append("") append_byte_array(lines, "WEBCONFIG_HTML_BR", br) lines.append("") rendered = "\n".join(lines) try: with open(OUTPUT, "r", encoding="utf-8", newline="") as f: if f.read() == rendered: return except OSError: pass os.makedirs(os.path.dirname(OUTPUT), exist_ok=True) with open(OUTPUT, "w", encoding="utf-8", newline="") as f: f.write(rendered) status("%s -> %s (%d bytes source, %d stripped, %d Brotli)" % (SOURCE, OUTPUT, len(src.encode("utf-8")), len(raw), len(br))) # This script is attached only to esp32_base. Generate unconditionally here: # CPPDEFINES is not fully resolved when PlatformIO loads pre-scripts, so trying # to detect the final role at this point can leave a stale embedded page. # Non-WebConfig ESP32 builds do not compile the generated data into firmware. main()