Files
OpenMQTT-Gateway/main/webUI.cpp
T
3b2b8a333a [WebUI] Fix BLE config page connection reset on load (#2295)
ble_script is defined as "" (empty string). Passing it to
server.sendContent("") signals end of HTTP chunked transfer,
closing the response prematurely. All subsequent chunks then
fail with errno 104 "Connection reset by peer".

Guard the call with a length check so the empty string is
never sent as a chunk terminator.

Co-authored-by: Florian <1technophile@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 07:23:06 -05:00

2718 lines
92 KiB
C++

/*
Theengs OpenMQTTGateway - We Unite Sensors in One Open-Source Interface
Act as a gateway between your 433mhz, infrared IR, BLE, LoRa signal and one interface like an MQTT broker
Send and receiving command by MQTT
Copyright: (c)Florian ROBERT
This file is part of OpenMQTTGateway.
OpenMQTTGateway is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenMQTTGateway is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "User_config.h"
#if defined(ZwebUI) && defined(ESP32)
# include <SPIFFS.h>
# include <WebServer.h> // Docs for this are here - https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer
# include <WiFi.h>
# include <cstdarg>
# include "TheengsCommon.h"
# include "config_WebContent.h"
# include "config_WebUI.h"
# if defined(ZgatewayCloud)
# include "config_Cloud.h"
# endif
# if defined(ZdisplaySSD1306)
# include "config_SSD1306.h"
# endif
extern String stateRFMeasures();
uint32_t requestToken = 0;
QueueHandle_t webUIQueue;
WebServer server(80);
webUIQueueMessage* currentWebUIMessage;
extern JsonArray modules;
extern char discovery_prefix[];
extern String latestVersion;
bool WebUIConfig_save();
void sendRestartPage();
bool WebUIConfig_load();
void WebUIConfig_init();
void addLog(const uint8_t* buffer, size_t size);
/*------------------- External functions ----------------------*/
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);
/*------------------- Web Console Globals ----------------------*/
# define ROW_LENGTH 512
const uint16_t LOG_BUFFER_SIZE = 3072;
uint32_t log_buffer_pointer;
void* log_buffer_mutex;
char* log_buffer = nullptr; // Lazy-allocated on first console access
const uint16_t MAX_LOGSZ = LOG_BUFFER_SIZE - 96;
# define LOG_BUFFER_IDLE_TIMEOUT 120000 // Free log buffer after 2 minutes of console inactivity
unsigned long log_buffer_last_access = 0;
static char* line = nullptr;
int lineIndex = 0;
const uint16_t TOPSZ = 151; // Max number of characters in topic string
uint8_t masterlog_level; // Master log level used to override set log level
bool reset_web_log_flag = false; // Reset web console log
const char* www_username = WEBUI_LOGIN;
String authFailResponse = "Authentication Failed";
bool webUISecure = WEBUI_AUTH;
boolean displayMetric = DISPLAY_METRIC;
static void ensureLogBuffer() {
if (!log_buffer) {
log_buffer = (char*)calloc(LOG_BUFFER_SIZE, 1);
log_buffer_pointer = 0;
}
log_buffer_last_access = millis();
}
static void freeLogBufferIfIdle() {
if (log_buffer && (millis() - log_buffer_last_access > LOG_BUFFER_IDLE_TIMEOUT)) {
free(log_buffer);
log_buffer = nullptr;
log_buffer_pointer = 0;
if (line) {
free(line);
line = nullptr;
lineIndex = 0;
}
}
}
/*********************************************************************************************\
* ESP32 AutoMutex
\*********************************************************************************************/
//////////////////////////////////////////
// automutex.
// create a mute in your driver with:
// void *mutex = nullptr;
//
// then protect any function with
// TasAutoMutex m(&mutex, "somename");
// - mutex is automatically initialised if not already intialised.
// - it will be automagically released when the function is over.
// - the same thread can take multiple times (recursive).
// - advanced options m.give() and m.take() allow you fine control within a function.
// - if take=false at creat, it will not be initially taken.
// - name is used in serial log of mutex deadlock.
// - maxWait in ticks is how long it will wait before failing in a deadlock scenario (and then emitting on serial)
class TasAutoMutex {
SemaphoreHandle_t mutex;
bool taken;
int maxWait;
const char* name;
public:
TasAutoMutex(SemaphoreHandle_t* mutex, const char* name = "", int maxWait = 40, bool take = true);
~TasAutoMutex();
void give();
void take();
static void init(SemaphoreHandle_t* ptr);
};
//////////////////////////////////////////
TasAutoMutex::TasAutoMutex(SemaphoreHandle_t* mutex, const char* name, int maxWait, bool take) {
if (mutex) {
if (!(*mutex)) {
TasAutoMutex::init(mutex);
}
this->mutex = *mutex;
this->maxWait = maxWait;
this->name = name;
if (take) {
this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait);
// if (!this->taken){
// Serial.printf("\r\nMutexfail %s\r\n", this->name);
// }
}
} else {
this->mutex = (SemaphoreHandle_t) nullptr;
}
}
TasAutoMutex::~TasAutoMutex() {
if (this->mutex) {
if (this->taken) {
xSemaphoreGiveRecursive(this->mutex);
this->taken = false;
}
}
}
void TasAutoMutex::init(SemaphoreHandle_t* ptr) {
SemaphoreHandle_t mutex = xSemaphoreCreateRecursiveMutex();
(*ptr) = mutex;
// needed, else for ESP8266 as we will initialis more than once in logging
// (*ptr) = (void *) 1;
}
void TasAutoMutex::give() {
if (this->mutex) {
if (this->taken) {
xSemaphoreGiveRecursive(this->mutex);
this->taken = false;
}
}
}
void TasAutoMutex::take() {
if (this->mutex) {
if (!this->taken) {
this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait);
// if (!this->taken){
// Serial.printf("\r\nMutexfail %s\r\n", this->name);
// }
}
}
}
// Get span until single character in string
size_t strchrspn(const char* str1, int character) {
size_t ret = 0;
char* start = (char*)str1;
char* end = strchr(str1, character);
if (end) ret = end - start;
return ret;
}
int WifiGetRssiAsQuality(int rssi) {
int quality = 0;
if (rssi <= -100) {
quality = 0;
} else if (rssi >= -50) {
quality = 100;
} else {
quality = 2 * (rssi + 100);
}
return quality;
}
char* GetTextIndexed(char* destination, size_t destination_size, uint32_t index, const char* haystack) {
// Returns empty string if not found
// Returns text of found
char* write = destination;
const char* read = haystack;
index++;
while (index--) {
size_t size = destination_size - 1;
write = destination;
char ch = '.';
while ((ch != '\0') && (ch != '|')) {
ch = pgm_read_byte(read++);
if (size && (ch != '|')) {
*write++ = ch;
size--;
}
}
if (0 == ch) {
if (index) {
write = destination;
}
break;
}
}
*write = '\0';
return destination;
}
const char kUnescapeCode[] = "&><\"\'\\";
const char kEscapeCode[] PROGMEM = "&amp;|&gt;|&lt;|&quot;|&apos;|&#92;";
String HtmlEscape(const String unescaped) {
char escaped[10];
size_t ulen = unescaped.length();
String result;
result.reserve(ulen); // pre-reserve the required space to avoid mutiple reallocations
for (size_t i = 0; i < ulen; i++) {
char c = unescaped[i];
char* p = strchr(kUnescapeCode, c);
if (p != nullptr) {
result += GetTextIndexed(escaped, sizeof(escaped), p - kUnescapeCode, kEscapeCode);
} else {
result += c;
}
}
return result;
}
void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_payload = nullptr, const char* log_data_retained = nullptr) {
// Store log_data in buffer
// To lower heap usage log_data_payload may contain the payload data from MqttPublishPayload()
// and log_data_retained may contain optional retained message from MqttPublishPayload()
# ifdef ESP32
// this takes the mutex, and will be release when the class is destroyed -
// i.e. when the functon leaves You CAN call mutex.give() to leave early.
TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex);
# endif // ESP32
char empty[2] = {0};
if (!log_data_payload) {
log_data_payload = empty;
}
if (!log_data_retained) {
log_data_retained = empty;
}
if (!log_buffer) {
return;
} // Leave now if there is no buffer available
// Delimited, zero-terminated buffer of log lines.
// Each entry has this format: [index][loglevel][log data]['\1']
// Truncate log messages longer than MAX_LOGSZ which is the log buffer size minus 64 spare
uint32_t log_data_len = strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained);
char too_long[TOPSZ];
if (log_data_len > MAX_LOGSZ) {
snprintf_P(too_long, sizeof(too_long) - 20, PSTR("%s%s"), log_data, log_data_payload); // 20 = strlen("... 123456 truncated")
snprintf_P(too_long, sizeof(too_long), PSTR("%s... %d truncated"), too_long, log_data_len);
log_data = too_long;
log_data_payload = empty;
log_data_retained = empty;
}
log_buffer_pointer &= 0xFF;
if (!log_buffer_pointer) {
log_buffer_pointer++; // Index 0 is not allowed as it is the end of char string
}
while (log_buffer_pointer == log_buffer[0] || // If log already holds the next index, remove it
strlen(log_buffer) + strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained) + 4 > LOG_BUFFER_SIZE) // 4 = log_buffer_pointer + '\1' + '\0'
{
char* it = log_buffer;
it++; // Skip log_buffer_pointer
it += strchrspn(it, '\1'); // Skip log line
it++; // Skip delimiting "\1"
memmove(log_buffer, it, LOG_BUFFER_SIZE - (it - log_buffer)); // Move buffer forward to remove oldest log line
}
snprintf_P(log_buffer, LOG_BUFFER_SIZE, PSTR("%s%c%c%s%s%s%s\1"),
log_buffer, log_buffer_pointer++, '0' + loglevel, "", log_data, log_data_payload, log_data_retained);
log_buffer_pointer &= 0xFF;
if (!log_buffer_pointer) {
log_buffer_pointer++; // Index 0 is not allowed as it is the end of char string
}
}
bool GetLog(uint32_t req_loglevel, uint32_t* index_p, char** entry_pp, size_t* len_p) {
if (!log_buffer) {
return false;
} // Leave now if there is no buffer available
if (uptime() < 3) {
return false;
} // Allow time to setup correct log level
uint32_t index = *index_p;
if (!req_loglevel || (index == log_buffer_pointer)) {
return false;
}
# ifdef ESP32
// this takes the mutex, and will be release when the class is destroyed -
// i.e. when the functon leaves You CAN call mutex.give() to leave early.
TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex);
# endif // ESP32
if (!index) { // Dump all
index = log_buffer[0];
}
do {
size_t len = 0;
uint32_t loglevel = 0;
char* entry_p = log_buffer;
do {
uint32_t cur_idx = *entry_p;
entry_p++;
size_t tmp = strchrspn(entry_p, '\1');
tmp++; // Skip terminating '\1'
if (cur_idx == index) { // Found the requested entry
loglevel = *entry_p - '0';
entry_p++; // Skip loglevel
len = tmp - 1;
break;
}
entry_p += tmp;
} while (entry_p < log_buffer + LOG_BUFFER_SIZE && *entry_p != '\0');
index++;
if (index > 255) {
index = 1;
} // Skip 0 as it is not allowed
*index_p = index;
if ((len > 0) &&
(loglevel <= req_loglevel) &&
(masterlog_level <= req_loglevel)) {
*entry_pp = entry_p;
*len_p = len;
return true;
}
delay(0);
} while (index != log_buffer_pointer);
return false;
}
bool NeedLogRefresh(uint32_t req_loglevel, uint32_t index) {
if (!log_buffer) {
return false;
} // Leave now if there is no buffer available
# ifdef ESP32
// this takes the mutex, and will be release when the class is destroyed -
// i.e. when the functon leaves You CAN call mutex.give() to leave early.
TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex);
# endif // ESP32
// Skip initial buffer fill
if (strlen(log_buffer) < LOG_BUFFER_SIZE / 2) {
return false;
}
char* line;
size_t len;
if (!GetLog(req_loglevel, &index, &line, &len)) {
return false;
}
return ((line - log_buffer) < LOG_BUFFER_SIZE / 4);
}
/*------------------- Local functions ----------------------*/
# ifdef WEBUI_DEVELOPMENT
//format bytes
String formatBytes(size_t bytes) {
if (bytes < 1024) {
return String(bytes) + "B";
} else if (bytes < (1024 * 1024)) {
return String(bytes / 1024.0) + "KB";
} else if (bytes < (1024 * 1024 * 1024)) {
return String(bytes / 1024.0 / 1024.0) + "MB";
} else {
return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
}
}
bool exists(String path) {
bool yes = false;
File file = FILESYSTEM.open(path, "r");
if (!file.isDirectory()) {
yes = true;
}
file.close();
return yes;
}
# endif
/*------------------- Chunked Response Helpers ----------------------*/
// Send HTML pages in chunks to avoid large String heap allocations.
// Each helper puts a temporary buffer on its own stack frame, sends
// the chunk, then the buffer is freed when the helper returns.
static void beginChunkedResponse() {
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", "");
}
static void sendHeaderChunk(const char* title) {
char buffer[768];
snprintf(buffer, sizeof(buffer), header_html, title);
server.sendContent(buffer);
}
static void sendBodyChunk(const char* fmt, ...) {
char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, fmt, args);
va_end(args);
server.sendContent(buffer);
}
static void sendFooterChunk() {
char buffer[256];
snprintf(buffer, sizeof(buffer), footer, OMG_VERSION);
server.sendContent(buffer);
server.sendContent(""); // End chunked transfer
}
/**
* @brief / - Page
*
*/
void handleRoot() {
WEBUI_TRACE_LOG(F("handleRoot: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("m")) {
if (currentWebUIMessage) {
server.send(200, "application/json", "{t}{s}<b>" + String(currentWebUIMessage->title) + "</b>{e}{s}" + String(currentWebUIMessage->line1) + "{e}{s}" + String(currentWebUIMessage->line2) + "{e}{s}" + String(currentWebUIMessage->line3) + "{e}{s}" + String(currentWebUIMessage->line4) + "{e}</table>");
} else {
server.send(200, "application/json", "{t}{s}Uptime:{m}" + String(uptime()) + "{e}</table>");
}
} else if (server.hasArg("rst")) { // TODO: This should redirect to the RST page
THEENGS_LOG_WARNING(F("[WebUI] Restart" CR));
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Restart").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Restart");
sendFooterChunk();
delay(2000); // Wait for web page to be sent before
ESPRestart(5);
} else {
// WEBUI_TRACE_LOG(F("Arguments %s" CR), message);
server.send(200, "text/plain", "00:14:36.767 RSL: RESULT = {\"Topic\":\"topic\"}");
}
} else {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Main Menu").c_str());
server.sendContent(root_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(root_body, jsonChar, gateway_name);
sendFooterChunk();
}
}
/**
* @brief /CN - Configuration Page
*
*/
void handleCN() {
WEBUI_SECURE
WEBUI_TRACE_LOG(F("handleCN: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleCN Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
} else {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configuration").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_body, jsonChar, gateway_name);
sendFooterChunk();
}
}
/**
* @brief /WU - Configuration Page
* T: handleWU: uri: /wu, args: 3, method: 1
* T: handleWU Arg: 0, dm=on - displayMetric
* T: handleWU Arg: 1, sw=on - webUISecure
* T: handleWU Arg: 2, save=
*/
void handleWU() {
WEBUI_TRACE_LOG(F("handleWU: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleWU Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
bool update = false;
if (displayMetric != server.hasArg("dm")) {
update = true;
}
displayMetric = server.hasArg("dm");
if (webUISecure != server.hasArg("sw")) {
update = true;
}
webUISecure = server.hasArg("sw");
if (server.hasArg("save") && update) {
WebUIConfig_save();
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure WebUI").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_webui_body, jsonChar, gateway_name, (displayMetric ? "checked" : ""), (webUISecure ? "checked" : ""));
sendFooterChunk();
}
/**
* @brief /WI - Configure WiFi Page
* T: handleWI: uri: /wi, args: 4, method: 1
* T: handleWI Arg: 0, s1=SSID
* T: handleWI Arg: 1, p1=xxxxxx
* T: handleWI Arg: 3, save=
*/
void handleWI() {
WEBUI_TRACE_LOG(F("handleWI: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
String WiFiScan = "";
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleWI Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("scan")) {
bool limitScannedNetworks = true;
int n = WiFi.scanNetworks();
WEBUI_TRACE_LOG(F("handleWI scan: found %d" CR), n);
if (0 == n) {
// WSContentSend_P(PSTR(D_NO_NETWORKS_FOUND));
// limitScannedNetworks = false; // in order to show D_SCAN_FOR_WIFI_NETWORKS
} else {
//sort networks
int indices[n];
for (uint32_t i = 0; i < n; i++) {
indices[i] = i;
}
// RSSI SORT
for (uint32_t i = 0; i < n; i++) {
for (uint32_t j = i + 1; j < n; j++) {
if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
std::swap(indices[i], indices[j]);
}
}
}
uint32_t networksToShow = n;
if ((limitScannedNetworks) && (networksToShow > MAX_WIFI_NETWORKS_TO_SHOW)) {
networksToShow = MAX_WIFI_NETWORKS_TO_SHOW;
}
for (uint32_t i = 0; i < n; i++) {
if (-1 == indices[i]) {
continue;
}
String cssid = WiFi.SSID(indices[i]);
uint32_t cschn = WiFi.channel(indices[i]);
for (uint32_t j = i + 1; j < n; j++) {
if ((cssid == WiFi.SSID(indices[j])) && (cschn == WiFi.channel(indices[j]))) {
WEBUI_TRACE_LOG(F("handleWI scan: duplicate %s" CR), WiFi.SSID(indices[j]).c_str());
indices[j] = -1; // set dup aps to index -1
}
}
}
//display networks in page
for (uint32_t i = 0; i < networksToShow; i++) {
if (-1 == indices[i]) {
continue;
} // skip dups
int32_t rssi = WiFi.RSSI(indices[i]);
WEBUI_TRACE_LOG(F("D_LOG_WIFI D_SSID %s, D_BSSID %s, D_CHANNEL %d, D_RSSI %d" CR),
WiFi.SSID(indices[i]).c_str(), WiFi.BSSIDstr(indices[i]).c_str(), WiFi.channel(indices[i]), rssi);
int quality = WifiGetRssiAsQuality(rssi);
String ssid_copy = WiFi.SSID(indices[i]);
if (!ssid_copy.length()) {
ssid_copy = F("no_name");
}
WiFiScan += "<div><a href='#p' onclick='c(this)'>" + HtmlEscape(ssid_copy) + "</a>&nbsp;(" + WiFi.channel(indices[i]) + ")&nbsp<span class='q'>" + quality + "% (" + rssi + " dBm)</span></div>";
}
}
WEBUI_TRACE_LOG(F("handleWI scan: results %s" CR), WiFiScan.c_str());
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure WiFi").c_str());
server.sendContent(wifi_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_wifi_body, jsonChar, gateway_name, WiFiScan.c_str(), WiFi.SSID().c_str());
sendFooterChunk();
return;
} else if (server.hasArg("save")) {
StaticJsonDocument<JSON_MSG_BUFFER> WEBtoSYSBuffer;
JsonObject WEBtoSYS = WEBtoSYSBuffer.to<JsonObject>();
bool update = false;
if (server.hasArg("s1")) {
WEBtoSYS["wifi_ssid"] = server.arg("s1");
if (strncmp((char*)WiFi.SSID().c_str(), server.arg("s1").c_str(), parameters_size)) {
update = true;
}
}
if (server.hasArg("p1")) {
WEBtoSYS["wifi_pass"] = server.arg("p1");
if (strncmp((char*)WiFi.psk().c_str(), server.arg("p1").c_str(), parameters_size)) {
update = true;
}
}
if (update) {
String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSset);
THEENGS_LOG_WARNING(F("[WebUI] Save WiFi and Restart" CR));
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Save WiFi and Restart").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Save WiFi and Restart");
sendFooterChunk();
delay(2000); // Wait for web page to be sent before
XtoSYS((char*)topic.c_str(), WEBtoSYS);
return;
} else {
THEENGS_LOG_WARNING(F("[WebUI] No changes" CR));
}
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure WiFi").c_str());
server.sendContent(wifi_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_wifi_body, jsonChar, gateway_name, WiFiScan.c_str(), WiFi.SSID().c_str());
sendFooterChunk();
}
/**
* @brief /MQ - Configure MQTT Page
* T: handleMQ: uri: /mq, args: 8, method: 1
* T: handleMQ Arg: 0, mh=192.168.1.11
* T: handleMQ Arg: 1, ml=1883
* T: handleMQ Arg: 2, mu=1234
* T: handleMQ Arg: 3, mp=
* T: handleMQ Arg: 4, sc=on
* T: handleMQ Arg: 5, h=
* T: handleMQ Arg: 6, mt=home/
* T: handleMQ Arg: 7 dp=homeassistant (#ifdef ZmqttDiscovery)
* T: handleMQ Arg: 8, save=
*/
void handleMQ() {
WEBUI_TRACE_LOG(F("handleMQ: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleMQ Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("save")) {
StaticJsonDocument<JSON_MSG_BUFFER> WEBtoSYSBuffer;
JsonObject WEBtoSYS = WEBtoSYSBuffer.to<JsonObject>();
bool update = false;
# if !MQTT_BROKER_MODE
if (server.hasArg("mh")) {
WEBtoSYS["mqtt_server"] = server.arg("mh");
if (strncmp(cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_server, server.arg("mh").c_str(), parameters_size)) {
update = true;
}
}
if (server.hasArg("ml")) {
WEBtoSYS["mqtt_port"] = server.arg("ml");
if (strncmp(cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_port, server.arg("ml").c_str(), 6)) {
update = true;
}
}
if (server.hasArg("mu")) {
WEBtoSYS["mqtt_user"] = server.arg("mu");
if (strncmp(cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_user, server.arg("mu").c_str(), parameters_size)) {
update = true;
}
}
if (server.hasArg("mp")) {
WEBtoSYS["mqtt_pass"] = server.arg("mp");
if (strncmp(cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_pass, server.arg("mp").c_str(), parameters_size)) {
update = true;
}
}
// SC - Secure Connection argument is only present when true
if (cnt_parameters_array[CNT_DEFAULT_INDEX].isConnectionSecure != server.hasArg("sc")) {
update = true;
}
WEBtoSYS["mqtt_secure"] = server.hasArg("sc");
if (!update) {
THEENGS_LOG_WARNING(F("[WebUI] clearing" CR));
for (JsonObject::iterator it = WEBtoSYS.begin(); it != WEBtoSYS.end(); ++it) {
WEBtoSYS.remove(it);
}
}
# endif
if (server.hasArg("h")) {
WEBtoSYS["gateway_name"] = server.arg("h");
if (strncmp(gateway_name, server.arg("h").c_str(), parameters_size)) {
update = true;
}
}
if (server.hasArg("mt")) {
WEBtoSYS["mqtt_topic"] = server.arg("mt");
if (strncmp(mqtt_topic, server.arg("mt").c_str(), parameters_size)) {
update = true;
}
}
# ifdef ZmqttDiscovery
if (server.hasArg("dp")) {
WEBtoSYS["discovery_prefix"] = server.arg("dp");
if (strncmp(discovery_prefix, server.arg("dp").c_str(), parameters_size)) {
update = true;
}
}
# endif
# ifndef ESPWifiManualSetup
if (update) {
THEENGS_LOG_WARNING(F("[WebUI] Save MQTT and Reconnect" CR));
WEBtoSYS["cnt_index"] = CNT_DEFAULT_INDEX;
WEBtoSYS["save_cnt"] = true;
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Save MQTT and Reconnect").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Save MQTT and Reconnect");
sendFooterChunk();
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);
return;
} else {
THEENGS_LOG_WARNING(F("[WebUI] No changes" CR));
}
# endif
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure MQTT").c_str());
server.sendContent(script);
server.sendContent(style);
// mqtt server (mh), mqtt port (ml), mqtt username (mu), mqtt password (mp), secure connection (sc), server certificate (msc), mqtt topic (mt), discovery prefix (dp) (last one only #ifdef ZmqttDiscovery)
# if MQTT_BROKER_MODE
# ifdef ZmqttDiscovery
sendBodyChunk(config_mqtt_body, jsonChar, gateway_name, "", "1883", "", "", gateway_name, mqtt_topic, discovery_prefix);
# else
sendBodyChunk(config_mqtt_body, jsonChar, gateway_name, "", "1883", "", "", gateway_name, mqtt_topic);
# endif
# else
# ifdef ZmqttDiscovery
sendBodyChunk(config_mqtt_body, jsonChar, gateway_name, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_server, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_port, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_user, (cnt_parameters_array[CNT_DEFAULT_INDEX].isConnectionSecure ? "checked" : ""), gateway_name, mqtt_topic, discovery_prefix);
# else
sendBodyChunk(config_mqtt_body, jsonChar, gateway_name, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_server, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_port, cnt_parameters_array[CNT_DEFAULT_INDEX].mqtt_user, (cnt_parameters_array[CNT_DEFAULT_INDEX].isConnectionSecure ? "checked" : ""), gateway_name, mqtt_topic);
# endif
# endif
sendFooterChunk();
}
# ifndef ESPWifiManualSetup
/**
* @brief /CG - Configure gateway Page
* T: handleCG: uri: /gw, args: 2, method: 1
* T: handleCG Arg: 0, gp=1234
* T: handleCG Arg: 1, save=
*/
void handleCG() {
WEBUI_TRACE_LOG(F("handleCG: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
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")) {
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 (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);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Save and Restart").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Save and Restart");
sendFooterChunk();
if (pwUpdate) {
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject WEBtoSYS = jsonBuffer.to<JsonObject>();
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];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure gateway").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_gateway_body, jsonChar, gateway_name
# ifdef ZmqttDiscovery
,
(SYSConfig.discovery ? "checked" : "")
# endif
);
sendFooterChunk();
}
# endif
/**
* @brief /LO - Configure Logging Page
* T: handleLO: uri: /lo, args: 2, method: 1
* T: handleLO Arg: 0, lo=5
* T: handleLO Arg: 1, save=
*/
void handleLO() {
WEBUI_TRACE_LOG(F("handleLO: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleLO Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("save") && server.hasArg("lo") && server.arg("lo").toInt() != Log.getLevel()) {
Log.fatal(F("[WebUI] Log level changed to: %d" CR), server.arg("lo").toInt());
Log.setLevel(server.arg("lo").toInt());
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure Logging").c_str());
server.sendContent(script);
server.sendContent(style);
int logLevel = Log.getLevel();
sendBodyChunk(config_logging_body, jsonChar, gateway_name, (logLevel == 0 ? "selected" : ""), (logLevel == 1 ? "selected" : ""), (logLevel == 2 ? "selected" : ""), (logLevel == 3 ? "selected" : ""), (logLevel == 4 ? "selected" : ""), (logLevel == 5 ? "selected" : ""), (logLevel == 6 ? "selected" : ""));
sendFooterChunk();
}
# 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
*/
void handleBL() {
WEBUI_TRACE_LOG(F("handleBL: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
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());
}
if (server.hasArg("save")) {
// 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<JSON_MSG_BUFFER> jsonBuffer;
JsonObject WEBtoBT = jsonBuffer.to<JsonObject>();
// 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();
}
// 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<JSON_MSG_BUFFER> sysBuffer;
JsonObject WEBtoSYS = sysBuffer.to<JsonObject>();
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
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure BLE").c_str());
if (strlen(ble_script) > 0) {
server.sendContent(ble_script);
}
server.sendContent(script);
server.sendContent(style);
// Scan settings
sendBodyChunk(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);
// Publish settings
sendBodyChunk(config_ble_body_publish,
(BTConfig.pubOnlySensors ? "checked" : ""),
(BTConfig.pubRandomMACs ? "checked" : ""),
(BTConfig.pubAdvData ? "checked" : ""),
(BTConfig.pubBeaconUuidForTopic ? "checked" : ""),
(BTConfig.filterConnectable ? "checked" : ""),
(BTConfig.ignoreWBlist ? "checked" : ""));
// Presence settings
sendBodyChunk(config_ble_body_presence,
(BTConfig.presenceEnable ? "checked" : ""),
(BTConfig.presenceUseBeaconUuid ? "checked" : ""),
BTConfig.presenceTopic.c_str(),
BTConfig.presenceAwayTimer,
BTConfig.movingTimer);
// External decoder settings
sendBodyChunk(config_ble_body_decoder,
(BTConfig.extDecoderEnable ? "checked" : ""),
BTConfig.extDecoderTopic.c_str(),
BTConfig.intervalConnect);
# if BLEDecryptor
// Encryption settings
std::string aeskeysstring;
JsonObject root = ble_aes_keys.as<JsonObject>();
for (JsonPair kv : root) {
aeskeysstring += kv.key().c_str();
aeskeysstring += ":";
aeskeysstring += kv.value().as<const char*>();
aeskeysstring += " ";
}
if (!aeskeysstring.empty()) {
aeskeysstring.pop_back();
}
sendBodyChunk(config_ble_body_encrypt,
ble_aes, aeskeysstring.c_str());
# endif
// Save button + footer
server.sendContent(config_ble_body_footer);
sendFooterChunk();
}
# endif
# ifdef ZgatewayLORA
# include "config_LORA.h"
extern void LORAConfig_fromJson(JsonObject& LORAdata);
extern String stateLORAMeasures();
extern LORAConfig_s LORAConfig;
/**
* @brief /LA - Configure LORA Page
* T: handleLA: uri: /la, args: 11, method: 1
* T: handleLA Arg: 0, lf=868100000
* T: handleLA Arg: 1, lt=14
* T: handleLA Arg: 2, ls=12
* T: handleLA Arg: 3, lb=125
* T: handleLA Arg: 4, lc=5
* T: handleLA Arg: 5, ll=8
* T: handleLA Arg: 6, lw=0
* T: handleLA Arg: 7, lr=1
* T: handleLA Arg: 8, li=0
* T: handleLA Arg: 9, ok=0
* T: handleLA Arg: 10, save=
*/
void handleLA() {
WEBUI_TRACE_LOG(F("handleLA: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleLA Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("save")) {
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject WEBtoLORA = jsonBuffer.to<JsonObject>();
bool update = false;
if (server.hasArg("lf")) {
WEBtoLORA["frequency"] = server.arg("lf");
update = true;
}
if (server.hasArg("lt")) {
WEBtoLORA["txpower"] = server.arg("lt");
update = true;
}
if (server.hasArg("ls")) {
WEBtoLORA["spreadingfactor"] = server.arg("ls");
update = true;
}
if (server.hasArg("lb")) {
WEBtoLORA["signalbandwidth"] = server.arg("lb");
update = true;
}
if (server.hasArg("lc")) {
WEBtoLORA["codingrate"] = server.arg("lc");
update = true;
}
if (server.hasArg("ll")) {
WEBtoLORA["preamblelength"] = server.arg("ll");
update = true;
}
if (server.hasArg("lw")) {
WEBtoLORA["syncword"] = server.arg("lw");
update = true;
}
if (server.hasArg("lr")) {
WEBtoLORA["enablecrc"] = server.arg("lr");
update = true;
} else {
WEBtoLORA["enablecrc"] = false;
update = true;
}
if (server.hasArg("li")) {
WEBtoLORA["invertiq"] = server.arg("li");
update = true;
} else {
WEBtoLORA["invertiq"] = false;
update = true;
}
if (server.hasArg("ok")) {
WEBtoLORA["onlyknown"] = server.arg("ok");
update = true;
} else {
WEBtoLORA["onlyknown"] = false;
update = true;
}
if (update) {
THEENGS_LOG_NOTICE(F("[WebUI] Save data" CR));
WEBtoLORA["save"] = true;
LORAConfig_fromJson(WEBtoLORA);
stateLORAMeasures();
THEENGS_LOG_TRACE(F("[WebUI] LORAConfig end" CR));
}
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure LORA").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_lora_body,
jsonChar,
gateway_name,
LORAConfig.frequency == 868000000 ? "selected" : "",
LORAConfig.frequency == 915000000 ? "selected" : "",
LORAConfig.frequency == 433000000 ? "selected" : "",
LORAConfig.txPower == 0 ? "selected" : "",
LORAConfig.txPower == 1 ? "selected" : "",
LORAConfig.txPower == 2 ? "selected" : "",
LORAConfig.txPower == 3 ? "selected" : "",
LORAConfig.txPower == 4 ? "selected" : "",
LORAConfig.txPower == 5 ? "selected" : "",
LORAConfig.txPower == 6 ? "selected" : "",
LORAConfig.txPower == 7 ? "selected" : "",
LORAConfig.txPower == 8 ? "selected" : "",
LORAConfig.txPower == 9 ? "selected" : "",
LORAConfig.txPower == 10 ? "selected" : "",
LORAConfig.txPower == 11 ? "selected" : "",
LORAConfig.txPower == 12 ? "selected" : "",
LORAConfig.txPower == 13 ? "selected" : "",
LORAConfig.txPower == 14 ? "selected" : "",
LORAConfig.spreadingFactor == 7 ? "selected" : "",
LORAConfig.spreadingFactor == 8 ? "selected" : "",
LORAConfig.spreadingFactor == 9 ? "selected" : "",
LORAConfig.spreadingFactor == 10 ? "selected" : "",
LORAConfig.spreadingFactor == 11 ? "selected" : "",
LORAConfig.spreadingFactor == 12 ? "selected" : "",
LORAConfig.signalBandwidth == 7800 ? "selected" : "",
LORAConfig.signalBandwidth == 10400 ? "selected" : "",
LORAConfig.signalBandwidth == 15600 ? "selected" : "",
LORAConfig.signalBandwidth == 20800 ? "selected" : "",
LORAConfig.signalBandwidth == 31250 ? "selected" : "",
LORAConfig.signalBandwidth == 41700 ? "selected" : "",
LORAConfig.signalBandwidth == 62500 ? "selected" : "",
LORAConfig.signalBandwidth == 125000 ? "selected" : "",
LORAConfig.signalBandwidth == 250000 ? "selected" : "",
LORAConfig.signalBandwidth == 500000 ? "selected" : "",
LORAConfig.codingRateDenominator == 5 ? "selected" : "",
LORAConfig.codingRateDenominator == 6 ? "selected" : "",
LORAConfig.codingRateDenominator == 7 ? "selected" : "",
LORAConfig.codingRateDenominator == 8 ? "selected" : "",
LORAConfig.preambleLength,
LORAConfig.syncWord,
LORAConfig.crc ? "checked" : "",
LORAConfig.invertIQ ? "checked" : "",
LORAConfig.onlyKnown ? "checked" : "");
sendFooterChunk();
}
# elif defined(ZgatewayRTL_433) || defined(ZgatewayPilight) || defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZactuatorSomfy)
# include <map>
# include "rf/RFConfiguration.h"
std::map<int, String> activeReceiverOptions = {
{0, "Inactive"},
# if defined(ZgatewayPilight) && !defined(ZradioSX127x)
{1, "PiLight"},
# endif
# if defined(ZgatewayRF) && !defined(ZradioSX127x)
{2, "RF"},
# endif
# ifdef ZgatewayRTL_433
{3, "RTL_433"},
# endif
# if defined(ZgatewayRF2) && !defined(ZradioSX127x)
{4, "RF2 (restart required)"}
# endif
};
extern RFConfiguration iRFConfig;
bool isValidReceiver(int receiverId) {
// Check if the receiverId exists in the activeReceiverOptions map
return activeReceiverOptions.find(receiverId) != activeReceiverOptions.end();
}
String generateActiveReceiverOptions(int currentSelection) {
String optionsHtml = "";
for (const auto& option : activeReceiverOptions) {
optionsHtml += "<option value='" + String(option.first) + "'";
if (currentSelection == option.first) {
optionsHtml += " selected";
}
optionsHtml += ">" + option.second + "</option>";
}
return optionsHtml;
}
/**
* @brief /RF - Configure RF Page
* T: handleRF: uri: /rf, args: 2, method: 1
* T: handleRF Arg: 0, rf=868.30
* T: handleRF Arg: 1, oo=0
* T: handleRF Arg: 2, rs=0
* T: handleRF Arg: 3, dg=0
* T: handleRF Arg: 4, ar=0
* T: handleRF Arg: 4, save=
* TODO: need a review, it's a bit strance set the config in the iRFConfig attribute and then
* setup a message and finally call the loadFromMessage
*/
void handleRF() {
WEBUI_TRACE_LOG(F("handleRF: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
bool update = false;
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject WEBtoRF = jsonBuffer.to<JsonObject>();
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleRF Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("save")) {
if (server.hasArg("rf")) {
String freqStr = server.arg("rf");
float freq = freqStr.toFloat();
if (iRFConfig.validFrequency(freq)) {
iRFConfig.setFrequency(freq);
WEBtoRF["frequency"] = iRFConfig.getFrequency();
update = true;
} else {
THEENGS_LOG_WARNING(F("[WebUI] Invalid Frequency" CR));
}
}
if (server.hasArg("ar")) {
int selectedReceiver = server.arg("ar").toInt();
if (isValidReceiver(selectedReceiver)) { // Assuming isValidReceiver is a validation function
iRFConfig.setActiveReceiver(selectedReceiver);
WEBtoRF["activereceiver"] = iRFConfig.getActiveReceiver();
update = true;
} else {
THEENGS_LOG_WARNING(F("[WebUI] Invalid Active Receiver" CR));
}
}
if (server.hasArg("oo")) {
iRFConfig.setNewOokThreshold(server.arg("oo").toInt());
WEBtoRF["ookthreshold"] = iRFConfig.getNewOokThreshold();
update = true;
}
if (server.hasArg("rs")) {
iRFConfig.setRssiThreshold(server.arg("rs").toInt());
WEBtoRF["rssithreshold"] = iRFConfig.getRssiThreshold();
update = true;
}
if (update) {
THEENGS_LOG_NOTICE(F("[WebUI] Save data" CR));
WEBtoRF["save"] = true;
iRFConfig.loadFromMessage(WEBtoRF);
stateRFMeasures();
THEENGS_LOG_TRACE(F("[WebUI] RFConfig end" CR));
}
}
}
String activeReceiverHtml = generateActiveReceiverOptions(iRFConfig.getActiveReceiver());
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure RF").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(config_rf_body, jsonChar, gateway_name, iRFConfig.getFrequency(), activeReceiverHtml.c_str());
sendFooterChunk();
}
# endif
/**
* @brief /RT - Reset configuration ( Erase and Restart ) from Configuration menu
*
*/
void handleRT() {
WEBUI_TRACE_LOG(F("handleRT: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleRT Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
}
if (server.hasArg("non")) {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
THEENGS_LOG_WARNING(F("[WebUI] Erase and Restart" CR));
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Erase and Restart").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Erase and Restart");
sendFooterChunk();
eraseConfig();
} else {
handleCN();
}
}
# if defined(ZgatewayCloud)
/**
* @brief /CL - Cloud Configuration
*
*/
void handleCL() {
WEBUI_TRACE_LOG(F("handleCL: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleCL Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
}
if (server.hasArg("save")) {
// T: handleCL: uri: /cl, args: 2, method: 1
// T: handleCL Arg: 0, cl-en=on
// T: handleCL Arg: 1, save=
if (server.hasArg("save") && server.method() == 1) {
setCloudEnabled(server.hasArg("cl-en"));
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Configure Cloud").c_str());
server.sendContent(script);
server.sendContent(style);
char cloudEnabled[8] = {0};
if (isCloudEnabled()) {
strncpy(cloudEnabled, "checked", 8);
}
char deviceToken[5] = {0};
if (!isCloudDeviceTokenSupplied()) {
strncpy(deviceToken, " Not", 4);
}
requestToken = esp_random();
# ifdef ESP32_ETHERNET
sendBodyChunk(config_cloud_body, jsonChar, gateway_name, " cloud checked", " Not", (String(CLOUDGATEWAY) + "token/start").c_str(), (char*)ETH.macAddress().c_str(), ("http://" + String(TheengsUtils::ip2CharArray(ETH.localIP())) + "/").c_str(), gateway_name, uptime(), requestToken);
# else
sendBodyChunk(config_cloud_body, jsonChar, gateway_name, cloudEnabled, deviceToken, (String(CLOUDGATEWAY) + "token/start").c_str(), (char*)WiFi.macAddress().c_str(), ("http://" + String(TheengsUtils::ip2CharArray(WiFi.localIP())) + "/").c_str(), gateway_name, uptime(), requestToken);
# endif
sendFooterChunk();
}
/**
* @brief /TK - Receive Cloud Device Token
*
*/
void handleTK() {
WEBUI_TRACE_LOG(F("handleTK: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleTK Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
}
if (server.hasArg("deviceToken") && server.hasArg("uptime") && server.hasArg("RT")) {
String deviceToken = server.arg("deviceToken");
if (setCloudDeviceToken(deviceToken) && server.arg("RT").toInt() == requestToken && server.arg("uptime").toInt() + 600 > uptime()) {
setCloudEnabled(true);
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Received Device Token").c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(token_body, jsonChar, gateway_name);
sendFooterChunk();
} else {
WEBUI_TRACE_LOG(F("handleTK: uptime: %u, uptime: %u, ok: %T" CR), server.arg("uptime").toInt(), uptime(), server.arg("uptime").toInt() + 600 > uptime());
WEBUI_TRACE_LOG(F("handleTK: RT: %d, RT: %d, ok: %T " CR), server.arg("RT").toInt(), requestToken, server.arg("RT").toInt() == requestToken);
THEENGS_LOG_ERROR(F("[WebUI] Invalid Token Response: RT: %T, uptime: %T" CR), server.arg("RT").toInt() == requestToken, server.arg("uptime").toInt() + 600 > uptime());
server.send(500, "text/html", "Internal ERROR - Invalid Token");
}
}
}
# endif
/**
* @brief /IN - Information Page
*
*/
void handleIN() {
WEBUI_TRACE_LOG(F("handleCN: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleIN Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
} else {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
String informationDisplay = stateMeasures(); // .replace(",\"", "}1"); // .replace("\":", "=2")
// }1 json-oled }2 true } }1 Cloud }2 cloudEnabled}2true}1c
# if defined(ZgatewayBT)
String stateBTMeasures(bool start);
informationDisplay += "1<BR>BT}2}1"; // }1 the bracket is not needed as the previous message ends with }
informationDisplay += stateBTMeasures(false);
# endif
# if defined(ZdisplaySSD1306)
informationDisplay += "1<BR>SSD1306}2}1"; // }1 the bracket is not needed as the previous message ends with }
informationDisplay += stateSSD1306Display();
# endif
# if defined(ZgatewayCloud)
informationDisplay += "1<BR>Cloud}2}1";
informationDisplay += stateCLOUDStatus();
# endif
# if defined(ZgatewayLORA)
informationDisplay += "1<BR>LORA}2}1";
informationDisplay += stateLORAMeasures();
# endif
# if defined(ZgatewayRF)
informationDisplay += "1<BR>RF}2}1";
informationDisplay += stateRFMeasures();
# endif
informationDisplay += "1<BR>WebUI}2}1";
informationDisplay += stateWebUIStatus();
// stateBTMeasures causes a Stack canary watchpoint triggered (loopTask)
// WEBUI_TRACE_LOG(F("[WebUI] informationDisplay before %s" CR), informationDisplay.c_str());
// TODO: need to fix display of modules array within SYStoMQTT
informationDisplay += "1}2";
informationDisplay.replace(",\"", "}1");
informationDisplay.replace("\":", "}2");
informationDisplay.replace("{\"", "");
informationDisplay.replace("\"", "\\\"");
// WEBUI_TRACE_LOG(F("[WebUI] informationDisplay after %s" CR), informationDisplay.c_str());
if (informationDisplay.length() > WEB_TEMPLATE_BUFFER_MAX_SIZE) {
THEENGS_LOG_WARNING(F("[WebUI] informationDisplay content length ( %d ) greater than WEB_TEMPLATE_BUFFER_MAX_SIZE. Display truncated" CR), informationDisplay.length());
}
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Information").c_str());
sendBodyChunk(information_script, informationDisplay.c_str());
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(information_body, jsonChar, gateway_name);
sendFooterChunk();
}
}
/**
* @brief /handleFavicon - Send Favicon
*
*/
void handleFavicon() {
WEBUI_TRACE_LOG(F("handleCN: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
server.sendHeader("Content-Type", "image/x-icon");
server.send_P(200, "image/x-icon", reinterpret_cast<const char*>(Openmqttgateway_logo_mini_ico), sizeof(Openmqttgateway_logo_mini_ico));
}
# if defined(ESP32) && defined(MQTT_HTTPS_FW_UPDATE)
/**
* @brief /UP - Firmware Upgrade Page
*
*/
void handleUP() {
WEBUI_TRACE_LOG(F("handleUP: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
if (server.args()) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleUP Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
DynamicJsonDocument jsonBuffer(JSON_MSG_BUFFER);
JsonObject WEBtoSYS = jsonBuffer.to<JsonObject>();
if (server.hasArg("o")) {
WEBtoSYS["url"] = server.arg("o");
WEBtoSYS["version"] = "test";
WEBtoSYS["password"] = ota_pass;
{
sendRestartPage();
String output;
serializeJson(WEBtoSYS, output);
THEENGS_LOG_NOTICE(F("[WebUI] XtoSYSupdate %s" CR), output.c_str());
}
String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSupdate);
MQTTHttpsFWUpdate((char*)topic.c_str(), WEBtoSYS);
return;
} else if (server.hasArg("le")) {
uint32_t le = server.arg("le").toInt();
if (le != 0) {
WEBtoSYS["version"] = (le == 1 ? "latest" : (le == 2 ? "dev" : "unknown"));
WEBtoSYS["password"] = ota_pass;
{
sendRestartPage();
String output;
serializeJson(WEBtoSYS, output);
THEENGS_LOG_NOTICE(F("[WebUI] XtoSYSupdate %s" CR), output.c_str());
}
String topic = String(mqtt_topic) + String(gateway_name) + String(subjectMQTTtoSYSupdate);
MQTTHttpsFWUpdate((char*)topic.c_str(), WEBtoSYS);
return;
}
}
}
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Firmware Upgrade").c_str());
server.sendContent(script);
server.sendContent(style);
String systemUrl = RELEASE_LINK + latestVersion + "/" + ENV_NAME + "-firmware.bin";
sendBodyChunk(upgrade_body, jsonChar, gateway_name, systemUrl.c_str());
sendFooterChunk();
}
# endif
void sendRestartPage() {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Updating Firmware and Restart").c_str());
server.sendContent(restart_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(reset_body, jsonChar, gateway_name, "Updating Firmware and Restart");
sendFooterChunk();
delay(2000); // Wait for web page to be sent before
}
/**
* @brief /CS - Serial Console and Command Line
*
*/
void handleCS() {
WEBUI_TRACE_LOG(F("handleCS: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
WEBUI_SECURE
ensureLogBuffer();
if (server.args() && server.hasArg("c2")) {
for (uint8_t i = 0; i < server.args(); i++) {
WEBUI_TRACE_LOG(F("handleCS Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str());
}
if (server.hasArg("c1")) {
String c1 = server.arg("c1");
String cmdTopic = String(mqtt_topic) + String(gateway_name) + "/" + c1.substring(0, c1.indexOf(' '));
String command = c1.substring(c1.indexOf(' ') + 1);
if (command.length()) {
WEBUI_TRACE_LOG(F("[WebUI] handleCS inject MQTT Command topic: '%s', command: '%s'" CR), cmdTopic.c_str(), command.c_str());
receivingDATA(cmdTopic.c_str(), command.c_str());
} else {
THEENGS_LOG_WARNING(F("[WebUI] Missing command: '%s', command: '%s'" CR), cmdTopic.c_str(), command.c_str());
}
}
uint32_t index = server.arg("c2").toInt();
String message = String(log_buffer_pointer) + "}1" + String(reset_web_log_flag) + "}1";
if (!reset_web_log_flag) {
index = 0;
reset_web_log_flag = true;
}
bool cflg = (index);
char* line;
size_t len;
while (GetLog(1, &index, &line, &len)) {
if (cflg) {
message += "\n";
}
for (int x = 0; x < len - 1; x++) {
message += line[x];
}
cflg = true;
}
message += "}1";
server.send(200, "text/plain", message);
} else {
char jsonChar[100];
serializeJson(modules, jsonChar, measureJson(modules) + 1);
beginChunkedResponse();
sendHeaderChunk((String(gateway_name) + " - Console").c_str());
server.sendContent(console_script);
server.sendContent(script);
server.sendContent(style);
sendBodyChunk(console_body, jsonChar, gateway_name);
sendFooterChunk();
}
}
/**
* @brief Page not found handler
*
*/
void notFound() {
WEBUI_SECURE
# ifdef WEBUI_DEVELOPMENT
String path = server.uri();
if (!exists(path)) {
if (exists(path + ".html")) {
path += ".html";
} else {
# endif
THEENGS_LOG_WARNING(F("[WebUI] notFound: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method());
server.send(404, "text/plain", "Not found");
return;
# ifdef WEBUI_DEVELOPMENT
}
}
WEBUI_TRACE_LOG(F("notFound returning: actual uri: %s, args: %d, method: %d" CR), path, server.args(), server.method());
File file = FILESYSTEM.open(path, "r");
server.streamFile(file, "text/html");
file.close();
# endif
}
void WebUISetup() {
WEBUI_TRACE_LOG(F("webUI setup start" CR));
WebUIConfig_load();
webUIQueue = xQueueCreate(5, sizeof(webUIQueueMessage*));
# ifdef WEBUI_DEVELOPMENT
FILESYSTEM.begin();
{
File root = FILESYSTEM.open("/");
File file = root.openNextFile();
while (file) {
String fileName = file.name();
size_t fileSize = file.size();
WEBUI_TRACE_LOG(F("FS File: %s, size: %s" CR), fileName.c_str(), formatBytes(fileSize).c_str());
file = root.openNextFile();
}
}
# endif
server.onNotFound(notFound);
server.on("/", handleRoot); // Main Menu
server.on("/in", handleIN); // Information
server.on("/cs", handleCS); // Console
# if defined(ESP32) && defined(MQTT_HTTPS_FW_UPDATE)
server.on("/up", handleUP); // Firmware Upgrade
# endif
server.on("/cn", handleCN); // Configuration
server.on("/wi", HTTP_POST, handleWI); // Configure Wifi
server.on("/mq", HTTP_POST, handleMQ); // Configure MQTT
# ifndef ESPWifiManualSetup
server.on("/cg", handleCG); // Configure gateway
# endif
server.on("/wu", handleWU); // Configure WebUI
# ifdef ZgatewayLORA
server.on("/la", handleLA); // Configure LORA
# elif defined(ZgatewayRTL_433) || defined(ZgatewayPilight) || defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZactuatorSomfy)
server.on("/rf", handleRF); // Configure RF
# endif
# if defined(ZgatewayCloud)
server.on("/cl", handleCL); // Configure Cloud
server.on("/tk", handleTK); // Store Device Token
# endif
server.on("/lo", handleLO); // Configure Logging
# ifdef ZgatewayBT
server.on("/bl", handleBL); // Configure BLE
# endif
server.on("/rt", handleRT); // Reset configuration ( Erase and Restart )
server.on("/favicon.ico", handleFavicon); // Information
server.begin();
Log.begin(LOG_LEVEL, &WebLog);
THEENGS_LOG_TRACE(F("[WebUI] displayMetric %T" CR), displayMetric);
THEENGS_LOG_TRACE(F("[WebUI] WebUI Secure %T" CR), webUISecure);
THEENGS_LOG_NOTICE(F("OpenMQTTGateway URL: http://%s/" CR), WiFi.localIP().toString().c_str());
displayPrint("URL: http://", (char*)WiFi.localIP().toString().c_str());
THEENGS_LOG_NOTICE(F("webUI setup done" CR));
}
unsigned long nextWebUIMessage = uptime() + DISPLAY_WEBUI_INTERVAL;
void WebUILoop() {
server.handleClient();
freeLogBufferIfIdle();
if (uptime() >= nextWebUIMessage && uxQueueMessagesWaiting(webUIQueue)) {
webUIQueueMessage* message = nullptr;
xQueueReceive(webUIQueue, &message, portMAX_DELAY);
# if defined(ZdisplaySSD1306)
newSSD1306Message = true;
# endif
if (currentWebUIMessage) {
free(currentWebUIMessage);
}
currentWebUIMessage = message;
nextWebUIMessage = uptime() + DISPLAY_WEBUI_INTERVAL;
}
}
void XtoWebUI(const char* topicOri, JsonObject& WebUIdata) { // json object decoding
bool success = false;
if (cmpToMainTopic(topicOri, subjectMQTTtoWebUIset)) {
WEBUI_TRACE_LOG(F("MQTTtoWebUI json set" CR));
// properties
if (WebUIdata.containsKey("displayMetric")) {
displayMetric = WebUIdata["displayMetric"].as<bool>();
THEENGS_LOG_NOTICE(F("Set displayMetric: %T" CR), displayMetric);
success = true;
}
// save, load, init, erase
if (WebUIdata.containsKey("save") && WebUIdata["save"]) {
success = WebUIConfig_save();
if (success) {
THEENGS_LOG_NOTICE(F("WebUI config saved" CR));
}
} else if (WebUIdata.containsKey("load") && WebUIdata["load"]) {
success = WebUIConfig_load();
if (success) {
THEENGS_LOG_NOTICE(F("WebUI config loaded" CR));
}
} else if (WebUIdata.containsKey("init") && WebUIdata["init"]) {
WebUIConfig_init();
success = true;
if (success) {
THEENGS_LOG_NOTICE(F("WebUI config initialised" CR));
}
} else if (WebUIdata.containsKey("erase") && WebUIdata["erase"]) {
// Erase config from NVS (non-volatile storage)
preferences.begin(Gateway_Short_Name, false);
success = preferences.remove("WebUIConfig");
preferences.end();
if (success) {
THEENGS_LOG_NOTICE(F("WebUI config erased" CR));
}
}
if (success) {
stateWebUIStatus();
} else {
THEENGS_LOG_ERROR(F("[ WebUI ] XtoWebUI Fail json" CR), WebUIdata);
}
}
}
String stateWebUIStatus() {
//Publish display state
StaticJsonDocument<JSON_MSG_BUFFER> WebUIdataBuffer;
JsonObject WebUIdata = WebUIdataBuffer.to<JsonObject>();
WebUIdata["displayMetric"] = (bool)displayMetric;
WebUIdata["webUISecure"] = (bool)webUISecure;
WebUIdata["displayQueue"] = uxQueueMessagesWaiting(webUIQueue);
String output;
serializeJson(WebUIdata, output);
// WebUIdata["currentMessage"] = currentWebUIMessage;
WebUIdata["origin"] = subjectWebUItoMQTT;
enqueueJsonObject(WebUIdata);
return output;
}
bool WebUIConfig_save() {
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
JsonObject jo = jsonBuffer.to<JsonObject>();
jo["displayMetric"] = (bool)displayMetric;
jo["webUISecure"] = (bool)webUISecure;
// Save config into NVS (non-volatile storage)
String conf = "";
serializeJson(jsonBuffer, conf);
preferences.begin(Gateway_Short_Name, false);
int result = preferences.putString("WebUIConfig", conf);
preferences.end();
THEENGS_LOG_TRACE(F("[WebUI] WebUIConfig_save: %s, result: %d" CR), conf.c_str(), result);
return true;
}
void WebUIConfig_init() {
displayMetric = DISPLAY_METRIC;
webUISecure = WEBUI_AUTH;
THEENGS_LOG_NOTICE(F("WebUI config initialised" CR));
}
bool WebUIConfig_load() {
StaticJsonDocument<JSON_MSG_BUFFER> jsonBuffer;
preferences.begin(Gateway_Short_Name, true);
if (preferences.isKey("WebUIConfig")) {
auto error = deserializeJson(jsonBuffer, preferences.getString("WebUIConfig", "{}"));
preferences.end();
if (error) {
THEENGS_LOG_ERROR(F("WebUI config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity());
return false;
}
if (jsonBuffer.isNull()) {
THEENGS_LOG_WARNING(F("WebUI config is null" CR));
return false;
}
JsonObject jo = jsonBuffer.as<JsonObject>();
displayMetric = jo["displayMetric"].as<bool>();
webUISecure = jo["webUISecure"].as<bool>();
return true;
} else {
preferences.end();
THEENGS_LOG_NOTICE(F("No WebUI config to load" CR));
return false;
}
}
/*
Workaround for c not having a string based switch/case function
*/
constexpr unsigned int webUIHash(const char* s, int off = 0) { // workaround for switching on a string https://stackoverflow.com/a/46711735/18643696
return !s[off] ? 5381 : (webUIHash(s, off + 1) * 33) ^ s[off];
}
/*
Parse json message from module into a format for display
*/
void webUIPubPrint(const char* topicori, JsonObject& data) {
WEBUI_TRACE_LOG(F("[ webUIPubPrint ] pub %s " CR), topicori);
if (webUIQueue) {
webUIQueueMessage* message = (webUIQueueMessage*)heap_caps_calloc(1, sizeof(webUIQueueMessage), MALLOC_CAP_8BIT);
if (message != NULL) {
// Initalize message
strlcpy(message->line1, "", WEBUI_TEXT_WIDTH);
strlcpy(message->line2, "", WEBUI_TEXT_WIDTH);
strlcpy(message->line3, "", WEBUI_TEXT_WIDTH);
strlcpy(message->line4, "", WEBUI_TEXT_WIDTH);
char* topic = strdup(topicori);
strlcpy(message->title, strtok(topic, "/"), WEBUI_TEXT_WIDTH);
free(topic);
// WEBUI_TRACE_LOG(F("[ webUIPubPrint ] switch %s " CR), message->title);
switch (webUIHash(message->title)) {
case webUIHash("SYStoMQTT"): {
// Line 1
if (data["version"]) {
strlcpy(message->line1, data["version"], WEBUI_TEXT_WIDTH);
} else {
strlcpy(message->line1, "", WEBUI_TEXT_WIDTH);
}
// Line 2
String uptime = data["uptime"];
String line = "uptime: " + uptime;
line.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Line 3
String freemem = data["freemem"];
line = "freemem: " + freemem;
line.toCharArray(message->line3, WEBUI_TEXT_WIDTH);
// Line 4
String ip = data["ip"];
line = "ip: " + ip;
line.toCharArray(message->line4, WEBUI_TEXT_WIDTH);
// Queue completed message
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] ERROR: webUIQueue full, discarding %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
break;
}
# ifdef ZgatewayRTL_433
case webUIHash("RTL_433toMQTT"): {
if (data["model"] && strncmp(data["model"], "status", 6)) { // Does not contain "status"
// {"model":"Acurite-Tower","id":2043,"channel":"B","battery_ok":1,"temperature_C":5.3,"humidity":81,"mic":"CHECKSUM","protocol":"Acurite 592TXR Temp/Humidity, 5n1 Weather Station, 6045 Lightning, 3N1, Atlas","rssi":-81,"duration":121060}
// Line 1
strlcpy(message->line1, data["model"], WEBUI_TEXT_WIDTH);
// Line 2
String line2 = "";
if (data["id"]) {
String id = data["id"];
line2 += "id: " + id + " ";
}
if (data["channel"]) {
String channel = data["channel"];
line2 += "channel: " + channel;
}
line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Line 3
String line3 = "";
if (data.containsKey("temperature_C")) {
float temperature_C = data["temperature_C"];
char temp[5];
if (displayMetric) {
dtostrf(temperature_C, 3, 1, temp);
line3 = "temp: " + (String)temp + "°C ";
} else {
dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp);
line3 = "temp: " + (String)temp + "°F ";
}
}
float humidity = data["humidity"];
if (data.containsKey("humidity") && humidity <= 100 && humidity >= 0) {
char hum[5];
dtostrf(humidity, 3, 1, hum);
line3 += "hum: " + (String)hum + "% ";
}
if (data.containsKey("wind_avg_km_h")) {
float wind_avg_km_h = data["wind_avg_km_h"];
char wind[6];
if (displayMetric) {
dtostrf(wind_avg_km_h, 3, 1, wind);
line3 += "wind: " + (String)wind + "km/h ";
} else {
dtostrf(convert_kmph2mph(wind_avg_km_h), 3, 1, wind);
line3 += "wind: " + (String)wind + "mp/h ";
}
}
float moisture = data["moisture"];
if (data.containsKey("moisture") && moisture <= 100 && moisture >= 0) {
char moist[5];
dtostrf(moisture, 3, 1, moist);
line3 += "moist: " + (String)moist + "% ";
}
line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH);
// Line 4
String line4 = "";
if (data["battery_ok"]) {
line4 = "batt: " + data["battery_ok"].as<String>();
} else {
line4 = "pulses: " + data["pulses"].as<String>();
}
line4 += " rssi: " + data["rssi"].as<String>();
line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH);
// Queue completed message
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
} else {
THEENGS_LOG_ERROR(F("[ WebUI ] rtl_433 not displaying %s" CR), message->title);
free(message);
}
break;
}
# endif
# ifdef ZsensorBME280
case webUIHash("CLIMAtoMQTT"): {
// {"tempc":17.06,"tempf":62.708,"hum":50.0752,"pa":98876.14,"altim":205.8725,"altift":675.4348}
// Line 1
strlcpy(message->line1, "bme280", WEBUI_TEXT_WIDTH);
// Line 2
String line2 = "";
if (data.containsKey("tempc")) {
char temp[5];
float temperature_C = data["tempc"];
if (displayMetric) {
dtostrf(temperature_C, 3, 1, temp);
line2 = "temp: " + (String)temp + "°C ";
} else {
dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp);
line2 = "temp: " + (String)temp + "°F ";
}
}
line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Line 3
String line3 = "";
float humidity = data["hum"];
if (data.containsKey("hum") && humidity <= 100 && humidity >= 0) {
char hum[5];
dtostrf(humidity, 3, 1, hum);
line3 += "hum: " + (String)hum + "% ";
}
line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH);
// Line 4
float pa = (int)data["pa"] / 100;
char pressure[6];
String line4 = "";
if (displayMetric) {
dtostrf(pa, 3, 1, pressure);
line4 = "pressure: " + (String)pressure + " hPa";
} else {
dtostrf(convert_hpa2inhg(pa), 3, 1, pressure);
line4 = "pressure: " + (String)pressure + " inHg";
}
line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH);
// Queue completed message
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
break;
}
# endif
# ifdef ZgatewayBT
case webUIHash("BTtoMQTT"): {
// {"id":"AA:BB:CC:DD:EE:FF","mac_type":0,"adv_type":0,"name":"sps","manufacturerdata":"de071f1000b1612908","rssi":-70,"brand":"Inkbird","model":"T(H) Sensor","model_id":"IBS-TH1/TH2/P01B","type":"THBX","cidc":false,"acts":true,"tempc":20.14,"tempf":68.252,"hum":41.27,"batt":41}
if (data["model_id"] != "MS-CDP" && data["model_id"] != "GAEN" && data["model_id"] != "APPLE_CONT" && data["model_id"] != "IBEACON") {
// Line 2, 3, 4
String line2 = "";
String line3 = "";
String line4 = "";
// Properties
String properties[6] = {"", "", "", "", "", ""};
int property = -1;
if (data["type"] == "THB" || data["type"] == "THBX" || data["type"] == "PLANT" || data["type"] == "AIR" || data["type"] == "BATT" || data["type"] == "ACEL" || (data["type"] == "UNIQ" && data["model_id"] == "SDLS")) {
if (data.containsKey("tempc")) {
property++;
char temp[5];
if (displayMetric) {
float temperature = data["tempc"];
dtostrf(temperature, 3, 1, temp);
properties[property] = "temp: " + (String)temp + "°C ";
} else {
float temperature = data["tempf"];
dtostrf(temperature, 3, 1, temp);
properties[property] = "temp: " + (String)temp + "°F ";
}
}
if (data.containsKey("tempc2_dp")) {
property++;
char tempdp[5];
if (displayMetric) {
float temperature = data["tempc2_dp"];
dtostrf(temperature, 3, 1, tempdp);
properties[property] = "dewp: " + (String)tempdp + "°C ";
} else {
float temperature = data["tempf2_dp"];
dtostrf(temperature, 3, 1, tempdp);
properties[property] = "dewp: " + (String)tempdp + "°F ";
}
}
if (data.containsKey("extprobe") && data["extprobe"]) {
property++;
properties[property] = " ext. probe";
}
if (data.containsKey("hum")) {
property++;
float humidity = data["hum"];
char hum[5];
dtostrf(humidity, 3, 1, hum);
properties[property] = "hum: " + (String)hum + "% ";
}
if (data.containsKey("pm25")) {
property++;
int pm25int = data["pm25"];
char pm25[3];
itoa(pm25int, pm25, 10);
if ((data.containsKey("pm10"))) {
properties[property] = "PM 2.5: " + (String)pm25 + " ";
} else {
properties[property] = "pm2.5: " + (String)pm25 + "μg/m³ ";
}
}
if (data.containsKey("pm10")) {
property++;
int pm10int = data["pm10"];
char pm10[3];
itoa(pm10int, pm10, 10);
if ((data.containsKey("pm25"))) {
properties[property] = "/ 10: " + (String)pm10 + "μg/m³ ";
} else {
properties[property] = "pm10: " + (String)pm10 + "μg/m³ ";
}
}
if (data.containsKey("for")) {
property++;
int formint = data["for"];
char form[3];
itoa(formint, form, 10);
properties[property] = "CH₂O: " + (String)form + "mg/m³ ";
}
if (data.containsKey("co2")) {
property++;
int co2int = data["co2"];
char co2[4];
itoa(co2int, co2, 10);
properties[property] = "co2: " + (String)co2 + "ppm ";
}
if (data.containsKey("moi")) {
property++;
int moiint = data["moi"];
char moi[4];
itoa(moiint, moi, 10);
properties[property] = "moi: " + (String)moi + "% ";
}
if (data.containsKey("lux")) {
property++;
int luxint = data["lux"];
char lux[5];
itoa(luxint, lux, 10);
properties[property] = "lux: " + (String)lux + "lx ";
}
if (data.containsKey("fer")) {
property++;
int ferint = data["fer"];
char fer[7];
itoa(ferint, fer, 10);
properties[property] = "fer: " + (String)fer + "µS/cm ";
}
if (data.containsKey("pres")) {
property++;
int presint = data["pres"];
char pres[4];
itoa(presint, pres, 10);
properties[property] = "pres: " + (String)pres + "hPa ";
}
if (data.containsKey("batt")) {
property++;
int battery = data["batt"];
char batt[5];
itoa(battery, batt, 10);
properties[property] = "batt: " + (String)batt + "% ";
}
if (data.containsKey("shake")) {
property++;
int shakeint = data["shake"];
char shake[3];
itoa(shakeint, shake, 10);
properties[property] = "shake: " + (String)shake + " ";
}
if (data.containsKey("volt")) {
property++;
float voltf = data["volt"];
char volt[5];
dtostrf(voltf, 3, 1, volt);
properties[property] = "volt: " + (String)volt + "V ";
}
if (data.containsKey("wake")) {
property++;
String wakestr = data["wake"];
properties[property] = "wake: " + wakestr + " ";
}
if (data.containsKey("gravity")) {
property++;
property++;
char sgrav[6];
float gravityf = data["gravity"];
dtostrf(gravityf, 5, 3, sgrav);
properties[property] = "SG: " + (String)sgrav + " ";
}
} else if (data["type"] == "BBQ") {
String tempcstr = "";
int j = 7;
if (data["model_id"] == "IBT-2X(S)") {
j = 3;
} else if (data["model_id"] == "IBT-4X(S/C)") {
j = 5;
}
for (int i = 0; i < j; i++) {
if (i == 0) {
if (displayMetric) {
tempcstr = "tempc";
} else {
tempcstr = "tempf";
}
i++;
} else {
if (displayMetric) {
tempcstr = "tempc" + (String)i;
} else {
tempcstr = "tempf" + (String)i;
}
}
if (data.containsKey(tempcstr)) {
char temp[5];
float temperature = data[tempcstr];
dtostrf(temperature, 3, 1, temp);
properties[i - 1] = "tp" + (String)i + ": " + (String)temp;
if (displayMetric) {
properties[i - 1] += "°C ";
} else {
properties[i - 1] += "°F ";
}
} else {
properties[i - 1] = "tp" + (String)i + ": " + "off ";
}
}
} else if (data["type"] == "BODY") {
if (data.containsKey("steps")) {
property++;
int stepsint = data["steps"];
char steps[5];
itoa(stepsint, steps, 10);
properties[property] = "steps: " + (String)steps + " ";
// next line
property++;
}
if (data.containsKey("act_bpm")) {
property++;
int actbpmint = data["act_bpm"];
char actbpm[3];
itoa(actbpmint, actbpm, 10);
properties[property] = "activity bpm: " + (String)actbpm + " ";
}
if (data.containsKey("bpm")) {
property++;
int bpmint = data["bpm"];
char bpm[3];
itoa(bpmint, bpm, 10);
properties[property] = "bpm: " + (String)bpm + " ";
}
} else if (data["type"] == "SCALE") {
if (data.containsKey("weighing_mode")) {
property++;
String mode = data["weighing_mode"];
properties[property] = mode + " ";
// next line
property++;
}
if (data.containsKey("weight")) {
property++;
float weightf = data["weight"];
char weight[7];
dtostrf(weightf, 3, 1, weight);
if (data.containsKey("unit")) {
String unit = data["unit"];
properties[property] = "weight: " + (String)weight + unit + " ";
} else {
properties[property] = "weight: " + (String)weight;
}
// next line
property++;
}
if (data.containsKey("impedance")) {
property++;
int impint = data["impedance"];
char imp[3];
itoa(impint, imp, 10);
properties[property] = "impedance: " + (String)imp + "ohm ";
}
} else if (data["type"] == "UNIQ") {
if (data["model_id"] == "M1017" || data["model_id"] == "HOBOMX2001") {
if (data.containsKey("lvl_cm")) {
property++;
char lvl[5];
if (displayMetric) {
float lvlf = data["lvl_cm"];
dtostrf(lvlf, 3, 1, lvl);
properties[property] = "level: " + (String)lvl + "cm ";
} else {
float lvlf = data["lvl_in"];
dtostrf(lvlf, 3, 1, lvl);
properties[property] = "level: " + (String)lvl + "\" ";
}
}
if (data.containsKey("quality")) {
property++;
int qualint = data["quality"];
char qual[3];
itoa(qualint, qual, 10);
properties[property] = "qy: " + (String)qual + " ";
}
if (data.containsKey("batt")) {
property++;
int battery = data["batt"];
char batt[5];
itoa(battery, batt, 10);
properties[property] = "batt: " + (String)batt + "% ";
}
}
}
line2 = properties[0] + properties[1];
line3 = properties[2] + properties[3];
line4 = properties[4] + properties[5];
if (!(line2 == "" && line3 == "" && line4 == "")) {
// Titel
char* topic = strdup(topicori);
String heading = strtok(topic, "/");
String line0 = heading + " " + data["id"].as<String>().substring(9, 17);
line0.toCharArray(message->title, WEBUI_TEXT_WIDTH);
free(topic);
// Line 1
strlcpy(message->line1, data["model"], WEBUI_TEXT_WIDTH);
line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH);
line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH);
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
} else {
WEBUI_TRACE_LOG(F("[ WebUI ] incomplete messaage %s" CR), topicori);
free(message);
}
break;
} else {
WEBUI_TRACE_LOG(F("[ WebUI ] incorrect model_id %s" CR), topicori);
free(message);
break;
}
}
# endif
# ifdef ZsensorRN8209
case webUIHash("RN8209toMQTT"): {
// {"volt":1073178,"current":0,"power":0}
// Line 1
String line1 = "";
if (data.containsKey("volt")) {
char volt[5];
float voltage = data["volt"];
dtostrf(voltage, 3, 1, volt);
line1 = "volt: " + (String)volt;
}
line1.toCharArray(message->line1, WEBUI_TEXT_WIDTH);
// Line 2
String line2 = "";
if (data.containsKey("current")) {
char curr[5];
float current = data["current"];
dtostrf(current, 3, 1, curr);
line2 = "current: " + (String)curr + " A";
}
line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Line 3
String line3 = "";
if (data.containsKey("power")) {
char pow[5];
float power = data["power"];
dtostrf(power, 3, 1, pow);
line3 = "power: " + (String)pow + " W";
}
line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH);
// Queue completed message
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
break;
}
# endif
# ifdef ZgatewayLORA
case webUIHash("LORAtoMQTT"): {
// {"tempc":25.4,"hum":0,"batt":0}
String line1 = "";
if (data.containsKey("tempc")) {
char temp[5];
float temperature_C = data["tempc"];
if (displayMetric) {
dtostrf(temperature_C, 3, 1, temp);
line1 = "temp: " + (String)temp + "°C ";
} else {
dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp);
line1 = "temp: " + (String)temp + "°F ";
}
}
line1.toCharArray(message->line1, WEBUI_TEXT_WIDTH);
// Line 2
String line2 = "";
float humidity = data["hum"];
if (data.containsKey("hum") && humidity <= 100 && humidity >= 0) {
char hum[5];
dtostrf(humidity, 3, 1, hum);
line2 += "hum: " + (String)hum + "% ";
}
line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Line 3
String line3 = "";
float adc = data["adc"];
if (data.containsKey("adc") && adc <= 100 && adc >= 0) {
char cAdc[5];
dtostrf(adc, 3, 1, cAdc);
line3 += "adc: " + (String)cAdc + "µS/cm ";
}
line3.toCharArray(message->line2, WEBUI_TEXT_WIDTH);
// Queue completed message
if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) {
THEENGS_LOG_WARNING(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title);
free(message);
} else {
// THEENGS_LOG_NOTICE(F("[ WebUI ] Queued %s" CR), message->title);
}
break;
}
# endif
default:
THEENGS_LOG_VERBOSE(F("[ WebUI ] unhandled topic %s" CR), message->title);
free(message);
}
} else {
THEENGS_LOG_ERROR(F("[ WebUI ] insufficent memory " CR));
}
} else {
THEENGS_LOG_ERROR(F("[ WebUI ] not initalized " CR));
}
}
/*------------------- Serial logging interceptor ----------------------*/
// This pattern was borrowed from HardwareSerial and modified to support the WebUI display
SerialWeb WebLog(0); // Not sure about this, came from Hardwareserial
SerialWeb::SerialWeb(int x) {
}
/*
Initialize WebUI oled display for use, and display OMG logo
*/
void SerialWeb::begin() {
// WebUI.begin(); // User OMG serial support
}
/*
Dummy virtual functions carried over from Serial
*/
int SerialWeb::available(void) {
}
/*
Dummy virtual functions carried over from Serial
*/
int SerialWeb::peek(void) {
}
/*
Dummy virtual functions carried over from Serial
*/
int SerialWeb::read(void) {
}
/*
Dummy virtual functions carried over from Serial
*/
void SerialWeb::flush(void) {
}
/*
Write line of text to the display with vertical scrolling of screen
*/
size_t SerialWeb::write(const uint8_t* buffer, size_t size) {
// Default to Serial output if the display is not available
addLog(buffer, size);
return Serial.write(buffer, size);
}
void addLog(const uint8_t* buffer, size_t size) {
if (!log_buffer) {
return; // Skip logging when console buffer is not allocated
}
if (!line) {
line = (char*)malloc(ROW_LENGTH);
if (!line) return;
lineIndex = 0;
}
for (int i = 0; i < size; i++) {
if (char(buffer[i]) == 10 | lineIndex >= ROW_LENGTH - 2) {
if (char(buffer[i]) != 10) {
line[lineIndex++] = char(buffer[i]);
}
line[lineIndex++] = char(0);
AddLogData(1, (const char*)&line[0]);
lineIndex = 0;
} else {
line[lineIndex++] = char(buffer[i]);
}
}
}
#endif