diff options
| author | vlapa <vlapa@ya.ru> | 2026-07-09 16:05:56 +0300 |
|---|---|---|
| committer | vlapa <vlapa@ya.ru> | 2026-07-09 16:05:56 +0300 |
| commit | d6e70f7fcefded671735d2c9a2af6e2dc5e157c9 (patch) | |
| tree | 4ca52357b93dd679a9609a52f44042ab13896ab8 /src/main.cpp | |
полностью переработано для esp32c3
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 565 |
1 files changed, 565 insertions, 0 deletions
diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a17661b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,565 @@ +///////////////////////////////////////////////// +// Tablo на WS2812B + HTTP + NTP +// Порт на ESP32-C3 +// 2026-07-09 +///////////////////////////////////////////////// +#include <Arduino.h> +#include <WiFi.h> +#include <WebServer.h> +#include <ESPmDNS.h> +#include <Update.h> +#include <NTPClient.h> +#include <WiFiUdp.h> +#include <HTTPClient.h> +#include <NeoPixelBus.h> + +#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 = + "<form name='loginForm'>" + "<table width='20%' bgcolor='A09F9F' align='center'>" + "<tr><td colspan=2><center><font size=4><b>ESP32-C3 Login Page</b></font></center><br></td></tr>" + "<td>Username:</td><td><input type='text' size=25 name='userid'><br></td></tr>" + "<tr><td>Password:</td><td><input type='Password' size=25 name='pwd'><br></td></tr>" + "<tr><td><input type='submit' onclick='check(this.form)' value='Login'></td></tr>" + "</table>" + "</form>" + "<script>" + "function check(form){" + "if(form.userid.value=='admin' && form.pwd.value=='admin'){" + "window.open('/serverIndex')" + "}else{" + "alert('Error Password or Username')" + "}}" + "</script>"; + +const char *serverIndex = + "<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>" + "<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>" + "<input type='file' name='update'>" + "<input type='submit' value='Update'>" + "</form>" + "<div id='prg'>progress: 0%</div>" + "<script>" + "$('form').submit(function(e){" + "e.preventDefault();" + "var form = $('#upload_form')[0];" + "var data = new FormData(form);" + "$.ajax({" + "url: '/update'," + "type: 'POST'," + "data: data," + "contentType: false," + "processData:false," + "xhr: function(){" + "var xhr = new window.XMLHttpRequest();" + "xhr.upload.addEventListener('progress', function(evt){" + "if(evt.lengthComputable){" + "var per = evt.loaded / evt.total;" + "$('#prg').html('progress: ' + Math.round(per*100) + '%');" + "}}, false);" + "return xhr;" + "}," + "success:function(d,s){console.log('success!')}," + "error: function(a,b,c){}" + "});" + "});" + "</script>"; + +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); +}
\ No newline at end of file |
