HU-061: Wi-Fi Годинник з Прогнозом Погоди

Конструктор HU-061 - це набір для самостійного збирання Wi-Fi годинника, який поєднує функції звичайного годинника та метеостанції. Пристрій оснащений модулем ESP8266 (ESP-01S) та OLED-дисплеєм з діагоналлю 0,96 дюйма, на якому відображаються час, температура, вологість, дата, день тижня і прогноз погоди на наступні три дні.

Основні характеристики:

  • Модуль Wi-Fi: ESP8266 (ESP-01S)
  • Дисплей: OLED 0,96", роздільна здатність 128x64 пікселі
  • Функції: відображення часу, температури, вологості, прогнозу погоди
  • Живлення: через Micro-USB порт
  • Налаштування: просте підключення до Wi-Fi

Переваги набору:

  • Простий у складанні та налаштуванні
  • Підходить для початківців у сфері електроніки
  • Стильний та компактний акриловий корпус
  • Персоналізація даних погоди завдяки API

Відео про збирання цього конструктору:

Недоліки:

Єдиним суттєвим недоліком конструктора є його стандартна прошивка, яка вимагає реєстрації на спеціальному сайті для отримання даних погоди. Процес реєстрації на сайті є складним і часто проблематичним.

Створення власної прошивки за допомогою ШІ:

Оскільки HU-061 оснащений ESP-01S, є можливість створити власну прошивку. Проте це вимагає певних знань, яких у мене не було. Тому я вирішив створити прошивку за допомогою ChatGPT. Перша спроба дала повністю неробочий код, оскільки я не вказав усіх необхідних деталей у запиті. Після кількох спроб вдалося отримати повністю функціональний код. Ось відео з вдалою спробою:

Пізніше виявилось, що прошивка працювала нестабільно через те що сервер по різному повертав погоду. Тому код прошивки було додатково виправлено за допомогою того ж ChatGPT. Ось цей оновлений код:

/**
 * ESP8266 ESP-01 Clock and Weather Firmware
 *
 * This firmware uses an SSD1306 128x64 OLED display to show alternating screens:
 * - Clock screen: shows day of week, current time (HH:MM), temperature & humidity, and date.
 * - Weather screen: shows city name, current temperature, weather condition, humidity, wind speed, and pressure.
 *
 * Wi-Fi Configuration:
 * If no configuration is found or if a button on GPIO3 is held at boot, the device
 * starts as an access point (SSID: ESP01-Setup) and serves a configuration page.
 * The page allows setting Wi-Fi SSID, password, city for weather, and timezone.
 * Settings are saved in EEPROM and the device reboots to normal mode.
 *
 * Normal Operation:
 * Connects to configured Wi-Fi and retrieves time via NTP (synchronized every 60s).
 * Retrieves weather from wttr.in for the specified city (HTTPS GET request).
 * Displays time and weather data on the OLED, switching screens every 15 seconds.
 *
 * Hardware:
 * - ESP8266 ESP-01 (GPIO0=SDA, GPIO2=SCL for I2C OLED; GPIO3 as input for config button).
 * - OLED display SSD1306 128x64 via I2C (address 0x3C).
 *
 * Note: Uses custom fonts (FreeMonoBold18pt7b for time, FreeMonoBold12pt7b for temperature).
 * All other text uses default font. Display is rotated 180 degrees (setRotation(2)).
 */
#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClientSecure.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <time.h>

// Pin definitions (ESP-01):
const uint8_t SDA_PIN = 0;           // I2C SDA connected to GPIO0
const uint8_t SCL_PIN = 2;           // I2C SCL connected to GPIO2
const uint8_t BUTTON_PIN = 3;        // Button on GPIO3 (RX pin)

// EEPROM addresses for configuration data:
const int EEPROM_SIZE = 512;
const int ADDR_SSID = 0;
const int ADDR_PASS = 70;
const int ADDR_CITY = 140;
const int ADDR_TZ   = 210;  // 4 bytes (int32) for timezone offset in seconds
const int ADDR_SIGNATURE = 500;  // 4-byte signature "CFG1" to indicate valid config

