diff --git a/SW/test_ESPLAN_V2/test_ESPLAN_V2.ino b/SW/test_ESPLAN_V2/test_ESPLAN_V2.ino index cc74787..b3abf0c 100755 --- a/SW/test_ESPLAN_V2/test_ESPLAN_V2.ino +++ b/SW/test_ESPLAN_V2/test_ESPLAN_V2.ino @@ -1,20 +1,27 @@ -/* - ESP32 + LAN8720 + WebServer + SD + RS485 + SHT40 + BMP280 + NeoPixel +/* Test for LaskaKit ESPlan V2 ESP32 LAN8720A RS485 PoE + * ESP32 + LAN8720 + WebServer + SD + RS485 + SHT40 + BMP280 + NeoPixel + * + * Funkce: + * - web rozhrani pres Ethernet + * - stav ETH, SD, SHT40, BMP280, RS485 + * - ovladani NeoPixel LED + * - odesilani textu pres RS485 + * - prijem dat z RS485 + * - prochazeni adresaru na SD + * - upload / stahnout / smazat souboru na SD + * - vytvareni a mazani adresaru na SD + * - automaticka reakce na vyndani / vlozeni SD karty + * - po vlozeni se SD karta ihned znovu nacte + * - SD panel se na webu obnovi jen pri zmene stavu SD karty + * - periodicky debug do seriove konzole + * - volba jazyka CZ / ENG - Funkce: - - web rozhrani pres Ethernet - - stav ETH, SD, SHT40, BMP280, RS485 - - ovladani NeoPixel LED - - odesilani textu pres RS485 - - prijem dat z RS485 - - prochazeni adresaru na SD - - upload / stahnout / smazat souboru na SD - - vytvareni a mazani adresaru na SD - - automaticka reakce na vyndani / vlozeni SD karty - - po vlozeni se SD karta ihned znovu nacte - - SD panel se na webu obnovi jen pri zmene stavu SD karty - - periodicky debug do seriove konzole -*/ + * Board: kaKit ESPlan V2 https://www.laskakit.cz/laskakit-esplan-esp32-lan8720a-max485-poe/ + * + * + * Email:podpora@laskakit.cz + * Web:laskakit.cz + */ #include #include @@ -23,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +52,6 @@ // ---------- RS485 ---------- #define RS485_TX_PIN 4 #define RS485_RX_PIN 36 -#define RS485_BAUDRATE 9600 // ---------- I2C ---------- #define I2C_SDA_PIN 33 @@ -66,6 +73,87 @@ #define NEOPIXEL_PIN 0 #define NEOPIXEL_COUNT 1 +// ===================================================== +// JAZYK UI +// ===================================================== + +enum UiLang { + LANG_CZ, + LANG_EN +}; + +UiLang currentUiLang = LANG_CZ; + +// ===================================================== +// STRUKTUROVANÝ LOG +// ===================================================== + +struct LogEntry { + uint8_t type; + String a; + String b; + String c; +}; + +enum LogType : uint8_t { + LOG_TEXT = 0, + LOG_SHT40_READ_FAILED, + LOG_SHT40_NOT_FOUND, + LOG_SHT40_DETECTED, + LOG_BMP280_READ_FAILED, + LOG_BMP280_DETECTED_76, + LOG_BMP280_DETECTED_77, + LOG_BMP280_NOT_FOUND, + LOG_I2C_INIT, + LOG_SD_NOT_INSERTED, + LOG_SD_INIT, + LOG_SD_BEGIN_FAILED, + LOG_SD_NOT_FOUND, + LOG_SD_OK, + LOG_SD_SIZE_MB, + LOG_SD_REMOVED, + LOG_SD_INSERTED, + LOG_RS485_INIT, + LOG_RS485_PINS, + LOG_ETH_STATUS_TITLE, + LOG_ETH_STATUS_LINK, + LOG_ETH_STATUS_IP, + LOG_ETH_STATUS_MAC, + LOG_ETH_START, + LOG_ETH_CONNECTED, + LOG_ETH_GOT_IP, + LOG_ETH_DISCONNECTED, + LOG_ETH_STOP, + LOG_ETH_INIT, + LOG_ETH_BEGIN_FAILED, + LOG_ETH_OK, + LOG_ETH_NO_IP, + LOG_LED_SET, + LOG_RS485_TX, + LOG_SD_DELETE_OK, + LOG_SD_DELETE_ERROR, + LOG_MKDIR_INVALID, + LOG_MKDIR_EXISTS, + LOG_MKDIR_OK, + LOG_MKDIR_ERROR, + LOG_UPLOAD_INVALID_NAME, + LOG_UPLOAD_START, + LOG_UPLOAD_OPEN_WRITE_FAILED, + LOG_UPLOAD_FINISHED, + LOG_UPLOAD_ABORTED, + LOG_WEB_STARTED, + LOG_START, + LOG_READY, + LOG_OPEN_BROWSER, + LOG_ETH_NO_IP_YET, + LOG_RS485_RX, + LOG_HEAP_TOTAL, + LOG_HEAP_FREE, + LOG_CPU_FREQ +}; + +std::vector eventLog; + // ===================================================== // GLOBÁLNÍ OBJEKTY // ===================================================== @@ -90,12 +178,13 @@ float lastPressure_hPa = NAN; uint8_t ledR = 0; uint8_t ledG = 0; -uint8_t ledB = 10; +uint8_t ledB = 0; +uint8_t ledBrightness = 50; // 0-255 String rs485LastSent = ""; String rs485LastReceived = ""; uint32_t rs485RxVersion = 0; -String eventLog = ""; +uint32_t rs485Baudrate = 9600; File uploadFile; @@ -113,6 +202,232 @@ uint32_t sdStateVersion = 0; // Heap baseline = total RAM chip heap uint32_t totalHeapBytes = 0; +// ===================================================== +// JAZYK HELPERY +// ===================================================== + +UiLang parseLang(const String& s) { + String v = s; + v.toLowerCase(); + return (v == "en") ? LANG_EN : LANG_CZ; +} + +String langCode(UiLang lang) { + return (lang == LANG_EN) ? "en" : "cz"; +} + +UiLang getCurrentLang() { + return parseLang(server.arg("lang")); +} + +void updateCurrentLangFromRequest() { + if (server.hasArg("lang")) { + currentUiLang = getCurrentLang(); + } +} + +String tr(UiLang lang, const String& cz, const String& en) { + return (lang == LANG_EN) ? en : cz; +} + +String trCurrent(const String& cz, const String& en) { + return tr(currentUiLang, cz, en); +} + +// ===================================================== +// LOG +// ===================================================== + +String renderLogEntry(const LogEntry& e, UiLang lang) { + switch (e.type) { + case LOG_TEXT: + return e.a; + + case LOG_SHT40_READ_FAILED: + return tr(lang, "CHYBA: čtení SHT40 selhalo", "ERROR: SHT40 read failed"); + + case LOG_SHT40_NOT_FOUND: + return tr(lang, "SHT40 nebyl nalezen", "SHT40 not found"); + + case LOG_SHT40_DETECTED: + return tr(lang, "SHT40 detekován OK", "SHT40 detected OK"); + + case LOG_BMP280_READ_FAILED: + return tr(lang, "CHYBA: čtení BMP280 selhalo", "ERROR: BMP280 read failed"); + + case LOG_BMP280_DETECTED_76: + return tr(lang, "BMP280 detekován na adrese 0x76", "BMP280 detected at address 0x76"); + + case LOG_BMP280_DETECTED_77: + return tr(lang, "BMP280 detekován na adrese 0x77", "BMP280 detected at address 0x77"); + + case LOG_BMP280_NOT_FOUND: + return tr(lang, "BMP280 nebyl nalezen", "BMP280 not found"); + + case LOG_I2C_INIT: + return tr(lang, "Inicializace I2C...", "Initializing I2C..."); + + case LOG_SD_NOT_INSERTED: + return tr(lang, "SD karta není vložena", "SD card is not inserted"); + + case LOG_SD_INIT: + return tr(lang, "Inicializace SD karty...", "Initializing SD card..."); + + case LOG_SD_BEGIN_FAILED: + return tr(lang, "CHYBA: SD.begin() selhalo", "ERROR: SD.begin() failed"); + + case LOG_SD_NOT_FOUND: + return tr(lang, "CHYBA: SD karta nenalezena", "ERROR: SD card not found"); + + case LOG_SD_OK: + return tr(lang, "SD karta OK", "SD card OK"); + + case LOG_SD_SIZE_MB: + return tr(lang, "Velikost karty: ", "Card size: ") + e.a + " MB"; + + case LOG_SD_REMOVED: + return tr(lang, "SD karta byla vyjmuta", "SD card was removed"); + + case LOG_SD_INSERTED: + return tr(lang, "SD karta byla vložena", "SD card was inserted"); + + case LOG_RS485_INIT: + return tr(lang, "RS485 inicializováno", "RS485 initialized"); + + case LOG_RS485_PINS: + return "TX=" + e.a + ", RX=" + e.b + ", baud=" + e.c; + + case LOG_ETH_STATUS_TITLE: + return tr(lang, "Ethernet stav:", "Ethernet status:"); + + case LOG_ETH_STATUS_LINK: + return " " + tr(lang, "Link: ", "Link: ") + e.a; + + case LOG_ETH_STATUS_IP: + return " IP: " + e.a; + + case LOG_ETH_STATUS_MAC: + return " MAC: " + e.a; + + case LOG_ETH_START: + return "[ETH] " + tr(lang, "START", "START"); + + case LOG_ETH_CONNECTED: + return "[ETH] " + tr(lang, "PŘIPOJENO", "CONNECTED"); + + case LOG_ETH_GOT_IP: + return "[ETH] " + tr(lang, "ZÍSKÁNA IP: ", "GOT IP: ") + e.a; + + case LOG_ETH_DISCONNECTED: + return "[ETH] " + tr(lang, "ODPOJENO", "DISCONNECTED"); + + case LOG_ETH_STOP: + return "[ETH] " + tr(lang, "STOP", "STOP"); + + case LOG_ETH_INIT: + return tr(lang, "Inicializace Ethernetu...", "Initializing Ethernet..."); + + case LOG_ETH_BEGIN_FAILED: + return tr(lang, "CHYBA: ETH.begin() selhalo", "ERROR: ETH.begin() failed"); + + case LOG_ETH_OK: + return tr(lang, "Ethernet OK", "Ethernet OK"); + + case LOG_ETH_NO_IP: + return tr(lang, "Ethernet bez IP adresy", "Ethernet without IP address"); + + case LOG_LED_SET: + return tr(lang, "LED nastavena: ", "LED set: ") + "R=" + e.a + " G=" + e.b + " B=" + e.c; + + case LOG_RS485_TX: + return "RS485 TX: " + e.a; + + case LOG_SD_DELETE_OK: + return tr(lang, "SD smazání OK: ", "SD delete OK: ") + e.a; + + case LOG_SD_DELETE_ERROR: + return tr(lang, "SD smazání CHYBA: ", "SD delete ERROR: ") + e.a; + + case LOG_MKDIR_INVALID: + return tr(lang, "MKDIR: neplatný název adresáře", "MKDIR: invalid directory name"); + + case LOG_MKDIR_EXISTS: + return tr(lang, "MKDIR: adresář již existuje: ", "MKDIR: directory already exists: ") + e.a; + + case LOG_MKDIR_OK: + return "MKDIR OK: " + e.a; + + case LOG_MKDIR_ERROR: + return tr(lang, "MKDIR CHYBA: ", "MKDIR ERROR: ") + e.a; + + case LOG_UPLOAD_INVALID_NAME: + return tr(lang, "Upload: neplatné jméno souboru", "Upload: invalid file name"); + + case LOG_UPLOAD_START: + return tr(lang, "Upload start: ", "Upload start: ") + e.a; + + case LOG_UPLOAD_OPEN_WRITE_FAILED: + return tr(lang, "Upload: nelze otevřít soubor pro zápis", "Upload: cannot open file for writing"); + + case LOG_UPLOAD_FINISHED: + return tr(lang, "Upload dokončen: ", "Upload finished: ") + e.a + " B"; + + case LOG_UPLOAD_ABORTED: + return tr(lang, "Upload přerušen", "Upload aborted"); + + case LOG_WEB_STARTED: + return tr(lang, "Web server spuštěn na portu 80", "Web server started on port 80"); + + case LOG_START: + return tr(lang, "START", "START"); + + case LOG_READY: + return tr(lang, "PŘIPRAVENO", "READY"); + + case LOG_OPEN_BROWSER: + return tr(lang, "Otevři v prohlížeči: http://", "Open in browser: http://") + e.a + "/"; + + case LOG_ETH_NO_IP_YET: + return tr(lang, "Ethernet zatím nemá IP adresu", "Ethernet does not have an IP address yet"); + + case LOG_RS485_RX: + return "RS485 RX: " + e.a; + + case LOG_HEAP_TOTAL: + return tr(lang, "Heap celkem: ", "Heap total: ") + e.a + " B"; + + case LOG_HEAP_FREE: + return tr(lang, "Heap volný: ", "Heap free: ") + e.a + " B (" + e.b + " %)"; + + case LOG_CPU_FREQ: + return "CPU: " + e.a + " MHz"; + + default: + return e.a; + } +} + +void pushLogEntry(uint8_t type, const String& a = "", const String& b = "", const String& c = "") { + LogEntry e{type, a, b, c}; + eventLog.push_back(e); + + const size_t maxEntries = 250; + if (eventLog.size() > maxEntries) { + eventLog.erase(eventLog.begin(), eventLog.begin() + (eventLog.size() - maxEntries)); + } +} + +void addLogEntry(uint8_t type, const String& a = "", const String& b = "", const String& c = "") { + LogEntry e{type, a, b, c}; + Serial.println(renderLogEntry(e, currentUiLang)); + pushLogEntry(type, a, b, c); +} + +void addLogText(const String& msg) { + Serial.println(msg); + pushLogEntry(LOG_TEXT, msg); +} + // ===================================================== // RESET REASON // ===================================================== @@ -137,18 +452,6 @@ void printResetReason() { } } -// ===================================================== -// LOG -// ===================================================== - -void addLog(const String& msg) { - Serial.println(msg); - eventLog += msg + "\n"; - if (eventLog.length() > 8000) { - eventLog.remove(0, eventLog.length() - 8000); - } -} - // ===================================================== // POMOCNÉ FUNKCE // ===================================================== @@ -197,7 +500,6 @@ String jsonEscape(const String& s) { return out; } - String urlEncode(const String& s) { String out; char hex[4]; @@ -352,17 +654,21 @@ void setPixelColor(uint8_t r, uint8_t g, uint8_t b) { ledG = g; ledB = b; + pixel.setBrightness(ledBrightness); pixel.setPixelColor(0, pixel.Color(r, g, b)); pixel.show(); } -void redirectHome() { - server.sendHeader("Location", "/"); +void redirectHome(UiLang lang) { + server.sendHeader("Location", "/?lang=" + langCode(lang)); server.send(303); } -void redirectToPath(const String& path) { - server.sendHeader("Location", "/?path=" + urlEncode(normalizeDirPath(path))); +void redirectToPath(const String& path, UiLang lang) { + server.sendHeader( + "Location", + "/?path=" + urlEncode(normalizeDirPath(path)) + "&lang=" + langCode(lang) + ); server.send(303); } @@ -378,21 +684,21 @@ void readSHT40() { lastTemperature = temp.temperature; lastHumidity = humidity.relative_humidity; } else { - addLog("CHYBA: cteni SHT40 selhalo"); + addLogEntry(LOG_SHT40_READ_FAILED); } } void initSHT40() { if (!sht4.begin(&Wire)) { sht40Found = false; - addLog("SHT40 nebyl nalezen"); + addLogEntry(LOG_SHT40_NOT_FOUND); return; } sht4.setPrecision(SHT4X_HIGH_PRECISION); sht4.setHeater(SHT4X_NO_HEATER); sht40Found = true; - addLog("SHT40 detekovan OK"); + addLogEntry(LOG_SHT40_DETECTED); readSHT40(); } @@ -407,7 +713,7 @@ void readBMP280() { float p = bmp280.readPressure(); if (isnan(t) || isnan(p) || p <= 0.0f) { - addLog("CHYBA: cteni BMP280 selhalo"); + addLogEntry(LOG_BMP280_READ_FAILED); return; } @@ -420,15 +726,15 @@ void initBMP280() { if (bmp280.begin(BMP280_ADDR_1)) { ok = true; - addLog("BMP280 detekovan na adrese 0x76"); + addLogEntry(LOG_BMP280_DETECTED_76); } else if (bmp280.begin(BMP280_ADDR_2)) { ok = true; - addLog("BMP280 detekovan na adrese 0x77"); + addLogEntry(LOG_BMP280_DETECTED_77); } if (!ok) { bmp280Found = false; - addLog("BMP280 nebyl nalezen"); + addLogEntry(LOG_BMP280_NOT_FOUND); return; } @@ -449,7 +755,7 @@ void initBMP280() { // ===================================================== void initI2CDevices() { - addLog("Inicializace I2C..."); + addLogEntry(LOG_I2C_INIT); Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); delay(50); @@ -471,12 +777,12 @@ void initSDCard() { if (!sdCardInserted) { sdOk = false; - addLog("SD karta neni vlozena"); + addLogEntry(LOG_SD_NOT_INSERTED); bumpSdStateVersion(); return; } - addLog("Inicializace SD karty..."); + addLogEntry(LOG_SD_INIT); SD.end(); sdSPI.end(); @@ -486,14 +792,14 @@ void initSDCard() { if (!SD.begin(SD_CS_PIN, sdSPI)) { sdOk = false; - addLog("CHYBA: SD.begin() selhalo"); + addLogEntry(LOG_SD_BEGIN_FAILED); bumpSdStateVersion(); return; } if (SD.cardType() == CARD_NONE) { sdOk = false; - addLog("CHYBA: SD karta nenalezena"); + addLogEntry(LOG_SD_NOT_FOUND); SD.end(); sdSPI.end(); bumpSdStateVersion(); @@ -501,8 +807,8 @@ void initSDCard() { } sdOk = true; - addLog("SD karta OK"); - addLog("Velikost karty: " + String((uint32_t)(SD.cardSize() / (1024ULL * 1024ULL))) + " MB"); + addLogEntry(LOG_SD_OK); + addLogEntry(LOG_SD_SIZE_MB, String((uint32_t)(SD.cardSize() / (1024ULL * 1024ULL)))); bumpSdStateVersion(); } @@ -533,18 +839,24 @@ void handleSdHotplug() { SD.end(); sdSPI.end(); sdOk = false; - addLog("SD karta byla vyjmuta"); + addLogEntry(LOG_SD_REMOVED); bumpSdStateVersion(); return; } - addLog("SD karta byla vlozena"); + addLogEntry(LOG_SD_INSERTED); delay(50); initSDCard(); } -String buildFileTable(const String& currentPath) { - if (!sdOk) return F("

