Complete reverse engineering of TJ-56-654 weather clock from AliExpress. Security fixes: - Eliminated WiFi password leak vulnerability - Removed dependency on Chinese cloud services (QWeather) - Secure WiFiManager captive portal setup - No hardcoded credentials Features: - Fully async architecture (zero blocking operations) - OTA firmware updates (web + ArduinoOTA) - NTP time sync with timezone + DST support - Open-Meteo weather API (free, no registration) - 3 display modes: time, weather, sunrise/sunset - REST API + web interface - EEPROM config persistence Performance: - Loop time: <1ms (was 10ms+) - Memory: 409KB flash (38%), 38KB RAM (46%), 62KB IRAM (94%) - Zero blocking delays Hardware: - ESP-01S (ESP8266EX, 1MB flash) - GM009605v4.3 OLED display (128x64, I2C) - Custom I2C mapping: SDA=GPIO0, SCL=GPIO2 Documentation: - Complete installation guide - Hardware specifications - API documentation - Troubleshooting guide - Version history v1.5 → v1.9.1 Built with Claude Code (Opus 4.5) Author: Andrey Petrochenko Date: 2026-01-03
219 lines
10 KiB
Markdown
219 lines
10 KiB
Markdown
# v1.9.1 - Hybrid Async Fix
|
||
|
||
## Проблема в v1.9.0
|
||
|
||
**Симптомы:**
|
||
- Дисплей показывает пустой экран ~10 секунд после загрузки
|
||
- Ошибка "DNS resolution failed" в логах
|
||
- Время появляется только через 10+ секунд
|
||
|
||
**Причина:**
|
||
```cpp
|
||
void setup() {
|
||
loadConfig();
|
||
setupWiFi(); // ← Возвращается СРАЗУ (async)
|
||
setupOTA(); // ← WiFi НЕ готов! ✗
|
||
setupWebServer(); // ← WiFi НЕ готов! ✗
|
||
testInternetConnectivity(); // ← WiFi НЕ готов! → "DNS resolution failed"
|
||
}
|
||
```
|
||
|
||
WiFi стал **полностью асинхронным**, но это **неправильно для setup()**:
|
||
- OTA, web server, NTP **требуют готовое WiFi соединение**
|
||
- `testInternetConnectivity()` запускался **до подключения WiFi**
|
||
- Время на дисплее появлялось только когда async WiFi наконец подключался
|
||
|
||
## Решение: Гибридная модель
|
||
|
||
| Фаза | WiFi режим | Блокировка | Причина |
|
||
|------|------------|------------|---------|
|
||
| **setup()** | **Синхронный** | 10 сек | Нужен для инициализации OTA/web/NTP |
|
||
| **loop()** | **Асинхронный** | 0 сек | Не замораживать при reconnect |
|
||
|
||
### Изменения в коде
|
||
|
||
#### 1. setupWiFi() - теперь синхронный
|
||
|
||
```cpp
|
||
void ICACHE_FLASH_ATTR setupWiFi() {
|
||
Serial.println("WiFi Setup - Synchronous for initial connection");
|
||
|
||
WiFi.hostname(config.hostname);
|
||
|
||
if (strlen(config.ssid) > 0) {
|
||
WiFi.mode(WIFI_STA);
|
||
WiFi.begin(config.ssid, config.password);
|
||
|
||
// СИНХРОННОЕ ожидание (max 10 секунд)
|
||
int attempts = 0;
|
||
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||
delay(500);
|
||
showNumber(attempts, false); // Показываем прогресс на дисплее
|
||
attempts++;
|
||
}
|
||
|
||
if (WiFi.status() == WL_CONNECTED) {
|
||
// ✅ WiFi готов для OTA/web/NTP!
|
||
showIP();
|
||
wifiConnState = WIFI_CONN_CONNECTED;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Fallback to WiFiManager если credentials не сработали
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 2. loop() - async reconnect
|
||
|
||
```cpp
|
||
void loop() {
|
||
// WiFi reconnection (async, non-blocking)
|
||
static unsigned long lastWiFiCheck = 0;
|
||
if (millis() - lastWiFiCheck > 5000) {
|
||
if (WiFi.status() != WL_CONNECTED && wifiConnState == WIFI_CONN_CONNECTED) {
|
||
Serial.println("⚠️ WiFi disconnected, attempting async reconnect...");
|
||
WiFi.begin(); // Async reconnect
|
||
wifiConnState = WIFI_CONN_CONNECTING;
|
||
wifiConnectStart = millis();
|
||
}
|
||
lastWiFiCheck = millis();
|
||
}
|
||
|
||
processWiFiConnection(); // Async обработка reconnect
|
||
|
||
// Остальные async операции
|
||
processNTPResponse();
|
||
fetchWeatherAsync();
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## Результаты тестирования
|
||
|
||
### До (v1.9.0)
|
||
```
|
||
⏱️ 0-5s → Display init
|
||
⏱️ 5-15s → WiFi connecting (async, setup() возвращается сразу)
|
||
⏱️ 15-20s → OTA/web init БЕЗ WiFi → ✗ Errors!
|
||
⏱️ 20s → testInternetConnectivity() БЕЗ WiFi → "DNS resolution failed"
|
||
⏱️ 15-25s → WiFi finally connects (async)
|
||
⏱️ 25-30s → NTP sync начинается
|
||
|
||
❌ Display blank for 10+ seconds
|
||
❌ "DNS resolution failed" errors
|
||
```
|
||
|
||
### После (v1.9.1)
|
||
```
|
||
⏱️ 0-5s → Display init + startup animation
|
||
⏱️ 5-15s → WiFi connection (SYNCHRONOUS, setup() waits)
|
||
✅ WiFi connected!
|
||
⏱️ 15-20s → OTA/web/NTP init С WiFi
|
||
✅ Internet test: PASSED
|
||
✅ No DNS errors!
|
||
⏱️ 20-30s → First async NTP sync
|
||
✅ Time synced!
|
||
|
||
✅ Display shows time immediately after WiFi connects (~15 sec)
|
||
✅ No "DNS resolution failed" errors
|
||
✅ Proper initialization order
|
||
```
|
||
|
||
## Startup Timeline
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ SETUP PHASE (Synchronous WiFi) │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [0s] ┌─────────────┐ │
|
||
│ │ Display │ Startup animation │
|
||
│ [5s] │ Init │ "Weather Clock v1.9.1" │
|
||
│ └─────────────┘ │
|
||
│ │
|
||
│ [5s] ┌─────────────────────────────────┐ │
|
||
│ │ WiFi Connect (SYNCHRONOUS) │ │
|
||
│ │ - Connecting to SibWings... │ │
|
||
│ [15s] │ - ✅ Connected! IP assigned │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ ↓ │
|
||
│ WiFi is READY here ✅ │
|
||
│ ↓ │
|
||
│ [15s] ┌─────────────────────────────────┐ │
|
||
│ │ OTA Init (needs WiFi ✅) │ │
|
||
│ │ Web Server (needs WiFi ✅) │ │
|
||
│ [20s] │ NTP Client (needs WiFi ✅) │ │
|
||
│ │ Internet Test (needs WiFi ✅) │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ [20s] Setup complete! → loop() starts │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────┘
|
||
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ LOOP PHASE (Async Operations) │
|
||
├──────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [Every loop] ┌──────────────────────────┐ │
|
||
│ │ WiFi Health Check │ │
|
||
│ │ (every 5 sec) │ │
|
||
│ │ If disconnected: │ │
|
||
│ │ → Async reconnect │ │
|
||
│ └──────────────────────────┘ │
|
||
│ │
|
||
│ [Every loop] ┌──────────────────────────┐ │
|
||
│ │ Async NTP Processing │ │
|
||
│ │ (non-blocking) │ │
|
||
│ └──────────────────────────┘ │
|
||
│ │
|
||
│ [Every 30m] ┌──────────────────────────┐ │
|
||
│ │ Async Weather Fetch │ │
|
||
│ │ (non-blocking) │ │
|
||
│ └──────────────────────────┘ │
|
||
│ │
|
||
│ Loop time: <1ms (no blocking!) │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Преимущества гибридного подхода
|
||
|
||
### ✅ В setup():
|
||
1. **Правильный порядок инициализации** - WiFi → OTA → web → NTP
|
||
2. **Нет ошибок DNS** - internet connectivity test запускается ПОСЛЕ WiFi
|
||
3. **Предсказуемое поведение** - setup() завершается когда всё готово
|
||
4. **Дисплей показывает время сразу** - не нужно ждать async WiFi
|
||
|
||
### ✅ В loop():
|
||
1. **Не зависает при reconnect** - async обработка потери WiFi
|
||
2. **Async NTP** - не блокирует loop
|
||
3. **Async weather** - не блокирует loop
|
||
4. **Exponential backoff** - умные retry при ошибках
|
||
5. **Loop <1ms** - всегда отзывчивое устройство
|
||
|
||
## Память
|
||
|
||
| Ресурс | v1.9.0 | v1.9.1 | Изменение |
|
||
|--------|--------|--------|-----------|
|
||
| RAM | 37,516 | 37,644 | +128 bytes |
|
||
| IRAM | 61,987 | 61,987 | 0 bytes |
|
||
| Flash | 408,540 | 408,844 | +304 bytes |
|
||
|
||
Минимальные изменения памяти (+0.3%) для критического улучшения UX.
|
||
|
||
## Заключение
|
||
|
||
**v1.9.1 реализует идеальный баланс:**
|
||
- Setup: Синхронный для надежной инициализации
|
||
- Loop: Асинхронный для отзывчивости
|
||
|
||
**Результат:**
|
||
- ✅ Дисплей показывает время через 15 сек (вместо 25+ сек)
|
||
- ✅ Никаких ошибок "DNS resolution failed"
|
||
- ✅ Правильный порядок старта
|
||
- ✅ Устройство не зависает при потере WiFi в работе
|
||
|
||
**Status**: Production ready 🚀
|