// Wi-Fi and server:
ESP8266WebServer server(80);
const char *AP_SSID = "ESP01-Setup";  // Access Point SSID for config mode

// Display:
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayInitialized = false;

// Time and weather:
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
unsigned long lastScreenSwitch = 0;
bool showWeatherScreen = false;
bool displayReady = false;
unsigned long lastWeatherFetch = 0;

// Configuration variables:
String wifiSSID = "";
String wifiPass = "";
String city = "";
int timezoneOffset = 0;  // in seconds

// Weather data variables:
String weatherTemp = "N/A";
String weatherCond = "";
String weatherHum = "";
String weatherWind = "";
String weatherPress = "";
bool weatherValid = false;

// Function prototypes:
void loadSettings();
void saveSettings();
void startConfigPortal();
void handleConfigForm();
void drawTimeScreen();
void drawWeatherScreen();
bool getWeather();

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(F("Booting..."));

    // Initialize display
    Wire.begin(SDA_PIN, SCL_PIN);
    if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
      displayInitialized = true;
      display.setRotation(2);
      display.clearDisplay();
      display.setTextColor(SSD1306_WHITE);
      display.print("Booting...");
      display.display();
      displayReady = true;
    } else {
      Serial.println(F("SSD1306 allocation failed"));
      // Leave displayInitialized as false
    }

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  // Initialize EEPROM and load stored settings if available
  EEPROM.begin(EEPROM_SIZE);
  bool configMode = false;

  // Check if config signature is present in EEPROM
  if (EEPROM.read(ADDR_SIGNATURE) == 'C' &&
      EEPROM.read(ADDR_SIGNATURE + 1) == 'F' &&
      EEPROM.read(ADDR_SIGNATURE + 2) == 'G' &&
      EEPROM.read(ADDR_SIGNATURE + 3) == '1') {
    Serial.println(F("Config signature found in EEPROM."));
  } else {
    Serial.println(F("No config signature found (EEPROM uninitialized)."));
  }

  // Check button press in first 300ms of boot
  bool buttonPressed = false;
  unsigned long startTime = millis();
  while (millis() - startTime < 300) {
    if (digitalRead(BUTTON_PIN) == LOW) {
      buttonPressed = true;
    }
    delay(10);
  }
  if (buttonPressed) {
    Serial.println(F("Config button held - entering AP configuration mode."));
  }

  // Determine if we should start config portal
  if (EEPROM.read(ADDR_SIGNATURE) != 'C' || EEPROM.read(ADDR_SIGNATURE+1) != 'F' ||
      EEPROM.read(ADDR_SIGNATURE+2) != 'G' || EEPROM.read(ADDR_SIGNATURE+3) != '1' ||
      buttonPressed) {
    configMode = true;
  }

  if (configMode) {
    // Load existing settings (if any) to pre-fill form
    if (EEPROM.read(ADDR_SIGNATURE) == 'C' && EEPROM.read(ADDR_SIGNATURE + 1) == 'F' &&
        EEPROM.read(ADDR_SIGNATURE + 2) == 'G' && EEPROM.read(ADDR_SIGNATURE + 3) == '1') {
      loadSettings();
    }
    // Start configuration portal (Access Point mode)
    startConfigPortal();
    // After configuration, ESP will reboot. If not rebooted (e.g. user didn't submit),
    // it will remain in AP mode and handleClient in loop.
  } else {
    // Load settings from EEPROM
    loadSettings();
    Serial.print(F("Connecting to WiFi: "));
    Serial.println(wifiSSID);
    WiFi.mode(WIFI_STA);
    WiFi.begin(wifiSSID.c_str(), wifiPass.c_str());
    // Wait up to 10 seconds for connection
    unsigned long wifiStart = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 10000) {
      delay(500);
      Serial.print('.');
    }
    Serial.println();
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println(F("WiFi connected."));
      Serial.print(F("IP Address: "));
      Serial.println(WiFi.localIP());
    } else {
      Serial.println(F("WiFi connection failed. Starting AP mode instead."));
      startConfigPortal();
      return; // Exit setup to avoid running normal mode without WiFi
    }
    // Setup NTP client with stored timezone offset
    timeClient.setPoolServerName("pool.ntp.org");
    timeClient.setTimeOffset(timezoneOffset);
    timeClient.setUpdateInterval(900000);  // 15 mins interval
    timeClient.begin();
    // Perform initial NTP update
    timeClient.update();

    // Prepare first weather fetch
    lastWeatherFetch = 0; // force immediate fetch on first weather screen display
    weatherValid = false;
    Serial.println(F("Setup complete, entering loop."));
  }
