Конструктор 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