/* 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 */ #include #include #include #include #include #include #include #include #include #include #include "esp_system.h" #include "logo_web.h" // ===================================================== // PINOUT // ===================================================== // ---------- Ethernet LAN8720 ---------- #define ETH_PHY_TYPE ETH_PHY_LAN8720 #define ETH_PHY_ADDR 0 #define ETH_PHY_MDC 23 #define ETH_PHY_MDIO 18 #define ETH_PHY_POWER -1 #define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT // ---------- RS485 ---------- #define RS485_TX_PIN 4 #define RS485_RX_PIN 36 #define RS485_BAUDRATE 9600 // ---------- I2C ---------- #define I2C_SDA_PIN 33 #define I2C_SCL_PIN 32 // ---------- BMP280 ---------- #define BMP280_ADDR_1 0x76 #define BMP280_ADDR_2 0x77 // ---------- SD karta ---------- #define SD_MISO_PIN 12 #define SD_MOSI_PIN 13 #define SD_SCK_PIN 14 #define SD_CS_PIN 2 #define SD_CD_PIN 34 #define SD_CD_ACTIVE_STATE HIGH // ---------- NeoPixel ---------- #define NEOPIXEL_PIN 0 #define NEOPIXEL_COUNT 1 // ===================================================== // GLOBÁLNÍ OBJEKTY // ===================================================== WebServer server(80); SPIClass sdSPI(VSPI); Adafruit_SHT4x sht4 = Adafruit_SHT4x(); Adafruit_BMP280 bmp280; Adafruit_NeoPixel pixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); bool ethConnected = false; bool ethGotIP = false; bool sdOk = false; bool sht40Found = false; bool bmp280Found = false; bool sdCardInserted = false; float lastTemperature = NAN; float lastHumidity = NAN; float lastBmpTemperature = NAN; float lastPressure_hPa = NAN; uint8_t ledR = 0; uint8_t ledG = 0; uint8_t ledB = 10; String rs485LastSent = ""; String rs485LastReceived = ""; uint32_t rs485RxVersion = 0; String eventLog = ""; File uploadFile; unsigned long lastSensorRead = 0; unsigned long lastStatusPrint = 0; // SD detect debounce / hotplug bool lastSdDetectRaw = false; bool lastSdDetectStable = false; unsigned long lastSdDetectChangeMs = 0; // SD state version for web refresh uint32_t sdStateVersion = 0; // Heap baseline = total RAM chip heap uint32_t totalHeapBytes = 0; // ===================================================== // RESET REASON // ===================================================== void printResetReason() { esp_reset_reason_t reason = esp_reset_reason(); Serial.print("Reset reason: "); switch (reason) { case ESP_RST_UNKNOWN: Serial.println("UNKNOWN"); break; case ESP_RST_POWERON: Serial.println("POWERON"); break; case ESP_RST_EXT: Serial.println("EXTERNAL"); break; case ESP_RST_SW: Serial.println("SOFTWARE"); break; case ESP_RST_PANIC: Serial.println("PANIC"); break; case ESP_RST_INT_WDT: Serial.println("INT_WDT"); break; case ESP_RST_TASK_WDT: Serial.println("TASK_WDT"); break; case ESP_RST_WDT: Serial.println("WDT"); break; case ESP_RST_DEEPSLEEP: Serial.println("DEEPSLEEP"); break; case ESP_RST_BROWNOUT: Serial.println("BROWNOUT"); break; case ESP_RST_SDIO: Serial.println("SDIO"); break; default: Serial.println((int)reason); break; } } // ===================================================== // LOG // ===================================================== void addLog(const String& msg) { Serial.println(msg); eventLog += msg + "\n"; if (eventLog.length() > 8000) { eventLog.remove(0, eventLog.length() - 8000); } } // ===================================================== // POMOCNÉ FUNKCE // ===================================================== String htmlEscape(const String& s) { String out; out.reserve(s.length() + 16); for (size_t i = 0; i < s.length(); i++) { char c = s[i]; switch (c) { case '&': out += F("&"); break; case '<': out += F("<"); break; case '>': out += F(">"); break; case '"': out += F("""); break; case '\'': out += F("'"); break; default: out += c; break; } } return out; } String jsonEscape(const String& s) { String out; out.reserve(s.length() + 16); for (size_t i = 0; i < s.length(); i++) { char c = s[i]; switch (c) { case '\\': out += F("\\\\"); break; case '"': out += F("\\\""); break; case '\b': out += F("\\b"); break; case '\f': out += F("\\f"); break; case '\n': out += F("\\n"); break; case '\r': out += F("\\r"); break; case '\t': out += F("\\t"); break; default: if ((uint8_t)c < 0x20) { char buf[7]; snprintf(buf, sizeof(buf), "\\u%04X", (uint8_t)c); out += buf; } else { out += c; } break; } } return out; } String urlEncode(const String& s) { String out; char hex[4]; for (size_t i = 0; i < s.length(); i++) { uint8_t c = (uint8_t)s[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' || c == '/') { out += (char)c; } else { snprintf(hex, sizeof(hex), "%%%02X", c); out += hex; } } return out; } String formatBytes(uint64_t bytes) { if (bytes < 1024ULL) return String((uint32_t)bytes) + " B"; if (bytes < 1024ULL * 1024ULL) return String((float)bytes / 1024.0f, 1) + " kB"; if (bytes < 1024ULL * 1024ULL * 1024ULL) return String((float)bytes / 1024.0f / 1024.0f, 1) + " MB"; return String((float)bytes / 1024.0f / 1024.0f / 1024.0f, 1) + " GB"; } String normalizePath(String path) { path.trim(); if (path.length() == 0) return "/"; if (!path.startsWith("/")) path = "/" + path; while (path.indexOf("//") >= 0) path.replace("//", "/"); return path; } String normalizeDirPath(String path) { path = normalizePath(path); if (path.length() > 1 && path.endsWith("/")) { path.remove(path.length() - 1); } return path; } bool isSafePath(String path) { if (!path.startsWith("/")) return false; if (path.indexOf("..") >= 0) return false; if (path.indexOf('\\') >= 0) return false; return true; } String parentPath(String path) { path = normalizeDirPath(path); if (path == "/") return "/"; int lastSlash = path.lastIndexOf('/'); if (lastSlash <= 0) return "/"; return path.substring(0, lastSlash); } String joinPath(String base, String name) { base = normalizeDirPath(base); if (base != "/" && base.endsWith("/")) { base.remove(base.length() - 1); } while (name.startsWith("/")) { name.remove(0, 1); } if (base == "/") return "/" + name; return base + "/" + name; } bool deleteRecursive(const String& path) { if (!SD.exists(path)) return false; File entry = SD.open(path); if (!entry) return false; bool isDir = entry.isDirectory(); entry.close(); if (!isDir) { return SD.remove(path); } File dir = SD.open(path); if (!dir || !dir.isDirectory()) { if (dir) dir.close(); return false; } File child = dir.openNextFile(); while (child) { String childPath = normalizePath(String(child.name())); bool childIsDir = child.isDirectory(); child.close(); if (childIsDir) { if (!deleteRecursive(childPath)) { dir.close(); return false; } } else { if (!SD.remove(childPath)) { dir.close(); return false; } } yield(); child = dir.openNextFile(); } dir.close(); return SD.rmdir(path); } String contentTypeFromFilename(const String& filename) { String lower = filename; lower.toLowerCase(); if (lower.endsWith(".htm") || lower.endsWith(".html")) return "text/html"; if (lower.endsWith(".css")) return "text/css"; if (lower.endsWith(".js")) return "application/javascript"; if (lower.endsWith(".txt")) return "text/plain"; if (lower.endsWith(".log")) return "text/plain"; if (lower.endsWith(".json")) return "application/json"; if (lower.endsWith(".csv")) return "text/csv"; if (lower.endsWith(".xml")) return "application/xml"; if (lower.endsWith(".pdf")) return "application/pdf"; if (lower.endsWith(".zip")) return "application/zip"; if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; if (lower.endsWith(".png")) return "image/png"; if (lower.endsWith(".gif")) return "image/gif"; return "application/octet-stream"; } String colorToHex(uint8_t r, uint8_t g, uint8_t b) { char buf[8]; snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b); return String(buf); } float getFreeHeapPercent() { if (totalHeapBytes == 0) return 0.0f; return ((float)ESP.getFreeHeap() * 100.0f) / (float)totalHeapBytes; } void bumpSdStateVersion() { sdStateVersion++; } void setPixelColor(uint8_t r, uint8_t g, uint8_t b) { ledR = r; ledG = g; ledB = b; pixel.setPixelColor(0, pixel.Color(r, g, b)); pixel.show(); } void redirectHome() { server.sendHeader("Location", "/"); server.send(303); } void redirectToPath(const String& path) { server.sendHeader("Location", "/?path=" + urlEncode(normalizeDirPath(path))); server.send(303); } // ===================================================== // SHT40 // ===================================================== void readSHT40() { if (!sht40Found) return; sensors_event_t humidity, temp; if (sht4.getEvent(&humidity, &temp)) { lastTemperature = temp.temperature; lastHumidity = humidity.relative_humidity; } else { addLog("CHYBA: cteni SHT40 selhalo"); } } void initSHT40() { if (!sht4.begin(&Wire)) { sht40Found = false; addLog("SHT40 nebyl nalezen"); return; } sht4.setPrecision(SHT4X_HIGH_PRECISION); sht4.setHeater(SHT4X_NO_HEATER); sht40Found = true; addLog("SHT40 detekovan OK"); readSHT40(); } // ===================================================== // BMP280 // ===================================================== void readBMP280() { if (!bmp280Found) return; float t = bmp280.readTemperature(); float p = bmp280.readPressure(); if (isnan(t) || isnan(p) || p <= 0.0f) { addLog("CHYBA: cteni BMP280 selhalo"); return; } lastBmpTemperature = t; lastPressure_hPa = p / 100.0f; } void initBMP280() { bool ok = false; if (bmp280.begin(BMP280_ADDR_1)) { ok = true; addLog("BMP280 detekovan na adrese 0x76"); } else if (bmp280.begin(BMP280_ADDR_2)) { ok = true; addLog("BMP280 detekovan na adrese 0x77"); } if (!ok) { bmp280Found = false; addLog("BMP280 nebyl nalezen"); return; } bmp280.setSampling( Adafruit_BMP280::MODE_NORMAL, Adafruit_BMP280::SAMPLING_X2, Adafruit_BMP280::SAMPLING_X16, Adafruit_BMP280::FILTER_X16, Adafruit_BMP280::STANDBY_MS_500 ); bmp280Found = true; readBMP280(); } // ===================================================== // I2C senzory // ===================================================== void initI2CDevices() { addLog("Inicializace I2C..."); Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); delay(50); initSHT40(); initBMP280(); } // ===================================================== // SD KARTA // ===================================================== bool isSdCardPresent() { pinMode(SD_CD_PIN, INPUT); return digitalRead(SD_CD_PIN) == SD_CD_ACTIVE_STATE; } void initSDCard() { sdCardInserted = isSdCardPresent(); if (!sdCardInserted) { sdOk = false; addLog("SD karta neni vlozena"); bumpSdStateVersion(); return; } addLog("Inicializace SD karty..."); SD.end(); sdSPI.end(); delay(10); sdSPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN); if (!SD.begin(SD_CS_PIN, sdSPI)) { sdOk = false; addLog("CHYBA: SD.begin() selhalo"); bumpSdStateVersion(); return; } if (SD.cardType() == CARD_NONE) { sdOk = false; addLog("CHYBA: SD karta nenalezena"); SD.end(); sdSPI.end(); bumpSdStateVersion(); return; } sdOk = true; addLog("SD karta OK"); addLog("Velikost karty: " + String((uint32_t)(SD.cardSize() / (1024ULL * 1024ULL))) + " MB"); bumpSdStateVersion(); } void handleSdHotplug() { bool raw = isSdCardPresent(); if (raw != lastSdDetectRaw) { lastSdDetectRaw = raw; lastSdDetectChangeMs = millis(); } if ((millis() - lastSdDetectChangeMs) < 150) { return; } if (raw == lastSdDetectStable) { return; } lastSdDetectStable = raw; sdCardInserted = raw; if (!raw) { if (uploadFile) { uploadFile.close(); } SD.end(); sdSPI.end(); sdOk = false; addLog("SD karta byla vyjmuta"); bumpSdStateVersion(); return; } addLog("SD karta byla vlozena"); delay(50); initSDCard(); } String buildFileTable(const String& currentPath) { if (!sdOk) return F("