Serial.println(F("Getting initial weather..."));
weatherValid = getWeather();
lastWeatherFetch = millis();
if (weatherValid) {
  Serial.println(F("Initial weather fetch successful."));
} else {
  Serial.println(F("Initial weather fetch failed."));
} 
}

void loop() {
  // If in config portal mode, handle web server
  if (WiFi.getMode() == WIFI_AP) {
    server.handleClient();
    // In AP mode, do not run normal display loop
    return;
  }

  // Regular mode: update time and handle display
  timeClient.update();
  unsigned long now = millis();

  // Switch screen every 15 seconds
  if (now - lastScreenSwitch > 15000) {
    showWeatherScreen = !showWeatherScreen;
    lastScreenSwitch = now;
    // If switching to weather screen, update weather data (limit fetch frequency)
    if (showWeatherScreen) {

// Update weather every 15 minutes
if (millis() - lastWeatherFetch > 900000UL || !weatherValid) {
  Serial.println(F("Updating weather data..."));
  weatherValid = getWeather();
  lastWeatherFetch = millis();
  if (weatherValid) {
    Serial.println(F("Weather update successful."));
  } else {
    Serial.println(F("Weather update failed or data invalid."));
  }
}
    }
  }

  // Draw the appropriate screen
  if (showWeatherScreen) {
    drawWeatherScreen();
  } else {
    drawTimeScreen();
  }

  // Small delay to yield to system
  delay(10);
}

void startConfigPortal() {
  // Stop any existing WiFi and start AP
  WiFi.disconnect();
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_SSID);
  IPAddress apIP = WiFi.softAPIP();
  Serial.print(F("Started AP mode with SSID "));
  Serial.print(AP_SSID);
  Serial.print(F(". Connect and browse to http://"));
  Serial.println(apIP);

    if (displayReady) {
      display.clearDisplay();
      display.setTextColor(SSD1306_WHITE);
      display.setTextSize(1);
      display.setCursor(0, 0);
      display.println("AP mode");
      display.setCursor(0, 10);
      display.println("SSID: ESP01-Setup");
      display.setCursor(0, 20);
      display.println("IP: 192.168.4.1");
      display.display();
    }
  // Setup web server routes
  server.on("/", HTTP_GET, []() {
    // HTML page for config
    String page = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
    page += String("<title>ESP8266 Setup</title><style>") +
            ".container{max-width:300px;margin:40px auto;padding:20px;background:#f7f7f7;border:1px solid #ccc;border-radius:5px;}" +
            "body{text-align:center;font-family:sans-serif;}h2{margin-bottom:15px;}label{display:block;text-align:left;margin-top:10px;}" +
            "input, select{width:100%;padding:8px;margin-top:5px;border:1px solid #ccc;border-radius:3px;}" +
            "input[type=submit]{margin-top:15px;background:#4caf50;color:white;border:none;cursor:pointer;border-radius:3px;font-size:16px;}" +
            "input[type=submit]:hover{background:#45a049;}" +
            "</style></head><body><div class='container'>";
    page += "<h2>Device Configuration</h2><form method='POST' action='/'>";
    // WiFi SSID field
    page += "<label>Wi-Fi SSID:</label><input type='text' name='ssid' value='" + wifiSSID + "' required>";
    // Password field
    page += "<label>Password:</label><input type='password' name='pass' value='" + wifiPass + "' placeholder=''>";
    // City field
    page += "<label>City:</label><input type='text' name='city' value='" + city + "' required>";
    // Timezone dropdown
    page += "<label>Timezone:</label><select name='tz'>";
    // Populate timezone options from UTC-12 to UTC+14
    for (int tzHour = -12; tzHour <= 14; ++tzHour) {
      long tzSeconds = tzHour * 3600;
      String option = "<option value='" + String(tzSeconds) + "'";
      if (tzSeconds == timezoneOffset) {
        option += " selected";
      }
      option += ">UTC";
      if (tzHour >= 0) option += "+" + String(tzHour);
      else option += String(tzHour);
      option += "</option>";
      page += option;
    }
    page += "</select>";
    // Submit button
    page += "<input type='submit' value='Save'></form></div></body></html>";
    server.send(200, "text/html", page);
  });

  server.on("/", HTTP_POST, handleConfigForm);
  server.begin();
  Serial.println(F("HTTP server started for config portal."));
}