SD karta neni dostupna.

"); +// ===================================================== +// SD PANEL +// ===================================================== + +String buildFileTable(const String& currentPath, UiLang lang) { + if (!sdOk) { + return String(F("

")) + tr(lang, "SD karta není dostupná.", "SD card is not available.") + F("

"); + } String path = normalizeDirPath(currentPath); if (!isSafePath(path)) path = "/"; @@ -556,34 +868,61 @@ String buildFileTable(const String& currentPath) { if (dir) dir.close(); String html; - html += F("

Chyba: Nelze otevřít adresář.

"); - html += F("

Požadovaná cesta: "); + html += F("

"); + html += tr(lang, "Chyba:", "Error:"); + html += F(" "); + html += tr(lang, "Nelze otevřít adresář.", "Cannot open directory."); + html += F("

"); + + html += F("

"); + html += tr(lang, "Požadovaná cesta:", "Requested path:"); + html += F(" "); html += htmlEscape(path); html += F("

"); html += F("

↩️

"); return html; } String html; - html += F("

Aktuální adresář: "); + html += F("

"); + html += tr(lang, "Aktuální adresář:", "Current directory:"); + html += F(" "); html += htmlEscape(path); html += F("

"); - html += F("
"); + html += F("'>"); if (path != "/") { html += F("