SD karta neni dostupna.

"); String path = normalizeDirPath(currentPath); if (!isSafePath(path)) path = "/"; String fallbackPath = parentPath(path); File dir = SD.open(path); if (!dir || !dir.isDirectory()) { if (dir) dir.close(); String html; html += F("

Chyba: Nelze otevřít adresář.

"); html += F("

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

"); html += F("

↩️

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

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

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

↩️

"); } html += F(""); File file = dir.openNextFile(); bool any = false; while (file) { any = true; String childName = String(file.name()); int slashPos = childName.lastIndexOf('/'); if (slashPos >= 0) { childName = childName.substring(slashPos + 1); } String entryPath = joinPath(path, childName); String displayName = childName; if (displayName.startsWith(path) && path != "/") { displayName = displayName.substring(path.length()); if (!displayName.startsWith("/")) displayName = "/" + displayName; } if (displayName.startsWith("/")) displayName.remove(0, 1); if (displayName.length() == 0) displayName = "(bez názvu)"; bool isDir = file.isDirectory(); uint64_t size = isDir ? 0 : file.size(); String entryEsc = htmlEscape(displayName); String entryUrl = urlEncode(entryPath); String currentUrl = urlEncode(path); html += F(""); yield(); file = dir.openNextFile(); } if (!any) { html += F(""); } html += F("
TypNázevVelikostAkce
"); html += isDir ? "📁" : "📄"; html += F(""); if (isDir) { html += F(""); html += entryEsc; html += F(""); } else { html += entryEsc; } html += F(""); html += isDir ? "-" : formatBytes(size); html += F(""); if (!isDir) { html += F("⬇️ | "); } html += F("🗑️"); html += F("
Adresář je prázdný
"); dir.close(); return html; } String buildSdPanel(const String& currentPath) { String html; html.reserve(18000); html += F("

