Fix WebConfig browser compatibility

This commit is contained in:
Kyle Duren committed 2026-09-30 16:58:36 -04:00
1 parent 61c4767c50
commit 70d183a3ae
4 files changed
+9361 -1936

No files matched your search

+3
View File
@@ -12,6 +12,7 @@
#
# Output: src/helpers/esp32/WebConfigHtml.h
# WEBCONFIG_HTML_LOADER[] - small bootstrap page (PROGMEM)
# WEBCONFIG_HTML_RAW[] - complete stripped page (PROGMEM)
# WEBCONFIG_HTML_BR[] - complete Brotli-compressed page (PROGMEM)
# WEBCONFIG_HTML_ETAG - quoted strong ETag for the loader page
#
@@ -125,6 +126,8 @@ def main():
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_RAW", raw)
lines.append("")
append_byte_array(lines, "WEBCONFIG_HTML_BR", br)
lines.append("")
File diff suppressed because it is too large. Load diff
+46 -17
View File
@@ -154,15 +154,11 @@ public:
if (_in_flight != 0) return 0;
if (_state == RESPONSE_WAIT_ACK) {
// The full Content-Length has now been acknowledged. ESPAsyncWebServer
// otherwise performs a graceful active close for every response, and
// this IDF 4.4 stack retains malloc-backed TCP PCBs in TIME_WAIT for about
// two minutes. Repeated WebConfig loads consume scarce internal heap and
// can also prevent AsyncTCP from retiring close events, wedging both
// infrastructure WiFi and ESP-NOW. Abort only after the final ACK so the
// peer has the complete page, while freeing this PCB immediately. Keep
// RESPONSE_WAIT_ACK: abort synchronously dispatches disconnect and the
// request's scoped owner then destroys this response.
if (client) client->abort();
// otherwise performs a graceful active close for every response. Keep
// the graceful close here too: browsers can report a complete,
// Content-Length-delimited response as a failed transfer when the peer
// force-aborts the TCP PCB immediately after the final ACK.
if (client) client->close();
return 0;
}
return queueNext(request);
@@ -1651,6 +1647,31 @@ void WebConfigServer::handleRoot(AsyncWebServerRequest* req) {
req->send(res);
}
static bool webConfigClientAcceptsBrotli(AsyncWebServerRequest* req) {
if (!req || !req->hasHeader("Accept-Encoding")) return false;
String encodings = req->getHeader("Accept-Encoding")->value();
encodings.toLowerCase();
int start = 0;
while (start < encodings.length()) {
int end = encodings.indexOf(',', start);
if (end < 0) end = encodings.length();
String item = encodings.substring(start, end);
item.trim();
int semi = item.indexOf(';');
String coding = semi < 0 ? item : item.substring(0, semi);
coding.trim();
if (coding == "br") {
if (semi < 0) return true;
String params = item.substring(semi + 1);
params.trim();
int qpos = params.indexOf("q=");
return qpos < 0 || params.substring(qpos + 2).toFloat() > 0.0f;
}
start = end + 1;
}
return false;
}
void WebConfigServer::handleUi(AsyncWebServerRequest* req) {
if (_mode == MODE_OFF) { req->send(503); return; }
_last_activity = millis();
@@ -1664,14 +1685,22 @@ void WebConfigServer::handleUi(AsyncWebServerRequest* req) {
req->send(res);
return;
}
AsyncWebServerResponse* res =
new WebConfigPacedProgmemResponse("text/html; charset=utf-8",
WEBCONFIG_HTML_BR,
WEBCONFIG_HTML_BR_LEN);
// Keep exactly one whole-page asset in flash. Brotli is decoded by the
// browser; the ESP32 only streams these precompressed bytes.
res->addHeader("Content-Encoding", "br");
res->addHeader("Cache-Control", "public, max-age=31536000, immutable");
AsyncWebServerResponse* res = nullptr;
if (webConfigClientAcceptsBrotli(req)) {
res = new WebConfigPacedProgmemResponse("text/html; charset=utf-8",
WEBCONFIG_HTML_BR,
WEBCONFIG_HTML_BR_LEN);
// Brotli is decoded by the browser; the ESP32 only streams these
// precompressed bytes.
res->addHeader("Content-Encoding", "br");
res->addHeader("Cache-Control", "public, max-age=31536000, immutable");
} else {
// Never send Brotli to clients that did not advertise support for it.
res = new WebConfigPacedProgmemResponse("text/html; charset=utf-8",
WEBCONFIG_HTML_RAW,
WEBCONFIG_HTML_RAW_LEN);
res->addHeader("Cache-Control", "no-cache");
}
req->send(res);
}
+6 -2
View File
@@ -2016,8 +2016,12 @@ function setBoardCmds(v,tries){
// Board-specific rows in the settings form. Hidden unless the node answered for
// the command at startup, so the form never shows a control that cannot apply.
function applyBoardCmds(){
$("#sw-fem-rx").classList.toggle("hide",!boardHas("radio.fem.rxgain"));
$("#sw-fem-tx").classList.toggle("hide",!boardHas("radio.fem.txgain"));
var rx=$("[data-k=\"radio.fem.rxgain\"]");
var rxRow=rx&&rx.closest(".sw");
if(rxRow)rxRow.classList.toggle("hide",!boardHas("radio.fem.rxgain"));
var tx=$("[data-k=\"radio.fem.txgain\"]");
var txRow=tx&&tx.closest(".sw");
if(txRow)txRow.classList.toggle("hide",!boardHas("radio.fem.txgain"));
}
// Per-slot keys, expanded across the slots this board actually runs.