#include "WebUi.h" #include "logo_web.h" String htmlEscape(const String& in) { String out; out.reserve(in.length() + 16); for (size_t i = 0; i < in.length(); i++) { switch (in[i]) { case '&': out += F("&"); break; case '<': out += F("<"); break; case '>': out += F(">"); break; case '"': out += F("""); break; case '\'': out += F("'"); break; default: out += in[i]; break; } } return out; } String fmtFloat(float v, uint8_t decimals) { if (isnan(v) || !isfinite(v)) return F("-"); return String(static_cast(v), static_cast(decimals)); } static String jsonFloat(float v, uint8_t decimals = 1) { if (isnan(v) || !isfinite(v)) return F("null"); return String(static_cast(v), static_cast(decimals)); } String jsonString(const String& in) { String out; out.reserve(in.length() + 8); for (size_t i = 0; i < in.length(); i++) { char c = in[i]; switch (c) { case '\"': out += F("\\\""); break; case '\\': out += F("\\\\"); break; case '\n': out += F("\\n"); break; case '\r': out += F("\\r"); break; case '\t': out += F("\\t"); break; case '\b': out += F("\\b"); break; case '\f': out += F("\\f"); break; default: if ((uint8_t)c < 32) { out += '?'; } else { out += c; } } } return out; } static String hex16(uint16_t value) { char buf[5]; snprintf(buf, sizeof(buf), "%04X", value); return String(buf); } static String statusBadge(bool present, const Texts& t) { String s; s.reserve(64); s += F("") : F("bad'>"); s += present ? t.present : t.absent; s += F(""); return s; } String buildStyle(const Texts&, UiLang) { return F( ""); } static void appendTop(String& html, UiLang lang, const Texts& t, const char* active) { html += F("

"); html += t.appTitle; html += F("

"); html += F("
"); html += F("
"); html += F(""); } static void appendRow(String& html, const String& id, const String& label, const String& value) { html += F(""); html += label; html += F(""); html += value; html += F(""); } static String buildConfigHistoryBox(const Texts& t, const WeatherStation& ws) { String html; html.reserve(1800); html += F("

"); html += t.modbusLog; html += F("

"); if (ws.configLogCount() == 0) { html += F("
"); html += t.noResult; html += F("
"); return html; } const CommLog& c = ws.configLogAt(0); html += F(""); appendRow(html, F("cfg-log-state"), t.modbusLastStatus, c.lastOk ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F("")); appendRow(html, F("cfg-log-action"), t.modbusAction, htmlEscape(c.lastAction)); appendRow(html, F("cfg-log-duration"), t.modbusDuration, String(c.lastDurationMs) + F(" ms")); appendRow(html, F("cfg-log-error"), t.modbusError, c.lastError.length() ? htmlEscape(c.lastError) : F("-")); html += F("
"); html += F("
"); html += t.modbusTx; html += F("
"); html += htmlEscape(c.lastRequestHex); html += F("
"); html += F("
"); html += t.modbusRx; html += F("
"); html += htmlEscape(c.lastResponseHex); html += F("
"); html += F(""); return html; } static void appendMainCards(String& html, const Texts& t, const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { (void)ethUp; (void)ip; (void)mac; const auto& p = ws.presence(); const auto& v = ws.values(); const auto& c = ws.commLog(); html += F("
"); html += F("

"); html += t.sensorStatus; html += F("

"); appendRow(html, F("st-wind"), t.sensorWind, statusBadge(p.wind, t)); appendRow(html, F("st-th"), t.sensorHumidityTemp, statusBadge(p.humidityTemp, t)); appendRow(html, F("st-noise"), t.sensorNoise, statusBadge(p.noise, t)); appendRow(html, F("st-air"), t.sensorAir, statusBadge(p.air, t)); appendRow(html, F("st-pressure"), t.sensorPressure, statusBadge(p.pressure, t)); appendRow(html, F("st-light"), t.sensorLight, statusBadge(p.light, t)); appendRow(html, F("st-rain"), t.sensorRain, statusBadge(p.rain, t)); appendRow(html, F("st-compass"), t.sensorCompass, statusBadge(p.compass, t)); appendRow(html, F("st-solar"), t.sensorSolar, statusBadge(p.solar, t)); html += F("
"); html += F("

"); html += t.values; html += F("

