summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorvlapa <vlapa@ya.ru>2026-07-09 16:05:56 +0300
committervlapa <vlapa@ya.ru>2026-07-09 16:05:56 +0300
commitd6e70f7fcefded671735d2c9a2af6e2dc5e157c9 (patch)
tree4ca52357b93dd679a9609a52f44042ab13896ab8 /src
полностью переработано для esp32c3
Diffstat (limited to 'src')
-rw-r--r--src/config-source.h40
-rw-r--r--src/main.cpp565
2 files changed, 605 insertions, 0 deletions
diff --git a/src/config-source.h b/src/config-source.h
new file mode 100644
index 0000000..628439d
--- /dev/null
+++ b/src/config-source.h
@@ -0,0 +1,40 @@
+//////////////////////////////////////////////////////////
+//
+// Файл нужно заполнить и переименовать в config.h !!!
+//
+/////////////////////////////////////////////////////////
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+// WiFi настройки:
+static const char *WIFI_SSID = "";
+static const char *WIFI_PASS = "";
+
+// NTP настройки:
+static const char *NTP_SERVER = "1.ru.pool.ntp.org"; // "ntp3.vniiftri.ru";
+
+// Серверные настройки:
+static const char *SERVER_HOST = "";
+static const int SERVER_PORT = 8000;
+static const char *SERVER_PATH = "/";
+
+// Настройки табло
+static const uint8_t TIME_DAY = 8;
+static const uint8_t TIME_NIGHT = 18;
+static const uint8_t BRIGHT_DAY = 4;
+static const uint8_t BRIGHT_NIGHT = 1;
+
+// Названия датчиков
+static const char* SENSOR_1 = ""; // улица
+static const char* SENSOR_2 = ""; // комната
+
+// Цвета (можно вынести для удобства)
+static const uint32_t COLOR_TIME = 16766720; // желтый
+static const uint32_t COLOR_TEMP = 16711935; // красный
+static const uint32_t COLOR_TEMP_OUT = 9127187; // коричневый
+static const uint32_t COLOR_PRESS = 9830400; // розовый
+static const uint32_t COLOR_WIFI = 16728935; // томато
+static const uint32_t COLOR_HUM = 52945; // бирюзовый
+
+#endif \ No newline at end of file
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