void handleConfigForm() {
  // Get form values
  String ssid = server.arg("ssid");
  String pass = server.arg("pass");
  String newCity = server.arg("city");
  String tzStr = server.arg("tz");

  Serial.println(F("Received configuration:"));
  Serial.print(F("SSID: ")); Serial.println(ssid);
  Serial.print(F("Password: ")); Serial.println(pass);
  Serial.print(F("City: ")); Serial.println(newCity);
  Serial.print(F("Timezone (s): ")); Serial.println(tzStr);

  if (ssid.length() > 0 && newCity.length() > 0 && tzStr.length() > 0) {
    wifiSSID = ssid;
    wifiPass = pass;
    city = newCity;
    timezoneOffset = tzStr.toInt();
    // Save to EEPROM
    saveSettings();
    // Send response page
    server.send(200, "text/html", "<html><body><h3>Settings saved. Rebooting...</h3></body></html>");
    delay(1000);
    ESP.restart();
  } else {
    server.send(400, "text/html", "<html><body><h3>Invalid input, please fill all required fields.</h3></body></html>");
  }
}

void loadSettings() {
  // Read SSID
  uint8_t len = EEPROM.read(ADDR_SSID);
  if (len > 0 && len < 0xFF) {
    char buf[80];
    for (int i = 0; i < len; ++i) {
      buf[i] = char(EEPROM.read(ADDR_SSID + 1 + i));
    }
    buf[len] = '\0';
    wifiSSID = String(buf);
  } else {
    wifiSSID = "";
  }
  // Read Password
  len = EEPROM.read(ADDR_PASS);
  if (len > 0 && len < 0xFF) {
    char buf[80];
    for (int i = 0; i < len; ++i) {
      buf[i] = char(EEPROM.read(ADDR_PASS + 1 + i));
    }
    buf[len] = '\0';
    wifiPass = String(buf);
  } else {
    wifiPass = "";
  }
  // Read City
  len = EEPROM.read(ADDR_CITY);
  if (len > 0 && len < 0xFF) {
    char buf[80];
    for (int i = 0; i < len; ++i) {
      buf[i] = char(EEPROM.read(ADDR_CITY + 1 + i));
    }
    buf[len] = '\0';
    city = String(buf);
  } else {
    city = "";
  }
  // Read Timezone offset (int32)
  uint32_t b0 = EEPROM.read(ADDR_TZ);
  uint32_t b1 = EEPROM.read(ADDR_TZ + 1);
  uint32_t b2 = EEPROM.read(ADDR_TZ + 2);
  uint32_t b3 = EEPROM.read(ADDR_TZ + 3);
  uint32_t raw = (b0 & 0xFF) | ((b1 & 0xFF) << 8) | ((b2 & 0xFF) << 16) | ((b3 & 0xFF) << 24);
  timezoneOffset = (int) raw;
}

