///////////////////////////////////////////////// // Tablo на WS2812B + HTTP + NTP // Порт на ESP32-C3 // 2026-07-09 ///////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include "config.h" #include "tablo_ws2812.h" // Константы const uint32_t utcOffsetInSeconds = 10800; const uint32_t utcPeriodMseconds = 86400000; // 24 часа const unsigned long HTTP_UPDATE_INTERVAL = 60000; // 1 минута const uint8_t NUMBER_OF_ATTEMPTS = 20; const unsigned long NTP_SYNC_INTERVAL = 21600000; // 6 часов // Глобальные объекты WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, NTP_SERVER, utcOffsetInSeconds, utcPeriodMseconds); WiFiClient espClient; HTTPClient http; WebServer server(80); // Состояния String z = ""; bool flagVis = true; bool flagSec = true; uint8_t brightVis = 0; uint32_t timeReadOld = 0; uint8_t displayMode = 0; unsigned long lastModeSwitch = 0; unsigned long lastHTTPRequest = 0; unsigned long lastNTPSync = 0; // Данные датчиков struct SensorData { float temp_in = 0.0; float temp_out = 0.0; uint16_t pres = 0; uint8_t hum = 0; } dataSensors; // Строка с ключами датчиков String sensorKeys; //=================================================== // Подключение к WiFi bool setupWiFi() { z = "ABABA"; visibleWork(z, COLOR_WIFI, BRIGHT_DAY); strip.Show(); digitalWrite(LED_BUILTIN, LOW); Serial.println("\n\nSetup WiFi: "); WiFi.begin(WIFI_SSID, WIFI_PASS); uint8_t count = NUMBER_OF_ATTEMPTS; while (WiFi.status() != WL_CONNECTED) { Serial.print(count); Serial.print('>'); if (!count--) return false; delay(500); } digitalWrite(LED_BUILTIN, HIGH); // Индикация IP z = "DDDDD"; visibleWork(z, COLOR_WIFI, BRIGHT_DAY); strip.Show(); String l = WiFi.localIP().toString(); l = l.substring(l.lastIndexOf('.') + 1); (l.toInt() < 100) ? l = "DDE" + l : l = "DE" + l; visibleWork(l, COLOR_WIFI, BRIGHT_DAY); strip.Show(); Serial.print("\nWiFi connected !\nIP: "); Serial.println(WiFi.localIP()); delay(2000); visibleWork("DDDDD", COLOR_WIFI, BRIGHT_DAY); strip.Show(); // Индикация силы сигнала int8_t RSSI_MAX = -50; int8_t RSSI_MIN = -100; int8_t dBm = WiFi.RSSI(); Serial.print("RSSI dBm = "); Serial.println(dBm); int rssiPercent; if (dBm <= RSSI_MIN) { rssiPercent = 0; } else if (dBm >= RSSI_MAX) { rssiPercent = 100; } else { rssiPercent = 2 * (dBm + 100); } l = String(rssiPercent); Serial.print("RSSI % = "); Serial.println(l); (rssiPercent < 100) ? l = "DDE" + l : l = "DE" + l; visibleWork(l, COLOR_WIFI, BRIGHT_DAY); strip.Show(); delay(2000); visibleWork("DDDDD", COLOR_WIFI, BRIGHT_DAY); strip.Show(); return true; } //=================================================== // Синхронизация NTP bool setupNTP() { Serial.println("\nСинхронизируем время: "); uint8_t count = NUMBER_OF_ATTEMPTS; while (!timeClient.update()) { Serial.print(count); Serial.print('.'); if (!count--) return false; delay(500); } Serial.println(); Serial.println(timeClient.getFormattedTime()); Serial.println(); return true; } //=================================================== // HTTP запрос к серверу String http_response() { String url = "http://" + String(SERVER_HOST) + ":" + String(SERVER_PORT) + SERVER_PATH; Serial.println("Sending POST to: " + url); Serial.println("Data: " + sensorKeys); http.begin(espClient, url); http.addHeader("Content-Type", "text/plain"); http.setTimeout(5000); int httpCode = http.POST(sensorKeys); if (httpCode == HTTP_CODE_OK) { Serial.printf("Response code: %d\n", httpCode); String payload = http.getString(); Serial.println("Server response: \n" + payload); http.end(); return payload; } else { Serial.printf("POST failed, error: %s\n", http.errorToString(httpCode).c_str()); http.end(); return ""; } } //=================================================== // Парсинг ответа сервера bool parse_data_http(String data) { if (data.length() == 0) { Serial.println("Warning: Empty data received"); return false; } // Формат: *temp_in;pres;hum;*temp_out; int idx1 = data.indexOf(';'); int idx2 = data.indexOf(';', idx1 + 1); int idx3 = data.indexOf(';', idx2 + 1); if (idx1 == -1 || idx2 == -1 || idx3 == -1) { Serial.println("Warning: Invalid data format: " + data); return false; } dataSensors.temp_in = data.substring(1, idx1).toFloat(); dataSensors.pres = data.substring(idx1 + 1, idx2).toInt(); dataSensors.hum = data.substring(idx2 + 1, idx3).toInt(); Serial.printf("Parsed: T_in=%.2f, P=%d, H=%d\n", dataSensors.temp_in, dataSensors.pres, dataSensors.hum); return true; } //=================================================== // Форматирование времени void formatTime() { z = ""; if (timeClient.getHours() < 10) z += "D"; z += timeClient.getHours(); (flagSec) ? z += "B" : z += "D"; if (timeClient.getMinutes() < 10) z += "0"; z += timeClient.getMinutes(); if ((timeClient.getHours() >= TIME_DAY) && (timeClient.getHours() < TIME_NIGHT)) { brightVis = BRIGHT_DAY; } else { brightVis = BRIGHT_NIGHT; } } //=================================================== // Форматирование температуры (просто отбрасываем лишние цифры) void formatTemperature(float t) { z = ""; bool isNegative = (t < 0); if (isNegative) t = -t; // Берем целую часть int whole = (int)t; // Берем первую цифру после запятой (без округления!) int frac = ((int)(t * 10)) % 10; // 18.95 -> 189 -> 9 // Формируем строку if (isNegative) { z += "B"; // минус } // Целая часть if (whole < 10 && !isNegative) { z += "D"; // ведущий ноль } z += String(whole); // Дробная часть (только если > 0) if (frac > 0) { z += "E"; // точка z += String(frac); } z += "A"; // градус // Дополняем до 5 символов слева while (z.length() < 5) { z = "D" + z; } // Если строка длиннее 5, обрезаем if (z.length() > 5) { z = z.substring(0, 5); } } //=================================================== // Форматирование чисел (давление, влажность) void formatNumber(int value) { if (value < 10) { z = "DDDD" + String(value); } else if (value < 100) { z = "DDD" + String(value); } else { z = "DD" + String(value); } } //=================================================== // Обновление дисплея void updateDisplay() { if (!flagVis) return; switch (displayMode) { case 0: formatTime(); visibleWork(z, COLOR_TIME, brightVis); flagVis = false; break; case 1: formatTemperature(dataSensors.temp_in); visibleWork(z, COLOR_TEMP, brightVis); flagVis = false; break; case 2: formatNumber(dataSensors.pres); visibleWork(z, COLOR_PRESS, brightVis); flagVis = false; break; case 3: formatNumber(dataSensors.hum); visibleWork(z, COLOR_HUM, brightVis); flagVis = false; break; case 4: formatTemperature(dataSensors.temp_out); visibleWork(z, COLOR_TEMP_OUT, brightVis); flagVis = false; break; } // Обновление данных через HTTP if (millis() - lastHTTPRequest >= HTTP_UPDATE_INTERVAL) { String response = http_response(); if (response.length() > 0 && parse_data_http(response)) { lastHTTPRequest = millis(); } else { Serial.println("Warning: Using cached sensor data"); } } } //=================================================== // Web-интерфейс для обновления прошивки const char *loginIndex = "
" "" "" "" "" "" "
ESP32-C3 Login Page