Správa souborů na SD

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

Nahrávání souborů

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

Vytvořit adresář

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

SD karta není dostupná.

"); } html += buildFileTable(currentPath); return html; } String buildSdStateJson() { String json = "{"; json += "\"inserted\":"; json += (sdCardInserted ? "true" : "false"); json += ",\"ok\":"; json += (sdOk ? "true" : "false"); json += ",\"version\":"; json += String(sdStateVersion); json += "}"; return json; } // ===================================================== // RS485 // ===================================================== void initRS485() { Serial2.begin(RS485_BAUDRATE, 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)); } // ===================================================== // ETHERNET // ===================================================== void printETHStatus() { addLog("Ethernet stav:"); addLog(" Link: " + String(ethConnected ? "UP" : "DOWN")); addLog(" IP: " + String(ethGotIP ? ETH.localIP().toString() : "neni")); if (ethConnected || ethGotIP) { addLog(" MAC: " + ETH.macAddress()); } } void WiFiEvent(WiFiEvent_t event) { switch (event) { case ARDUINO_EVENT_ETH_START: addLog("[ETH] START"); ETH.setHostname("esp32-control"); break; case ARDUINO_EVENT_ETH_CONNECTED: addLog("[ETH] CONNECTED"); ethConnected = true; break; case ARDUINO_EVENT_ETH_GOT_IP: addLog("[ETH] GOT IP: " + ETH.localIP().toString()); ethGotIP = true; printETHStatus(); break; case ARDUINO_EVENT_ETH_DISCONNECTED: addLog("[ETH] DISCONNECTED"); ethConnected = false; ethGotIP = false; break; case ARDUINO_EVENT_ETH_STOP: addLog("[ETH] STOP"); ethConnected = false; ethGotIP = false; break; default: break; } } void initEthernet() { addLog("Inicializace Ethernetu..."); WiFi.onEvent(WiFiEvent); bool ok = ETH.begin( ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_POWER, ETH_CLK_MODE ); if (!ok) { addLog("CHYBA: ETH.begin() selhalo"); return; } unsigned long start = millis(); while (millis() - start < 15000) { if (ethGotIP) { addLog("Ethernet OK"); return; } delay(100); } addLog("Ethernet bez IP adresy"); } // ===================================================== // HTML // ===================================================== String buildStatusPanel() { String ip = ethGotIP ? ETH.localIP().toString() : "neni"; 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 html; html.reserve(2600); html += F("