void saveSettings() {
  // Write SSID
  uint8_t len = wifiSSID.length();
  if (len > 0) {
    EEPROM.write(ADDR_SSID, len);
    for (int i = 0; i < len; ++i) {
      EEPROM.write(ADDR_SSID + 1 + i, wifiSSID[i]);
    }
  } else {
    EEPROM.write(ADDR_SSID, 0);
  }
  // Write Password
  len = wifiPass.length();
  if (len > 0) {
    EEPROM.write(ADDR_PASS, len);
    for (int i = 0; i < len; ++i) {
      EEPROM.write(ADDR_PASS + 1 + i, wifiPass[i]);
    }
  } else {
    EEPROM.write(ADDR_PASS, 0);
  }
  // Write City
  len = city.length();
  if (len > 0) {
    EEPROM.write(ADDR_CITY, len);
    for (int i = 0; i < len; ++i) {
      EEPROM.write(ADDR_CITY + 1 + i, city[i]);
    }
  } else {
    EEPROM.write(ADDR_CITY, 0);
  }
  // Write Timezone (int32)
  int tz = timezoneOffset;
  EEPROM.write(ADDR_TZ, tz & 0xFF);
  EEPROM.write(ADDR_TZ + 1, (tz >> 8) & 0xFF);
  EEPROM.write(ADDR_TZ + 2, (tz >> 16) & 0xFF);
  EEPROM.write(ADDR_TZ + 3, (tz >> 24) & 0xFF);
  // Write signature 'CFG1'
  EEPROM.write(ADDR_SIGNATURE, 'C');
  EEPROM.write(ADDR_SIGNATURE + 1, 'F');
  EEPROM.write(ADDR_SIGNATURE + 2, 'G');
  EEPROM.write(ADDR_SIGNATURE + 3, '1');
  // Commit changes to EEPROM
  EEPROM.commit();
  Serial.println(F("Configuration saved to EEPROM."));
}