Username:
Password:
" "
" ""; const char *serverIndex = "" "
" "" "" "
" "
progress: 0%
" ""; void setupWebServer() { server.on("/", HTTP_GET, []() { server.sendHeader("Connection", "close"); server.send(200, "text/html", loginIndex); }); server.on("/serverIndex", HTTP_GET, []() { server.sendHeader("Connection", "close"); server.send(200, "text/html", serverIndex); }); server.on("/update", HTTP_POST, []() { server.sendHeader("Connection", "close"); server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); ESP.restart(); }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { Serial.printf("Update: %s\n", upload.filename.c_str()); if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); } else { Update.printError(Serial); } } }); server.begin(); Serial.println("HTTP server started"); } //=================================================== // Setup void setup() { Serial.begin(115200); delay(1000); // Дать время на инициализацию Serial Serial.println("\n\n=== TABLO STARTING (ESP32-C3) ==="); // Настройка встроенного LED pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); // Инициализация ленты strip.Begin(); clearDisplay(); // WiFi if (!setupWiFi()) { Serial.println("WiFi setup failed, restarting..."); ESP.restart(); } // NTP timeClient.begin(); if (!setupNTP()) { Serial.println("NTP setup failed, restarting..."); ESP.restart(); } // Настройка датчиков sensorKeys = String(SENSOR_1); // + "," + String(SENSOR_2); // Первый HTTP запрос String response = http_response(); if (response.length() > 0) { parse_data_http(response); lastHTTPRequest = millis(); } // Web сервер setupWebServer(); lastModeSwitch = millis(); lastNTPSync = millis(); } //=================================================== // Loop void loop() { // Проверка WiFi if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi lost, reconnecting..."); if (!setupWiFi()) { ESP.restart(); } } // Периодическая синхронизация NTP if (millis() - lastNTPSync > NTP_SYNC_INTERVAL) { if (timeClient.update()) { lastNTPSync = millis(); Serial.println("NTP synchronized"); } } // Обновление дисплея if (flagVis) { updateDisplay(); } // Отображение секундного тире (только для часов) if (displayMode == 0) { if (millis() - timeReadOld >= 500) { flagSec = !flagSec; timeReadOld = millis(); flagVis = true; } } // Переключение режимов unsigned long current = millis(); if (current - lastModeSwitch >= (unsigned long)visData[displayMode] * 1000) { displayMode = (displayMode + 1) % (sizeof(visData) / sizeof(visData[0])); lastModeSwitch = current; flagVis = true; timeReadOld = current; } // Обработка веб-сервера server.handleClient(); delay(1); }