На aliexpress можно купить маленькую wi-fi погодную станцию (назовём это так) ESP8266 + ST7789 240×240 (GeekMagic SmallTV / WA54HC048I)
Выглядит она примерно так. Цены разные и в разное время. Я купил за 7.60 Евро, через пару дней встречал за 6 Евро. Кто-то продаёт за 15-20 Евро, зависит от жадности продавца. Данное устройство имеет собственный wi-fi и при первом включении предложит подключится к собственной точки доступа, чтобы указать ваши настрйоки wi-fi. В стандартной вебморде можно указать свой город и вам будет показывать погоду. Также загрузить небольшую анимацию. Выглядит это примерно так.

Можно всё так и оставить, а можно сделать свою программу при помощи ИИ и залить её в данное устройство.
Для этого нам понадобиться CP2102 USB 2.0 to TTL UART Module 6Pin — я покупал за 1.5 Евро

Устройство легко разбирается. На нижней плате можно припаять кабеля питания и массы, а вот пины для прошивки и замыкания контактов для перевода в прошивку, нужно припаивать уже непосредственно на самой микросхеме. Либо можно взять всё на микросхеме. Контакты довольно крупные, не должно быть проблем. Главное не перегревать.

Нам нужны контакты GPIO1, GPIO2, 3.3V, GND и GPIO0 (который нужно замыкать на GND перед подключением, чтобы перевести устройство в режим прошивки)
Если вы уже припаяли контакты, установили драйвера CP2102 (их легко найти в интернете CP210x_Universal_Windows_Driver.zip) Можем на всякий случай слить заводскую прошивку с устройства. Чтобы мы могли восстановить в случае чего. Также нам понадобиться python-3.13.12-amd64.exe
Всё это устанавливаем. Открываем Терминал от администратора и устанавливаем:
pip install esptool
Если всё успешно установилось, пробуем считать информацию с устройства. Зажимает контакты GND и GPIO0 и подключаем наш CP2102 к ПК или ноутбуку. Дальше даём команду
python -m esptool flash_id
— если увидели вывод, значит можно работать дальше.
python -m esptool --port COM5 --baud 460800 read_flash 0 0x400000 factory_backup.bin
— так слить нашу заводскую прошивку
python -m esptool --port COM5 --baud 115200 write_flash 0x00000 D:\Arduino\factory_backup.bin
— так восстановить
Для программирования и написания кода, нам понадобиться arduino-ide_2.3.8_Windows_64bit.exe
Я организовал на данном устройстве отображение двух прогнозов погоды на разные города. Также внизу у меня выведен мониторинг серверов в сети. Раз в минуту запрашивает json файлик со статусом и выводит всё это на экранчик. Раз в минуту запрашивает сообщения с телеграм бота, выводит на минуту текст или картинку, которые я отправляю на телеграм бота. Также написана вебморда с настройками яркости и часовым поясом. Всё это для примера. Данная статья даст вам толчёк или подсказку в решении каких либо проблем.