void drawTimeScreen() {
  if (!displayInitialized) {
    return;
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  // Day name at top center
  // Get current epoch time and derive day of week
  time_t epoch = timeClient.getEpochTime();
  // Calculate day of week (0=Sunday .. 6=Saturday)
  struct tm *tm = gmtime(&epoch);
  int wday = tm->tm_wday;  // tm_wday: days since Sunday (0-6)
  String dayName = "";
  switch(wday) {
    case 0: dayName = "Sunday"; break;
    case 1: dayName = "Monday"; break;
    case 2: dayName = "Tuesday"; break;
    case 3: dayName = "Wednesday"; break;
    case 4: dayName = "Thursday"; break;
    case 5: dayName = "Friday"; break;
    case 6: dayName = "Saturday"; break;
  }
  display.setFont(NULL); // default font
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(dayName, 0, 0, &x1, &y1, &w, &h);
  // center horizontally
  int dayX = (128 - w) / 2;
  display.setCursor(dayX, 0);
  display.print(dayName);

  // Time HH:MM in large font, centered
  display.setFont(&FreeMonoBold18pt7b);
  // Format time as HH:MM
  char timeBuf[6];
  snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", timeClient.getHours(), timeClient.getMinutes());
  String timeStr = String(timeBuf);
  display.getTextBounds(timeStr, 0, 30, &x1, &y1, &w, &h);
  int timeX = (128 - w) / 2;
  // Vertically center the text around mid (y=32)
  int timeY = 32 + (h / 2);
  display.setCursor(timeX, timeY);
  display.print(timeStr);

  // Bottom left: temperature and humidity
  display.setFont(NULL);
  display.setCursor(0, 56);
  if (weatherValid && weatherTemp != "N/A" && weatherHum != "") {
    String tempDisplay = weatherTemp;
    tempDisplay.trim();
    display.print(tempDisplay);
    display.print((char)247); // degree symbol
    display.print("C ");
    display.print(weatherHum);
  } else {
    display.print("N/A");
  }

  // Bottom right: date dd.mm.yyyy
  tm = gmtime(&epoch);
  int day = tm->tm_mday;
  int month = tm->tm_mon + 1;
  int year = tm->tm_year + 1900;
  char dateBuf[12];
  snprintf(dateBuf, sizeof(dateBuf), "%02d.%02d.%04d", day, month, year);
  String dateStr = String(dateBuf);
  display.getTextBounds(dateStr, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(128 - w, 56);
  display.print(dateStr);

  display.display();
}

void drawWeatherScreen() {
  if (!displayInitialized) {
    return;
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setFont(NULL);
  // Top center: city name
  String cityName = city;
  int16_t x1, y1; uint16_t w, h;
  display.getTextBounds(cityName, 0, 0, &x1, &y1, &w, &h);
  int cityX = (128 - w) / 2;
  display.setCursor(cityX, 0);
  display.print(cityName);
  // Middle center: temperature (big font FreeMonoBold12pt7b)
  display.setFont(&FreeMonoBold12pt7b);
  if (weatherValid && weatherTemp != "N/A") {
    // weatherTemp is e.g. "+12" or "-3" as string (cleaned)
    String tempNum = weatherTemp;
    // Center the numeric part
    display.getTextBounds(tempNum, 0, 30, &x1, &y1, &w, &h);
    int tempX = (128 - (w + 12)) / 2; // leave space for degree and C (~12px)
    int tempY = 30; // baseline for 12pt font around mid screen
    display.setCursor(tempX, tempY);
    display.print(tempNum);
    // Now draw degree symbol and 'C' in default font after the number
    //display.setFont(NULL);
    // Place small degree symbol near top of big text and 'C' after it
    int degX = tempX + w + 3; // position degree just right of number
    int degY = tempY - 16;    // raise small text (approx half big font height)
    if (degY < 0) degY = 0;
    display.setCursor(degX, degY);
    //display.print((char)247);
    display.drawCircle(degX, degY, 2, SSD1306_WHITE);
    display.setCursor(degX + 3, tempY);
    display.setFont(&FreeMonoBold12pt7b);    
    display.print("C");
    display.setFont(NULL);
  } else {
    // If weather not available, show N/A in big font
    String na = "N/A";
    display.getTextBounds(na, 0, 30, &x1, &y1, &w, &h);
    int naX = (128 - w) / 2;
    display.setCursor(naX, 30);
    display.print(na);
    // No degree symbol or 'C' in this case
  }
  // Below temperature: condition string (centered)
  display.setFont(NULL);
  String cond = weatherValid ? weatherCond : "";
  cond.trim();
  display.getTextBounds(cond, 0, 40, &x1, &y1, &w, &h);
  int condX = (128 - w) / 2;
  display.setCursor(condX, 40);
  display.print(cond);
  // Bottom right: H:% W:m/s P:mm
  display.setFont(NULL);
  if (weatherValid && weatherTemp != "N/A") {
    String bottomStr = String("H:") + weatherHum;
    bottomStr += " W:" + weatherWind;
    if (weatherWind != "N/A") bottomStr += "m/s";
    bottomStr += " P:" + weatherPress + "mm";
    display.getTextBounds(bottomStr, 0, 56, &x1, &y1, &w, &h);
    display.setCursor(128 - w, 56);
    display.print(bottomStr);
  } else {
    String bottomStr = "H:N/A W:N/A P:N/A";
    display.getTextBounds(bottomStr, 0, 56, &x1, &y1, &w, &h);
    display.setCursor(128 - w, 56);
    display.print(bottomStr);
  }
  display.display();
}

bool getWeather() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println(F("WiFi not connected, cannot get weather."));
    return false;
  }

  WiFiClientSecure client;
  client.setInsecure(); // Skip certificate validation for simplicity
  const char* host = "wttr.in";
  String url = "/" + city + "?format=%25t|%25C|%25h|%25w|%25P";

  Serial.print(F("Connecting to weather server: "));
  Serial.println(host);

  if (!client.connect(host, 443)) {
    Serial.println(F("Connection failed."));
    return false;
  }

  // Send GET request
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: ESP8266\r\n" +
               "Connection: close\r\n\r\n");
 // Read full response as raw text
  String response = "";
  unsigned long timeout = millis() + 5000;
  while (millis() < timeout && client.connected()) {
    while (client.available()) {
      char c = client.read();
      response += c;
    }
  }
  client.stop();

  Serial.println(F("---- RAW RESPONSE ----"));
  Serial.println(response);
  Serial.println(F("----------------------"));

  // --- Extract line with weather data ---
  String result = "";
  int from = 0;
  while (from >= 0) {
    int to = response.indexOf('\n', from);
    if (to == -1) break;
    String line = response.substring(from, to);
    line.replace("\r", "");
    line.trim();

    Serial.print("DEBUG LINE: >");
    Serial.print(line);
    Serial.println("<");

    if (line.indexOf('|') != -1) {
      result = line;
      break;
    }

    from = to + 1;
  }

  // If no \n at the end — catch the remaining part
  if (result.length() == 0 && from < response.length()) {
    String line = response.substring(from);
    line.replace("\r", "");
    line.trim();
    Serial.print("FALLBACK LINE: >");
    Serial.print(line);
    Serial.println("<");
    if (line.indexOf('|') != -1) {
      result = line;
    }
  }

  Serial.print(F("Weather raw response: "));
  Serial.println(result);

  if (result.length() == 0) {
    Serial.println(F("No weather data found."));
    return false;
  }

  // Parse fields: temp|cond|hum|wind|press
  int idx1 = result.indexOf('|');
  int idx2 = result.indexOf('|', idx1 + 1);
  int idx3 = result.indexOf('|', idx2 + 1);
  int idx4 = result.indexOf('|', idx3 + 1);
  if (idx1 < 0 || idx2 < 0 || idx3 < 0 || idx4 < 0) return false;

  String tempStr   = result.substring(0, idx1);
  String condStr   = result.substring(idx1 + 1, idx2);
  String humStr    = result.substring(idx2 + 1, idx3);
  String windStr   = result.substring(idx3 + 1, idx4);
  String pressStr  = result.substring(idx4 + 1);

  // Clean Temperature: remove degree symbol and 'C'
  tempStr.replace("°C", "");
  tempStr.replace("°", "");
  tempStr.replace("C", "");
  tempStr.trim();
  weatherTemp = tempStr; // keep + or - sign if present

  // Clean Condition:
  condStr.trim();
  weatherCond = condStr;

  // Clean Humidity: ensure '%' present
  humStr.trim();
  if (!humStr.endsWith("%")) {
    weatherHum = humStr + "%";
  } else {
    weatherHum = humStr;
  }

  // Clean Wind: extract numeric part (km/h)
  String windNum = "";
  for (uint i = 0; i < windStr.length(); ++i) {
    char c = windStr.charAt(i);
    if ((c >= '0' && c <= '9') || c == '.') {
      windNum += c;
    } else if (c == ' ' && windNum.length() > 0) {
      break;
    }
  }
  if (windNum.length() == 0) {
    weatherWind = "N/A";
  } else {
    float windKmh = windNum.toFloat();
    float windMs = windKmh / 3.6;
    int windMsRounded = (int)round(windMs);
    weatherWind = String(windMsRounded);
  }

  // Clean Pressure: extract numeric part (hPa)
  String pressNum = "";
  for (uint i = 0; i < pressStr.length(); ++i) {
    char c = pressStr.charAt(i);
    if ((c >= '0' && c <= '9') || c == '.') {
      pressNum += c;
    } else if (!pressNum.isEmpty()) {
      break;
    }
  }
  if (pressNum.length() == 0) {
    weatherPress = "N/A";
  } else {
    float pressHpa = pressNum.toFloat();
    float pressMm = pressHpa * 0.75006;
    int pressRounded = (int) round(pressMm);
    weatherPress = String(pressRounded);
  }

  Serial.println(F("Parsed weather data:"));
  Serial.print(F("Temp=")); Serial.println(weatherTemp);
  Serial.print(F("Cond=")); Serial.println(weatherCond);
  Serial.print(F("Hum=")); Serial.println(weatherHum);
  Serial.print(F("Wind(m/s)=")); Serial.println(weatherWind);
  Serial.print(F("Pressure(mmHg)=")); Serial.println(weatherPress);

  // Validate critical fields
  if (weatherTemp == "" || weatherCond == "") {
    return false;
  }
  return true;
}