"); appendRow(html, F("v-wind-short"), t.windSpeed, fmtFloat(v.windSpeed, 1)); appendRow(html, F("v-beaufort-short"), t.beaufort, (v.windForce >= 0 ? String(v.windForce) : F("-"))); appendRow(html, F("v-dir8-short"), t.windDir8, (v.windDir8 >= 0 ? String(v.windDir8) : F("-"))); appendRow(html, F("v-dirdeg-short"), t.windDirDeg, (v.windDirDeg >= 0 ? String(v.windDirDeg) : F("-"))); appendRow(html, F("v-hum-short"), t.humidity, fmtFloat(v.humidity, 1)); appendRow(html, F("v-temp-short"), t.temperature, fmtFloat(v.temperature, 1)); appendRow(html, F("v-noise-short"), t.noise, fmtFloat(v.noise, 1)); appendRow(html, F("v-air1-short"), ws.airLabel1(), (v.air1 >= 0 ? String(v.air1) : F("-"))); appendRow(html, F("v-air2-short"), ws.airLabel2(), (v.air2 >= 0 ? String(v.air2) : F("-"))); appendRow(html, F("v-pressure-short"), t.pressure, fmtFloat(v.pressureKpa, 1)); appendRow(html, F("v-luxh-short"), t.lightHigh, String(v.lightHigh)); appendRow(html, F("v-luxl-short"), t.lightLow, String(v.lightLow)); appendRow(html, F("v-lux-short"), t.lightLux100, String(v.lightLux)); appendRow(html, F("v-rain-short"), t.rain, fmtFloat(v.rain, 1)); appendRow(html, F("v-compass-short"), t.compass, fmtFloat(v.compassDeg, 2)); appendRow(html, F("v-solar-short"), t.solarRadiation, (v.solarRadiation >= 0 ? String(v.solarRadiation) : F("-"))); appendRow(html, F("data-age"), t.pollAge, String((millis() - v.lastReadMs) / 1000UL) + String(F(" ")) + t.seconds); html += F("
"); html += F("

"); html += t.modbusLog; html += F("

"); appendRow(html, F("mb-state"), t.modbusLastStatus, c.lastOk ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F("")); appendRow(html, F("mb-action"), t.modbusAction, htmlEscape(c.lastAction)); appendRow(html, F("mb-dur"), t.modbusDuration, String(c.lastDurationMs) + F(" ms")); appendRow(html, F("mb-stats"), t.modbusCounters, String(c.successCount) + F(" / ") + String(c.errorCount)); appendRow(html, F("mb-err"), t.modbusError, c.lastError.length() ? htmlEscape(c.lastError) : F("-")); html += F("
"); html += F("

"); html += t.modbusFrames; html += F("

"); html += F("
"); html += t.modbusTx; html += F("
"); html += htmlEscape(c.lastRequestHex); html += F("
"); html += F("
"); html += t.modbusRx; html += F("
"); html += htmlEscape(c.lastResponseHex); html += F("
"); html += F("
"); html += t.rawRegisters; html += F("
"); for (uint8_t i = 0; i < REG_DATA_COUNT; i++) { html += F("R"); html += String(REG_DATA_START + i); html += F(" = "); html += String(v.raw[i]); if (i + 1 < REG_DATA_COUNT) html += '\n'; } html += F("
"); html += F("
"); } static String buildMainScript(UiLang lang, const Texts& t) { String s; s.reserve(9000); s += F(""); return s; } static String buildManualCommandScript() { String s; s.reserve(2400); s += F(""); return s; } String buildMainPage(UiLang lang, const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { const auto& t = T(lang); String html; html.reserve(24000); html += F(""); html += t.appTitle; html += F(""); html += buildStyle(t, lang); html += F("
"); appendTop(html, lang, t, "main"); appendMainCards(html, t, ws, ethUp, ip, mac); html += buildMainScript(lang, t); html += F("
"); return html; } String buildConfigPage(UiLang lang, const WeatherStation& ws, const String& flashMsg) { const auto& t = T(lang); String html; html.reserve(24000); (void)flashMsg; html += F(""); html += t.tabConfig; html += F(""); html += buildStyle(t, lang); html += F("
"); appendTop(html, lang, t, "config"); html += F("
"); html += buildConfigHistoryBox(t, ws); html += F("

"); html += t.manualCommand; html += F("

"); html += t.manualNote; html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
-
"); html += F("
"); html += F("
"); html += F("

"); html += t.rawHexTitle; html += F("

"); html += t.rawHexNote; html += F("
"); html += F("
"); html += F(""); html += F("
"); html += F("

"); html += t.configTitle; html += F("

