From 7d6a01969952411c52c4941770103a4f78ae2705 Mon Sep 17 00:00:00 2001
From: Florian
Date: Sun, 22 Mar 2026 18:12:31 -0500
Subject: [PATCH] [WebUI] Add full BLE configuration page and MQTT Discovery
toggle (#2293)
Expand the BLE config page from encryption-only (BLEDecryptor) to a
comprehensive settings page available for all ZgatewayBT builds, covering
scan, publish, presence, external decoder, and encryption settings.
Add MQTT Discovery checkbox to the gateway configuration page.
Co-authored-by: Florian <1technophile@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context)
---
main/config_WebContent.h | 77 ++++++++++-
main/webUI.cpp | 275 +++++++++++++++++++++++++++++----------
2 files changed, 273 insertions(+), 79 deletions(-)
diff --git a/main/config_WebContent.h b/main/config_WebContent.h
index 4c6ad58..a535c2d 100644
--- a/main/config_WebContent.h
+++ b/main/config_WebContent.h
@@ -62,8 +62,8 @@
#else
# define configure_6
#endif
-#if BLEDecryptor
-# define configure_7 "
"
+#ifdef ZgatewayBT
+# define configure_7 ""
#else
# define configure_7
#endif
@@ -112,7 +112,16 @@ const char config_mqtt_body[] = body_header "" body_footer_config_menu;
#endif
#ifndef ESPWifiManualSetup
-const char config_gateway_body[] = body_header "" body_footer_config_menu;
+const char config_gateway_body[] = body_header
+ "" body_footer_config_menu;
#endif
const char config_logging_body[] = body_header
"" body_footer_config_menu;
-#if BLEDecryptor
-const char config_ble_body[] = body_header "" body_footer_config_menu;
+#ifdef ZgatewayBT
+// BLE config page split into parts to fit within WEB_TEMPLATE_BUFFER_MAX_SIZE
-// Client side javascript validation of the Default AES Key is hex, and the custom keys are in the correct format. This pushes the theengs-bridge-v11 way of the file size, so reducing it now and leaving it commented out
-//const char ble_script[] = "function bkv(){let e=document.getElementById('bk'),t=document.getElementById('bke'),l=e.value.trim();0===l.length||/^[A-Fa-f0-9]{32}$/.test(l)?(t.textContent='',e.style.color=''):(t.textContent='Invalid key',e.style.color='red')}function kpv(){let e=document.getElementById('kp'),t=document.getElementById('kpe'),l=e.value.split(/ /),n=/^[A-Fa-f0-9]{12}:[A-Fa-f0-9]{32}$/,o=l.map((e,t)=>({line:e,index:t})).filter(e=>!n.test(e.line));if(0===e.value.trim().length||0===o.length)t.textContent='',e.style.color='';else{let i=o.map(e=>e.index+1).join(', ');t.textContent=`Invalid format on line(s): ${i}`,e.style.color='red'}}";
+// Part 1: Header + Scan settings (body_header %s=modules, %s=gateway_name, then: %s=enabled, %s=adaptivescan, %lu=interval, %lu=intervalacts, %lu=scanduration, %s=forcepscn, %s=bleconnect, %d=minrssi)
+const char config_ble_body_scan[] = body_header
+ "" body_footer_config_menu;
const char ble_script[] = "";
#endif
diff --git a/main/webUI.cpp b/main/webUI.cpp
index b301116..443fb61 100644
--- a/main/webUI.cpp
+++ b/main/webUI.cpp
@@ -64,6 +64,7 @@ extern void eraseConfig();
extern unsigned long uptime();
extern void ESPRestart(byte reason);
extern void XtoSYS(const char* topicOri, JsonObject& SYSdata);
+extern void SYSConfig_save();
extern String stateMeasures();
extern void MQTTHttpsFWUpdate(const char* topicOri, JsonObject& HttpsFwUpdateData);
extern void receivingDATA(const char* topicOri, const char* datacallback);
@@ -862,44 +863,61 @@ void handleMQ() {
void handleCG() {
WEBUI_TRACE_LOG(F("handleCG: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
- bool update = false;
- StaticJsonDocument jsonBuffer;
- JsonObject WEBtoSYS = jsonBuffer.to();
+ bool pwUpdate = false;
+ bool sysUpdate = false;
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleCG Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
- if (server.hasArg("save") && server.hasArg("gp") && strcmp(ota_pass, server.arg("gp").c_str())) {
- strncpy(ota_pass, server.arg("gp").c_str(), parameters_size);
- WEBtoSYS["gw_pass"] = ota_pass;
- update = true;
+ if (server.hasArg("save")) {
+ if (server.hasArg("gp") && server.arg("gp").length() > 0 && strcmp(ota_pass, server.arg("gp").c_str())) {
+ strncpy(ota_pass, server.arg("gp").c_str(), parameters_size);
+ pwUpdate = true;
+ }
+# ifdef ZmqttDiscovery
+ if (SYSConfig.discovery != server.hasArg("dc")) {
+ SYSConfig.discovery = server.hasArg("dc");
+ sysUpdate = true;
+ }
+# endif
}
}
- if (update) {
- THEENGS_LOG_WARNING(F("[WebUI] Save Password and Restart" CR));
+ if (pwUpdate || sysUpdate) {
+ THEENGS_LOG_NOTICE(F("[WebUI] Save gateway config and restart" CR));
+
+ if (sysUpdate) {
+ SYSConfig_save();
+ }
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE];
- snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Save Password and Restart").c_str());
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Save and Restart").c_str());
String response = String(buffer);
response += String(restart_script);
response += String(script);
response += String(style);
- snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, reset_body, jsonChar, gateway_name, "Save Password and Restart");
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, reset_body, jsonChar, gateway_name, "Save and Restart");
response += String(buffer);
snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION);
response += String(buffer);
server.send(200, "text/html", response);
- delay(2000); // Wait for web page to be sent before
- String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSset);
- XtoSYS((char*)topic.c_str(), WEBtoSYS);
- } else {
- THEENGS_LOG_WARNING(F("[WebUI] No changes" CR));
+ if (pwUpdate) {
+ StaticJsonDocument jsonBuffer;
+ JsonObject WEBtoSYS = jsonBuffer.to();
+ WEBtoSYS["gw_pass"] = ota_pass;
+ delay(2000);
+ String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSset);
+ XtoSYS((char*)topic.c_str(), WEBtoSYS);
+ } else {
+ delay(2000);
+ ESPRestart(5);
+ }
+ return;
}
char jsonChar[100];
@@ -911,7 +929,12 @@ void handleCG() {
String response = String(buffer);
response += String(script);
response += String(style);
- snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_gateway_body, jsonChar, gateway_name, ota_pass);
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_gateway_body, jsonChar, gateway_name
+# ifdef ZmqttDiscovery
+ ,
+ (SYSConfig.discovery ? "checked" : "")
+# endif
+ );
response += String(buffer);
snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION);
response += String(buffer);
@@ -955,74 +978,122 @@ void handleLO() {
server.send(200, "text/html", response);
}
-# if BLEDecryptor
+# ifdef ZgatewayBT
+# include "config_BT.h"
+extern void BTConfig_fromJson(JsonObject& BTdata, bool startup);
+extern String stateBTMeasures(bool start);
+extern BTConfig_s BTConfig;
+
/**
* @brief /BL - Config BLE
- * T: handleBL: uri: /bl, args: 3, method: 1
- * T: handleBL Arg: 0, dn=on - Force Device Name
- * T: handleBL Arg: 1, bk=on - BLE AES Key
- * T: handleBL Arg: 2, save=
*/
void handleBL() {
WEBUI_TRACE_LOG(F("handleBL: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
- StaticJsonDocument jsonBuffer;
- JsonObject WEBtoSYS = jsonBuffer.to();
-
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleBL Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
- bool update = false;
-
if (server.hasArg("save")) {
- // Default BLE AES Key
- if (server.hasArg("bk")) {
- WEBtoSYS["ble_aes"] = server.arg("bk");
- update = true;
- }
+ // Save BLE config in its own scope so the JSON buffer is freed before stateBTMeasures
+ // runs (via BTConfig_fromJson), avoiding stack overflow from nested StaticJsonDocument allocations
+ {
+ StaticJsonDocument jsonBuffer;
+ JsonObject WEBtoBT = jsonBuffer.to();
- // Split Custom BLE AES key pair string add to config
- if (server.hasArg("kp")) {
- String kp = server.arg("kp");
- while (kp.length() > 0) {
- int kpindex = kp.indexOf(' ');
- if (kpindex == -1) {
- if (kp.indexOf(':') == 12) {
- WEBtoSYS["ble_aes_keys"][kp.substring(0, 12)] = kp.substring(13, 45);
- }
- break;
- } else {
- if (kp.indexOf(':') == 12) {
- WEBtoSYS["ble_aes_keys"][kp.substring(0, 12)] = kp.substring(13, 45);
- }
- kp = kp.substring(kpindex + 1);
- }
+ // Scan settings
+ WEBtoBT["enabled"] = server.hasArg("en");
+ WEBtoBT["adaptivescan"] = server.hasArg("as");
+ if (server.hasArg("bi")) {
+ WEBtoBT["interval"] = server.arg("bi").toInt();
+ }
+ if (server.hasArg("ai")) {
+ WEBtoBT["intervalacts"] = server.arg("ai").toInt();
+ }
+ if (server.hasArg("sd")) {
+ WEBtoBT["scanduration"] = server.arg("sd").toInt();
+ }
+ WEBtoBT["forcepscn"] = server.hasArg("fp");
+ WEBtoBT["bleconnect"] = server.hasArg("bc");
+ if (server.hasArg("mr")) {
+ WEBtoBT["minrssi"] = server.arg("mr").toInt();
}
- update = true;
- }
- if (update) {
- THEENGS_LOG_WARNING(F("[BLE] Save Config" CR));
- String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSset);
- XtoSYS((char*)topic.c_str(), WEBtoSYS);
- } else {
- THEENGS_LOG_WARNING(F("[BLE] No changes" CR));
+ // Publish settings
+ WEBtoBT["onlysensors"] = server.hasArg("os");
+ WEBtoBT["randommacs"] = server.hasArg("rm");
+ WEBtoBT["pubadvdata"] = server.hasArg("pa");
+ WEBtoBT["pubuuid4topic"] = server.hasArg("ut");
+ WEBtoBT["filterConnectable"] = server.hasArg("fc");
+ WEBtoBT["ignoreWBlist"] = server.hasArg("iw");
+
+ // Presence settings
+ WEBtoBT["hasspresence"] = server.hasArg("hp");
+ WEBtoBT["presuseuuid"] = server.hasArg("pu");
+ if (server.hasArg("pt")) {
+ WEBtoBT["prestopic"] = server.arg("pt");
+ }
+ if (server.hasArg("at")) {
+ WEBtoBT["presenceawaytimer"] = server.arg("at").toInt();
+ }
+ if (server.hasArg("mo")) {
+ WEBtoBT["movingtimer"] = server.arg("mo").toInt();
+ }
+
+ // External decoder
+ WEBtoBT["extDecoderEnable"] = server.hasArg("ed");
+ if (server.hasArg("et")) {
+ WEBtoBT["extDecoderTopic"] = server.arg("et");
+ }
+ if (server.hasArg("ci")) {
+ WEBtoBT["intervalcnct"] = server.arg("ci").toInt();
+ }
+
+ THEENGS_LOG_NOTICE(F("[WebUI] Save BLE config" CR));
+ WEBtoBT["save"] = true;
+ BTConfig_fromJson(WEBtoBT, false);
+ } // jsonBuffer freed here before stateBTMeasures internal allocations complete
+
+# if BLEDecryptor
+ // Encryption settings (via SYS topic) - separate scope
+ {
+ StaticJsonDocument sysBuffer;
+ JsonObject WEBtoSYS = sysBuffer.to();
+ bool sysUpdate = false;
+
+ if (server.hasArg("bk")) {
+ WEBtoSYS["ble_aes"] = server.arg("bk");
+ sysUpdate = true;
+ }
+ if (server.hasArg("kp")) {
+ String kp = server.arg("kp");
+ while (kp.length() > 0) {
+ int kpindex = kp.indexOf(' ');
+ if (kpindex == -1) {
+ if (kp.indexOf(':') == 12) {
+ WEBtoSYS["ble_aes_keys"][kp.substring(0, 12)] = kp.substring(13, 45);
+ }
+ break;
+ } else {
+ if (kp.indexOf(':') == 12) {
+ WEBtoSYS["ble_aes_keys"][kp.substring(0, 12)] = kp.substring(13, 45);
+ }
+ kp = kp.substring(kpindex + 1);
+ }
+ }
+ sysUpdate = true;
+ }
+
+ if (sysUpdate) {
+ THEENGS_LOG_NOTICE(F("[WebUI] Save BLE encryption config" CR));
+ String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSset);
+ XtoSYS((char*)topic.c_str(), WEBtoSYS);
+ }
}
+# endif
}
}
- // Build BLE Key Pair string
- std::string aeskeysstring;
- JsonObject root = ble_aes_keys.as();
- for (JsonPair kv : root) {
- aeskeysstring += kv.key().c_str();
- aeskeysstring += ":";
- aeskeysstring += kv.value().as();
- aeskeysstring += " ";
- }
- aeskeysstring.pop_back();
-
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
@@ -1033,9 +1104,69 @@ void handleBL() {
response += String(ble_script);
response += String(script);
response += String(style);
- int logLevel = Log.getLevel();
- snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body, jsonChar, gateway_name, ble_aes, aeskeysstring.c_str());
+
+ // Scan settings
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body_scan,
+ jsonChar, gateway_name,
+ (BTConfig.enabled ? "checked" : ""),
+ (BTConfig.adaptiveScan ? "checked" : ""),
+ BTConfig.BLEinterval,
+ BTConfig.intervalActiveScan,
+ BTConfig.scanDuration,
+ (BTConfig.forcePassiveScan ? "checked" : ""),
+ (BTConfig.bleConnect ? "checked" : ""),
+ BTConfig.minRssi);
response += String(buffer);
+
+ // Publish settings
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body_publish,
+ (BTConfig.pubOnlySensors ? "checked" : ""),
+ (BTConfig.pubRandomMACs ? "checked" : ""),
+ (BTConfig.pubAdvData ? "checked" : ""),
+ (BTConfig.pubBeaconUuidForTopic ? "checked" : ""),
+ (BTConfig.filterConnectable ? "checked" : ""),
+ (BTConfig.ignoreWBlist ? "checked" : ""));
+ response += String(buffer);
+
+ // Presence settings
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body_presence,
+ (BTConfig.presenceEnable ? "checked" : ""),
+ (BTConfig.presenceUseBeaconUuid ? "checked" : ""),
+ BTConfig.presenceTopic.c_str(),
+ BTConfig.presenceAwayTimer,
+ BTConfig.movingTimer);
+ response += String(buffer);
+
+ // External decoder settings
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body_decoder,
+ (BTConfig.extDecoderEnable ? "checked" : ""),
+ BTConfig.extDecoderTopic.c_str(),
+ BTConfig.intervalConnect);
+ response += String(buffer);
+
+# if BLEDecryptor
+ // Encryption settings
+ std::string aeskeysstring;
+ JsonObject root = ble_aes_keys.as();
+ for (JsonPair kv : root) {
+ aeskeysstring += kv.key().c_str();
+ aeskeysstring += ":";
+ aeskeysstring += kv.value().as();
+ aeskeysstring += " ";
+ }
+ if (!aeskeysstring.empty()) {
+ aeskeysstring.pop_back();
+ }
+
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_ble_body_encrypt,
+ ble_aes, aeskeysstring.c_str());
+ response += String(buffer);
+# endif
+
+ // Footer
+ snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, "%s", config_ble_body_footer);
+ response += String(buffer);
+
snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION);
response += String(buffer);
server.send(200, "text/html", response);
@@ -1757,7 +1888,7 @@ void WebUISetup() {
server.on("/wi", HTTP_POST, handleWI); // Configure Wifi
server.on("/mq", HTTP_POST, handleMQ); // Configure MQTT
# ifndef ESPWifiManualSetup
- server.on("/cg", HTTP_POST, handleCG); // Configure gateway"
+ server.on("/cg", handleCG); // Configure gateway
# endif
server.on("/wu", handleWU); // Configure WebUI
# ifdef ZgatewayLORA
@@ -1771,8 +1902,8 @@ void WebUISetup() {
# endif
server.on("/lo", handleLO); // Configure Logging
-# if BLEDecryptor
- server.on("/bl", HTTP_POST, handleBL); // Configure BLE
+# ifdef ZgatewayBT
+ server.on("/bl", handleBL); // Configure BLE
# endif
server.on("/rt", handleRT); // Reset configuration ( Erase and Restart )