Files
MeshCore-mqtt-observer/scripts/generate_webconfig_html.py
agessaman a7c1cc631f feat(webconfig): add web configuration portal and MQTT enhancements
Introduce a web configuration portal for easier node management and
provisioning without serial CLI. Enhance MQTT functionality with
improved IATA code validation, dynamic slot management, and
background NTP synchronization. Update web UI elements for better
user experience and security notes regarding open AP usage.
2026-07-18 09:34:11 -07:00

107 lines
3.6 KiB
Python

#!/usr/bin/env python
#
# Pre-build script: gzip webui/index.html into a PROGMEM C header so the
# webconfig portal can serve the page straight from flash with
# Content-Encoding: gzip. The generated header is .gitignored; this script
# regenerates it whenever the source page (or this script) content changes.
#
# Freshness is decided by a content hash of the build inputs (not timestamps):
# a generated header written with a skewed/future mtime would otherwise mask a
# later source edit and embed stale UI. gzip output is deterministic (mtime=0),
# so an unchanged source produces an unchanged header.
#
# The generator is idempotent and cheap: when the inputs are unchanged it only
# hashes two small files and returns, so running it from esp32_base on every
# ESP32 build is negligible even for targets that don't compile the portal.
#
# Output: src/helpers/esp32/WebConfigHtml.h
# WEBCONFIG_HTML_GZ[] - gzipped page (PROGMEM)
# WEBCONFIG_HTML_GZ_LEN - byte length
# WEBCONFIG_HTML_ETAG - quoted strong ETag (sha256 prefix of the gz body)
import gzip
import hashlib
import os
import sys
Import("env") # noqa: F821
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
SCRIPT = os.path.join("scripts", "generate_webconfig_html.py")
HASH_MARKER = "// build-inputs-sha256: "
def status(msg):
sys.stderr.write("WebConfig HTML: %s\n" % msg)
def content_hash():
# Hash the source page and this generator so any change to either forces a
# regenerate, independent of file timestamps.
h = hashlib.sha256()
with open(SOURCE, "rb") as f:
h.update(f.read())
if os.path.isfile(SCRIPT):
h.update(b"\0")
with open(SCRIPT, "rb") as f:
h.update(f.read())
return h.hexdigest()
def stored_hash():
try:
with open(OUTPUT, "r") as f:
for line in f:
if line.startswith(HASH_MARKER):
return line[len(HASH_MARKER):].strip()
if line.startswith("#"): # reached the C preprocessor lines
break
except OSError:
return None
return None
def main():
if not os.path.isfile(SOURCE):
status("ERROR: %s not found" % SOURCE)
sys.exit(2)
src_hash = content_hash()
if os.path.isfile(OUTPUT) and stored_hash() == src_hash:
return
with open(SOURCE, "rb") as f:
raw = f.read()
# mtime=0 keeps the gzip output (and therefore the ETag) deterministic
gz = gzip.compress(raw, compresslevel=9, mtime=0)
etag = hashlib.sha256(gz).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("%s%s" % (HASH_MARKER, src_hash))
lines.append("#pragma once")
lines.append("#include <stdint.h>")
lines.append("#include <pgmspace.h>")
lines.append("")
lines.append("const uint32_t WEBCONFIG_HTML_GZ_LEN = %d;" % len(gz))
lines.append('const char WEBCONFIG_HTML_ETAG[] = "\\"%s\\"";' % etag)
lines.append("const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {")
for i in range(0, len(gz), 16):
chunk = gz[i:i + 16]
lines.append(" " + "".join("0x%02x," % b for b in chunk))
lines.append("};")
lines.append("")
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
with open(OUTPUT, "w") as f:
f.write("\n".join(lines))
status("%s -> %s (%d bytes raw, %d bytes gzipped)" % (SOURCE, OUTPUT, len(raw), len(gz)))
main()