"); html += F(""); html += F(""); html += F(""); html += F(""); html += F(""); html += F(""); html += F(""); html += F(""); html += F("
"); html += t.tableItem; html += F(""); html += t.tableSetting; html += F(""); html += t.tableNote; html += F("
"); html += t.address; html += F(""); html += F("
"); html += F(""); html += F(""); html += F(""); html += F("
"); html += t.noteAddress; html += F("
"); html += t.baudrate; html += F(""); html += F("
"); html += F(""); html += F(""); html += F(""); html += F("
"); html += t.noteBaudrate; html += F("
"); html += t.windOffset; html += F(""); html += F("
"); html += F(""); html += F(""); html += F(""); html += F("
"); html += t.noteWindOffset; html += F("
"); html += t.rainSensitivity; html += F(""); html += F("
"); html += F(""); html += F(""); html += F(""); html += F("
"); html += t.noteRainSensitivity; html += F("
"); html += t.commandWindZero; html += F("
"); html += t.noteWindZero; html += F("
"); html += t.commandRainZero; html += F("
"); html += t.noteRainZero; html += F("
"); html += F("
"); html += buildManualCommandScript(); html += F("
"); return html; } String buildMainDataJson(const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { const auto& v = ws.values(); const auto& p = ws.presence(); const auto& c = ws.commLog(); String json; json.reserve(2400); json += F("{"); json += F("\"eth\":"); json += ethUp ? F("true") : F("false"); json += F(",\"ip\":\""); json += jsonString(ip); json += F("\""); json += F(",\"mac\":\""); json += jsonString(mac); json += F("\""); json += F(",\"age\":"); json += String((millis() - v.lastReadMs) / 1000UL); json += F(",\"wind\":"); json += jsonFloat(v.windSpeed); json += F(",\"beaufort\":"); json += (v.windForce >= 0 ? String(v.windForce) : F("-1")); json += F(",\"dir8\":"); json += (v.windDir8 >= 0 ? String(v.windDir8) : F("-1")); json += F(",\"dirdeg\":"); json += (v.windDirDeg >= 0 ? String(v.windDirDeg) : F("-1")); json += F(",\"temp\":"); json += jsonFloat(v.temperature); json += F(",\"hum\":"); json += jsonFloat(v.humidity); json += F(",\"noise\":"); json += jsonFloat(v.noise); json += F(",\"air1\":"); json += String(v.air1); json += F(",\"air2\":"); json += String(v.air2); json += F(",\"press\":"); json += jsonFloat(v.pressureKpa); json += F(",\"rain\":"); json += jsonFloat(v.rain); json += F(",\"solar\":"); json += String(v.solarRadiation); json += F(",\"light\":"); json += String(v.lightLux); json += F(",\"lightHigh\":"); json += String(v.lightHigh); json += F(",\"lightLow\":"); json += String(v.lightLow); json += F(",\"compass\":"); json += jsonFloat(v.compassDeg, 2); json += F(",\"pwind\":"); json += p.wind ? F("true") : F("false"); json += F(",\"pth\":"); json += p.humidityTemp ? F("true") : F("false"); json += F(",\"pnoise\":"); json += p.noise ? F("true") : F("false"); json += F(",\"pair\":"); json += p.air ? F("true") : F("false"); json += F(",\"ppress\":"); json += p.pressure ? F("true") : F("false"); json += F(",\"plight\":"); json += p.light ? F("true") : F("false"); json += F(",\"prain\":"); json += p.rain ? F("true") : F("false"); json += F(",\"pcomp\":"); json += p.compass ? F("true") : F("false"); json += F(",\"psolar\":"); json += p.solar ? F("true") : F("false"); json += F(",\"mbOk\":"); json += c.lastOk ? F("true") : F("false"); json += F(",\"mbDuration\":"); json += String(c.lastDurationMs); json += F(",\"mbOkCount\":"); json += String(c.successCount); json += F(",\"mbErrCount\":"); json += String(c.errorCount); json += F(",\"mbAction\":\""); json += jsonString(c.lastAction); json += F("\""); json += F(",\"mbError\":\""); json += jsonString(c.lastError); json += F("\""); json += F(",\"mbTx\":\""); json += jsonString(c.lastRequestHex); json += F("\""); json += F(",\"mbRx\":\""); json += jsonString(c.lastResponseHex); json += F("\""); json += F(",\"raw\":["); for (uint8_t i = 0; i < REG_DATA_COUNT; i++) { if (i) json += ','; json += String(v.raw[i]); } json += F("]}"); return json; }