↩️

"); } - html += F(""); + html += F("
TypNázevVelikostAkce
"); File file = dir.openNextFile(); bool any = false; @@ -622,6 +961,8 @@ String buildFileTable(const String& currentPath) { if (isDir) { html += F(""); html += entryEsc; html += F(""); @@ -636,6 +977,8 @@ String buildFileTable(const String& currentPath) { if (!isDir) { html += F("⬇️ | "); } @@ -643,7 +986,11 @@ String buildFileTable(const String& currentPath) { html += entryUrl; html += F("&path="); html += currentUrl; - html += F("' onclick=\"return confirm('Opravdu smazat?')\">🗑️"); + html += F("&lang="); + html += langCode(lang); + html += F("' onclick=\"return confirm('"); + html += tr(lang, "Opravdu smazat?", "Really delete?"); + html += F("')\">🗑️"); html += F(""); @@ -652,7 +999,9 @@ String buildFileTable(const String& currentPath) { } if (!any) { - html += F(""); + html += F(""); } html += F("
"); + html += tr(lang, "Typ", "Type"); + html += F(""); + html += tr(lang, "Název", "Name"); + html += F(""); + html += tr(lang, "Velikost", "Size"); + html += F(""); + html += tr(lang, "Akce", "Actions"); + html += F("
Adresář je prázdný
"); + html += tr(lang, "Adresář je prázdný", "Directory is empty"); + html += F("
"); @@ -660,40 +1009,60 @@ String buildFileTable(const String& currentPath) { return html; } -String buildSdPanel(const String& currentPath) { +String buildSdPanel(const String& currentPath, UiLang lang) { String html; html.reserve(18000); - html += F("

Správa souborů na SD

"); + html += F("

"); + html += tr(lang, "Správa souborů na SD", "SD file manager"); + html += F("

"); if (sdOk) { html += F("
"); - html += F("

Nahrávání souborů

" + html += F("

"); + html += tr(lang, "Nahrávání souborů", "File upload"); + html += F("

" "
" "" + "" "" - "" + "" "
"); - html += F("

Vytvořit adresář

" + html += F("

"); + html += tr(lang, "Vytvořit adresář", "Create directory"); + html += F("

" "
" "" - "" + "" + "" "" - "" + "" "
"); html += F("
"); } else { - html += F("

SD karta není dostupná.

"); + html += F("

"); + html += tr(lang, "SD karta není dostupná.", "SD card is not available."); + html += F("

"); } - html += buildFileTable(currentPath); + html += buildFileTable(currentPath, lang); return html; } @@ -713,11 +1082,40 @@ String buildSdStateJson() { // RS485 // ===================================================== +bool isSupportedRs485Baud(uint32_t baud) { + switch (baud) { + case 1200: + case 2400: + case 4800: + case 9600: + case 19200: + case 38400: + case 57600: + case 115200: + return true; + default: + return false; + } +} + +void applyRS485Baudrate(uint32_t baud) { + if (!isSupportedRs485Baud(baud)) return; + if (rs485Baudrate == baud) return; + + Serial2.flush(); + Serial2.end(); + rs485Baudrate = baud; + Serial2.begin(rs485Baudrate, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); + delay(50); + + addLogText("RS485 baud changed to " + String(rs485Baudrate)); +} + void initRS485() { - Serial2.begin(RS485_BAUDRATE, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); + Serial2.begin(rs485Baudrate, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); delay(100); - addLog("RS485 inicializovano"); - addLog("TX=" + String(RS485_TX_PIN) + ", RX=" + String(RS485_RX_PIN) + ", baud=" + String(RS485_BAUDRATE)); + addLogEntry(LOG_RS485_INIT); + addLogEntry(LOG_RS485_PINS, String(RS485_TX_PIN), String(RS485_RX_PIN), String(rs485Baudrate)); } // ===================================================== @@ -725,40 +1123,40 @@ void initRS485() { // ===================================================== void printETHStatus() { - addLog("Ethernet stav:"); - addLog(" Link: " + String(ethConnected ? "UP" : "DOWN")); - addLog(" IP: " + String(ethGotIP ? ETH.localIP().toString() : "neni")); + addLogEntry(LOG_ETH_STATUS_TITLE); + addLogEntry(LOG_ETH_STATUS_LINK, ethConnected ? "UP" : "DOWN"); + addLogEntry(LOG_ETH_STATUS_IP, ethGotIP ? ETH.localIP().toString() : trCurrent("není", "none")); if (ethConnected || ethGotIP) { - addLog(" MAC: " + ETH.macAddress()); + addLogEntry(LOG_ETH_STATUS_MAC, ETH.macAddress()); } } void WiFiEvent(WiFiEvent_t event) { switch (event) { case ARDUINO_EVENT_ETH_START: - addLog("[ETH] START"); + addLogEntry(LOG_ETH_START); ETH.setHostname("esp32-control"); break; case ARDUINO_EVENT_ETH_CONNECTED: - addLog("[ETH] CONNECTED"); + addLogEntry(LOG_ETH_CONNECTED); ethConnected = true; break; case ARDUINO_EVENT_ETH_GOT_IP: - addLog("[ETH] GOT IP: " + ETH.localIP().toString()); + addLogEntry(LOG_ETH_GOT_IP, ETH.localIP().toString()); ethGotIP = true; printETHStatus(); break; case ARDUINO_EVENT_ETH_DISCONNECTED: - addLog("[ETH] DISCONNECTED"); + addLogEntry(LOG_ETH_DISCONNECTED); ethConnected = false; ethGotIP = false; break; case ARDUINO_EVENT_ETH_STOP: - addLog("[ETH] STOP"); + addLogEntry(LOG_ETH_STOP); ethConnected = false; ethGotIP = false; break; @@ -769,7 +1167,7 @@ void WiFiEvent(WiFiEvent_t event) { } void initEthernet() { - addLog("Inicializace Ethernetu..."); + addLogEntry(LOG_ETH_INIT); WiFi.onEvent(WiFiEvent); bool ok = ETH.begin( @@ -782,81 +1180,122 @@ void initEthernet() { ); if (!ok) { - addLog("CHYBA: ETH.begin() selhalo"); + addLogEntry(LOG_ETH_BEGIN_FAILED); return; } unsigned long start = millis(); while (millis() - start < 15000) { if (ethGotIP) { - addLog("Ethernet OK"); + addLogEntry(LOG_ETH_OK); return; } delay(100); } - addLog("Ethernet bez IP adresy"); + addLogEntry(LOG_ETH_NO_IP); } // ===================================================== // HTML // ===================================================== -String buildStatusPanel() { - String ip = ethGotIP ? ETH.localIP().toString() : "neni"; +String buildStatusPanel(UiLang lang) { + String ip = ethGotIP ? ETH.localIP().toString() : tr(lang, "není", "none"); String mac = (ethConnected || ethGotIP) ? ETH.macAddress() : "-"; - String temp = (sht40Found && !isnan(lastTemperature)) ? String(lastTemperature, 2) + " °C" : "neni"; - String hum = (sht40Found && !isnan(lastHumidity)) ? String(lastHumidity, 2) + " %RH" : "neni"; - String bmpTemp = (bmp280Found && !isnan(lastBmpTemperature)) ? String(lastBmpTemperature, 2) + " °C" : "neni"; - String pressure = (bmp280Found && !isnan(lastPressure_hPa)) ? String(lastPressure_hPa, 2) + " hPa" : "neni"; + String temp = (sht40Found && !isnan(lastTemperature)) ? String(lastTemperature, 2) + " °C" : tr(lang, "není", "none"); + String hum = (sht40Found && !isnan(lastHumidity)) ? String(lastHumidity, 2) + " %RH" : tr(lang, "není", "none"); + String bmpTemp = (bmp280Found && !isnan(lastBmpTemperature)) ? String(lastBmpTemperature, 2) + " °C" : tr(lang, "není", "none"); + String pressure = (bmp280Found && !isnan(lastPressure_hPa)) ? String(lastPressure_hPa, 2) + " hPa" : tr(lang, "není", "none"); String html; - html.reserve(2600); + html.reserve(3000); - html += F("

Stav systému

"); - html += F("

Ethernet link: "); + html += F("

"); + html += tr(lang, "Stav systému", "System status"); + html += F("

"); + + html += F("

"); + html += tr(lang, "Ethernet link:", "Ethernet link:"); + html += F(" "); html += (ethConnected ? "UP" : "DOWN"); + html += F("

IP: "); html += ip; + html += F("

MAC: "); html += mac; - html += F("

SD karta: "); - html += (sdOk ? "OK" : "CHYBA"); - html += F("

SD detect: "); - html += (sdCardInserted ? "vložena" : "není vložena"); + + html += F("

"); + html += tr(lang, "SD karta:", "SD card:"); + html += F(" "); + html += (sdOk ? "OK" : "" + tr(lang, "CHYBA", "ERROR") + ""); + + html += F("

"); + html += tr(lang, "SD detect:", "SD detect:"); + html += F(" "); + html += (sdCardInserted ? "" + tr(lang, "vložena", "inserted") + "" + : "" + tr(lang, "není vložena", "not inserted") + ""); + html += F("

SHT40: "); - html += (sht40Found ? "OK" : "není nalezen"); - html += F("

Teplota SHT40: "); + html += (sht40Found ? "OK" : "" + tr(lang, "není nalezen", "not found") + ""); + + html += F("

"); + html += tr(lang, "SHT40 teplota:", "SHT40 temperature:"); + html += F(" "); html += temp; - html += F("

Vlhkost: "); + + html += F("

"); + html += tr(lang, "Vlhkost:", "Humidity:"); + html += F(" "); html += hum; + html += F("

BMP280: "); - html += (bmp280Found ? "OK" : "není nalezen"); - html += F("

Teplota BMP280: "); + html += (bmp280Found ? "OK" : "" + tr(lang, "není nalezen", "not found") + ""); + + html += F("

"); + html += tr(lang, "BMP280 teplota:", "BMP280 temperature:"); + html += F(" "); html += bmpTemp; - html += F("

Tlak: "); + + html += F("

"); + html += tr(lang, "Tlak:", "Pressure:"); + html += F(" "); html += pressure; + html += F("

FreeHeap: "); html += String(ESP.getFreeHeap()); html += F(" B ("); html += String(getFreeHeapPercent(), 1); - html += F(" %)

Heap celkem: "); + html += F(" %)

