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>
2718 lines
92 KiB
C++
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 = "&|>|<|"|'|\";
|
|
|
|
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> (" + WiFi.channel(indices[i]) + ") <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
|