Stav systému

"); html += F("

Ethernet link: "); 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("

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

Teplota SHT40: "); html += temp; html += F("

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

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

Teplota BMP280: "); html += bmpTemp; html += F("

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

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

Heap celkem: "); html += String(totalHeapBytes); html += F(" B

"); html += F("

Obnovit celou stránku

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

NeoPixel

"); html += F("
"); html += F(""); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F(""); html += F("
"); html += F("
"); html += F("
"); html += F(""); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
"); html += F("
Aktuální barva: R="); html += String(ledR); html += F(" G="); html += String(ledG); html += F(" B="); html += String(ledB); html += F("
"); html += F(""); html += F("
"); html += F("
" "" "" "" "" "
"); return html; } String buildRS485Panel() { String html; html.reserve(2600); html += F("

RS485

" "
" "" "" "" "
" "

Poslední odesláno:

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

Poslední přijato:

"); html += htmlEscape(rs485LastReceived); html += F("
"); return html; } String buildRS485RxJson() { String json = "{"; json += "\"version\":"; json += String(rs485RxVersion); json += ",\"received\":\""; json += jsonEscape(rs485LastReceived); json += "\"}"; return json; } String buildLogPanel() { String html; html.reserve(8500); html += F("

Log

"); html += htmlEscape(eventLog); html += F("
"); return html; } String buildMainPage(const String& currentPath) { String html; html.reserve(50000); html += F( "" "" "ESPLan v2.x Control Panel" "
" "
""""

ESPLan v2.x Control Panel

""
" ); html += F("
"); html += F("
"); html += buildStatusPanel(); html += F("
"); html += F("
"); html += buildNeoPixelPanel(); html += F("
"); html += F("
"); html += buildRS485Panel(); html += F("
"); html += F("
"); html += buildSdPanel(currentPath); html += F("
"); html += F("
"); html += buildLogPanel(); html += F("
"); html += F("
"); html += F( "" ); html += F("
"); return html; } // ===================================================== // HTTP HANDLERY // ===================================================== void handleRoot() { String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; server.send(200, "text/html; charset=utf-8", buildMainPage(path)); } void handleStatus() { server.send(200, "text/html; charset=utf-8", buildStatusPanel()); } void handleRS485Panel() { server.send(200, "text/html; charset=utf-8", buildRS485Panel()); } void handleRS485Rx() { server.send(200, "application/json; charset=utf-8", buildRS485RxJson()); } void handleLogPanel() { server.send(200, "text/html; charset=utf-8", buildLogPanel()); } void handleSdPanel() { String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; server.send(200, "text/html; charset=utf-8", buildSdPanel(path)); } void handleSdState() { server.send(200, "application/json; charset=utf-8", buildSdStateJson()); } void handleLed() { 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); setPixelColor(r, g, b); addLog("LED nastavena: R=" + String(r) + " G=" + String(g) + " B=" + String(b)); redirectHome(); } void handleRS485Send() { String msg = server.arg("msg"); msg.replace("\r", ""); if (msg.length() == 0) { redirectHome(); return; } Serial2.println(msg); Serial2.flush(); rs485LastSent = msg; addLog("RS485 TX: " + msg); redirectHome(); } void handleDownload() { if (!sdOk) { server.send(500, "text/plain", "SD karta není dostupná"); return; } String name = normalizePath(server.arg("name")); if (!isSafePath(name)) { server.send(400, "text/plain", "Neplatná cesta"); return; } if (!SD.exists(name)) { server.send(404, "text/plain", "Soubor neexistuje"); return; } File file = SD.open(name, FILE_READ); if (!file) { server.send(500, "text/plain", "Nelze otevřít soubor"); return; } if (file.isDirectory()) { file.close(); server.send(400, "text/plain", "Adresář nelze stáhnout"); return; } String downloadName = String(file.name()); downloadName = normalizePath(downloadName); if (downloadName.startsWith("/")) { downloadName = downloadName.substring(1); } server.sendHeader("Content-Disposition", "attachment; filename=\"" + downloadName + "\""); server.streamFile(file, contentTypeFromFilename(name)); file.close(); } void handleDelete() { if (!sdOk) { server.send(500, "text/plain", "SD karta není dostupná"); return; } String name = normalizePath(server.arg("name")); String currentPath = normalizeDirPath(server.arg("path")); if (!isSafePath(name) || !isSafePath(currentPath)) { server.send(400, "text/plain", "Neplatná cesta"); return; } if (!SD.exists(name)) { server.send(404, "text/plain", "Soubor nebo adresář neexistuje"); return; } File entry = SD.open(name); if (!entry) { server.send(500, "text/plain", "Nelze otevřít položku"); return; } bool isDir = entry.isDirectory(); entry.close(); bool ok = false; if (isDir) { ok = deleteRecursive(name); } else { ok = SD.remove(name); } if (ok) { addLog(String("SD delete OK: ") + name); } else { addLog(String("SD delete CHYBA: ") + name); } bumpSdStateVersion(); redirectToPath(currentPath); } void handleMkdir() { if (!sdOk) { server.send(500, "text/plain", "SD karta není dostupná"); return; } String currentPath = normalizeDirPath(server.arg("path")); String name = server.arg("name"); name.trim(); if (!isSafePath(currentPath)) currentPath = "/"; if (name.length() == 0) { redirectToPath(currentPath); return; } name.replace("\\", "/"); while (name.startsWith("/")) name.remove(0, 1); 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); return; } String newDir = joinPath(currentPath, name); if (SD.exists(newDir)) { addLog("MKDIR: adresář již existuje: " + newDir); redirectToPath(currentPath); return; } if (SD.mkdir(newDir)) { addLog("MKDIR OK: " + newDir); } else { addLog("MKDIR CHYBA: " + newDir); } bumpSdStateVersion(); redirectToPath(currentPath); } void handleUploadFinished() { String path = normalizeDirPath(server.arg("path")); if (!isSafePath(path)) path = "/"; bumpSdStateVersion(); redirectToPath(path); } void handleFileUpload() { if (!sdOk) return; HTTPUpload& upload = server.upload(); static String uploadTargetPath = "/"; if (upload.status == UPLOAD_FILE_START) { uploadTargetPath = normalizeDirPath(server.arg("path")); if (!isSafePath(uploadTargetPath)) uploadTargetPath = "/"; String cleanName = upload.filename; cleanName.replace("\\", "/"); int slashPos = cleanName.lastIndexOf('/'); if (slashPos >= 0) cleanName = cleanName.substring(slashPos + 1); String filename = joinPath(uploadTargetPath, cleanName); if (!isSafePath(filename)) { addLog("Upload: neplatné jméno souboru"); return; } addLog("Upload start: " + filename); if (SD.exists(filename)) { SD.remove(filename); } uploadFile = SD.open(filename, FILE_WRITE); if (!uploadFile) { addLog("Upload: nelze otevřít soubor pro zápis"); } } else if (upload.status == UPLOAD_FILE_WRITE) { if (uploadFile) { uploadFile.write(upload.buf, upload.currentSize); yield(); } } else if (upload.status == UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); addLog("Upload konec: " + String(upload.totalSize) + " B"); } } else if (upload.status == UPLOAD_FILE_ABORTED) { if (uploadFile) { uploadFile.close(); } addLog("Upload přerušen"); } } 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"); } void setupWebServer() { server.on("/", HTTP_GET, handleRoot); server.on("/status", HTTP_GET, handleStatus); server.on("/rs485panel", HTTP_GET, handleRS485Panel); server.on("/rs485rx", HTTP_GET, handleRS485Rx); server.on("/logpanel", HTTP_GET, handleLogPanel); server.on("/sdpanel", HTTP_GET, handleSdPanel); server.on("/sdstate", HTTP_GET, handleSdState); server.on("/logo.png", HTTP_GET, handleLogo); server.on("/led", HTTP_GET, handleLed); server.on("/rs485", HTTP_POST, handleRS485Send); server.on("/download", HTTP_GET, handleDownload); server.on("/delete", HTTP_GET, handleDelete); server.on("/mkdir", HTTP_GET, handleMkdir); server.on("/upload", HTTP_POST, handleUploadFinished, handleFileUpload); server.onNotFound(handleNotFound); server.begin(); addLog("Web server spusten na portu 80"); } // ===================================================== // SETUP / LOOP // ===================================================== void setup() { Serial.begin(115200); delay(500); printResetReason(); delay(500); addLog(""); addLog("======================================"); addLog("START"); addLog("======================================"); pixel.begin(); pixel.clear(); pixel.show(); setPixelColor(0, 0, 10); 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) + " %)"); initI2CDevices(); lastSdDetectRaw = isSdCardPresent(); lastSdDetectStable = lastSdDetectRaw; lastSdDetectChangeMs = millis(); initSDCard(); initRS485(); initEthernet(); setupWebServer(); addLog("READY"); if (ethGotIP) { addLog("Otevri v browseru: http://" + ETH.localIP().toString() + "/"); } else { addLog("Ethernet zatim nema IP adresu"); } } void loop() { server.handleClient(); handleSdHotplug(); bool rs485RxChanged = false; static String rs485LineBuffer = ""; while (Serial2.available()) { char c = (char)Serial2.read(); Serial.write(c); rs485LastReceived += c; rs485LineBuffer += c; rs485RxChanged = true; if (rs485LastReceived.length() > 2000) { rs485LastReceived.remove(0, rs485LastReceived.length() - 2000); } if (c == '\n') { String line = rs485LineBuffer; line.trim(); if (line.length() > 0) { addLog("RS485 RX: " + line); } rs485LineBuffer = ""; } if ((rs485LastReceived.length() & 0xFF) == 0) { yield(); } } if (rs485RxChanged) { rs485RxVersion++; } if (rs485RxChanged) { rs485RxVersion++; } if (millis() - lastSensorRead > 10000) { lastSensorRead = millis(); if (sht40Found) { readSHT40(); } if (bmp280Found) { readBMP280(); } } if (millis() - lastStatusPrint > 5000) { lastStatusPrint = millis(); 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"); 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"); if (bmp280Found && !isnan(lastBmpTemperature) && !isnan(lastPressure_hPa)) { Serial.printf("BMP280 -> T=%.2f C, P=%.2f hPa\n", lastBmpTemperature, lastPressure_hPa); } 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.println(F("------------------")); } delay(1); }