"); + html += tr(lang, "Heap celkem:", "Total heap:"); + html += F(" "); html += String(totalHeapBytes); html += F(" B

"); - html += F("

Obnovit celou stránku

"); + + html += F("

"); + html += tr(lang, "Obnovit celou stránku", "Reload full page"); + html += F("

"); return html; } -String buildNeoPixelPanel() { +String buildNeoPixelPanel(UiLang lang) { String html; - html.reserve(7000); + html.reserve(7500); html += F("

NeoPixel

"); html += F("
"); + html += F(""); - html += F(""); + html += F(""); html += F("
"); html += F("
"); @@ -865,7 +1304,9 @@ String buildNeoPixelPanel() { html += F("
"); html += F("
"); - html += F(""); + html += F(""); html += F("
"); html += F("
"); html += F("
"); @@ -889,9 +1330,18 @@ String buildNeoPixelPanel() { html += F("
"); + html += F("
"); + html += F("
"); + html += F("
Aktuální barva: R="); + html += F("'>
"); + html += tr(lang, "Aktuální barva:", "Current color:"); + html += F(" R="); html += String(ledR); html += F(" G="); html += String(ledG); @@ -899,32 +1349,70 @@ String buildNeoPixelPanel() { html += String(ledB); html += F("
"); - html += F(""); + html += F(""); html += F("
"); html += F("
" + "" "" "" "" - "" + "" "
"); return html; } -String buildRS485Panel() { +String buildRS485Panel(UiLang lang) { String html; - html.reserve(2600); + html.reserve(3600); html += F("

