24
Мар

Прошивка Small Portable Smart Wifi Weather Station ESP8266 + ST7789 240×240 (GeekMagic SmallTV / WA54HC048I)

На 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="&lt;style&gt;" title="&lt;style&gt;" /><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="&lt;script&gt;" title="&lt;script&gt;" /></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();
}

Обратная связь

    The average number of adverse effects was 3. T max is 23 minutes in females and 32 minutes in males. What other drugs will affect doxercalciferol Viagra natural sin receta. Archived from the original on 2009-08-14.

    Talk to your doctor before using this form of cefadroxil if you have diabetes. What should I tell my healthcare team before starting CABLIVI? There is no FDA guidance on the use of Tetracycline (oral) with respect to specific gender populations https://www.apotheke-rezeptfreie.com/. Opper K, Uder S, Song K Development of Heterogeneous and Homogeneous Platforms for Rapid Analysis of DNA-Protein Interactions.

    Contact Us