Це запит, який було використано для ChatGPT (запит англійською бо з нею виходить кращий результат на даний момент):

Create a complete ESP8266 (ESP-01) firmware for a weather and clock project using Arduino IDE with the following requirements:

Display
    SSD1306 OLED 128x64 via I2C (SDA = GPIO0, SCL = GPIO2)
    Rotation: 180° via display.setRotation(2);
    Text color must be set to WHITE using display.setTextColor(SSD1306_WHITE);
    All elements must fit the 128x64 screen without clipping or overlapping
Wi-Fi & Configuration
    If configuration is missing or GPIO3 button is held during the first 300ms of boot:
        Start Access Point mode with SSID: "ESP01-Setup"
        Display on screen:
            Line 1: "AP mode"
            Line 2: "SSID: ESP01-Setup"
            Line 3: "IP: 192.168.4.1"
        Serve a Web 2.0 style setup page allowing the user to input:
            Wi-Fi SSID
            Password (as password field with masking)
            City
            Timezone offset (dropdown list from UTC-12 to UTC+14)
        Save values to EEPROM at fixed addresses
        Include 4-byte EEPROM signature "CFG1" to validate stored config
Time
    Use NTPClient to sync time using the saved timezone offset (in seconds)
    Update NTP time every minute
    Display on time screen:
        Top (centered): full day name (e.g., "Monday")
        Center (big font): time in HH:MM format using FreeMonoBold18pt7b
        Bottom:
            Left: temperature and humidity, formatted as +12°C 87%
            Extract temp from weather, remove existing degree symbol before displaying
            Add (char)247 for degree symbol and "C"
            Right: date as dd.mm.yyyy