Очень важный файл, чтобы заработал наш дисплей User_Setup.h
// User_Setup.h для ESP8266 + ST7789 240×240 (GeekMagic SmallTV / WA54HC048I) #define USER_SETUP_ID 99 #define USER_SETUP_INFO "ESP8266 ST7789 240x240 GeekMagic SmallTV" #define ST7789_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 240 // Пины SPI (фиксированные для ESP8266 hardware SPI) #define TFT_MOSI 13 #define TFT_SCLK 14 #define TFT_MISO -1 // не используется // Управляющие пины (по комментарию покупателя и твоему тесту) #define TFT_CS -1 // -1 работает стабильно на этой плате (рекомендую оставить) // если хочешь — попробуй 15, но -1 обычно лучше #define TFT_DC 0 #define TFT_RST 2 // Подсветка — КЛЮЧЕВОЙ МОМЕНТ (у тебя LOW включает подсветку) #define TFT_BL 5 #define TFT_BACKLIGHT_ON LOW // Важные опции для стабильности и правильных цветов #define CGRAM_OFFSET // часто спасает от чёрного экрана / сдвига #define TFT_RGB_ORDER TFT_BGR // если цвета неправильные → поменяй на TFT_RGB // Шрифты #define LOAD_GLCD #define LOAD_FONT2 #define LOAD_FONT4 #define LOAD_GFXFF // для FreeFonts (если будешь использовать) #define SMOOTH_FONT // Частота SPI — 27 МГц надёжнее на ESP8266 #define SPI_FREQUENCY 27000000 // #define SPI_FREQUENCY 40000000 // можно попробовать позже, если всё стабильно #define SUPPORT_TRANSACTIONS
sketch_mar14a.ino Сам скетч
</pre>
#include <FS.h>
#include <LittleFS.h>
#include <WiFiManager.h>
#include <ArduinoJson.h>
#include <TFT_eSPI.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
#include <ESP8266WebServer.h>
#include <vector>
#include <U8g2_for_TFT_eSPI.h>
#include <TJpg_Decoder.h>
TFT_eSPI tft = TFT_eSPI();
U8g2_for_TFT_eSPI u8f;
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
ESP8266WebServer server(80);
const int BL_PIN = 5;
int brightness = 200;
// --- НАСТРОЙКИ АВТО-ЯРКОСТИ ---
int dayBr = 250, nightBr = 50;
String dayStartStr = "08:00", nightStartStr = "23:30";
unsigned long lastBrCheck = 0;
char city1[32]="Zaporizhzhia", city2[32]="Kiev", api_key[48]="32b3318456fa9cec28582bf53fc02e9";
char tg_token[64]="844487:AAHBXzxаавпвапвап9y1dpUudYGIM";
char status_url[100]="https://sky.zp.ua/up.php", auth_cred[64]="a:b";
int tz_hour = 3;
struct WData { float temp; float t_min; float t_max; int hum; float wind; String desc; String icon; bool ok; };
WData w1, w2;
String lastWUpdate = "--:--", lastSUpdate = "--:--", botMsg = "";
long last_update_id = 0;
bool lastWOk = false, lastSOk = false, showBot = false;
unsigned long botDisplayTimer = 0;
const unsigned long DISPLAY_DURATION = 60000;
struct ServStat { String dispName; int port; bool ok; };
std::vector<ServStat> servers;
unsigned long lastDataUpdate=0, lastWeatherUpdate=0, lastPageSwitch=0;
int currentPage = 0;
bool forceUpdateReq = false, needRedrawUI = true;
// --- КОЛЛБЭК ДЛЯ ТОЧКИ ДОСТУПА ---
void configModeCallback (WiFiManager *myWiFiManager) {
Serial.println("Entered config mode");
Serial.println(WiFi.softAPIP());
Serial.println(myWiFiManager->getConfigPortalSSID());
tft.fillScreen(TFT_BLACK);
u8f.setFont(u8g2_font_unifont_t_cyrillic);
u8f.setForegroundColor(TFT_YELLOW);
u8f.drawUTF8(10, 40, "WiFi НЕ НАЙДЕН!");
u8f.setForegroundColor(TFT_WHITE);
u8f.drawUTF8(10, 70, "Создана точка:");
u8f.setForegroundColor(TFT_CYAN);
u8f.drawUTF8(10, 95, myWiFiManager->getConfigPortalSSID().c_str());
u8f.setForegroundColor(TFT_WHITE);
u8f.drawUTF8(10, 130, "Адрес: 192.168.4.1");
}
int timeToMins(String t) {
return t.substring(0, 2).toInt() * 60 + t.substring(3, 5).toInt();
}
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
if (y >= tft.height()) return false;
tft.pushImage(x, y, w, h, bitmap);
return true;
}
void setBacklight(int val) {
brightness = val;
analogWrite(BL_PIN, 255 - brightness);
}
bool isDayTimeNow() {
int nowMins = timeClient.getHours() * 60 + timeClient.getMinutes();
int dMins = timeToMins(dayStartStr);
int nMins = timeToMins(nightStartStr);
if (nMins > dMins) return (nowMins >= dMins && nowMins < nMins);
else return (nowMins >= dMins || nowMins < nMins);
}
void updateAutoBrightness() {
int target = isDayTimeNow() ? dayBr : nightBr;
if (brightness != target) setBacklight(target);
}
String translateWeather(String eng) {
if (eng == "Clear") return "Ясно";
if (eng == "Clouds") return "Облачно";
if (eng == "Few clouds") return "Малооблачно";
if (eng == "Scattered clouds") return "Облачно";
if (eng == "Broken clouds") return "Пасмурно";
if (eng == "Overcast clouds") return "Пасмурно";
if (eng == "Rain") return "Дождь";
if (eng == "Light rain") return "Небольшой дождь";
if (eng == "Moderate rain") return "Умеренный дождь";
if (eng == "Heavy intensity rain") return "Сильный дождь";
if (eng == "Drizzle") return "Морось";
if (eng == "Thunderstorm") return "Гроза";
if (eng == "Snow") return "Снег";
if (eng == "Mist") return "Туман";
if (eng == "Smoke") return "Дым";
if (eng == "Haze") return "Мгла";
if (eng == "Fog") return "Густой туман";
if (eng == "Dust") return "Пыль";
if (eng == "Sand") return "Песок";
if (eng == "Ash") return "Пепел";
if (eng == "Squall") return "Шквал";
if (eng == "Tornado") return "Торнадо";
return eng;
}
void drawWeatherIcon(int x, int y, String code) {
tft.fillRect(x, y, 45, 35, TFT_BLACK);
if (code.startsWith("01")) { // Clear
tft.fillCircle(x+22, y+16, 11, TFT_YELLOW);
for(int i=0; i<360; i+=45){ float r = i * 0.0174; tft.drawLine(x+22, y+16, x+22+cos(r)*19, y+16+sin(r)*19, TFT_YELLOW); }
} else if (code.startsWith("09") || code.startsWith("10")) { // RAIN (Cloud + Lines)
tft.fillCircle(x+15, y+18, 8, TFT_LIGHTGREY); tft.fillCircle(x+23, y+14, 10, TFT_WHITE); tft.fillCircle(x+32, y+18, 8, TFT_LIGHTGREY);
tft.drawLine(x+18, y+26, x+16, y+33, TFT_BLUE); tft.drawLine(x+25, y+26, x+23, y+33, TFT_BLUE); tft.drawLine(x+31, y+26, x+29, y+33, TFT_BLUE);
} else if (code.startsWith("02") || code.startsWith("03") || code.startsWith("04")) { // Clouds
tft.fillCircle(x+12, y+20, 10, TFT_LIGHTGREY); tft.fillCircle(x+22, y+15, 13, TFT_WHITE); tft.fillCircle(x+33, y+20, 10, TFT_LIGHTGREY);
} else if (code.startsWith("11")) { // Thunder
tft.fillCircle(x+22, y+15, 10, TFT_LIGHTGREY); tft.drawLine(x+22, y+22, x+15, y+30, TFT_YELLOW); tft.drawLine(x+15, y+30, x+25, y+30, TFT_YELLOW); tft.drawLine(x+25, y+30, x+18, y+38, TFT_YELLOW);
} else { tft.fillCircle(x+22, y+16, 11, TFT_YELLOW); }
}
bool getWeather(String c, WData &d) {
WiFiClient client; HTTPClient http;
String url = "http://api.openweathermap.org/data/2.5/weather?q="+c+"&appid="+String(api_key)+"&units=metric&lang=ru";
if (http.begin(client, url)) {
int code = http.GET();
Serial.println("WEATHER [" + String(code) + "]: " + url);
if (code == 200) {
DynamicJsonDocument doc(2048); deserializeJson(doc, http.getString());
d.temp = doc["main"]["temp"]; d.t_min = doc["main"]["temp_min"]; d.t_max = doc["main"]["temp_max"];
d.hum = doc["main"]["humidity"]; d.wind = doc["wind"]["speed"];
d.desc = doc["weather"][0]["main"].as<String>(); d.icon = doc["weather"][0]["icon"].as<String>();
return true;
}
http.end();
}
return false;
}
void fetchData(bool weather) {
if (weather) { lastWOk = (getWeather(city1, w1) && getWeather(city2, w2)); }
std::unique_ptr<BearSSL::WiFiClientSecure> client(new BearSSL::WiFiClientSecure);
client->setInsecure(); client->setBufferSizes(1024, 1024);
HTTPClient https;
if (https.begin(*client, status_url)) {
https.addHeader("Authorization", "Basic " + base64::encode(auth_cred));
int code = https.GET();
Serial.println("PING [" + String(code) + "]: " + String(status_url));
if (code == 200) {
DynamicJsonDocument doc(8192); deserializeJson(doc, https.getString());
lastSUpdate = (doc["time"].as<String>()).substring(0,5);
JsonArray items = doc["items"]; servers.clear();
for (JsonObject v : items) { servers.push_back({v["h"].as<String>(), v["p"].as<int>(), v["s"] == 1}); }
lastSOk = true;
} else { lastSOk = false; }
https.end();
}
}
void checkTelegram() {
if (strlen(tg_token) < 10) return;
std::unique_ptr<BearSSL::WiFiClientSecure> client(new BearSSL::WiFiClientSecure);
client->setInsecure(); client->setBufferSizes(1024, 1024);
HTTPClient https;
String url = "https://api.telegram.org/bot" + String(tg_token) + "/getUpdates?limit=1&offset=" + String(last_update_id + 1);
if (https.begin(*client, url)) {
int code = https.GET();
Serial.println("TELEGRAM [" + String(code) + "]: " + url);
if (code == 200) {
DynamicJsonDocument doc(4096); deserializeJson(doc, https.getString());
if (doc["result"].size() > 0) {
JsonObject msg = doc["result"][0]["message"];
last_update_id = doc["result"][0]["update_id"].as<long>();
showBot = true; botDisplayTimer = millis(); tft.fillScreen(TFT_BLACK);
if (msg.containsKey("photo")) {
JsonArray photos = msg["photo"]; int targetIdx = 0;
for(int i=0; i<(int)photos.size(); i++) if(photos[i]["width"] <= 320) targetIdx = i;
String f_id = photos[targetIdx]["file_id"].as<String>();
https.end();
https.begin(*client, "https://api.telegram.org/bot" + String(tg_token) + "/getFile?file_id=" + f_id);
if (https.GET() == 200) {
DynamicJsonDocument fDoc(512); deserializeJson(fDoc, https.getString());
String imgUrl = "https://api.telegram.org/file/bot" + String(tg_token) + "/" + fDoc["result"]["file_path"].as<String>();
https.end();
if (https.begin(*client, imgUrl)) {
if (https.GET() == 200) {
File f = LittleFS.open("/temp.jpg", "w");
if (f) { https.writeToStream(&f); f.close(); tft.setSwapBytes(true); TJpgDec.drawFsJpg(0, 0, "/temp.jpg", LittleFS); tft.setSwapBytes(false); }
}
}
}
}
botMsg = msg.containsKey("text") ? msg["text"].as<String>() : (msg.containsKey("caption") ? msg["caption"].as<String>() : "");
needRedrawUI = true;
}
}
https.end();
}
}
void handleRoot() {
String s = "<html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cstyle%3E%22%3B%0A%20%20s%20%2B%3D%20%22body%7Bbackground%3A%23121212%3Bcolor%3A%23eee%3Bfont-family%3Asans-serif%3Bpadding%3A15px%3Btext-align%3Acenter%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22.card%7Bbackground%3A%231e1e1e%3Bborder-radius%3A12px%3Bpadding%3A15px%3Bmargin%3Aauto%3Bmax-width%3A400px%3Bborder%3A1px%20solid%20%23333%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22input%7Bwidth%3A100%25%3Bpadding%3A10px%3Bmargin%3A5px%200%3Bbackground%3A%232c2c2c%3Bborder%3A1px%20solid%20%23444%3Bcolor%3A%23fff%3Bborder-radius%3A6px%3Btext-align%3Acenter%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22.btn%7Bdisplay%3Ainline-block%3Bpadding%3A12px%3Bmargin%3A10px%202px%3Bborder%3Anone%3Bborder-radius%3A6px%3Bfont-weight%3Abold%3Bcursor%3Apointer%3Bcolor%3A%23fff%3Bwidth%3A45%25%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22.b-sav%7Bbackground%3A%234caf50%3Bwidth%3A95%25%3B%7D%20.b-upd%7Bbackground%3A%23e91e63%3B%7D%20.b-tg%7Bbackground%3A%230088cc%3B%7D%20label%7Bdisplay%3Ablock%3Bmargin-top%3A10px%3Bfont-size%3A0.8em%3Bcolor%3A%23aaa%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22%3C%2Fstyle%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<style>" title="<style>" /><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3E%22%3B%0A%20%20s%20%2B%3D%20%22function%20brLive(v%2C%20t)%7B%20fetch('%2Fbr_live%3Fv%3D'%2Bv%2B'%26t%3D'%2Bt)%3B%20%7D%22%3B%0A%20%20s%20%2B%3D%20%22function%20saveAll()%7Bconst%20fd%3Dnew%20FormData(document.getElementById('f1'))%3B%20fetch('%2Fsave'%2C%7Bmethod%3A'POST'%2Cbody%3Afd%7D).then(()%3D%3Ealert('Saved!%20Rebooting...'))%3B%7D%22%3B%0A%20%20s%20%2B%3D%20%22%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="<script>" title="<script>" /></head><body><div class='card'><h3>SmallTV Config</h3><form id='f1'>";
s += "<label>День (Яркость)</label><input type='range' name='db' min='0' max='255' value='"+String(dayBr)+"' oninput='brLive(this.value,\"day\")'>";
s += "<label>Ночь (Яркость)</label><input type='range' name='nb' min='0' max='255' value='"+String(nightBr)+"' oninput='brLive(this.value,\"night\")'>";
s += "<label>Начало дня</label><input type='time' name='ds' value='"+dayStartStr+"'>";
s += "<label>Начало ночи</label><input type='time' name='ns' value='"+nightStartStr+"'>";
s += "<label>GMT Offset</label><input type='number' name='tz' value='"+String(tz_hour)+"'>";
s += "<hr style='border:0.1px solid #333'>City L:<input name='c1' value='"+String(city1)+"'>City R:<input name='c2' value='"+String(city2)+"'>";
s += "Weather Key:<input name='key' value='"+String(api_key)+"'>TG Token:<input name='tgt' value='"+String(tg_token)+"'>";
s += "<button class='btn b-sav' type='button' onclick='saveAll()'>СОХРАНИТЬ И REBOOT</button></form>";
s += "<button class='btn b-tg' onclick='fetch(\"/f_tg\")'>TG CHECK</button><button class='btn b-upd' onclick='fetch(\"/f_upd\")'>RELOAD DATA</button></div></body></html>";
server.send(200, "text/html", s);
}
void drawUI() {
if (showBot) {
if (needRedrawUI) {
u8f.setFont(u8g2_font_unifont_t_cyrillic); u8f.setFontMode(1);
u8f.setForegroundColor(TFT_CYAN); u8f.drawUTF8(5, 20, "Max Telegram:");
tft.drawFastHLine(0, 25, 240, TFT_DARKGREY);
u8f.setForegroundColor(TFT_WHITE);
int charPerLine = 45;
for (int i = 0; i < (int)botMsg.length(); i += charPerLine) {
String sub = botMsg.substring(i, min((int)botMsg.length(), i + charPerLine));
u8f.drawUTF8(5, 45 + (i/charPerLine)*20, sub.c_str());
}
needRedrawUI = false;
}
return;
}
if (needRedrawUI) {
tft.drawFastHLine(0, 50, 240, TFT_PINK); tft.drawFastHLine(0, 112, 240, TFT_GREEN);
auto dW = [](int x, String city, WData &d, uint32_t clr) {
u8f.setFontMode(1); u8f.setFont(u8g2_font_unifont_t_cyrillic);
u8f.setForegroundColor(clr); u8f.drawUTF8(x, 66, city.c_str());
tft.setTextColor(TFT_WHITE, TFT_BLACK);
int tWidth = tft.drawString(String(d.temp,1), x, 70, 4);
tft.drawCircle(x + tWidth + 2, 73, 2, TFT_WHITE);
tft.setTextColor(TFT_BLUE, TFT_BLACK); tft.drawString(String(d.hum) + "%", x + 48, 82, 1);
tft.fillRoundRect(x, 92, 70, 17, 4, TFT_WHITE);
u8f.setFontMode(0); u8f.setBackgroundColor(TFT_WHITE); u8f.setFont(u8g2_font_unifont_t_cyrillic);
u8f.setForegroundColor(TFT_BLACK); u8f.drawUTF8(x, 105, translateWeather(d.desc).c_str());
tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.drawString(String(d.wind,1)+"m/s", x+75, 54, 1);
tft.setTextColor(TFT_RED, TFT_BLACK);
String mm = (d.t_min>0?"+":"")+String((int)d.t_min)+"/"+(d.t_max>0?"+":"")+String((int)d.t_max);
tft.drawRightString(mm, x+112, 102, 1);
};
dW(5, city1, w1, TFT_ORANGE); drawWeatherIcon(75, 62, w1.icon);
dW(125, city2, w2, TFT_MAGENTA); drawWeatherIcon(193, 62, w2.icon);
tft.setTextColor(TFT_BLUE, TFT_BLACK); tft.drawString(WiFi.localIP().toString(), 2, 116, 1);
tft.setTextColor(TFT_SKYBLUE, TFT_BLACK); tft.drawString("w:", 105, 116, 1);
tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(lastWUpdate, 117, 116, 1);
tft.setTextColor(lastWOk?TFT_GREEN:TFT_RED, TFT_BLACK); tft.drawString(lastWOk?"[ok]":"[x]", 147, 116, 1);
tft.setTextColor(TFT_SKYBLUE, TFT_BLACK); tft.drawString("s:", 185, 116, 1);
tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(lastSUpdate, 197, 116, 1);
tft.setTextColor(lastSOk?TFT_GREEN:TFT_RED, TFT_BLACK); tft.drawString(lastSOk?"[ok]":"[x]", 227, 116, 1);
int startIdx = currentPage * 10;
for (int i = 0; i < 10; i++) {
int idx = startIdx + i; if (idx >= (int)servers.size()) break;
int col = (i < 5) ? 5 : 125; int row = 132 + (i % 5) * 20;
tft.fillCircle(col + 4, row + 8, 3, servers[idx].ok ? TFT_GREEN : TFT_RED);
tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString(servers[idx].dispName + ":" + String(servers[idx].port), col + 15, row, 2);
}
needRedrawUI = false;
}
String fT = timeClient.getFormattedTime();
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
int nx = tft.drawString(fT.substring(0, 6), 8, 0, 7);
tft.setTextColor(TFT_BLUE, TFT_BLACK); tft.drawString(fT.substring(6, 8), nx + 8, 0, 7);
}
void setup() {
Serial.begin(115200);
Serial.println("\n\nStarting SmallTV...");
LittleFS.begin();
TJpgDec.setJpgScale(1); TJpgDec.setCallback(tft_output);
analogWriteRange(255); pinMode(BL_PIN, OUTPUT);
tft.init(); tft.setRotation(0); tft.invertDisplay(true); tft.fillScreen(TFT_BLACK); u8f.begin(tft);
File f = LittleFS.open("/conf.json", "r");
if(f){
Serial.println("Loading config...");
DynamicJsonDocument doc(1024); deserializeJson(doc, f);
if(doc["c1"]) strcpy(city1, doc["c1"]); if(doc["c2"]) strcpy(city2, doc["c2"]);
if(doc["k"]) strcpy(api_key, doc["k"]); if(doc.containsKey("tz")) tz_hour = doc["tz"];
if(doc["tgt"]) strcpy(tg_token, doc["tgt"]);
if(doc.containsKey("db")) dayBr = doc["db"]; if(doc.containsKey("nb")) nightBr = doc["nb"];
if(doc["ds"]) dayStartStr = doc["ds"].as<String>(); if(doc["ns"]) nightStartStr = doc["ns"].as<String>();
f.close();
}
// --- ПОПЫТКА ПОДКЛЮЧЕНИЯ ---
tft.fillScreen(TFT_BLACK);
u8f.setFont(u8g2_font_unifont_t_cyrillic);
u8f.setForegroundColor(TFT_WHITE);
String lastSSID = WiFi.SSID();
if (lastSSID == "") lastSSID = "Auto Connect";
Serial.print("Connecting to: "); Serial.println(lastSSID);
u8f.drawUTF8(10, 80, "Подключение к:");
u8f.setForegroundColor(TFT_CYAN);
u8f.drawUTF8(10, 105, lastSSID.c_str());
WiFiManager wm;
wm.setAPCallback(configModeCallback);
wm.setConfigPortalTimeout(60);
if (!wm.autoConnect("SmallTV-Config")) {
Serial.println("Failed to connect, timeout reached. Restarting...");
delay(1000);
ESP.restart();
}
Serial.println("WiFi connected!");
Serial.print("IP address: "); Serial.println(WiFi.localIP());
timeClient.setTimeOffset(tz_hour * 3600); timeClient.begin(); timeClient.update();
updateAutoBrightness();
server.on("/", handleRoot);
server.on("/br_live", [](){
int val = server.arg("v").toInt(); String t = server.arg("t");
if(t=="day") dayBr = val; else nightBr = val;
if((t=="day" && isDayTimeNow()) || (t=="night" && !isDayTimeNow())) setBacklight(val);
server.send(200);
});
server.on("/f_upd", [](){ forceUpdateReq = true; server.send(200); });
server.on("/f_tg", [](){ checkTelegram(); server.send(200); });
server.on("/save", HTTP_POST, [](){
Serial.println("Saving new config...");
if(server.hasArg("c1")) strcpy(city1, server.arg("c1").c_str());
if(server.hasArg("c2")) strcpy(city2, server.arg("c2").c_str());
if(server.hasArg("key")) strcpy(api_key, server.arg("key").c_str());
if(server.hasArg("tgt")) strcpy(tg_token, server.arg("tgt").c_str());
if(server.hasArg("tz")) tz_hour = server.arg("tz").toInt();
if(server.hasArg("db")) dayBr = server.arg("db").toInt();
if(server.hasArg("nb")) nightBr = server.arg("nb").toInt();
if(server.hasArg("ds")) dayStartStr = server.arg("ds");
if(server.hasArg("ns")) nightStartStr = server.arg("ns");
File f = LittleFS.open("/conf.json", "w"); DynamicJsonDocument doc(1024);
doc["c1"]=city1; doc["c2"]=city2; doc["k"]=api_key; doc["tz"]=tz_hour; doc["tgt"]=tg_token;
doc["db"]=dayBr; doc["nb"]=nightBr; doc["ds"]=dayStartStr; doc["ns"]=nightStartStr;
serializeJson(doc, f); f.close();
server.send(200); delay(1000); ESP.restart();
});
server.begin();
Serial.println("HTTP Server started");
fetchData(true);
tft.fillScreen(TFT_BLACK);
}
void loop() {
server.handleClient(); timeClient.update();
if (millis() - lastBrCheck > 15000) { updateAutoBrightness(); lastBrCheck = millis(); }
if (showBot && (millis() - botDisplayTimer > DISPLAY_DURATION)) { showBot = false; needRedrawUI = true; tft.fillScreen(TFT_BLACK); }
if (millis() - lastDataUpdate > 60000 || forceUpdateReq) {
checkTelegram();
bool wNeeded = (forceUpdateReq || (millis() - lastWeatherUpdate > 900000));
fetchData(wNeeded);
if (wNeeded) { lastWeatherUpdate = millis(); lastWUpdate = timeClient.getFormattedTime().substring(0,5); }
lastDataUpdate = millis(); forceUpdateReq = false;
if (!showBot) { needRedrawUI = true; tft.fillScreen(TFT_BLACK); }
}
if (!showBot && millis() - lastPageSwitch > 5000 && servers.size() > 10) {
lastPageSwitch = millis(); currentPage = (currentPage == 0) ? 1 : 0;
tft.fillRect(0, 130, 240, 110, TFT_BLACK); needRedrawUI = true;
}
drawUI();
}