RS485

" "
" - "" - "" - "" + "" + "" + ""); + + html += F("
"); + + html += F("
"); + + html += F("
" "
" - "

Poslední odesláno:

"); + "

"); + html += tr(lang, "Poslední odesláno:", "Last sent:"); + html += F("

"); html += htmlEscape(rs485LastSent); - html += F("

Poslední přijato:

"); + html += F("

"); + html += tr(lang, "Poslední přijato:", "Last received:"); + html += F("

"); html += htmlEscape(rs485LastReceived); html += F("
"); @@ -941,20 +1429,26 @@ String buildRS485RxJson() { return json; } -String buildLogPanel() { +String buildLogPanel(UiLang lang) { String html; - html.reserve(8500); + html.reserve(12000); + + html += F("

"); + html += tr(lang, "Log", "Log"); + html += F("

"); + + for (size_t i = 0; i < eventLog.size(); i++) { + html += htmlEscape(renderLogEntry(eventLog[i], lang)); + if (i + 1 < eventLog.size()) html += '\n'; + } - html += F("

Log

"); - html += htmlEscape(eventLog); html += F("
"); - return html; } -String buildMainPage(const String& currentPath) { +String buildMainPage(const String& currentPath, UiLang lang) { String html; - html.reserve(50000); + html.reserve(52000); html += F( "" @@ -968,9 +1462,12 @@ String buildMainPage(const String& currentPath) { ".card-wide{grid-column:1 / -1;}" ".tools{display:grid;grid-template-columns:2fr 1fr;gap:16px;align-items:start;margin-bottom:16px;}" ".toolbox{background:#f8fafc;border:1px solid #dbe3ee;border-radius:12px;padding:12px;}" - "h1,h2,h3{margin-top:0}"".page-title{display:flex;align-items:center;gap:18px;margin-bottom:22px;flex-wrap:wrap}"".page-title h1{margin:0}"".brand-logo{display:block;max-width:180px;width:100%;height:auto}" + "h1,h2,h3{margin-top:0}" + ".page-title{display:flex;align-items:center;gap:18px;flex-wrap:wrap}" + ".page-title h1{margin:0}" + ".brand-logo{display:block;max-width:180px;width:100%;height:auto}" "label{display:block;margin:8px 0 4px;font-weight:bold}" - "input[type=text],input[type=number],input[type=file],textarea{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px;box-sizing:border-box}" + "input[type=text],input[type=number],input[type=file],textarea,select{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px;box-sizing:border-box}" "button{padding:10px 14px;border:0;border-radius:10px;background:#1f6feb;color:#fff;cursor:pointer;margin-top:8px}" "button:hover{opacity:.92}" "table{width:100%;border-collapse:collapse}" @@ -983,35 +1480,63 @@ String buildMainPage(const String& currentPath) { "a:hover{text-decoration:underline}" "@media (max-width:900px){.tools{grid-template-columns:1fr;}.brand-logo{max-width:240px;}}" "
" - "
""""

ESPLan v2.x Control Panel

""
" ); + html += F("
"); + + html += F("
" + "" + "

ESPLan v2.x Control Panel

" + "
"); + + html += F("
"); + html += F(""); + html += F(""); + html += F("
"); + + html += F("
"); + html += F("
"); html += F("
"); - html += buildStatusPanel(); + html += buildStatusPanel(lang); html += F("
"); html += F("
"); - html += buildNeoPixelPanel(); + html += buildNeoPixelPanel(lang); html += F("
"); html += F("
"); - html += buildRS485Panel(); + html += buildRS485Panel(lang); html += F("
"); html += F("
"); - html += buildSdPanel(currentPath); + html += buildSdPanel(currentPath, lang); html += F("
"); html += F("
"); - html += buildLogPanel(); + html += buildLogPanel(lang); html += F("
"); html += F("
"); + html += F("" ); + html += F(""); html += F("
"); return html; } @@ -1258,53 +1783,79 @@ String buildMainPage(const String& currentPath) { // ===================================================== void handleRoot() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; - server.send(200, "text/html; charset=utf-8", buildMainPage(path)); + server.send(200, "text/html; charset=utf-8", buildMainPage(path, lang)); } void handleStatus() { - server.send(200, "text/html; charset=utf-8", buildStatusPanel()); + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + server.send(200, "text/html; charset=utf-8", buildStatusPanel(lang)); } void handleRS485Panel() { - server.send(200, "text/html; charset=utf-8", buildRS485Panel()); + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + server.send(200, "text/html; charset=utf-8", buildRS485Panel(lang)); } void handleRS485Rx() { + updateCurrentLangFromRequest(); server.send(200, "application/json; charset=utf-8", buildRS485RxJson()); } void handleLogPanel() { - server.send(200, "text/html; charset=utf-8", buildLogPanel()); + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + server.send(200, "text/html; charset=utf-8", buildLogPanel(lang)); } void handleSdPanel() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; - server.send(200, "text/html; charset=utf-8", buildSdPanel(path)); + server.send(200, "text/html; charset=utf-8", buildSdPanel(path, lang)); } void handleSdState() { + updateCurrentLangFromRequest(); server.send(200, "application/json; charset=utf-8", buildSdStateJson()); } void handleLed() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + int r = constrain(server.arg("r").toInt(), 0, 255); int g = constrain(server.arg("g").toInt(), 0, 255); int b = constrain(server.arg("b").toInt(), 0, 255); + int brightness = constrain(server.arg("brightness").toInt(), 0, 255); + ledBrightness = brightness; + setPixelColor(r, g, b); - addLog("LED nastavena: R=" + String(r) + " G=" + String(g) + " B=" + String(b)); - redirectHome(); + addLogEntry(LOG_LED_SET, String(r), String(g), String(b)); + redirectHome(lang); } void handleRS485Send() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + + uint32_t requestedBaud = (uint32_t)server.arg("baud").toInt(); + if (isSupportedRs485Baud(requestedBaud) && requestedBaud != rs485Baudrate) { + applyRS485Baudrate(requestedBaud); + } + String msg = server.arg("msg"); msg.replace("\r", ""); if (msg.length() == 0) { - redirectHome(); + redirectHome(lang); return; } @@ -1312,37 +1863,39 @@ void handleRS485Send() { Serial2.flush(); rs485LastSent = msg; - addLog("RS485 TX: " + msg); - redirectHome(); + addLogEntry(LOG_RS485_TX, msg); + redirectHome(lang); } void handleDownload() { + updateCurrentLangFromRequest(); + if (!sdOk) { - server.send(500, "text/plain", "SD karta není dostupná"); + server.send(500, "text/plain", trCurrent("SD karta není dostupná", "SD card is not available")); return; } String name = normalizePath(server.arg("name")); if (!isSafePath(name)) { - server.send(400, "text/plain", "Neplatná cesta"); + server.send(400, "text/plain", trCurrent("Neplatná cesta", "Invalid path")); return; } if (!SD.exists(name)) { - server.send(404, "text/plain", "Soubor neexistuje"); + server.send(404, "text/plain", trCurrent("Soubor neexistuje", "File does not exist")); return; } File file = SD.open(name, FILE_READ); if (!file) { - server.send(500, "text/plain", "Nelze otevřít soubor"); + server.send(500, "text/plain", trCurrent("Nelze otevřít soubor", "Cannot open file")); return; } if (file.isDirectory()) { file.close(); - server.send(400, "text/plain", "Adresář nelze stáhnout"); + server.send(400, "text/plain", trCurrent("Adresář nelze stáhnout", "Directory cannot be downloaded")); return; } @@ -1358,8 +1911,11 @@ void handleDownload() { } void handleDelete() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + if (!sdOk) { - server.send(500, "text/plain", "SD karta není dostupná"); + server.send(500, "text/plain", trCurrent("SD karta není dostupná", "SD card is not available")); return; } @@ -1367,18 +1923,18 @@ void handleDelete() { String currentPath = normalizeDirPath(server.arg("path")); if (!isSafePath(name) || !isSafePath(currentPath)) { - server.send(400, "text/plain", "Neplatná cesta"); + server.send(400, "text/plain", trCurrent("Neplatná cesta", "Invalid path")); return; } if (!SD.exists(name)) { - server.send(404, "text/plain", "Soubor nebo adresář neexistuje"); + server.send(404, "text/plain", trCurrent("Soubor nebo adresář neexistuje", "File or directory does not exist")); return; } File entry = SD.open(name); if (!entry) { - server.send(500, "text/plain", "Nelze otevřít položku"); + server.send(500, "text/plain", trCurrent("Nelze otevřít položku", "Cannot open item")); return; } @@ -1393,18 +1949,21 @@ void handleDelete() { } if (ok) { - addLog(String("SD delete OK: ") + name); + addLogEntry(LOG_SD_DELETE_OK, name); } else { - addLog(String("SD delete CHYBA: ") + name); + addLogEntry(LOG_SD_DELETE_ERROR, name); } bumpSdStateVersion(); - redirectToPath(currentPath); + redirectToPath(currentPath, lang); } void handleMkdir() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; + if (!sdOk) { - server.send(500, "text/plain", "SD karta není dostupná"); + server.send(500, "text/plain", trCurrent("SD karta není dostupná", "SD card is not available")); return; } @@ -1415,7 +1974,7 @@ void handleMkdir() { if (!isSafePath(currentPath)) currentPath = "/"; if (name.length() == 0) { - redirectToPath(currentPath); + redirectToPath(currentPath, lang); return; } @@ -1424,34 +1983,36 @@ void handleMkdir() { while (name.endsWith("/")) name.remove(name.length() - 1); if (name.length() == 0 || name.indexOf("..") >= 0 || name.indexOf('/') >= 0) { - addLog("MKDIR: neplatný název adresáře"); - redirectToPath(currentPath); + addLogEntry(LOG_MKDIR_INVALID); + redirectToPath(currentPath, lang); return; } String newDir = joinPath(currentPath, name); if (SD.exists(newDir)) { - addLog("MKDIR: adresář již existuje: " + newDir); - redirectToPath(currentPath); + addLogEntry(LOG_MKDIR_EXISTS, newDir); + redirectToPath(currentPath, lang); return; } if (SD.mkdir(newDir)) { - addLog("MKDIR OK: " + newDir); + addLogEntry(LOG_MKDIR_OK, newDir); } else { - addLog("MKDIR CHYBA: " + newDir); + addLogEntry(LOG_MKDIR_ERROR, newDir); } bumpSdStateVersion(); - redirectToPath(currentPath); + redirectToPath(currentPath, lang); } void handleUploadFinished() { + updateCurrentLangFromRequest(); + UiLang lang = currentUiLang; String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; bumpSdStateVersion(); - redirectToPath(path); + redirectToPath(path, lang); } void handleFileUpload() { @@ -1461,6 +2022,8 @@ void handleFileUpload() { static String uploadTargetPath = "/"; if (upload.status == UPLOAD_FILE_START) { + updateCurrentLangFromRequest(); + uploadTargetPath = normalizeDirPath(server.arg("path")); if (!isSafePath(uploadTargetPath)) uploadTargetPath = "/"; @@ -1472,11 +2035,11 @@ void handleFileUpload() { String filename = joinPath(uploadTargetPath, cleanName); if (!isSafePath(filename)) { - addLog("Upload: neplatné jméno souboru"); + addLogEntry(LOG_UPLOAD_INVALID_NAME); return; } - addLog("Upload start: " + filename); + addLogEntry(LOG_UPLOAD_START, filename); if (SD.exists(filename)) { SD.remove(filename); @@ -1484,7 +2047,7 @@ void handleFileUpload() { uploadFile = SD.open(filename, FILE_WRITE); if (!uploadFile) { - addLog("Upload: nelze otevřít soubor pro zápis"); + addLogEntry(LOG_UPLOAD_OPEN_WRITE_FAILED); } } else if (upload.status == UPLOAD_FILE_WRITE) { @@ -1496,25 +2059,25 @@ void handleFileUpload() { else if (upload.status == UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); - addLog("Upload konec: " + String(upload.totalSize) + " B"); + addLogEntry(LOG_UPLOAD_FINISHED, String(upload.totalSize)); } } else if (upload.status == UPLOAD_FILE_ABORTED) { if (uploadFile) { uploadFile.close(); } - addLog("Upload přerušen"); + addLogEntry(LOG_UPLOAD_ABORTED); } } - void handleLogo() { server.sendHeader("Cache-Control", "public, max-age=86400"); server.send_P(200, "image/png", (PGM_P)logo_web_png, logo_web_png_len); } void handleNotFound() { - server.send(404, "text/plain", "404 Not Found"); + updateCurrentLangFromRequest(); + server.send(404, "text/plain", trCurrent("404 Nenalezeno", "404 Not Found")); } void setupWebServer() { @@ -1535,7 +2098,7 @@ void setupWebServer() { server.onNotFound(handleNotFound); server.begin(); - addLog("Web server spusten na portu 80"); + addLogEntry(LOG_WEB_STARTED); } // ===================================================== @@ -1548,21 +2111,22 @@ void setup() { printResetReason(); delay(500); - addLog(""); - addLog("======================================"); - addLog("START"); - addLog("======================================"); + addLogText(""); + addLogText("======================================"); + addLogEntry(LOG_START); + addLogText("======================================"); pixel.begin(); pixel.clear(); pixel.show(); - setPixelColor(0, 0, 10); + pixel.setBrightness(ledBrightness); + setPixelColor(0, 255, 0); totalHeapBytes = ESP.getHeapSize(); - addLog("CPU: " + String(ESP.getCpuFreqMHz()) + " MHz"); - addLog("Heap celkem: " + String(totalHeapBytes) + " B"); - addLog("Heap free: " + String(ESP.getFreeHeap()) + " B (" + String(getFreeHeapPercent(), 1) + " %)"); + addLogEntry(LOG_CPU_FREQ, String(ESP.getCpuFreqMHz())); + addLogEntry(LOG_HEAP_TOTAL, String(totalHeapBytes)); + addLogEntry(LOG_HEAP_FREE, String(ESP.getFreeHeap()), String(getFreeHeapPercent(), 1)); initI2CDevices(); @@ -1575,11 +2139,11 @@ void setup() { initEthernet(); setupWebServer(); - addLog("READY"); + addLogEntry(LOG_READY); if (ethGotIP) { - addLog("Otevri v browseru: http://" + ETH.localIP().toString() + "/"); + addLogEntry(LOG_OPEN_BROWSER, ETH.localIP().toString()); } else { - addLog("Ethernet zatim nema IP adresu"); + addLogEntry(LOG_ETH_NO_IP_YET); } } @@ -1589,7 +2153,6 @@ void loop() { handleSdHotplug(); bool rs485RxChanged = false; - static String rs485LineBuffer = ""; while (Serial2.available()) { @@ -1608,7 +2171,7 @@ void loop() { String line = rs485LineBuffer; line.trim(); if (line.length() > 0) { - addLog("RS485 RX: " + line); + addLogEntry(LOG_RS485_RX, line); } rs485LineBuffer = ""; } @@ -1622,10 +2185,6 @@ void loop() { rs485RxVersion++; } - if (rs485RxChanged) { - rs485RxVersion++; - } - if (millis() - lastSensorRead > 10000) { lastSensorRead = millis(); @@ -1644,23 +2203,24 @@ void loop() { Serial.println(); Serial.println(F("----- STATUS -----")); Serial.printf("ETH link: %s\n", ethConnected ? "UP" : "DOWN"); - Serial.printf("ETH IP: %s\n", ethGotIP ? ETH.localIP().toString().c_str() : "neni"); - Serial.printf("SD: %s\n", sdOk ? "OK" : "CHYBA"); - Serial.printf("SD detect: %s\n", sdCardInserted ? "vlozena" : "neni"); - Serial.printf("SD verze: %lu\n", (unsigned long)sdStateVersion); - Serial.printf("SHT40: %s\n", sht40Found ? "OK" : "neni"); + Serial.printf("ETH IP: %s\n", ethGotIP ? ETH.localIP().toString().c_str() : "none"); + Serial.printf("SD: %s\n", sdOk ? "OK" : "ERROR"); + Serial.printf("SD detect: %s\n", sdCardInserted ? "inserted" : "not inserted"); + Serial.printf("SD version: %lu\n", (unsigned long)sdStateVersion); + Serial.printf("SHT40: %s\n", sht40Found ? "OK" : "not found"); if (sht40Found && !isnan(lastTemperature) && !isnan(lastHumidity)) { Serial.printf("SHT40 -> T=%.2f C, RH=%.2f %%\n", lastTemperature, lastHumidity); } - Serial.printf("BMP280: %s\n", bmp280Found ? "OK" : "neni"); + Serial.printf("BMP280: %s\n", bmp280Found ? "OK" : "not found"); if (bmp280Found && !isnan(lastBmpTemperature) && !isnan(lastPressure_hPa)) { Serial.printf("BMP280 -> T=%.2f C, P=%.2f hPa\n", lastBmpTemperature, lastPressure_hPa); } + Serial.printf("RS485 baud: %lu\n", (unsigned long)rs485Baudrate); Serial.printf("LED -> R=%u G=%u B=%u\n", ledR, ledG, ledB); Serial.printf("FreeHeap: %u B (%.1f %%)\n", ESP.getFreeHeap(), getFreeHeapPercent()); - Serial.printf("Heap celkem: %u B\n", totalHeapBytes); + Serial.printf("Heap total: %u B\n", totalHeapBytes); Serial.println(F("------------------")); }