Weather
    Use HTTPS GET to:
    https://wttr.in/{city}?format=%25t|%25C|%25h|%25w|%25P
    Parse response into:
        Temperature
        Condition
        Humidity
        Wind (km/h)
        Pressure (hPa)
    Convert:
        Pressure → mmHg
        Wind → m/s (rounded)
    Update weather every hour

Weather Screen Layout
    Top (centered): city name
    Center (large font): temperature using FreeMonoBold12pt7b
        Draw temperature number without "°C"
        Draw degree symbol manually as a small circle to the top-right
        Draw "C" next to it using the FreeMonoBold12pt7b font
    Below (centered): weather condition
    Bottom right: line with "H:87% W:3m/s P:754mm"

EEPROM
    EEPROM size: 512 bytes
    Custom read/write functions for fixed-length strings
    Store SSID, password, city, timezone, and signature
    Validate config by checking signature "CFG1"

Button on GPIO3
    Use INPUT_PULLUP mode
    During first 300ms of setup, if button is LOW, trigger AP mode
    After settings are saved, reboot and attempt connection

Switching Between Screens

    Switch between time screen and weather screen every 15 seconds

 Fallback

    If weather is unavailable, show "N/A" on the screen

Fonts

    Only use large fonts for:
    Time: FreeMonoBold18pt7b
    Temperature on weather screen: FreeMonoBold12pt7b
    All other text: default font

Code Style
    Code must be clean, modular, and easy to follow
    Group code into:
        setup()
        loop()
        drawTimeScreen()
        drawWeatherScreen()
        getWeather()
        EEPROM helpers
    Use descriptive variable names

Висновок:

Цей набір є досить непоганим варіантом для тих, хто хоче вивчати програмування мікроконтролерів. Він дозволяє не лише зібрати корисний пристрій, але й зануритися у процес створення власної прошивки з нуля, навіть якщо у вас лише базові знання програмування. За допомогою штучного інтелекту цілком реально створити робочу прошивку для ESP-01S, маючи лише базові знання з програмування.

Дата: 23.04.2025