9 Commits
Author SHA1 Message Date
Alex PetrochenkoandClaude Sonnet 4.6 e4ef5c8e07 chore: bump version to 1.9.4, update docs
- FIRMWARE_VERSION 1.9.3 → 1.9.4
- CHANGELOG: add v1.9.3 (modular refactor) and v1.9.4 (bug fixes) entries
- README: add v1.9.3/v1.9.4 journey sections, fix project structure tree,
  update version references, correct SDK credentials description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 18:52:26 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 3a34244ac8 refactor: rename firmware directory to weather_clock
clock_ntp_ota_v1.9/ → weather_clock/ (version no longer baked into path)
clock_ntp_ota_v1.9.ino → weather_clock.ino

Version is tracked in config.h (FIRMWARE_VERSION), not in filenames.
Updated references in README, CONTRIBUTING, and docs/INSTALLATION.md.

Note: forks using the old path will need to sync this rename before
submitting new PRs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:52:34 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 55bec76d88 chore: remove internal version notes, clean up CHANGELOG links
docs/v1.9_RELEASE_NOTES.md, v1.9.1_HYBRID_FIX.md, v1.9.2_WIFI_RESILIENCE.md
were internal working documents (incomplete checklists, dev notes).
CHANGELOG.md already covers all versions — links removed, status updated to v1.9.3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:42:59 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 29d7c7f8ad chore: remove monolithic src/ copy, firmware/ is now canonical
src/clock_ntp_ota_v1.9.ino was a duplicate of firmware/clock_ntp_ota_v1.9/
that had already drifted out of sync. Removing it eliminates the need to
patch both copies on every PR.

Updated CONTRIBUTING.md build path to point to firmware/clock_ntp_ota_v1.9.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:40:36 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 98b40ff0cf chore: remove internal dev documents from public repo
PROJECT_CONTEXT.md, PROJECT_STRUCTURE.md, PUBLISH_TO_GITHUB.md were
working notes not intended for contributors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:40:04 +01:00
8d404506c4 fix: resolve compiler warnings (from PR #8 by @waltje)
- weather.cpp: StaticJsonDocument → JsonDocument (ArduinoJson v7 API)
- display.cpp: daylightStr buffer 16 → 32 bytes (suppress GCC sprintf warning)
- ntp_client.cpp: remove unused weekday variable

Applied manually due to repo layout mismatch in original PR.

Co-authored-by: waltje <waltje@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:34:35 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 09b8680408 fix(wifi): skip SDK-cached credentials when SSID is already configured (closes #3)
WiFi.begin() without params connects to any SDK-cached network, including
open public hotspots, and then overwrites config.ssid with the wrong SSID.

Now Try 1 (SDK credentials) is only attempted on first boot when no SSID
is configured. Once the user has saved credentials, we go straight to
Try 2 (EEPROM config), so the correct network is always used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:29:47 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 c606bcc589 docs: add ArduinoJson to required libraries list (closes #4)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:24:17 +01:00
441c61676b fix: date timezone + weather periodic refresh (closes #5, #7)
- Use localTime instead of epochTime when formatting date string,
  so date rolls over at local midnight, not UTC midnight (fixes #5)
- Remove WEATHER_SUCCESS state assignment that blocked periodic refresh;
  state correctly stays WEATHER_IDLE after successful fetch (fixes #7)
- Fix weather debug page "Last update" to show elapsed seconds
  (millis() - lastUpdate) instead of raw timestamp

Applied from PR #6 by @Mysteoa with a compilation fix in web_server.cpp
(missing closing parenthesis in snprintf_P call).

Co-authored-by: Mysteoa <mysteoa@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:20:08 +01:00
20 changed files with 297 additions and 4525 deletions
+50 -7
View File
@@ -5,15 +5,46 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.9.4] - 2026-05-14
### Fixed
- **Date timezone** (#5): Date string now uses local time, so date rolls over at local midnight instead of UTC midnight
- **Weather periodic refresh** (#7): Removed `WEATHER_SUCCESS` state that permanently blocked periodic weather updates after first fetch; also fixed "Last update" counter showing raw timestamp instead of elapsed seconds
- **WiFi connects to wrong network** (#3): `WiFi.begin()` without params is now skipped when a saved SSID exists, preventing connection to SDK-cached open hotspots and config corruption
### Changed
- WiFi startup: SDK-cached credentials only attempted on first boot (no saved SSID); subsequent boots go directly to saved credentials
- ArduinoJson: `StaticJsonDocument` → `JsonDocument` for compatibility with ArduinoJson v7
### Removed
- Unused `weekday` variable in NTP DST calculation (compiler warning)
### Internal
- Split `clock_ntp_ota_v1.9/` directory renamed to `weather_clock/` — version no longer baked into path
- Removed internal development documents from repository
## [1.9.3] - 2026-01-06
### Changed
- Refactored monolithic 2,100-line `.ino` into modular structure:
`display.cpp`, `ntp_client.cpp`, `weather.cpp`, `web_server.cpp`, `wifi_manager.cpp`
## [1.9.2] - 2026-01-06 ## [1.9.2] - 2026-01-06
### Fixed ### Fixed
- **CRITICAL**: WiFi credentials no longer cleared on connection failure - **CRITICAL**: WiFi credentials no longer cleared on connection failure
- Previous behavior erased SSID/password after failed connection attempts - Previous behavior erased SSID/password after failed connection attempts
- Now credentials persist indefinitely through WiFi outages - Now credentials persist indefinitely through WiFi outages
- OTA update credential loss issue (SDK credentials now tried first, then EEPROM) - OTA update credential loss issue (SDK credentials now tried first, then EEPROM)
### Added ### Added
- **WiFi Resilience**: Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max) - **WiFi Resilience**: Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
- **Fallback AP**: "TJ56654-Setup" enabled after ~5 min of failed attempts - **Fallback AP**: "TJ56654-Setup" enabled after ~5 min of failed attempts
- Device continues retry attempts while AP is active (dual STA+AP mode) - Device continues retry attempts while AP is active (dual STA+AP mode)
@@ -23,11 +54,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **SDK credentials support**: Tries WiFiManager-stored credentials first - **SDK credentials support**: Tries WiFiManager-stored credentials first
### Changed ### Changed
- Clock continues running with last synced time during WiFi outages - Clock continues running with last synced time during WiFi outages
- Improved user experience during network failures - Improved user experience during network failures
- Removed aggressive credential clearing behavior - Removed aggressive credential clearing behavior
### Network Activity ### Network Activity
- NTP sync: 1 hour interval (pool.ntp.org:123 UDP) - NTP sync: 1 hour interval (pool.ntp.org:123 UDP)
- Weather fetch: 30 min interval (api.open-meteo.com HTTP) - Weather fetch: 30 min interval (api.open-meteo.com HTTP)
- mDNS: continuous (224.0.0.251 UDP multicast) - mDNS: continuous (224.0.0.251 UDP multicast)
@@ -36,22 +69,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.9.1] - 2026-01-03 ## [1.9.1] - 2026-01-03
### Fixed ### Fixed
- **CRITICAL**: WiFi startup sequence - synchronous connection in setup() to ensure proper initialization order - **CRITICAL**: WiFi startup sequence - synchronous connection in setup() to ensure proper initialization order
- Display blank screen for 10+ seconds on boot (now shows time after ~15 seconds) - Display blank screen for 10+ seconds on boot (now shows time after ~15 seconds)
- "DNS resolution failed" errors during startup - "DNS resolution failed" errors during startup
- Sunrise/sunset labels cut off on 128px screen (removed labels, arrows are self-explanatory) - Sunrise/sunset labels cut off on 128px screen (removed labels, arrows are self-explanatory)
### Changed ### Changed
- Hybrid WiFi model: synchronous in setup(), async reconnect in loop() - Hybrid WiFi model: synchronous in setup(), async reconnect in loop()
- Display formatting: superscript degree symbol and lowercase 'c' for temperature - Display formatting: superscript degree symbol and lowercase 'c' for temperature
- Sunrise/sunset screen now shows daylight duration (e.g., "Day 9h 41m") instead of static "Sun Times" text - Sunrise/sunset screen now shows daylight duration (e.g., "Day 9h 41m") instead of static "Sun Times" text
### Documentation ### Documentation
- Added detailed v1.9.1_HYBRID_FIX.md explaining startup sequence problem and solution - Added detailed v1.9.1_HYBRID_FIX.md explaining startup sequence problem and solution
## [1.9.0] - 2026-01-02 ## [1.9.0] - 2026-01-02
### Added ### Added
- Fully async architecture (zero blocking operations in loop) - Fully async architecture (zero blocking operations in loop)
- Custom async NTP implementation (manual UDP packet handling) - Custom async NTP implementation (manual UDP packet handling)
- Async HTTP weather fetch (AsyncHTTPRequest library) - Async HTTP weather fetch (AsyncHTTPRequest library)
@@ -59,12 +96,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Independent epoch tracking for accurate time between NTP syncs - Independent epoch tracking for accurate time between NTP syncs
### Changed ### Changed
- Replaced blocking NTPClient with custom async UDP implementation - Replaced blocking NTPClient with custom async UDP implementation
- Replaced blocking HTTP weather with AsyncHTTPRequest - Replaced blocking HTTP weather with AsyncHTTPRequest
- Removed all delay() calls from loop() - Removed all delay() calls from loop()
- WiFi connection now async (later fixed in v1.9.1) - WiFi connection now async (later fixed in v1.9.1)
### Performance ### Performance
- Loop time: 10ms → <1ms (10x improvement) - Loop time: 10ms → <1ms (10x improvement)
- Weather fetch: 1-10s blocking → 0ms - Weather fetch: 1-10s blocking → 0ms
- NTP sync: 5-20s blocking → 0ms - NTP sync: 5-20s blocking → 0ms
@@ -72,6 +111,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- OTA updates now work during active weather fetching - OTA updates now work during active weather fetching
### Technical ### Technical
- RAM usage: +536 bytes (36,980 → 37,516) - RAM usage: +536 bytes (36,980 → 37,516)
- Flash usage: +1040 bytes (407,500 → 408,540) - Flash usage: +1040 bytes (407,500 → 408,540)
- IRAM: 61,987 bytes (94% - stable) - IRAM: 61,987 bytes (94% - stable)
@@ -79,12 +119,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.8.0] - 2026-01-01 ## [1.8.0] - 2026-01-01
### Security ### Security
- **CRITICAL**: Removed hardcoded WiFi credentials - **CRITICAL**: Removed hardcoded WiFi credentials
- Integrated WiFiManager for secure captive portal setup - Integrated WiFiManager for secure captive portal setup
- Added config validation (magic number check) - Added config validation (magic number check)
- Input sanitization to prevent buffer overflows - Input sanitization to prevent buffer overflows
### Fixed ### Fixed
- IRAM overflow crisis (94% → 70% via ICACHE_FLASH_ATTR) - IRAM overflow crisis (94% → 70% via ICACHE_FLASH_ATTR)
- NTP interval bug (config value was ignored, always used hardcoded 1 hour) - NTP interval bug (config value was ignored, always used hardcoded 1 hour)
- Boolean parsing errors in JSON config import/export - Boolean parsing errors in JSON config import/export
@@ -92,17 +134,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Memory leaks from String concatenation in web handlers - Memory leaks from String concatenation in web handlers
### Changed ### Changed
- Web responses now use chunked transfer (eliminated 140+ String concatenations) - Web responses now use chunked transfer (eliminated 140+ String concatenations)
- Applied ICACHE_FLASH_ATTR to 26 functions (moved code from IRAM to Flash) - Applied ICACHE_FLASH_ATTR to 26 functions (moved code from IRAM to Flash)
- Improved error handling throughout codebase - Improved error handling throughout codebase
### Performance ### Performance
- Peak heap usage reduced by ~8KB - Peak heap usage reduced by ~8KB
- EEPROM validation prevents loading corrupted config - EEPROM validation prevents loading corrupted config
## [1.7.0] - 2025-12-31 ## [1.7.0] - 2025-12-31
### Added ### Added
- Initial working firmware with correct display support - Initial working firmware with correct display support
- NTP time synchronization - NTP time synchronization
- Weather data from Open-Meteo API (free, no API key required) - Weather data from Open-Meteo API (free, no API key required)
@@ -114,11 +159,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- EEPROM configuration persistence - EEPROM configuration persistence
### Hardware Discovery ### Hardware Discovery
- Identified display as GM009605v4.3 (not TM1637 or TM1650) - Identified display as GM009605v4.3 (not TM1637 or TM1650)
- Discovered swapped I2C pins: SDA=GPIO0, SCL=GPIO2 - Discovered swapped I2C pins: SDA=GPIO0, SCL=GPIO2
- Switched to Adafruit_SSD1306 library - Switched to Adafruit_SSD1306 library
### Replaced ### Replaced
- QWeather API → Open-Meteo (no registration required) - QWeather API → Open-Meteo (no registration required)
- Proprietary firmware → Open source custom firmware - Proprietary firmware → Open source custom firmware
- Insecure WiFi handling → WiFiManager with timeout - Insecure WiFi handling → WiFiManager with timeout
@@ -126,11 +173,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.6.0] - 2025-12-30 (unreleased) ## [1.6.0] - 2025-12-30 (unreleased)
### Attempted ### Attempted
- TM1650 LED driver support (incorrect - device has OLED) - TM1650 LED driver support (incorrect - device has OLED)
## [1.5.0] - 2025-12-29 (unreleased) ## [1.5.0] - 2025-12-29 (unreleased)
### Attempted ### Attempted
- TM1637 7-segment display support (incorrect - device has OLED) - TM1637 7-segment display support (incorrect - device has OLED)
--- ---
@@ -141,12 +190,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Minor version** (1.X.0): New features, backward-compatible - **Minor version** (1.X.0): New features, backward-compatible
- **Patch version** (1.9.X): Bug fixes, no new features - **Patch version** (1.9.X): Bug fixes, no new features
## Links
- [Full v1.9 Release Notes](docs/v1.9_RELEASE_NOTES.md)
- [v1.9.1 Hybrid Fix Details](docs/v1.9.1_HYBRID_FIX.md)
- [v1.9.2 WiFi Resilience](docs/v1.9.2_WIFI_RESILIENCE.md)
--- ---
**Status**: v1.9.2 is production-ready and actively used 24/7. **Status**: v1.9.3 is production-ready and actively used 24/7.
+10 -2
View File
@@ -7,6 +7,7 @@ Thank you for your interest in contributing! This project welcomes improvements,
### Reporting Bugs ### Reporting Bugs
If you find a bug, please open an issue with: If you find a bug, please open an issue with:
- Clear description of the problem - Clear description of the problem
- Steps to reproduce - Steps to reproduce
- Expected vs actual behavior - Expected vs actual behavior
@@ -16,6 +17,7 @@ If you find a bug, please open an issue with:
### Suggesting Features ### Suggesting Features
Feature requests are welcome! Please include: Feature requests are welcome! Please include:
- Use case description - Use case description
- Why this would be useful - Why this would be useful
- Any implementation ideas - Any implementation ideas
@@ -38,16 +40,19 @@ Feature requests are welcome! Please include:
### Code Guidelines ### Code Guidelines
**Memory Safety:** **Memory Safety:**
- Check IRAM usage after adding code - Check IRAM usage after adding code
- Use fixed-size buffers instead of dynamic allocation where possible - Use fixed-size buffers instead of dynamic allocation where possible
- Prefer `snprintf` over String concatenation - Prefer `snprintf` over String concatenation
**Async Architecture:** **Async Architecture:**
- Keep loop() non-blocking (no delay() calls) - Keep loop() non-blocking (no delay() calls)
- Use state machines for multi-step operations - Use state machines for multi-step operations
- Add exponential backoff to network operations - Add exponential backoff to network operations
**Testing:** **Testing:**
- Test on ESP-01S hardware (1MB flash, 80KB RAM) - Test on ESP-01S hardware (1MB flash, 80KB RAM)
- Verify OTA updates work - Verify OTA updates work
- Check 24h stability - Check 24h stability
@@ -55,18 +60,21 @@ Feature requests are welcome! Please include:
## Development Setup ## Development Setup
### Requirements ### Requirements
- Arduino IDE 1.8.x or 2.x - Arduino IDE 1.8.x or 2.x
- ESP8266 board support (v3.0.0+) - ESP8266 board support (v3.0.0+)
- Libraries (see README) - Libraries (see README)
### Building ### Building
```bash ```bash
# Arduino IDE: Sketch → Verify/Compile # Arduino IDE: Sketch → Verify/Compile
# Or use arduino-cli: # Or use arduino-cli:
arduino-cli compile --fqbn esp8266:esp8266:generic src/clock_ntp_ota_v1.9.ino arduino-cli compile --fqbn esp8266:esp8266:generic firmware/weather_clock
``` ```
### Testing ### Testing
```bash ```bash
# Upload via FTDI (first time) # Upload via FTDI (first time)
arduino-cli upload -p /dev/cu.usbserial* --fqbn esp8266:esp8266:generic arduino-cli upload -p /dev/cu.usbserial* --fqbn esp8266:esp8266:generic
@@ -79,7 +87,7 @@ curl -u admin:admin -F "file=@build/*.bin" http://192.168.x.x/update
``` ```
esp8266-weather-clock-opensource/ esp8266-weather-clock-opensource/
├── src/ # Main firmware source ├── firmware/ # Main firmware source (weather_clock/)
├── docs/ # Documentation ├── docs/ # Documentation
├── images/ # Photos and screenshots ├── images/ # Photos and screenshots
├── README.md # Main documentation ├── README.md # Main documentation
-800
View File
@@ -1,800 +0,0 @@
# ESP8266 Weather Clock - Full Project Context
**Last Updated**: 2026-01-03
**Current Version**: v1.9.1 (Production Ready)
**GitHub**: https://github.com/petrochen/esp8266-weather-clock-opensource
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Hardware Details](#hardware-details)
3. [Development History](#development-history)
4. [Current Status (v1.9.1)](#current-status-v191)
5. [Technical Architecture](#technical-architecture)
6. [Security Issues Fixed](#security-issues-fixed)
7. [Files & Structure](#files--structure)
8. [Git Repository](#git-repository)
9. [Next Steps](#next-steps)
10. [Key Learnings](#key-learnings)
---
## Project Overview
### What Is This?
Complete reverse engineering and firmware replacement for an ESP8266-based weather clock purchased from AliExpress (model TJ-56-654, €5).
### Why?
**Original firmware had critical security vulnerabilities:**
- WiFi password displayed in plaintext
- Persistent open access point running in parallel to home WiFi
- Dependency on Chinese cloud service (QWeather) requiring API key registration
- No OTA updates (required physical FTDI access for updates)
**Solution:** Complete custom firmware with security, performance, and features.
### Key Features
- ✅ **Security**: No password leaks, WiFiManager captive portal, no hardcoded credentials
- ✅ **Performance**: Fully async architecture, <1ms loop time (was 10ms+)
- ✅ **Weather**: Open-Meteo API (free, no registration, no API key)
- ✅ **Updates**: OTA via web interface + ArduinoOTA
- ✅ **Interface**: Web UI, REST API, full configuration
- ✅ **Display**: 3 rotating modes (time, weather, sunrise/sunset with daylight duration)
- ✅ **Time**: NTP sync with timezone + automatic European DST
---
## Hardware Details
### Device Purchased
- **Product**: ESP8266 Mini Weather Clock Kit
- **Model**: TJ-56-654
- **Source**: [AliExpress Link](https://pt.aliexpress.com/item/1005008333782531.html)
- **Price**: €5 EUR (~$5.50 USD)
- **Size**: 40mm x 40mm x 43mm (transparent acrylic case)
### Components
#### ESP-01S WiFi Module
- **Chip**: ESP8266EX
- **Flash**: 1MB (8Mbit)
- **RAM**: 80KB total
- **CPU**: 80MHz
- **WiFi**: 802.11 b/g/n (2.4GHz only)
- **GPIO**: Only GPIO0 and GPIO2 available
- **Voltage**: 3.3V ⚠️ NOT 5V tolerant
#### Display Module
- **Model**: GM009605v4.3
- **Type**: OLED (128x64 pixels, 0.96 inches)
- **Controller**: SSD1306/SH1106 compatible
- **Interface**: I2C
- **I2C Address**: 0x3C (default), 0x3D (fallback)
- **Colors**: Monochrome (white on black)
#### Pin Mapping (CRITICAL!)
**Non-standard I2C mapping:**
- SDA: GPIO0 (not GPIO4 as typical)
- SCL: GPIO2 (not GPIO5 as typical)
This mapping is **backwards** from standard ESP8266 breakout boards!
```cpp
Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
```
### Power Supply
- Input: 5V via Micro-USB
- Current: 80-120mA typical
- Regulator: Onboard 3.3V LDO
---
## Development History
### Timeline
**2025-12-29**: v1.5 (unreleased)
- Attempted TM1637 7-segment display support
- ❌ Wrong - device has OLED, not 7-segment LEDs
**2025-12-30**: v1.6 (unreleased)
- Attempted TM1650 LED driver support
- ❌ Wrong - I2C addresses didn't match
**2025-12-31**: v1.7 ✅
- ✅ Identified correct display: GM009605v4.3 (SSD1306-compatible OLED)
- ✅ Discovered swapped pins: SDA=GPIO0, SCL=GPIO2
- ✅ Switched to Adafruit_SSD1306 library
- ✅ Basic time display working
- ✅ WiFiManager integration
- ✅ First OTA deployment
**2026-01-01**: v1.8 🔒
- 🔒 **Security fixes**:
- Removed hardcoded WiFi credentials
- Added config validation (magic number check)
- Input sanitization (buffer overflow protection)
- 🛠️ **Stability fixes**:
- IRAM crisis fix (94% → 70% via ICACHE_FLASH_ATTR on 26 functions)
- NTP interval bug (config value was ignored)
- Boolean parsing errors in JSON import/export
- Infinite loop protection in display rotation
- ⚡ **Performance**:
- Chunked HTTP responses (eliminated 140+ String concatenations)
- Peak heap usage reduced by ~8KB
- Memory leaks fixed
**2026-01-02**: v1.9.0 ⚡
- ⚡ **Full async refactoring**:
- Async HTTP weather fetch (AsyncHTTPRequest library)
- Custom async NTP implementation (manual UDP packets)
- Async WiFi connection (state machine)
- Removed all delay() calls from loop()
- Exponential backoff retry logic
- 📊 **Performance results**:
- Loop time: 10ms → <1ms (10x improvement)
- Weather fetch: 1-10s blocking → 0ms
- NTP sync: 5-20s blocking → 0ms
- WiFi reconnect: 15s blocking → 0ms
- ❌ **Problem discovered**:
- Display blank for 10+ seconds on boot
- "DNS resolution failed" errors
**2026-01-03**: v1.9.1 (Current) 🎯
- 🔧 **Critical fix**: Hybrid WiFi model
- Synchronous WiFi in setup() (waits up to 10 seconds)
- Async WiFi reconnect in loop() (non-blocking)
- Ensures proper initialization order: WiFi → OTA → web → NTP
- 📺 **Display improvements**:
- Fixed sunrise/sunset labels cutoff (removed labels, kept arrows)
- Superscript degree symbol (°c)
- Daylight duration display instead of static "Sun Times"
- ✅ **Result**: Production ready, tested 24/7
---
## Current Status (v1.9.1)
### Version Information
**Firmware**: v1.9.1 (Production Ready)
**Released**: 2026-01-03
**Status**: Actively running 24/7, stable
### Memory Usage
| Resource | Used | Total | Usage | Status |
|----------|------|-------|-------|--------|
| Flash | 408,844 | 1,048,576 | 38% | ✅ Plenty |
| RAM | 37,644 | 80,192 | 46% | ✅ Safe |
| IRAM | 61,987 | 65,536 | 94% | ⚠️ Critical but stable |
**IRAM Note**: 94% is acceptable because:
- ICACHE_FLASH_ATTR applied to 26 functions
- Stable across v1.8 → v1.9.1
- No growth observed in testing
### Performance Metrics
- **Loop time**: <1ms (was 10ms+ before async)
- **Boot to time display**: ~15 seconds
- **WiFi connection**: 5-10 seconds (synchronous in setup)
- **NTP sync interval**: Configurable (default 1 hour)
- **Weather update interval**: Configurable (default 30 minutes)
### Uptime
- ✅ 24+ hours stable
- ✅ No memory leaks
- ✅ No crashes or reboots
- ✅ OTA updates work during operation
---
## Technical Architecture
### Async State Machines
#### Weather State Machine
```cpp
enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED };
```
- Library: AsyncHTTPRequest_Generic v1.13.0
- API: Open-Meteo (free, no API key)
- Callback: `onWeatherResponse()`
- Retry: Exponential backoff (1s → 2s → 4s, max 3 retries)
#### NTP State Machine
```cpp
enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED };
```
- Custom manual UDP packet building/parsing
- Independent epoch tracking: `syncedEpoch`, `syncedMillis`, `timeIsSynced`
- Non-blocking UDP checks via `parsePacket()`
- Timeout: 5 seconds
- Why manual? NTPClient library is inherently blocking
#### WiFi State Machine
```cpp
enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED };
```
**Hybrid model** (critical for v1.9.1):
- **Setup phase**: Synchronous (waits up to 10 seconds)
- Why? OTA, web server, NTP all need WiFi ready
- Prevents "DNS resolution failed" errors
- Ensures time appears on display immediately after WiFi connects
- **Loop phase**: Asynchronous (checks every 5 seconds)
- Why? Don't freeze device if WiFi drops during operation
- Graceful reconnection without user impact
### Configuration Storage
**EEPROM struct (512 bytes, 26 fields):**
```cpp
struct Config {
char ssid[32]; // WiFi network name
char password[64]; // WiFi password
int timezone_offset; // Seconds from UTC
bool dst_enabled; // Auto DST (European rules)
uint8_t brightness; // Display brightness (0-7)
char ntp_server[64]; // NTP server address
unsigned long ntp_interval; // NTP sync interval (seconds)
bool hour_format_24; // 24h vs 12h display
char hostname[32]; // mDNS hostname
float latitude; // Weather location
float longitude; // Weather location
char city_name[32]; // Display in weather mode
bool weather_enabled; // Feature toggle
unsigned long weather_interval; // Update interval (seconds)
unsigned long display_rotation_sec; // Mode switch interval
bool show_weather; // Enable weather mode
bool show_sunrise_sunset; // Enable sunrise/sunset mode
uint8_t display_orientation; // Screen rotation (0-3)
uint32_t magic; // 0xC10CC10C - validation
};
```
**Validation**: Magic number check prevents loading corrupted EEPROM data.
### Display Modes
**Mode 1: Time Mode**
- Large HH:MM display
- Blinking colon (500ms interval)
- Day of week + date
- 24/12 hour format support
**Mode 2: Weather Mode**
- Temperature with superscript °c
- City name at bottom
- Example: "15.4°c" + "Portimao"
**Mode 3: Sunrise/Sunset Mode**
- Sunrise time with ↑ arrow
- Sunset time with ↓ arrow
- Daylight duration (e.g., "Day 9h 41m")
- Calculation: sunset - sunrise = total daylight minutes
**Rotation**: Configurable interval (default 5 seconds), gracefully skips disabled modes.
### Memory Optimization Techniques
**1. ICACHE_FLASH_ATTR**
Applied to 26 functions to move code from IRAM to Flash:
- All web handlers (15 functions)
- Setup functions (5 functions)
- Utilities (6 functions)
Result: IRAM usage manageable at 94%
**2. Chunked HTTP Responses**
```cpp
// ❌ BAD - 140+ concatenations
String html = "";
html += F("<!DOCTYPE html>");
html += F("<head>...");
// ✅ GOOD - Chunked transfer
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", "");
server.sendContent_P(HTML_HEADER);
server.sendContent_P(HTML_FOOTER);
server.sendContent("");
```
Result: ~8KB peak heap reduction
**3. Fixed-Size Buffers**
No dynamic String allocations in loops:
```cpp
char buf[150];
snprintf_P(buf, sizeof(buf), PSTR("<div>IP: %s</div>"),
WiFi.localIP().toString().c_str());
```
---
## Security Issues Fixed
### Original Firmware Vulnerabilities
**1. WiFi Password Leak (CRITICAL)**
- Open access point remained active after setup
- Web interface displayed WiFi password in plaintext
- Anyone within range could connect and read password
- **Impact**: Full network compromise
**2. Cloud Dependency**
- Required QWeather API (Chinese service)
- Needed account registration + API key
- Unknown data collection practices
- **Impact**: Privacy concerns, vendor lock-in
**3. No OTA Updates**
- Required physical FTDI connection for updates
- Difficult for non-technical users
- **Impact**: Security vulnerabilities can't be patched remotely
**4. Hardcoded Credentials**
- Default WiFi credentials in source code
- No secure setup flow
- **Impact**: Easy attack vector
### Custom Firmware Solutions
**1. WiFiManager Integration ✅**
- Captive portal for secure first-time setup
- AP automatically closes after 180 seconds
- Fallback AP only on connection failure
- Password-protected fallback (customizable)
**2. Open-Meteo API ✅**
- Free weather service
- No registration required
- No API key needed
- European service (GDPR compliant)
**3. OTA Updates ✅**
- Web-based upload at `/update`
- ArduinoOTA for IDE uploads
- Password-protected (admin/admin - customizable)
- Non-blocking during operation
**4. No Hardcoded Secrets ✅**
- All credentials stored in EEPROM
- Magic number validation
- Factory reset capability
- Configuration import/export
---
## Files & Structure
### Project Directory
**Location**: `/Users/apetrochenko/Library/Mobile Documents/com~apple~CloudDocs/src/arduino/clock/esp8266-weather-clock-opensource`
### Key Files
**Root Level:**
- `README.md` (26KB) - Main documentation (blog-style)
- `LICENSE` - MIT License
- `CHANGELOG.md` - Version history
- `CONTRIBUTING.md` - Contribution guidelines
- `PROJECT_STRUCTURE.md` - Directory layout
- `PROJECT_CONTEXT.md` - This file
- `PUBLISH_TO_GITHUB.md` - GitHub publishing guide
- `.gitignore` - Git exclusions
**Source Code:**
- `src/clock_ntp_ota_v1.9.ino` (65KB, 2,096 lines)
**Documentation:**
- `docs/INSTALLATION.md` (18KB) - Complete installation guide
- `docs/HARDWARE.md` (8KB) - Hardware specs + pinout
- `docs/v1.9_RELEASE_NOTES.md` (7KB) - v1.9.0 changelog
- `docs/v1.9.1_HYBRID_FIX.md` (6KB, Russian) - v1.9.1 fix explanation
**Images:**
- `images/product/` - 6 AliExpress product photos
- 01-main-product.webp
- 02-components.webp
- 03-weather-forecast.webp
- 04-temperature-display.webp
- 05-details.webp
- 06-size.webp
- `images/build/` - 3 display screenshots
- display-time.png
- display-temperature.png
- display-sunrise-sunset.png
**GitHub Config:**
- `.github/workflows/build.yml` - CI/CD pipeline
- `.github/ISSUE_TEMPLATE/bug_report.md`
- `.github/ISSUE_TEMPLATE/feature_request.md`
### Compiled Artifacts (not in git)
```
build/
├── clock_ntp_ota_v1.9.ino.bin (409KB) - Flash this via OTA
├── clock_ntp_ota_v1.9.ino.elf - Debug symbols
└── clock_ntp_ota_v1.9.ino.map - Memory map
```
---
## Git Repository
### GitHub Details
- **Repository**: https://github.com/petrochen/esp8266-weather-clock-opensource
- **Owner**: petrochen (Andrey Petrochenko)
- **Visibility**: Public
- **License**: MIT
- **Created**: 2026-01-03
### Repository Configuration
**Enabled Features:**
- ✅ Issues
- ✅ Discussions
- ✅ GitHub Actions (CI/CD)
- ✅ Releases
**Topics (Tags):**
- `esp8266`, `arduino`, `iot`, `weather-station`
- `reverse-engineering`, `security`, `oled-display`
- `ntp`, `ota-updates`, `open-meteo`
**Badges:**
- Release version (auto-updates)
- License (MIT)
- Build status (CI/CD)
- Open issues count
- Hardware (ESP-01S)
- Status (Production Ready)
### Current Release
**Tag**: v1.9.1
**URL**: https://github.com/petrochen/esp8266-weather-clock-opensource/releases/tag/v1.9.1
**Status**: Production Ready
**Created**: 2026-01-03
### Git Commits
**Total**: 3 commits
1. `8f0df02` - Initial commit: v1.9.1 production firmware
2. `f2dc2fb` - Add dynamic GitHub badges to README
3. `2205445` - Update price: $12 → €5
### Web Interface URLs
- **Main**: https://github.com/petrochen/esp8266-weather-clock-opensource
- **Issues**: https://github.com/petrochen/esp8266-weather-clock-opensource/issues
- **Discussions**: https://github.com/petrochen/esp8266-weather-clock-opensource/discussions
- **Actions**: https://github.com/petrochen/esp8266-weather-clock-opensource/actions
- **Releases**: https://github.com/petrochen/esp8266-weather-clock-opensource/releases
---
## Next Steps
### Planned Features (v1.10 or v2.0)
**1. Home Assistant Integration (High Priority)**
User specifically wants custom display screens pulling data from Home Assistant.
**Implementation ideas:**
- Add REST API client to fetch HA sensor data
- New display modes:
- Energy usage dashboard
- Room temperatures (multiple sensors)
- Air quality / CO2 levels
- Automation states (alarm, doors, lights)
- Configuration: HA server URL, access token, entity IDs
- Update interval: configurable (default 30 seconds)
**Technical approach:**
- Use AsyncHTTPRequest for non-blocking HA API calls
- JSON parsing with ArduinoJson library
- Store HA config in EEPROM (new fields)
- New web UI section for HA configuration
**2. MQTT Support**
- Publish time/weather data to MQTT broker
- Subscribe to topics for display content
- Enable automation triggers
- Library: PubSubClient (async wrapper needed)
**3. WebSocket Live Updates**
- Replace polling with WebSocket
- Real-time config changes
- Live display preview in web UI
- Push notifications for updates
**4. Multiple Weather Locations**
- Store 2-3 favorite locations
- Rotate between them
- Useful for travelers or multiple homes
**5. Display Animations**
- Smooth transitions between modes
- Weather icons (sunny, cloudy, rainy)
- Sunrise/sunset animations
### Code Quality Improvements
**1. Modular Architecture (v2.0)**
Split monolith (2,096 lines) into modules:
```
src/
├── main.ino
├── config.h
├── display.cpp/h
├── network.cpp/h
├── weather.cpp/h
├── webserver.cpp/h
└── home_assistant.cpp/h (new)
```
**2. ArduinoJson Integration**
Replace manual JSON parsing (173 lines) with library:
- Cleaner code
- Better error handling
- Type safety
**3. Constants Organization**
Eliminate magic numbers:
```cpp
namespace Hardware {
constexpr uint8_t I2C_SDA_PIN = 0;
constexpr uint8_t I2C_SCL_PIN = 2;
}
```
**4. Unit Tests**
- Test state machines
- Test JSON parsing
- Test time calculations
- Mock network calls
### Documentation Improvements
**1. Video Tutorial**
- YouTube walkthrough
- FTDI connection demo
- OTA update demo
- Web configuration walkthrough
**2. Troubleshooting Flow Chart**
Visual guide for common issues:
- Display not working
- WiFi not connecting
- Time not syncing
- Weather not updating
**3. Localization**
Translate docs to:
- Russian (v1.9.1_HYBRID_FIX.md already in Russian)
- Spanish
- Portuguese
**4. Home Assistant Integration Guide**
Complete guide when HA support is added.
### Community Engagement
**1. Reddit Posts**
Subreddits to share on:
- r/esp8266
- r/arduino
- r/selfhosted
- r/homeassistant (after HA integration)
- r/ReverseEngineering
**2. Hackaday**
Submit project tip: https://hackaday.com/submit-a-tip/
**3. Hackster.io**
Create full project page with build guide.
**4. Awesome Lists**
Add to "awesome ESP8266" and "awesome IoT" lists.
---
## Key Learnings
### Hardware
1. **Always verify pinouts** - Don't assume standard mappings
2. **FTDI is essential** - $2 adapter unlocks any ESP8266 device
3. **Read PCB markings** - Model numbers save hours of guessing
4. **Test voltage** - ESP8266 is NOT 5V tolerant
5. **Document discoveries** - Pin mappings, I2C addresses, display models
### Software
1. **Async is hard but worth it** - Fully non-blocking eliminates freezes
2. **IRAM is precious** - Use ICACHE_FLASH_ATTR liberally on ESP8266
3. **Hybrid approaches work** - Don't be dogmatic (sync WiFi in setup() was correct)
4. **State machines scale** - Better than callback hell for complex async
5. **Test on real hardware** - Emulators miss pin issues and memory constraints
### Security
1. **IoT security is often terrible** - Always audit before trusting
2. **Open source is safer** - Closed firmware is a black box
3. **Defaults matter** - Insecure defaults (open AP, plaintext passwords) are vulnerabilities
4. **Defense in depth** - Multiple layers catch mistakes
5. **Update mechanism is critical** - OTA enables security patches
### Development
1. **OTA from day 1** - FTDI flashing gets old fast
2. **Version control** - Backups (.bak, .bak2) saved the project multiple times
3. **Document as you go** - Release notes prevent "what was I thinking?" moments
4. **Incremental improvements** - v1.7 → v1.8 → v1.9.x made debugging manageable
5. **User testing** - Photos from user revealed display cutoff issues
### Project Management
1. **Understand user needs** - User wanted Home Assistant integration (plan for it)
2. **Security first** - Privacy/security was main motivation
3. **Performance matters** - 10ms loop → <1ms dramatically improves UX
4. **Documentation is product** - Good docs = more users = more contributors
5. **Publish early** - GitHub repo enables community contributions
---
## Quick Reference
### Current Device Configuration
**Hardware:**
- Device: TJ-56-654 Weather Clock
- Location: User's home network
- IP: 192.168.2.47
- Hostname: tj56654-clock.local
**Software:**
- Firmware: v1.9.1
- WiFi: SibWings
- Weather: Portimao, Portugal (37.19°N, 8.54°W)
- Timezone: UTC+0 (Lisbon) with auto DST
- NTP: pool.ntp.org (1 hour interval)
**Access:**
- Web UI: http://192.168.2.47 or http://tj56654-clock.local
- OTA Update: http://192.168.2.47/update (admin/admin)
- API Base: http://192.168.2.47/api/
### Important Commands
**Compile:**
```bash
arduino-cli compile --fqbn esp8266:esp8266:generic \
src/clock_ntp_ota_v1.9.ino
```
**Upload OTA:**
```bash
curl -u admin:admin \
-F "file=@build/clock_ntp_ota_v1.9.ino.bin" \
http://192.168.2.47/update
```
**Check status:**
```bash
curl http://192.168.2.47/api/status | jq
```
**View logs:**
```bash
# Connect via serial (if FTDI attached)
screen /dev/cu.usbserial* 115200
```
### Library Dependencies
Required libraries (install via Arduino Library Manager):
| Library | Version | Purpose |
|---------|---------|---------|
| Adafruit GFX Library | 1.11.0+ | Graphics primitives |
| Adafruit SSD1306 | 2.5.0+ | OLED display driver |
| NTPClient | 3.2.0+ | NTP time sync (base) |
| WiFiManager | 2.0.0+ | Captive portal |
| AsyncHTTPRequest_Generic | 1.13.0+ | Async weather fetch |
| ESPAsyncTCP | 1.2.2+ | Async TCP layer |
### External APIs
**Open-Meteo:**
- URL: https://api.open-meteo.com/v1/forecast
- Authentication: None (free, no API key)
- Rate limit: None (reasonable use)
- Documentation: https://open-meteo.com/en/docs
**NTP:**
- Default: pool.ntp.org
- Protocol: UDP port 123
- Fallbacks: time.google.com, time.cloudflare.com
---
## Contact & Collaboration
**GitHub**: https://github.com/petrochen/esp8266-weather-clock-opensource
**Issues**: https://github.com/petrochen/esp8266-weather-clock-opensource/issues
**Discussions**: https://github.com/petrochen/esp8266-weather-clock-opensource/discussions
**Contributions welcome!** See [CONTRIBUTING.md](CONTRIBUTING.md)
---
## Summary
This project successfully transformed a €5 AliExpress IoT device with critical security vulnerabilities into a fully secure, high-performance, feature-rich smart clock with open-source firmware.
**Key achievements:**
- ✅ Eliminated WiFi password leak vulnerability
- ✅ Achieved <1ms loop time (10x performance improvement)
- ✅ Enabled OTA updates (no more FTDI wiring)
- ✅ Free weather API (no registration)
- ✅ Full async architecture (zero blocking)
- ✅ Production-ready stability (24/7 uptime)
- ✅ Open-source on GitHub (MIT license)
- ✅ Comprehensive documentation (100KB+ docs)
**Next milestone**: Home Assistant integration for custom display screens.
**Status**: Project complete and ready for community contributions! 🚀
---
**Last session work (2026-01-03):**
1. ✅ Compiled v1.9.1 with daylight duration feature
2. ✅ Uploaded via OTA to device (192.168.2.47)
3. ✅ Created complete GitHub repository structure
4. ✅ Published to https://github.com/petrochen/esp8266-weather-clock-opensource
5. ✅ Created release v1.9.1
6. ✅ Added badges, topics, documentation
7. ✅ Fixed price ($12 → €5)
8. ✅ Saved full project context
**Repository ready for sharing on Reddit, Hackaday, and other communities!**
-249
View File
@@ -1,249 +0,0 @@
# Project Structure
This document describes the organization of the ESP8266 Weather Clock firmware repository.
## Directory Layout
```
esp8266-weather-clock-opensource/
│
├── README.md # Main documentation (start here!)
├── LICENSE # MIT License
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Contribution guidelines
├── PROJECT_STRUCTURE.md # This file
├── .gitignore # Git exclusions
│
├── src/ # Source code
│ └── clock_ntp_ota_v1.9.ino # Main firmware (2,096 lines)
│
├── docs/ # Documentation
│ ├── INSTALLATION.md # Complete installation guide
│ ├── HARDWARE.md # Hardware specs and pinout
│ ├── v1.9_RELEASE_NOTES.md # v1.9.0 release notes
│ └── v1.9.1_HYBRID_FIX.md # v1.9.1 WiFi startup fix
│
├── images/ # Photos and screenshots
│ ├── product/ # AliExpress product photos
│ │ ├── 01-main-product.webp # Main product shot
│ │ ├── 02-components.webp # Kit components
│ │ ├── 03-weather-forecast.webp
│ │ ├── 04-temperature-display.webp
│ │ ├── 05-details.webp # Transparent case details
│ │ └── 06-size.webp # Dimensions (40x40x43mm)
│ │
│ └── build/ # Custom firmware screenshots
│ ├── display-time.png # Time display mode
│ ├── display-temperature.png # Weather display mode
│ └── display-sunrise-sunset.png # Solar display mode
│
└── .github/ # GitHub-specific files
├── workflows/
│ └── build.yml # CI: Auto-build on push
│
└── ISSUE_TEMPLATE/
├── bug_report.md # Bug report template
└── feature_request.md # Feature request template
```
## Key Files
### Root Level
**README.md** (11KB)
- Main project documentation
- Blog-style narrative about reverse engineering
- Security issues discovered
- Complete feature list
- Installation quickstart
- API documentation
**LICENSE** (MIT)
- Permissive open source license
- Use freely, modify, distribute
**CHANGELOG.md**
- Version history: v1.5 → v1.9.1
- Features, fixes, breaking changes
- Migration notes
**CONTRIBUTING.md**
- How to contribute
- Code style guidelines
- Testing requirements
### Source Code (`/src`)
**clock_ntp_ota_v1.9.ino**
- Main firmware file (2,096 lines)
- ESP8266 Arduino sketch
- Requires libraries:
- Adafruit GFX & SSD1306
- NTPClient
- WiFiManager
- AsyncHTTPRequest_Generic
- ESPAsyncTCP
**Architecture:**
- Fully async (zero blocking in loop)
- State machines: WiFi, NTP, Weather
- Hybrid model: sync WiFi in setup(), async in loop()
- Memory-optimized: ICACHE_FLASH_ATTR on 26 functions
**Configuration:**
- 26-field struct stored in EEPROM
- Magic number validation
- Web-based config UI
### Documentation (`/docs`)
**INSTALLATION.md** (18KB)
- Complete step-by-step installation guide
- Arduino IDE setup
- FTDI wiring diagrams
- OTA update instructions
- Comprehensive troubleshooting
**HARDWARE.md** (8KB)
- ESP-01S specifications
- Pin mapping (SDA=GPIO0, SCL=GPIO2)
- Display module details (GM009605v4.3)
- Power requirements
- Memory layout
- Safety warnings
**v1.9_RELEASE_NOTES.md** (7KB)
- Detailed v1.9.0 changelog
- Performance improvements
- Async architecture explanation
- Memory usage comparison
- Testing checklist
**v1.9.1_HYBRID_FIX.md** (Russian, 6KB)
- Critical startup fix documentation
- WiFi synchronous vs async tradeoffs
- Timeline diagrams
- Before/after comparison
### Images (`/images`)
**Product Photos** (`/product`)
- Original AliExpress product images
- DIY kit components
- Transparent acrylic case
- Size reference (40mm cube)
**Build Photos** (`/build`)
- Custom firmware screenshots
- Three display modes:
1. Time mode (10:34 + date)
2. Weather mode (15.4°c + city)
3. Sunrise/sunset mode (times + daylight duration)
### GitHub Config (`/.github`)
**Workflows**
- `build.yml`: CI pipeline
- Auto-compile on push
- Check firmware size < 470KB
- Upload build artifacts
- Attach binaries to releases
**Issue Templates**
- `bug_report.md`: Structured bug reports
- `feature_request.md`: Feature suggestions
## Build Artifacts (ignored by git)
When you compile locally, these are created:
```
build/
├── clock_ntp_ota_v1.9.ino.bin # Flash this via OTA
├── clock_ntp_ota_v1.9.ino.elf # Debug symbols
└── clock_ntp_ota_v1.9.ino.map # Memory map
```
**Note**: `build/` is in `.gitignore` - artifacts not committed to repo.
## File Sizes
| File | Size | Description |
|------|------|-------------|
| `src/*.ino` | 65KB | Main source code |
| `build/*.bin` | 409KB | Compiled firmware |
| `README.md` | 45KB | Main docs |
| `docs/INSTALLATION.md` | 18KB | Install guide |
| `docs/HARDWARE.md` | 8KB | Hardware specs |
## Memory Usage
**Compiled firmware (v1.9.1):**
- Flash: 408,844 / 1,048,576 bytes (38%)
- RAM: 37,644 / 80,192 bytes (46%)
- IRAM: 61,987 / 65,536 bytes (94%) ⚠️
**Why 94% IRAM is acceptable:**
- ICACHE_FLASH_ATTR applied to all web handlers
- Stable across versions v1.8-v1.9.1
- No IRAM growth observed in testing
## Version Control
**Branches:**
- `main`: Stable releases (v1.9.1)
- `develop`: Work-in-progress features
- `feature/*`: New feature branches
**Tags:**
- `v1.9.1`: Current production release
- `v1.9.0`: Async refactoring
- `v1.8.0`: Security + stability fixes
- `v1.7.0`: Initial working firmware
## Not Included (Why)
**What's NOT in this repo:**
- Build artifacts (`.bin`, `.elf`, `.map`) - generated locally
- Backup files (`.bak`, `.bak2`) - development artifacts
- IDE configs (`.vscode/`, `.idea/`) - personal preferences
- macOS metadata (`.DS_Store`) - system files
- Secrets (`config_local.h`) - would leak credentials
These are excluded via `.gitignore`.
## How to Navigate
**For users:**
1. Start with `README.md` (overview + quickstart)
2. Follow `docs/INSTALLATION.md` (step-by-step setup)
3. Check `CHANGELOG.md` (version history)
**For developers:**
1. Read `CONTRIBUTING.md` (guidelines)
2. Study `src/clock_ntp_ota_v1.9.ino` (source code)
3. Review `docs/HARDWARE.md` (hardware constraints)
4. Check `.github/workflows/build.yml` (CI setup)
**For hardware hackers:**
1. Check `docs/HARDWARE.md` (pinout, specs)
2. View `images/product/` (original device photos)
3. Read `README.md` section "Hardware Discovery"
**For troubleshooters:**
1. Open `docs/INSTALLATION.md`
2. Jump to "Troubleshooting" section
3. Check `images/build/` for reference screenshots
## Quick Links
- **Main docs**: [README.md](README.md)
- **Install guide**: [docs/INSTALLATION.md](docs/INSTALLATION.md)
- **Hardware specs**: [docs/HARDWARE.md](docs/HARDWARE.md)
- **Changelog**: [CHANGELOG.md](CHANGELOG.md)
- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md)
---
**Last updated**: 2026-01-03
**Repository**: https://github.com/your-username/esp8266-weather-clock-opensource
-440
View File
@@ -1,440 +0,0 @@
# Publishing to GitHub - Step by Step Guide
This file contains instructions for publishing this project to GitHub.
## Prerequisites
1. **GitHub account** - Sign up at https://github.com if you don't have one
2. **Git installed** - Check with `git --version` in terminal
3. **GitHub CLI (optional)** - Makes repository creation easier: https://cli.github.com/
---
## Option 1: Using GitHub Web Interface (Easiest)
### Step 1: Create Repository on GitHub
1. Go to https://github.com/new
2. Fill in:
- **Repository name**: `esp8266-weather-clock-opensource`
- **Description**: `Secure open-source firmware for ESP8266 weather clock - reverse engineered from AliExpress DIY kit`
- **Visibility**: Public ✅
- **Initialize**: ❌ Do NOT check "Add README" (we have one)
3. Click: **Create repository**
### Step 2: Initialize Local Git Repository
Open terminal and navigate to project directory:
```bash
cd "/Users/apetrochenko/Library/Mobile Documents/com~apple~CloudDocs/src/arduino/clock/esp8266-weather-clock-opensource"
```
Initialize git and add files:
```bash
# Initialize git
git init
# Add all files
git add .
# Create first commit
git commit -m "Initial commit: v1.9.1 production firmware
- Complete reverse engineering of TJ-56-654 weather clock
- Fixes security issues (WiFi password leak)
- Fully async architecture (zero blocking)
- OTA updates, web interface, REST API
- Open-Meteo weather (free, no API key)
- Comprehensive documentation"
```
### Step 3: Connect to GitHub
Replace `YOUR_USERNAME` with your actual GitHub username:
```bash
# Add remote
git remote add origin https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource.git
# Set main branch
git branch -M main
# Push to GitHub
git push -u origin main
```
**If prompted for credentials:**
- Username: Your GitHub username
- Password: Use **Personal Access Token** (not your password!)
- Create token at: https://github.com/settings/tokens
- Select scopes: `repo` (full control of private repositories)
### Step 4: Verify Upload
1. Browse to: `https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource`
2. You should see:
- README.md rendered nicely
- All directories and files
- First commit visible
---
## Option 2: Using GitHub CLI (Faster)
If you have GitHub CLI installed:
```bash
# Navigate to project
cd "/Users/apetrochenko/Library/Mobile Documents/com~apple~CloudDocs/src/arduino/clock/esp8266-weather-clock-opensource"
# Authenticate (one-time)
gh auth login
# Create repo and push in one command
gh repo create esp8266-weather-clock-opensource \
--public \
--source=. \
--description="Secure open-source firmware for ESP8266 weather clock" \
--push
```
Done! Repository is created and pushed.
---
## Step 5: Configure Repository Settings
### Add Topics (Tags)
1. Go to your repo on GitHub
2. Click: **⚙️ Settings** (top right near About)
3. Under "Topics", add:
- `esp8266`
- `arduino`
- `iot`
- `weather-station`
- `reverse-engineering`
- `security`
- `oled-display`
- `ntp`
- `ota-updates`
- `open-meteo`
### Update About Section
1. Go to repo main page
2. Click: **⚙️** (gear icon) next to "About"
3. Set:
- **Description**: `Secure open-source firmware for ESP8266 weather clock - reverse engineered from AliExpress DIY kit to fix security flaws`
- **Website**: `https://open-meteo.com` (or your personal site if you blog about it)
- **Topics**: Should already be set from above
### Enable Features
In **Settings → General**:
**Features**:
- ✅ Issues (for bug reports)
- ✅ Discussions (for questions)
- ❌ Wiki (not needed, we have docs/)
- ❌ Projects (not needed yet)
**Pull Requests**:
- ✅ Allow squash merging
- ✅ Automatically delete head branches
### Set Up GitHub Actions
The CI workflow should activate automatically on first push. Check:
1. Go to: **Actions** tab
2. You should see: "Build Firmware" workflow
3. It should run and ✅ pass (compiles firmware)
If it fails:
- Check library names in `.github/workflows/build.yml`
- Some libraries may need exact version pinning
---
## Step 6: Create First Release
### Tag the Release Locally
```bash
# Create annotated tag
git tag -a v1.9.1 -m "Release v1.9.1: Production-ready firmware
Features:
- Hybrid WiFi model (sync on boot, async in loop)
- Daylight duration display
- Fully async NTP, weather, WiFi reconnect
- OTA updates, web interface, REST API
- Open-Meteo weather (free API)
- Security fixes (no WiFi password leak)
Fixes:
- Startup display blank for 10+ seconds
- DNS resolution failed errors
- Sunrise/sunset label cutoff"
# Push tag to GitHub
git push origin v1.9.1
```
### Create Release on GitHub
1. Go to: **Releases** (right sidebar)
2. Click: **Draft a new release**
3. Fill in:
- **Tag**: `v1.9.1` (should appear in dropdown)
- **Release title**: `v1.9.1 - Production Ready`
- **Description**:
```markdown
## 🎉 First Public Release
Secure, open-source replacement firmware for ESP8266 weather clocks.
### ✨ Highlights
- **Security**: Fixes WiFi password leak in original firmware
- **Performance**: Fully async architecture, <1ms loop time
- **Features**: OTA updates, web UI, REST API, NTP time, weather
- **Free API**: Uses Open-Meteo (no registration required)
### 📦 Downloads
- `esp8266-weather-clock-v1.9.1.bin` - Flash this via OTA or FTDI
### 📖 Documentation
- [Installation Guide](docs/INSTALLATION.md)
- [Hardware Specs](docs/HARDWARE.md)
- [Full Changelog](CHANGELOG.md)
### 🚀 Quick Start
1. Download `.bin` file
2. Flash via FTDI (first time) or OTA (updates)
3. Connect to `TJ56654-Setup` WiFi
4. Configure your network
5. Access web UI at `http://tj56654-clock.local`
See [README](README.md) for complete instructions.
### 🐛 Known Issues
None! This release is production-ready and tested 24/7.
```
4. **Attach binary** (if you have it locally):
- Compile firmware first: Arduino IDE → Sketch → Export Compiled Binary
- Or use GitHub Actions artifact
- Drag `build/clock_ntp_ota_v1.9.ino.bin` to release assets
- Rename to: `esp8266-weather-clock-v1.9.1.bin`
5. Click: **Publish release**
---
## Step 7: Add Shields/Badges to README
Edit `README.md` and add at the top (after title):
```markdown
<p align="center">
<a href="https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource/releases">
<img src="https://img.shields.io/github/v/release/YOUR_USERNAME/esp8266-weather-clock-opensource?style=flat-square" alt="Release">
</a>
<a href="https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/YOUR_USERNAME/esp8266-weather-clock-opensource?style=flat-square" alt="License">
</a>
<a href="https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource/actions">
<img src="https://img.shields.io/github/actions/workflow/status/YOUR_USERNAME/esp8266-weather-clock-opensource/build.yml?style=flat-square" alt="Build">
</a>
<a href="https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource/issues">
<img src="https://img.shields.io/github/issues/YOUR_USERNAME/esp8266-weather-clock-opensource?style=flat-square" alt="Issues">
</a>
</p>
```
Replace `YOUR_USERNAME` with actual username.
Commit and push:
```bash
git add README.md
git commit -m "Add badges to README"
git push
```
---
## Step 8: Share Your Project
### Post on Social Media
**Reddit:**
- r/esp8266
- r/arduino
- r/selfhosted
- r/homeassistant (when you add HA integration)
**Hackaday:**
- Submit project tip: https://hackaday.com/submit-a-tip/
**Hackster.io:**
- Create project page: https://www.hackster.io/
**Twitter/X:**
```
Just reverse-engineered a $12 AliExpress weather clock and found it was leaking WiFi passwords!
Replaced the firmware with secure open-source version:
- ✅ No password leak
- ✅ OTA updates
- ✅ Free weather API
- ✅ Full async arch
Check it out: [your-repo-link]
#ESP8266 #IoTSecurity #Arduino
```
### Add to Awesome Lists
Search for "awesome ESP8266" and submit PR to add your project.
---
## Maintenance Tips
### Keep README Updated
When you add features:
1. Update README.md
2. Update CHANGELOG.md
3. Create new git tag
4. Create GitHub release
### Respond to Issues
Enable email notifications:
1. Go to: repo → **Watch** → **Custom**
2. Check: ✅ Issues, ✅ Pull requests, ✅ Discussions
### Version Numbering
Use semantic versioning (semver.org):
- `v2.0.0`: Breaking changes (incompatible config)
- `v1.10.0`: New features (backward-compatible)
- `v1.9.2`: Bug fixes only
### Automated Releases
GitHub Actions can auto-build on new tags. Check `.github/workflows/build.yml`.
---
## Troubleshooting
### "Permission denied" when pushing
**Solution**: Use Personal Access Token instead of password
1. Generate: https://github.com/settings/tokens
2. Scopes: `repo` (full control)
3. Use token as password when prompted
Or configure SSH keys:
```bash
# Generate SSH key
ssh-keygen -t ed25519 -C "your_email@example.com"
# Add to GitHub: Settings → SSH Keys → New SSH key
# Paste contents of ~/.ssh/id_ed25519.pub
# Change remote to SSH
git remote set-url origin git@github.com:YOUR_USERNAME/esp8266-weather-clock-opensource.git
```
### "This repository is empty"
You forgot to push:
```bash
git push -u origin main
```
### Files too large
GitHub has 100MB file size limit. If you accidentally added build artifacts:
```bash
# Remove from staging
git reset HEAD build/
# Add to .gitignore
echo "build/" >> .gitignore
# Commit
git commit -m "Ignore build artifacts"
```
### CI build fails
Check:
- Library names are correct in `build.yml`
- All libraries are available via Arduino Library Manager
- Firmware compiles locally first
---
## Next Steps After Publishing
1. **Star your own repo** (to make it discoverable)
2. **Watch releases** (be notified of activity)
3. **Enable Discussions** (for community Q&A)
4. **Create SECURITY.md** (if you want responsible disclosure process)
5. **Add funding links** (GitHub Sponsors, Buy Me a Coffee, etc.)
---
## GitHub Repository Best Practices
### Essential Files (✅ You have these!)
- ✅ README.md
- ✅ LICENSE
- ✅ CONTRIBUTING.md
- ✅ CHANGELOG.md
- ✅ .gitignore
- ✅ Issue templates
### Nice-to-Have
- CODE_OF_CONDUCT.md (for community standards)
- SECURITY.md (vulnerability disclosure policy)
- FUNDING.yml (donation links)
### Pin Important Files
On your repo page, pin:
1. README.md (auto-pinned)
2. INSTALLATION.md (pin in About section)
3. Latest release (pin in sidebar)
---
## Success Checklist
After publishing, verify:
- [ ] Repository is public and accessible
- [ ] README renders correctly (images, links work)
- [ ] All documentation files are present
- [ ] CI/CD pipeline passes (green checkmark)
- [ ] First release is tagged and published
- [ ] Binary is attached to release
- [ ] Topics/tags are set
- [ ] License is visible
- [ ] Issues and Discussions are enabled
---
**Congratulations!** Your project is now public and ready to help the world build secure IoT devices. 🚀
---
**Repository URL**: https://github.com/YOUR_USERNAME/esp8266-weather-clock-opensource
Don't forget to replace `YOUR_USERNAME` with your actual GitHub username!
+135 -62
View File
@@ -27,7 +27,7 @@ I bought a cute weather clock kit from AliExpress ([TJ-56-654](https://pt.aliexp
- [The Investigation](#the-investigation) - [The Investigation](#the-investigation)
- [The Solution: Custom Firmware](#the-solution-custom-firmware) - [The Solution: Custom Firmware](#the-solution-custom-firmware)
- [Technical Deep Dive](#technical-deep-dive) - [Technical Deep Dive](#technical-deep-dive)
- [The Journey: v1.7 → v1.9.2](#the-journey-v17--v192) - [The Journey: v1.7 → v1.9.4](#the-journey-v17--v194)
- [What's Next: Home Assistant Integration](#whats-next-home-assistant-integration) - [What's Next: Home Assistant Integration](#whats-next-home-assistant-integration)
- [How to Flash This Firmware](#how-to-flash-this-firmware) - [How to Flash This Firmware](#how-to-flash-this-firmware)
- [Web Interface](#web-interface) - [Web Interface](#web-interface)
@@ -60,6 +60,7 @@ When you first set up the device, it creates an access point with a default pass
5. **Your WiFi password is displayed in plaintext on the config page** 5. **Your WiFi password is displayed in plaintext on the config page**
Anyone within WiFi range could: Anyone within WiFi range could:
- Connect to the device's AP (weak default password) - Connect to the device's AP (weak default password)
- Browse to 192.168.4.1 - Browse to 192.168.4.1
- Read your WiFi password in plaintext - Read your WiFi password in plaintext
@@ -78,13 +79,13 @@ This is a textbook example of poor IoT security design. No thanks.
### Original Hardware Specifications ### Original Hardware Specifications
| Component | Details | | Component | Details |
|-----------|---------| | ----------- | ---------------------------------------- |
| **MCU** | ESP-01S (ESP8266EX, 1MB flash, 80KB RAM) | | **MCU** | ESP-01S (ESP8266EX, 1MB flash, 80KB RAM) |
| **Display** | GM009605v4.3 OLED (128x64, I2C) | | **Display** | GM009605v4.3 OLED (128x64, I2C) |
| **Power** | 5V USB (Micro-USB) | | **Power** | 5V USB (Micro-USB) |
| **Case** | Transparent acrylic (40x40x43mm) | | **Case** | Transparent acrylic (40x40x43mm) |
| **PCB** | TJ-56-654 main board | | **PCB** | TJ-56-654 main board |
### What It Came With ### What It Came With
@@ -119,6 +120,7 @@ The transparent case made inspection easy - just unscrew the brass standoffs. In
- **No additional sensors** (temperature/humidity were from weather API, not local) - **No additional sensors** (temperature/humidity were from weather API, not local)
The ESP-01S pinout is printed right on the PCB: The ESP-01S pinout is printed right on the PCB:
``` ```
3V3 | GND 3V3 | GND
TX | GPIO0 (I2C SDA) TX | GPIO0 (I2C SDA)
@@ -135,6 +137,7 @@ To flash custom firmware, you need:
3. **Steady hands** 3. **Steady hands**
**Wiring:** **Wiring:**
``` ```
FTDI ESP-01S FTDI ESP-01S
──────────────────── ────────────────────
@@ -146,12 +149,14 @@ FTDI ESP-01S
``` ```
**Boot into flash mode:** **Boot into flash mode:**
1. Connect GPIO0 to GND 1. Connect GPIO0 to GND
2. Power on the device 2. Power on the device
3. Remove GPIO0 to GND connection after boot 3. Remove GPIO0 to GND connection after boot
4. Device is now in programming mode 4. Device is now in programming mode
**Programming:** **Programming:**
- Use Arduino IDE with ESP8266 board support - Use Arduino IDE with ESP8266 board support
- Select board: "Generic ESP8266 Module" - Select board: "Generic ESP8266 Module"
- Flash size: 1MB (FS:64KB OTA:~470KB) - Flash size: 1MB (FS:64KB OTA:~470KB)
@@ -176,6 +181,7 @@ I decided to write a complete replacement firmware with:
### Features Implemented ### Features Implemented
#### 🌐 Network & Time #### 🌐 Network & Time
- **WiFiManager** captive portal for secure first-time setup - **WiFiManager** captive portal for secure first-time setup
- **Hybrid WiFi**: Synchronous on boot (ensures proper init), async reconnect during operation - **Hybrid WiFi**: Synchronous on boot (ensures proper init), async reconnect during operation
- **NTP time sync** with configurable server and interval - **NTP time sync** with configurable server and interval
@@ -183,12 +189,14 @@ I decided to write a complete replacement firmware with:
- **mDNS**: Access via `http://tj56654-clock.local/` - **mDNS**: Access via `http://tj56654-clock.local/`
#### 🌦️ Weather Data #### 🌦️ Weather Data
- **Open-Meteo API**: Free, no registration, no API key - **Open-Meteo API**: Free, no registration, no API key
- **Configurable location**: Latitude/longitude + city name - **Configurable location**: Latitude/longitude + city name
- **Data**: Temperature, sunrise, sunset, daylight duration - **Data**: Temperature, sunrise, sunset, daylight duration
- **Smart updates**: Async fetch every 30 minutes (configurable) - **Smart updates**: Async fetch every 30 minutes (configurable)
#### 🔄 OTA Updates #### 🔄 OTA Updates
- **Web-based OTA**: Upload .bin files via browser at `/update` - **Web-based OTA**: Upload .bin files via browser at `/update`
- **ArduinoOTA**: Update directly from Arduino IDE - **ArduinoOTA**: Update directly from Arduino IDE
- **Non-blocking**: System stays responsive during updates - **Non-blocking**: System stays responsive during updates
@@ -245,33 +253,39 @@ All endpoints return JSON:
The firmware uses **zero blocking operations** in the main loop. Everything is state-machine-based: The firmware uses **zero blocking operations** in the main loop. Everything is state-machine-based:
#### Weather State Machine #### Weather State Machine
```cpp ```cpp
enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED }; enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED };
``` ```
Uses `AsyncHTTPRequest` library: Uses `AsyncHTTPRequest` library:
- Non-blocking HTTP requests - Non-blocking HTTP requests
- Callback-based response handling - Callback-based response handling
- Exponential backoff on failures (1s → 2s → 4s) - Exponential backoff on failures (1s → 2s → 4s)
- Maximum 3 retries before giving up - Maximum 3 retries before giving up
#### NTP State Machine #### NTP State Machine
```cpp ```cpp
enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED }; enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED };
``` ```
Custom manual NTP implementation: Custom manual NTP implementation:
- Builds raw UDP packets (48 bytes) - Builds raw UDP packets (48 bytes)
- Non-blocking `parsePacket()` checks - Non-blocking `parsePacket()` checks
- 5-second timeout - 5-second timeout
- Independent epoch tracking for accuracy between syncs - Independent epoch tracking for accuracy between syncs
#### WiFi State Machine #### WiFi State Machine
```cpp ```cpp
enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED }; enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED };
``` ```
**Hybrid model** (this was critical!): **Hybrid model** (this was critical!):
- **Setup phase**: Synchronous connection (waits up to 10 seconds) - **Setup phase**: Synchronous connection (waits up to 10 seconds)
- Why? OTA, web server, NTP all need WiFi ready - Why? OTA, web server, NTP all need WiFi ready
- Without this, device shows blank display for 10+ seconds - Without this, device shows blank display for 10+ seconds
@@ -282,11 +296,11 @@ enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED };
ESP8266 has strict memory limits: ESP8266 has strict memory limits:
| Memory Type | Total | Used | Usage | Status | | Memory Type | Total | Used | Usage | Status |
|-------------|-------|------|-------|--------| | ----------- | --------- | ------- | ------- | ----------- |
| **Flash** | 1,048,576 | 408,844 | 38% | ✅ Plenty | | **Flash** | 1,048,576 | 408,844 | 38% | ✅ Plenty |
| **RAM** | 80,192 | 37,644 | 46% | ✅ Safe | | **RAM** | 80,192 | 37,644 | 46% | ✅ Safe |
| **IRAM** | 65,536 | 61,987 | **94%** | ⚠️ Critical | | **IRAM** | 65,536 | 61,987 | **94%** | ⚠️ Critical |
**IRAM Crisis Solution:** **IRAM Crisis Solution:**
@@ -304,6 +318,7 @@ Applied to 26 functions (web handlers, display, config utilities), reducing IRAM
**String Safety:** **String Safety:**
Avoid String concatenation in loops (causes heap fragmentation): Avoid String concatenation in loops (causes heap fragmentation):
```cpp ```cpp
// ❌ BAD - 140+ concatenations // ❌ BAD - 140+ concatenations
String html = ""; String html = "";
@@ -353,29 +368,34 @@ struct Config {
This took **3 firmware iterations** to get right: This took **3 firmware iterations** to get right:
**v1.5**: Assumed TM1637 (7-segment LED driver) **v1.5**: Assumed TM1637 (7-segment LED driver)
- ❌ Wrong - device has OLED, not 7-segment LEDs - ❌ Wrong - device has OLED, not 7-segment LEDs
**v1.6**: Tried TM1650 (another LED driver) **v1.6**: Tried TM1650 (another LED driver)
- ❌ Wrong - I2C addresses didn't match - ❌ Wrong - I2C addresses didn't match
**v1.7**: Identified GM009605v4.3 (SSD1306-compatible OLED) **v1.7**: Identified GM009605v4.3 (SSD1306-compatible OLED)
- ✅ Correct! Used Adafruit_SSD1306 library - ✅ Correct! Used Adafruit_SSD1306 library
- ✅ Discovered swapped pins: SDA on GPIO0, SCL on GPIO2 - ✅ Discovered swapped pins: SDA on GPIO0, SCL on GPIO2
**Pin mapping quirk:** **Pin mapping quirk:**
Standard ESP8266 I2C uses GPIO4 (SDA) and GPIO5 (SCL), but ESP-01S only exposes GPIO0 and GPIO2. The board designer mapped: Standard ESP8266 I2C uses GPIO4 (SDA) and GPIO5 (SCL), but ESP-01S only exposes GPIO0 and GPIO2. The board designer mapped:
- GPIO0 → SDA (unusual) - GPIO0 → SDA (unusual)
- GPIO2 → SCL (unusual) - GPIO2 → SCL (unusual)
This is **backwards** from typical breakout boards, but works perfectly once configured: This is **backwards** from typical breakout boards, but works perfectly once configured:
```cpp ```cpp
Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2 Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
``` ```
--- ---
## The Journey: v1.7 → v1.9.2 ## The Journey: v1.7 → v1.9.4
### v1.7: Display Discovery ✅ ### v1.7: Display Discovery ✅
@@ -389,6 +409,7 @@ Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
**Goals**: Fix memory issues, eliminate security holes **Goals**: Fix memory issues, eliminate security holes
**Changes:** **Changes:**
- IRAM optimization (added `ICACHE_FLASH_ATTR` to 26 functions) - IRAM optimization (added `ICACHE_FLASH_ATTR` to 26 functions)
- Removed hardcoded WiFi credentials - Removed hardcoded WiFi credentials
- Fixed NTP interval bug (config value was ignored) - Fixed NTP interval bug (config value was ignored)
@@ -403,6 +424,7 @@ Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
**Goals**: Eliminate all blocking operations **Goals**: Eliminate all blocking operations
**Changes:** **Changes:**
- Async HTTP weather fetch (AsyncHTTPRequest library) - Async HTTP weather fetch (AsyncHTTPRequest library)
- Custom async NTP implementation (manual UDP packets) - Custom async NTP implementation (manual UDP packets)
- Async WiFi connection (state machine) - Async WiFi connection (state machine)
@@ -411,12 +433,12 @@ Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
**Performance:** **Performance:**
| Operation | Before (v1.8) | After (v1.9.0) | Improvement | | Operation | Before (v1.8) | After (v1.9.0) | Improvement |
|-----------|---------------|----------------|-------------| | -------------- | -------------- | -------------- | ------------------- |
| Weather fetch | 1-10s blocking | 0ms | ✅ Async callback | | Weather fetch | 1-10s blocking | 0ms | ✅ Async callback |
| NTP sync | 5-20s blocking | 0ms | ✅ Non-blocking UDP | | NTP sync | 5-20s blocking | 0ms | ✅ Non-blocking UDP |
| WiFi reconnect | 15s blocking | 0ms | ✅ State machine | | WiFi reconnect | 15s blocking | 0ms | ✅ State machine |
| Loop time | 10ms minimum | <1ms | ✅ 10x faster | | Loop time | 10ms minimum | <1ms | ✅ 10x faster |
**Result**: Device stays responsive during OTA updates while weather is fetching! **Result**: Device stays responsive during OTA updates while weather is fetching!
@@ -429,6 +451,7 @@ After deploying v1.9.0, the display showed **blank screen for 10 seconds** after
**Root Cause:** **Root Cause:**
Making WiFi fully async broke the **initialization order**: Making WiFi fully async broke the **initialization order**:
```cpp ```cpp
void setup() { void setup() {
setupWiFi(); // Returns immediately (async) setupWiFi(); // Returns immediately (async)
@@ -440,18 +463,19 @@ void setup() {
**Solution: Hybrid Model** **Solution: Hybrid Model**
| Phase | WiFi Mode | Blocking? | Why? | | Phase | WiFi Mode | Blocking? | Why? |
|-------|-----------|-----------|------| | --------- | ------------ | --------- | --------------------------- |
| `setup()` | Synchronous | 10s max | OTA/web/NTP need WiFi ready | | `setup()` | Synchronous | 10s max | OTA/web/NTP need WiFi ready |
| `loop()` | Asynchronous | 0s | Don't freeze on reconnect | | `loop()` | Asynchronous | 0s | Don't freeze on reconnect |
**Results:** **Results:**
- ✅ Display shows time immediately after WiFi connects (~15 sec boot) - ✅ Display shows time immediately after WiFi connects (~15 sec boot)
- ✅ No "DNS resolution failed" errors - ✅ No "DNS resolution failed" errors
- ✅ Proper initialization order guaranteed - ✅ Proper initialization order guaranteed
- ✅ Device never freezes on WiFi loss during operation - ✅ Device never freezes on WiFi loss during operation
### v1.9.2: WiFi Resilience (Current) 🛡️ ### v1.9.2: WiFi Resilience 🛡️
**Problem Discovered:** **Problem Discovered:**
@@ -460,6 +484,7 @@ After WiFi outages, the device would **clear stored credentials** and enter AP m
**Root Cause:** **Root Cause:**
Aggressive credential clearing on connection failure: Aggressive credential clearing on connection failure:
```cpp ```cpp
if (wifiRetry.currentRetry >= wifiRetry.maxRetries) { if (wifiRetry.currentRetry >= wifiRetry.maxRetries) {
memset(config.ssid, 0, sizeof(config.ssid)); // ❌ Clears credentials! memset(config.ssid, 0, sizeof(config.ssid)); // ❌ Clears credentials!
@@ -471,40 +496,43 @@ if (wifiRetry.currentRetry >= wifiRetry.maxRetries) {
**Solution: Resilient WiFi** **Solution: Resilient WiFi**
| Feature | Before (v1.9.1) | After (v1.9.2) | | Feature | Before (v1.9.1) | After (v1.9.2) |
|---------|-----------------|----------------| | ------------------- | -------------------------- | ------------------------------- |
| Credential clearing | After 5 failed attempts | Never | | Credential clearing | After 5 failed attempts | Never |
| Retry strategy | Give up after 5 tries | Infinite with backoff | | Retry strategy | Give up after 5 tries | Infinite with backoff |
| Max retry interval | N/A | 5 minutes | | Max retry interval | N/A | 5 minutes |
| Fallback AP | After clearing credentials | After ~5 min (dual STA+AP mode) | | Fallback AP | After clearing credentials | After ~5 min (dual STA+AP mode) |
| Clock during outage | Blank display | Shows last synced time | | Clock during outage | Blank display | Shows last synced time |
**Key Changes:** **Key Changes:**
- **Never clear credentials** on connection failure - **Never clear credentials** on connection failure
- **Exponential backoff**: 5s → 10s → 20s → ... → 5min max - **Exponential backoff**: 5s → 10s → 20s → ... → 5min max
- **Fallback AP** ("TJ56654-Setup") enabled after ~5 min, while still retrying - **Fallback AP** ("TJ56654-Setup") enabled after ~5 min, while still retrying
- **Dual STA+AP mode**: Device continues reconnect attempts while AP is active - **Dual STA+AP mode**: Device continues reconnect attempts while AP is active
- **SDK credentials support**: Tries WiFiManager-stored credentials first, then EEPROM - **SDK credentials**: Used only on first boot (no saved SSID); subsequent boots go straight to saved credentials
- **"No WiFi" display**: Shows retry countdown instead of cryptic numbers - **"No WiFi" display**: Shows retry countdown instead of cryptic numbers
- **"!" indicator**: Shown in date line when WiFi disconnected - **"!" indicator**: Shown in date line when WiFi disconnected
**Network Activity Summary:** **Network Activity Summary:**
| Service | Interval | Endpoint | Protocol | | Service | Interval | Endpoint | Protocol |
|---------|----------|----------|----------| | ------- | ---------- | ------------------ | ------------- |
| NTP | 1 hour | pool.ntp.org:123 | UDP | | NTP | 1 hour | pool.ntp.org:123 | UDP |
| Weather | 30 min | api.open-meteo.com | HTTP | | Weather | 30 min | api.open-meteo.com | HTTP |
| mDNS | continuous | 224.0.0.251 | UDP multicast | | mDNS | continuous | 224.0.0.251 | UDP multicast |
~50 requests/day total. ~50 requests/day total.
**Results:** **Results:**
- ✅ Credentials persist through WiFi outages - ✅ Credentials persist through WiFi outages
- ✅ Device automatically reconnects when WiFi returns - ✅ Device automatically reconnects when WiFi returns
- ✅ Clock continues running with last synced time - ✅ Clock continues running with last synced time
- ✅ User can reconfigure via fallback AP if needed - ✅ User can reconfigure via fallback AP if needed
**Startup Timeline:** **Startup Timeline:**
``` ```
[0-5s] Display init, startup animation [0-5s] Display init, startup animation
[5-15s] WiFi connection (SYNCHRONOUS in setup()) [5-15s] WiFi connection (SYNCHRONOUS in setup())
@@ -515,15 +543,40 @@ if (wifiRetry.currentRetry >= wifiRetry.maxRetries) {
✅ Time synced and displayed ✅ Time synced and displayed
``` ```
### v1.9.3: Modular Architecture 🗂️
Split monolithic 2,100-line `.ino` into focused modules:
| File | Responsibility |
| ------------------- | --------------------------------------- |
| `weather_clock.ino` | Entry point: `setup()` and `loop()` |
| `config.h` | Config struct, EEPROM layout, constants |
| `globals.h` | Shared state and extern declarations |
| `display.cpp` | OLED rendering |
| `ntp_client.cpp` | Async NTP sync |
| `weather.cpp` | Open-Meteo API fetch |
| `web_server.cpp` | Web UI and REST API |
| `wifi_manager.cpp` | WiFi connection and resilience |
### v1.9.4: Bug Fixes & Cleanup ✅ (Current)
Community-reported bugs fixed:
- **Date timezone** (#5): Date now changes at local midnight, not UTC midnight
- **Weather refresh** (#7): Periodic weather updates no longer blocked after first fetch
- **WiFi hotspot** (#3): Device no longer connects to open SDK-cached networks (e.g. public hotspots) when a saved SSID exists, preventing config corruption
- **ArduinoJson v7**: Updated `StaticJsonDocument` → `JsonDocument` for library compatibility
- **Compiler warnings**: Removed unused variable, fixed sprintf buffer size
### Memory Evolution ### Memory Evolution
| Version | RAM Usage | IRAM Usage | Flash Usage | Notes | | Version | RAM Usage | IRAM Usage | Flash Usage | Notes |
|---------|-----------|------------|-------------|-------| | ------- | ------------ | ---------------- | ------------- | --------------------- |
| v1.7 | 34,980 (43%) | **61,987 (94%)** | 407,500 (38%) | IRAM crisis | | v1.7 | 34,980 (43%) | **61,987 (94%)** | 407,500 (38%) | IRAM crisis |
| v1.8 | 36,980 (46%) | **45,120 (68%)** | 407,800 (38%) | ICACHE_FLASH_ATTR fix | | v1.8 | 36,980 (46%) | **45,120 (68%)** | 407,800 (38%) | ICACHE_FLASH_ATTR fix |
| v1.9.0 | 37,516 (46%) | **61,987 (94%)** | 408,540 (38%) | Async libs added | | v1.9.0 | 37,516 (46%) | **61,987 (94%)** | 408,540 (38%) | Async libs added |
| v1.9.1 | 37,644 (46%) | **61,987 (94%)** | 408,844 (38%) | Hybrid WiFi fix | | v1.9.1 | 37,644 (46%) | **61,987 (94%)** | 408,844 (38%) | Hybrid WiFi fix |
| v1.9.2 | 37,800 (47%) | **61,987 (94%)** | 409,100 (39%) | WiFi resilience | | v1.9.2 | 37,800 (47%) | **61,987 (94%)** | 409,100 (39%) | WiFi resilience |
**Verdict**: Stable memory usage, no leaks detected after 24h+ uptime tests. **Verdict**: Stable memory usage, no leaks detected after 24h+ uptime tests.
@@ -536,6 +589,7 @@ The firmware is designed to be extensible. Next planned features:
### Custom Display Screens ### Custom Display Screens
Pull data from Home Assistant via REST API: Pull data from Home Assistant via REST API:
- **Smart home stats**: Energy usage, room temperatures - **Smart home stats**: Energy usage, room temperatures
- **Sensor data**: Air quality, CO2 levels - **Sensor data**: Air quality, CO2 levels
- **Automation states**: Alarm status, door locks - **Automation states**: Alarm status, door locks
@@ -549,6 +603,7 @@ Pull data from Home Assistant via REST API:
### WebSocket Live Updates ### WebSocket Live Updates
Replace polling with WebSocket for: Replace polling with WebSocket for:
- Real-time config changes without page refresh - Real-time config changes without page refresh
- Live display preview in web UI - Live display preview in web UI
- Push notifications for firmware updates - Push notifications for firmware updates
@@ -579,6 +634,7 @@ Replace polling with WebSocket for:
- `WiFiManager` (by tzapu) - `WiFiManager` (by tzapu)
- `AsyncHTTPRequest_Generic` - `AsyncHTTPRequest_Generic`
- `ESPAsyncTCP` - `ESPAsyncTCP`
- `ArduinoJson` (by Benoit Blanchon)
3. **Board Configuration** 3. **Board Configuration**
- Board: "Generic ESP8266 Module" - Board: "Generic ESP8266 Module"
@@ -591,6 +647,7 @@ Replace polling with WebSocket for:
### First Flash (via FTDI) ### First Flash (via FTDI)
1. **Wire the ESP-01S**: 1. **Wire the ESP-01S**:
``` ```
FTDI 3.3V → ESP-01S 3V3 FTDI 3.3V → ESP-01S 3V3
FTDI GND → ESP-01S GND FTDI GND → ESP-01S GND
@@ -600,7 +657,7 @@ Replace polling with WebSocket for:
``` ```
2. **Compile and Upload**: 2. **Compile and Upload**:
- Open `clock_ntp_ota_v1.9.ino` - Open `weather_clock.ino`
- Sketch → Upload - Sketch → Upload
- Wait for "Done uploading" - Wait for "Done uploading"
- Remove GPIO0-to-GND jumper - Remove GPIO0-to-GND jumper
@@ -633,6 +690,7 @@ Replace polling with WebSocket for:
## Web Interface ## Web Interface
### Home Page (`/`) ### Home Page (`/`)
Current time display with live updates via JavaScript (fetches `/api/time` every second). Current time display with live updates via JavaScript (fetches `/api/time` every second).
### Configuration Page (`/config`) ### Configuration Page (`/config`)
@@ -640,11 +698,13 @@ Current time display with live updates via JavaScript (fetches `/api/time` every
Comprehensive settings form: Comprehensive settings form:
**WiFi Settings** **WiFi Settings**
- SSID - SSID
- Password - Password
- Hostname (for mDNS) - Hostname (for mDNS)
**Time Settings** **Time Settings**
- Timezone offset (seconds from UTC) - Timezone offset (seconds from UTC)
- DST enabled (European rules) - DST enabled (European rules)
- NTP server address - NTP server address
@@ -652,6 +712,7 @@ Comprehensive settings form:
- Hour format (12h/24h) - Hour format (12h/24h)
**Weather Settings** **Weather Settings**
- Enabled/disabled toggle - Enabled/disabled toggle
- Latitude - Latitude
- Longitude - Longitude
@@ -659,6 +720,7 @@ Comprehensive settings form:
- Update interval (seconds) - Update interval (seconds)
**Display Settings** **Display Settings**
- Brightness (0-7) - Brightness (0-7)
- Rotation (0°, 90°, 180°, 270°) - Rotation (0°, 90°, 180°, 270°)
- Display rotation interval (seconds) - Display rotation interval (seconds)
@@ -692,6 +754,7 @@ All endpoints return JSON (except `/update` which is for file upload).
Current time information. Current time information.
**Response:** **Response:**
```json ```json
{ {
"current": "14:23:45", "current": "14:23:45",
@@ -707,6 +770,7 @@ Current time information.
System status overview. System status overview.
**Response:** **Response:**
```json ```json
{ {
"wifi": { "wifi": {
@@ -733,6 +797,7 @@ System status overview.
Current weather data. Current weather data.
**Response:** **Response:**
```json ```json
{ {
"temperature": 15.4, "temperature": 15.4,
@@ -751,6 +816,7 @@ Current weather data.
Export full configuration as JSON. Export full configuration as JSON.
**Response:** **Response:**
```json ```json
{ {
"ssid": "MyNetwork", "ssid": "MyNetwork",
@@ -780,6 +846,7 @@ Import configuration from JSON.
**Request Body**: Same structure as export response (password field optional for security). **Request Body**: Same structure as export response (password field optional for security).
**Response:** **Response:**
```json ```json
{ {
"status": "ok" "status": "ok"
@@ -793,6 +860,7 @@ Device automatically reboots after import.
Factory reset (clears EEPROM). Factory reset (clears EEPROM).
**Response:** **Response:**
```json ```json
{ {
"status": "cleared" "status": "cleared"
@@ -806,6 +874,7 @@ Device reboots to WiFiManager captive portal.
Remote reboot. Remote reboot.
**Response:** **Response:**
```json ```json
{ {
"status": "rebooting" "status": "rebooting"
@@ -820,15 +889,15 @@ Device reboots immediately.
### What Changed from Original Firmware ### What Changed from Original Firmware
| Issue | Original | Custom Firmware | | Issue | Original | Custom Firmware |
|-------|----------|-----------------| | ---------------------- | ------------------------------ | --------------------------------- |
| **WiFi Password Leak** | Plaintext in open AP | No open AP after setup | | **WiFi Password Leak** | Plaintext in open AP | No open AP after setup |
| **Persistent AP** | Always active | Only on first boot or failure | | **Persistent AP** | Always active | Only on first boot or failure |
| **API Keys** | QWeather requires registration | Open-Meteo (no key needed) | | **API Keys** | QWeather requires registration | Open-Meteo (no key needed) |
| **Cloud Dependency** | Chinese servers | Direct API calls, no intermediary | | **Cloud Dependency** | Chinese servers | Direct API calls, no intermediary |
| **Firmware Updates** | Manual FTDI only | OTA via WiFi (password-protected) | | **Firmware Updates** | Manual FTDI only | OTA via WiFi (password-protected) |
| **Config Access** | No authentication | Admin password required | | **Config Access** | No authentication | Admin password required |
| **Code Transparency** | Closed source | Open source (you're reading it!) | | **Code Transparency** | Closed source | Open source (you're reading it!) |
### Best Practices Implemented ### Best Practices Implemented
@@ -842,16 +911,19 @@ Device reboots immediately.
### Recommended Post-Flash Steps ### Recommended Post-Flash Steps
1. **Change OTA password**: Edit line ~60 in `.ino` file: 1. **Change OTA password**: Edit line ~60 in `.ino` file:
```cpp ```cpp
ArduinoOTA.setPassword("admin"); // Change this! ArduinoOTA.setPassword("admin"); // Change this!
``` ```
2. **Change web admin password**: Edit line ~430: 2. **Change web admin password**: Edit line ~430:
```cpp ```cpp
if (!server.authenticate("admin", "admin")) { // Change this! if (!server.authenticate("admin", "admin")) { // Change this!
``` ```
3. **Set strong WiFi AP fallback password**: Edit line ~780: 3. **Set strong WiFi AP fallback password**: Edit line ~780:
```cpp ```cpp
WiFi.softAP("TJ56654-Clock", "12345678"); // Change this! WiFi.softAP("TJ56654-Clock", "12345678"); // Change this!
``` ```
@@ -900,6 +972,7 @@ Device reboots immediately.
**Firmware**: Written from scratch with love and frustration **Firmware**: Written from scratch with love and frustration
**Libraries Used**: **Libraries Used**:
- [ESP8266 Arduino Core](https://github.com/esp8266/Arduino) - [ESP8266 Arduino Core](https://github.com/esp8266/Arduino)
- [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) - [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306)
- [WiFiManager](https://github.com/tzapu/WiFiManager) - [WiFiManager](https://github.com/tzapu/WiFiManager)
@@ -907,9 +980,11 @@ Device reboots immediately.
- [NTPClient](https://github.com/arduino-libraries/NTPClient) - [NTPClient](https://github.com/arduino-libraries/NTPClient)
**APIs**: **APIs**:
- [Open-Meteo](https://open-meteo.com/) - Free weather API, no registration required - [Open-Meteo](https://open-meteo.com/) - Free weather API, no registration required
**Tools**: **Tools**:
- Arduino IDE 2.x - Arduino IDE 2.x
- FTDI FT232RL USB-to-Serial adapter - FTDI FT232RL USB-to-Serial adapter
- Lots of coffee ☕ - Lots of coffee ☕
@@ -921,14 +996,11 @@ Device reboots immediately.
``` ```
esp8266-weather-clock/ esp8266-weather-clock/
├── firmware/ ├── firmware/
│ └── clock_ntp_ota_v1.9/ │ └── weather_clock/
│ └── clock_ntp_ota_v1.9.ino # Main firmware (~2,100 lines) │ └── weather_clock.ino # Main firmware (~2,100 lines)
├── docs/ ├── docs/
│ ├── HARDWARE.md # Hardware specifications │ ├── HARDWARE.md # Hardware specifications
│ ├── INSTALLATION.md # Flashing guide │ └── INSTALLATION.md # Flashing guide
│ ├── v1.9_RELEASE_NOTES.md # v1.9.0 async refactoring
│ ├── v1.9.1_HYBRID_FIX.md # WiFi startup fix
│ └── v1.9.2_WIFI_RESILIENCE.md # WiFi resilience documentation
├── CHANGELOG.md # Version history ├── CHANGELOG.md # Version history
└── README.md # This file └── README.md # This file
``` ```
@@ -946,6 +1018,7 @@ This project is released into the public domain. Do whatever you want with it. I
This project started as "I don't trust this device" and ended as "I built something better." This project started as "I don't trust this device" and ended as "I built something better."
The original firmware had security holes you could drive a truck through. The custom replacement: The original firmware had security holes you could drive a truck through. The custom replacement:
- ✅ Doesn't leak WiFi passwords - ✅ Doesn't leak WiFi passwords
- ✅ Uses free, open APIs - ✅ Uses free, open APIs
- ✅ Updates over WiFi - ✅ Updates over WiFi
@@ -965,4 +1038,4 @@ Now go make something cool. 🚀
**Author**: apetrochenko **Author**: apetrochenko
**Date**: 2026-01-06 **Date**: 2026-01-06
**Firmware Version**: v1.9.2 (Production Ready) **Firmware Version**: v1.9.4 (Production Ready)
+63 -28
View File
@@ -64,16 +64,17 @@ Go to: **Sketch → Include Library → Manage Libraries**
Install the following libraries (search by name): Install the following libraries (search by name):
| Library | Author | Min Version | Purpose | | Library | Author | Min Version | Purpose |
|---------|--------|-------------|---------| | ---------------------------- | ---------------- | ----------- | ----------------------------- |
| **Adafruit GFX Library** | Adafruit | 1.11.0 | Graphics primitives | | **Adafruit GFX Library** | Adafruit | 1.11.0 | Graphics primitives |
| **Adafruit SSD1306** | Adafruit | 2.5.0 | OLED display driver | | **Adafruit SSD1306** | Adafruit | 2.5.0 | OLED display driver |
| **NTPClient** | Fabrice Weinberg | 3.2.0 | NTP time sync (base) | | **NTPClient** | Fabrice Weinberg | 3.2.0 | NTP time sync (base) |
| **WiFiManager** | tzapu | 2.0.0 | Captive portal setup | | **WiFiManager** | tzapu | 2.0.0 | Captive portal setup |
| **AsyncHTTPRequest_Generic** | Khoi Hoang | 1.13.0 | Async weather fetch | | **AsyncHTTPRequest_Generic** | Khoi Hoang | 1.13.0 | Async weather fetch |
| **ESPAsyncTCP** | me-no-dev | 1.2.2 | Async TCP (required by above) | | **ESPAsyncTCP** | me-no-dev | 1.2.2 | Async TCP (required by above) |
**Installation steps for each library:** **Installation steps for each library:**
1. Search library name in Library Manager 1. Search library name in Library Manager
2. Click **Install** 2. Click **Install**
3. Wait for "INSTALLED" badge 3. Wait for "INSTALLED" badge
@@ -87,17 +88,17 @@ Install the following libraries (search by name):
2. Select: **Generic ESP8266 Module** 2. Select: **Generic ESP8266 Module**
3. Configure settings: 3. Configure settings:
| Setting | Value | Why | | Setting | Value | Why |
|---------|-------|-----| | ----------------- | -------------------------- | ---------------------------------------- |
| Flash Size | `1MB (FS:64KB OTA:~470KB)` | Enables OTA with 470KB max firmware | | Flash Size | `1MB (FS:64KB OTA:~470KB)` | Enables OTA with 470KB max firmware |
| Flash Mode | `DIO` | Compatible with most ESP-01S modules | | Flash Mode | `DIO` | Compatible with most ESP-01S modules |
| Flash Frequency | `40MHz` | Safe default for all ESP8266 | | Flash Frequency | `40MHz` | Safe default for all ESP8266 |
| CPU Frequency | `80MHz` | Standard (can use 160MHz for more speed) | | CPU Frequency | `80MHz` | Standard (can use 160MHz for more speed) |
| Crystal Frequency | `26MHz` | Default for ESP-01S | | Crystal Frequency | `26MHz` | Default for ESP-01S |
| Upload Speed | `115200` | Balance between speed and reliability | | Upload Speed | `115200` | Balance between speed and reliability |
| Debug Level | `None` | Reduces firmware size | | Debug Level | `None` | Reduces firmware size |
| IwIP Variant | `v2 Lower Memory` | Better for 1MB flash devices | | IwIP Variant | `v2 Lower Memory` | Better for 1MB flash devices |
| Erase Flash | `Only Sketch` | Preserves config on re-flash | | Erase Flash | `Only Sketch` | Preserves config on re-flash |
--- ---
@@ -106,6 +107,7 @@ Install the following libraries (search by name):
### Step 1: Identify Pins ### Step 1: Identify Pins
ESP-01S pinout (looking at module from top, antenna up): ESP-01S pinout (looking at module from top, antenna up):
``` ```
┌─────────────┐ ┌─────────────┐
│ │ │ │
@@ -123,15 +125,16 @@ ESP-01S pinout (looking at module from top, antenna up):
**Connections:** **Connections:**
| FTDI Pin | ESP-01S Pin | Wire Color | Notes | | FTDI Pin | ESP-01S Pin | Wire Color | Notes |
|----------|-------------|------------|-------| | -------- | ----------- | ---------- | --------------------------------- |
| 3.3V | 3V3 | Red | Power (NOT 5V!) | | 3.3V | 3V3 | Red | Power (NOT 5V!) |
| GND | GND | Black | Ground | | GND | GND | Black | Ground |
| TX | RX | Yellow | Data: FTDI transmit → ESP receive | | TX | RX | Yellow | Data: FTDI transmit → ESP receive |
| RX | TX | Green | Data: FTDI receive → ESP transmit | | RX | TX | Green | Data: FTDI receive → ESP transmit |
| GND | GPIO0 | Blue | **Programming mode** (temporary) | | GND | GPIO0 | Blue | **Programming mode** (temporary) |
**⚠️ CRITICAL**: **⚠️ CRITICAL**:
- **Never connect 5V to ESP-01S** - it's not 5V tolerant! - **Never connect 5V to ESP-01S** - it's not 5V tolerant!
- Double-check polarity before powering on - Double-check polarity before powering on
- GPIO0-to-GND connection is **temporary** (only for programming mode) - GPIO0-to-GND connection is **temporary** (only for programming mode)
@@ -152,8 +155,8 @@ ESP-01S is now in programming mode, ready to receive firmware.
### Step 1: Open Project ### Step 1: Open Project
1. Download or clone this repository 1. Download or clone this repository
2. Navigate to: `esp8266-weather-clock-opensource/src/` 2. Navigate to: `esp8266-weather-clock-opensource/firmware/weather_clock/`
3. Open: `clock_ntp_ota_v1.9.ino` in Arduino IDE 3. Open: `weather_clock.ino` in Arduino IDE
### Step 2: Verify Board Settings ### Step 2: Verify Board Settings
@@ -166,6 +169,7 @@ ESP-01S is now in programming mode, ready to receive firmware.
- Windows: `COM3`, `COM4`, etc. - Windows: `COM3`, `COM4`, etc.
If port doesn't appear: If port doesn't appear:
- Check USB cable is data-capable (not charge-only) - Check USB cable is data-capable (not charge-only)
- Install FTDI drivers - Install FTDI drivers
- Try different USB port - Try different USB port
@@ -222,10 +226,12 @@ If port doesn't appear:
### Step 2: Captive Portal ### Step 2: Captive Portal
**Automatic (iOS/Android):** **Automatic (iOS/Android):**
- Captive portal should pop up automatically - Captive portal should pop up automatically
- If not, manually browse to: http://192.168.4.1 - If not, manually browse to: http://192.168.4.1
**Manual (laptop):** **Manual (laptop):**
- Browse to: http://192.168.4.1 - Browse to: http://192.168.4.1
### Step 3: Configure WiFi ### Step 3: Configure WiFi
@@ -240,16 +246,19 @@ If port doesn't appear:
### Step 4: Find Device IP ### Step 4: Find Device IP
**Method 1: Router Admin Panel** **Method 1: Router Admin Panel**
- Log into your router - Log into your router
- Look for device: "tj56654-clock" - Look for device: "tj56654-clock"
- Note its IP address (e.g., 192.168.1.47) - Note its IP address (e.g., 192.168.1.47)
**Method 2: mDNS (if your OS supports it)** **Method 2: mDNS (if your OS supports it)**
- Browse to: http://tj56654-clock.local/ - Browse to: http://tj56654-clock.local/
- Works on macOS, Linux, iOS out-of-box - Works on macOS, Linux, iOS out-of-box
- Windows: Install [Bonjour Print Services](https://support.apple.com/kb/DL999) - Windows: Install [Bonjour Print Services](https://support.apple.com/kb/DL999)
**Method 3: Serial Monitor** **Method 3: Serial Monitor**
1. Keep FTDI connected (no GPIO0 to GND!) 1. Keep FTDI connected (no GPIO0 to GND!)
2. Open: **Tools → Serial Monitor** 2. Open: **Tools → Serial Monitor**
3. Set baud rate: **115200** 3. Set baud rate: **115200**
@@ -261,6 +270,7 @@ If port doesn't appear:
Browse to: `http://<device-ip>/` or `http://tj56654-clock.local/` Browse to: `http://<device-ip>/` or `http://tj56654-clock.local/`
You should see: You should see:
- Current time display - Current time display
- Navigation links (Config, Debug, Update) - Navigation links (Config, Debug, Update)
@@ -276,6 +286,7 @@ You should see:
4. Device reboots with new settings 4. Device reboots with new settings
**Timezone examples:** **Timezone examples:**
- UTC+0 (London winter): `0` - UTC+0 (London winter): `0`
- UTC+1 (Paris winter): `3600` - UTC+1 (Paris winter): `3600`
- UTC-5 (New York winter): `-18000` - UTC-5 (New York winter): `-18000`
@@ -319,6 +330,7 @@ curl -u admin:admin -F "file=@/path/to/firmware.bin" http://192.168.x.x/update
``` ```
Replace: Replace:
- `192.168.x.x` with your device IP - `192.168.x.x` with your device IP
- `/path/to/firmware.bin` with actual path to .bin file - `/path/to/firmware.bin` with actual path to .bin file
@@ -329,43 +341,51 @@ Replace:
### Upload Fails ### Upload Fails
**Error: "espcomm_open failed"** **Error: "espcomm_open failed"**
- Check: GPIO0 was grounded during power-on - Check: GPIO0 was grounded during power-on
- Check: FTDI driver installed - Check: FTDI driver installed
- Try: Different USB port - Try: Different USB port
- Try: Lower upload speed (57600 instead of 115200) - Try: Lower upload speed (57600 instead of 115200)
**Error: "espcomm_upload_mem failed"** **Error: "espcomm_upload_mem failed"**
- Check: Wire connections (especially RX↔TX swap) - Check: Wire connections (especially RX↔TX swap)
- Check: FTDI is 3.3V (not 5V) - Check: FTDI is 3.3V (not 5V)
- Try: Power ESP-01S from external 3.3V supply (FTDI may not provide enough current) - Try: Power ESP-01S from external 3.3V supply (FTDI may not provide enough current)
**Error: "Chip sync error"** **Error: "Chip sync error"**
- GPIO0 must be LOW during boot - GPIO0 must be LOW during boot
- Try: Hold GPIO0 to GND, reset ESP, then release GPIO0 - Try: Hold GPIO0 to GND, reset ESP, then release GPIO0
### Compilation Fails ### Compilation Fails
**Error: "library not found"** **Error: "library not found"**
- Install missing library via Library Manager - Install missing library via Library Manager
- Restart Arduino IDE after installing - Restart Arduino IDE after installing
**Error: "Sketch too big"** **Error: "Sketch too big"**
- Flash size must be set to 1MB - Flash size must be set to 1MB
- Reduce features if necessary (disable weather, etc.) - Reduce features if necessary (disable weather, etc.)
**IRAM overflow error** **IRAM overflow error**
- Some functions missing `ICACHE_FLASH_ATTR` - Some functions missing `ICACHE_FLASH_ATTR`
- Use version from this repo (already optimized) - Use version from this repo (already optimized)
### WiFi Connection Fails ### WiFi Connection Fails
**Device creates AP but won't connect to home WiFi** **Device creates AP but won't connect to home WiFi**
- ESP8266 only supports 2.4GHz (not 5GHz) - ESP8266 only supports 2.4GHz (not 5GHz)
- Try: Different WiFi channel (1, 6, or 11) - Try: Different WiFi channel (1, 6, or 11)
- Check: WiFi password is correct - Check: WiFi password is correct
- Check: Router supports 802.11n - Check: Router supports 802.11n
**Device reboots in a loop** **Device reboots in a loop**
- Likely: Power supply too weak (brownout) - Likely: Power supply too weak (brownout)
- Solution: Use powered USB hub or different power adapter - Solution: Use powered USB hub or different power adapter
- Minimum: 500mA @ 5V - Minimum: 500mA @ 5V
@@ -373,28 +393,33 @@ Replace:
### Display Issues ### Display Issues
**Display is blank** **Display is blank**
- Check: I2C wiring (SDA=GPIO0, SCL=GPIO2) - Check: I2C wiring (SDA=GPIO0, SCL=GPIO2)
- Check: Display I2C address (try 0x3C and 0x3D in code) - Check: Display I2C address (try 0x3C and 0x3D in code)
- Test: Use `/api/i2c-scan` endpoint to detect display - Test: Use `/api/i2c-scan` endpoint to detect display
**Display shows garbage** **Display shows garbage**
- Wrong display library or initialization - Wrong display library or initialization
- This firmware is for SSD1306-compatible OLED - This firmware is for SSD1306-compatible OLED
- Verify display model is GM009605v4.3 or similar - Verify display model is GM009605v4.3 or similar
**Display is upside down** **Display is upside down**
- Change `display_orientation` in `/config` - Change `display_orientation` in `/config`
- Values: 0 (normal), 1 (90°), 2 (180°), 3 (270°) - Values: 0 (normal), 1 (90°), 2 (180°), 3 (270°)
### Time Not Syncing ### Time Not Syncing
**Time shows 00:00:00** **Time shows 00:00:00**
- Check: WiFi is connected (`/api/status`) - Check: WiFi is connected (`/api/status`)
- Check: NTP server is reachable (default: pool.ntp.org) - Check: NTP server is reachable (default: pool.ntp.org)
- Check: Router firewall allows UDP port 123 - Check: Router firewall allows UDP port 123
- Try: Different NTP server (e.g., time.google.com) - Try: Different NTP server (e.g., time.google.com)
**Time is wrong by hours** **Time is wrong by hours**
- Check: Timezone offset in `/config` - Check: Timezone offset in `/config`
- Remember: Offset is in **seconds**, not hours - Remember: Offset is in **seconds**, not hours
- Example: UTC+1 = 3600 seconds - Example: UTC+1 = 3600 seconds
@@ -402,6 +427,7 @@ Replace:
### Weather Not Updating ### Weather Not Updating
**Temperature shows 0.0°C** **Temperature shows 0.0°C**
- Check: Internet connectivity (`/api/debug`) - Check: Internet connectivity (`/api/debug`)
- Check: Latitude/longitude are correct - Check: Latitude/longitude are correct
- Check: Open-Meteo API is accessible (visit https://open-meteo.com/ in browser) - Check: Open-Meteo API is accessible (visit https://open-meteo.com/ in browser)
@@ -410,11 +436,13 @@ Replace:
### OTA Update Fails ### OTA Update Fails
**Web upload hangs at 0%** **Web upload hangs at 0%**
- Check: Device is online and responsive - Check: Device is online and responsive
- Try: Smaller firmware (disable features) - Try: Smaller firmware (disable features)
- Try: Upload via Arduino IDE instead - Try: Upload via Arduino IDE instead
**Upload completes but device doesn't reboot** **Upload completes but device doesn't reboot**
- Wait 30 seconds (sometimes slow) - Wait 30 seconds (sometimes slow)
- Manually power cycle device - Manually power cycle device
- Check serial output for errors - Check serial output for errors
@@ -422,10 +450,12 @@ Replace:
### Serial Monitor Shows Errors ### Serial Monitor Shows Errors
**"DNS resolution failed"** **"DNS resolution failed"**
- In v1.9.0 (fixed in v1.9.1) - In v1.9.0 (fixed in v1.9.1)
- Upgrade to v1.9.1 or later - Upgrade to v1.9.1 or later
**Watchdog reset / exception** **Watchdog reset / exception**
- Likely: Code bug or memory corruption - Likely: Code bug or memory corruption
- Check: IRAM usage < 95% - Check: IRAM usage < 95%
- Report: Open issue with serial log - Report: Open issue with serial log
@@ -437,6 +467,7 @@ Replace:
### Change OTA Password ### Change OTA Password
Edit in source code (line ~60): Edit in source code (line ~60):
```cpp ```cpp
ArduinoOTA.setPassword("your-secret-password"); ArduinoOTA.setPassword("your-secret-password");
``` ```
@@ -444,6 +475,7 @@ ArduinoOTA.setPassword("your-secret-password");
### Change Web Admin Password ### Change Web Admin Password
Edit in source code (line ~430): Edit in source code (line ~430):
```cpp ```cpp
if (!server.authenticate("admin", "your-secret-password")) { if (!server.authenticate("admin", "your-secret-password")) {
``` ```
@@ -453,13 +485,16 @@ if (!server.authenticate("admin", "your-secret-password")) {
To save memory, disable unused features: To save memory, disable unused features:
**Disable weather:** **Disable weather:**
- Set `weather_enabled = false` in `/config` - Set `weather_enabled = false` in `/config`
- Or remove weather code from source - Or remove weather code from source
**Disable sunrise/sunset:** **Disable sunrise/sunset:**
- Set `show_sunrise_sunset = false` in `/config` - Set `show_sunrise_sunset = false` in `/config`
**Disable display rotation:** **Disable display rotation:**
- Set `display_rotation_sec = 0` (manual switch only) - Set `display_rotation_sec = 0` (manual switch only)
--- ---
-218
View File
@@ -1,218 +0,0 @@
# v1.9.1 - Hybrid Async Fix
## Problem in v1.9.0
**Symptoms:**
- Display shows blank screen ~10 seconds after boot
- "DNS resolution failed" errors in logs
- Time appears only after 10+ seconds
**Root Cause:**
```cpp
void setup() {
loadConfig();
setupWiFi(); // Returns IMMEDIATELY (async)
setupOTA(); // WiFi NOT ready!
setupWebServer(); // WiFi NOT ready!
testInternetConnectivity(); // WiFi NOT ready! -> "DNS resolution failed"
}
```
WiFi became **fully asynchronous**, but this is **wrong for setup()**:
- OTA, web server, NTP **require a ready WiFi connection**
- `testInternetConnectivity()` ran **before WiFi connected**
- Time on display appeared only when async WiFi finally connected
## Solution: Hybrid Model
| Phase | WiFi Mode | Blocking | Reason |
|-------|-----------|----------|--------|
| **setup()** | **Synchronous** | 10 sec | Required for OTA/web/NTP initialization |
| **loop()** | **Asynchronous** | 0 sec | Don't freeze on reconnect |
### Code Changes
#### 1. setupWiFi() - Now Synchronous
```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);
// SYNCHRONOUS wait (max 10 seconds)
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
showNumber(attempts, false); // Show progress on display
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
// WiFi ready for OTA/web/NTP!
showIP();
wifiConnState = WIFI_CONN_CONNECTED;
return;
}
}
// Fallback to WiFiManager if credentials didn't work
// ...
}
```
#### 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 handling
// Other async operations
processNTPResponse();
fetchWeatherAsync();
// ...
}
```
## Test Results
### Before (v1.9.0)
```
[0-5s] -> Display init
[5-15s] -> WiFi connecting (async, setup() returns immediately)
[15-20s] -> OTA/web init WITHOUT WiFi -> Errors!
[20s] -> testInternetConnectivity() WITHOUT WiFi -> "DNS resolution failed"
[15-25s] -> WiFi finally connects (async)
[25-30s] -> NTP sync begins
Display blank for 10+ seconds
"DNS resolution failed" errors
```
### After (v1.9.1)
```
[0-5s] -> Display init + startup animation
[5-15s] -> WiFi connection (SYNCHRONOUS, setup() waits)
WiFi connected!
[15-20s] -> OTA/web/NTP init WITH 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 network... | |
| [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!) |
| |
+----------------------------------------------------------+
```
## Benefits of Hybrid Approach
### In setup():
1. **Correct initialization order** - WiFi -> OTA -> web -> NTP
2. **No DNS errors** - internet connectivity test runs AFTER WiFi
3. **Predictable behavior** - setup() completes when everything is ready
4. **Display shows time immediately** - no need to wait for async WiFi
### In loop():
1. **No freeze on reconnect** - async handling of WiFi loss
2. **Async NTP** - doesn't block loop
3. **Async weather** - doesn't block loop
4. **Exponential backoff** - smart retry on errors
5. **Loop <1ms** - always responsive device
## Memory
| Resource | v1.9.0 | v1.9.1 | Change |
|----------|--------|--------|--------|
| RAM | 37,516 | 37,644 | +128 bytes |
| IRAM | 61,987 | 61,987 | 0 bytes |
| Flash | 408,540 | 408,844 | +304 bytes |
Minimal memory changes (+0.3%) for critical UX improvement.
## Conclusion
**v1.9.1 implements the ideal balance:**
- Setup: Synchronous for reliable initialization
- Loop: Asynchronous for responsiveness
**Result:**
- Display shows time after 15 sec (instead of 25+ sec)
- No "DNS resolution failed" errors
- Correct startup order
- Device doesn't freeze on WiFi loss during operation
**Status**: Production ready
-313
View File
@@ -1,313 +0,0 @@
# v1.9.2 - WiFi Resilience
## Problem in v1.9.1
**Symptoms:**
- After WiFi outage (router restart, temporary network issues), device enters AP mode
- WiFi credentials are cleared, requiring manual reconfiguration
- User must reconnect to "TJ56654-Setup" AP and re-enter WiFi password
- This happens every time WiFi goes down temporarily
**Root Cause:**
Aggressive credential clearing after connection failures:
```cpp
void processWiFiConnection() {
// After 5 failed attempts...
if (wifiRetry.currentRetry >= wifiRetry.maxRetries) {
// Clear credentials!
memset(config.ssid, 0, sizeof(config.ssid));
memset(config.password, 0, sizeof(config.password));
saveConfig();
// Start AP mode for reconfiguration
WiFiManager wm;
wm.startConfigPortal("TJ56654-Setup", "12345678");
}
}
```
**Impact:**
- Poor user experience during network outages
- Unnecessary manual intervention required
- Device unusable until reconfigured
## Solution: Resilient WiFi
### Key Changes
| Feature | Before (v1.9.1) | After (v1.9.2) |
|---------|-----------------|----------------|
| Credential clearing | After 5 failed attempts | Never |
| Retry strategy | Give up after 5 tries | Infinite retries |
| Retry interval | Fixed 5 seconds | Exponential backoff (5s-5min) |
| Fallback AP | After clearing credentials | After ~5 min (keeps credentials) |
| Clock during outage | Shows connection counter | Shows last synced time |
| User notification | Numeric counter | "No WiFi" message |
### Implementation
#### 1. WiFi Retry Configuration
```cpp
// Infinite retries with exponential backoff
struct WiFiRetryConfig {
uint8_t currentRetry = 0;
unsigned long nextRetryTime = 0;
static const unsigned long MAX_BACKOFF_MS = 300000; // Max 5 minutes
unsigned long getBackoffDelay() {
// 5s, 10s, 20s, 40s, 80s, 160s, 300s (max)
unsigned long delay = 5000UL * (1UL << currentRetry);
return (delay > MAX_BACKOFF_MS) ? MAX_BACKOFF_MS : delay;
}
void recordAttempt() {
currentRetry++;
nextRetryTime = millis() + getBackoffDelay();
}
void reset() {
currentRetry = 0;
nextRetryTime = 0;
}
};
```
#### 2. No Credential Clearing
```cpp
void processWiFiConnection() {
if (WiFi.status() == WL_CONNECTED) {
if (wifiConnState != WIFI_CONN_CONNECTED) {
Serial.println("WiFi connected!");
wifiConnState = WIFI_CONN_CONNECTED;
wifiRetry.reset();
// Disable fallback AP if it was enabled
if (WiFi.getMode() == WIFI_AP_STA) {
Serial.println("Disabling fallback AP");
WiFi.mode(WIFI_STA);
}
}
return;
}
// Connection failed - but DON'T clear credentials!
if (millis() >= wifiRetry.nextRetryTime) {
Serial.printf("WiFi retry %d, next in %lu sec\n",
wifiRetry.currentRetry,
wifiRetry.getBackoffDelay() / 1000);
wifiRetry.recordAttempt();
// Try to connect with SDK or EEPROM credentials
if (strlen(config.password) > 0) {
WiFi.begin(config.ssid, config.password);
} else {
WiFi.begin(); // Use SDK-stored credentials
}
// Enable fallback AP after 5+ attempts (~2.5 min)
if (wifiRetry.currentRetry >= 5 && WiFi.getMode() != WIFI_AP_STA) {
Serial.println("Enabling fallback AP (dual mode)");
WiFi.mode(WIFI_AP_STA);
WiFi.softAP("TJ56654-Setup", "12345678");
}
}
}
```
#### 3. "No WiFi" Display
```cpp
void ICACHE_FLASH_ATTR showNoWiFi(unsigned long nextRetrySeconds) {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
// Center "No WiFi" text
display.setCursor(20, 8);
display.println("No WiFi");
// Show retry countdown
display.setTextSize(1);
display.setCursor(10, 52);
if (nextRetrySeconds < 60) {
display.printf("Retry in %lu sec", nextRetrySeconds);
} else {
display.printf("Retry in %lu min", nextRetrySeconds / 60);
}
display.display();
}
```
#### 4. Clock Continues During Outage
```cpp
void updateDisplay() {
if (WiFi.status() != WL_CONNECTED) {
// Show "No WiFi" status
unsigned long nextRetry = (wifiRetry.nextRetryTime > millis())
? (wifiRetry.nextRetryTime - millis()) / 1000
: 0;
showNoWiFi(nextRetry);
return;
}
// Normal display modes when WiFi is connected
// ...
}
```
#### 5. WiFi Disconnect Indicator
When WiFi is disconnected but time is still valid, show "!" in date line:
```cpp
void showTimeMode() {
// ... time display ...
// Date line with WiFi status indicator
display.setCursor(0, 0);
if (WiFi.status() != WL_CONNECTED) {
display.print("! "); // WiFi disconnected indicator
}
display.print(dateString);
}
```
#### 6. SDK Credentials Support
WiFiManager stores credentials in ESP SDK flash, not EEPROM. This caused issues after OTA updates:
```cpp
void setupWiFi() {
// Try SDK-stored credentials first (from WiFiManager)
if (strlen(config.password) > 0) {
// EEPROM has credentials - use them
WiFi.begin(config.ssid, config.password);
} else {
// EEPROM empty - try SDK credentials
WiFi.begin(); // Uses last saved WiFi from SDK
}
}
```
## Retry Backoff Schedule
| Attempt | Delay | Cumulative Time |
|---------|-------|-----------------|
| 1 | 5 sec | 5 sec |
| 2 | 10 sec | 15 sec |
| 3 | 20 sec | 35 sec |
| 4 | 40 sec | 1 min 15 sec |
| 5 | 80 sec | 2 min 35 sec |
| 6 | 160 sec | 5 min 15 sec |
| 7+ | 300 sec (5 min) | +5 min each |
**Fallback AP enabled after attempt 5** (~2.5 min of failures)
## Network Activity Summary
| Service | Interval | Endpoint | Protocol | Daily Requests |
|---------|----------|----------|----------|----------------|
| NTP | 1 hour | pool.ntp.org:123 | UDP | 24 |
| Weather | 30 min | api.open-meteo.com | HTTP | 48 |
| mDNS | continuous | 224.0.0.251 | UDP multicast | N/A |
**Total**: ~50-75 requests/day (minimal network load)
## Dual STA+AP Mode
When fallback AP is enabled, device operates in dual mode:
- **STA (Station)**: Continues trying to connect to configured WiFi
- **AP (Access Point)**: Allows user to reconfigure if needed
```cpp
// Enable dual mode
WiFi.mode(WIFI_AP_STA);
WiFi.softAP("TJ56654-Setup", "12345678");
// Continue WiFi connection attempts in STA mode
WiFi.begin(config.ssid, config.password);
```
When WiFi reconnects:
```cpp
if (WiFi.status() == WL_CONNECTED && WiFi.getMode() == WIFI_AP_STA) {
// Disable AP, return to STA-only mode
WiFi.mode(WIFI_STA);
Serial.println("WiFi reconnected, disabled fallback AP");
}
```
## Test Scenarios
### Scenario 1: Router Restart (5 minutes)
**Before (v1.9.1):**
1. WiFi disconnects
2. 5 retry attempts (25 seconds)
3. Credentials cleared
4. Device enters AP mode
5. User must reconfigure WiFi
**After (v1.9.2):**
1. WiFi disconnects
2. Retry with backoff (5s, 10s, 20s, 40s, 80s)
3. After ~2.5 min: Fallback AP enabled (dual mode)
4. Device continues retrying
5. Router comes back
6. Device reconnects automatically
7. Fallback AP disabled
8. No user intervention needed
### Scenario 2: Extended Outage (30+ minutes)
**v1.9.2 Behavior:**
1. Retries every 5 minutes after initial backoff
2. Fallback AP available for reconfiguration if needed
3. Clock shows last synced time
4. "No WiFi" indicator on display
5. Reconnects automatically when WiFi returns
### Scenario 3: OTA Update
**Problem**: WiFiManager stores credentials in SDK flash, but our EEPROM may be empty after update.
**Solution**: Try SDK credentials first:
```cpp
if (strlen(config.password) > 0) {
WiFi.begin(config.ssid, config.password);
} else {
WiFi.begin(); // SDK credentials
}
```
## Memory Impact
| Resource | v1.9.1 | v1.9.2 | Change |
|----------|--------|--------|--------|
| RAM | 37,644 | 37,800 | +156 bytes |
| IRAM | 61,987 | 61,987 | 0 bytes |
| Flash | 408,844 | 409,100 | +256 bytes |
Minimal memory increase (+0.4%) for significant UX improvement.
## Conclusion
**v1.9.2 provides true WiFi resilience:**
- Never clears credentials
- Infinite retries with smart backoff
- Fallback AP for emergency reconfiguration
- Clock continues during outages
- Clear user feedback on display
**Results:**
- No more manual reconfiguration after WiFi outages
- Device recovers automatically when network returns
- User can still reconfigure via fallback AP if needed
- Minimal network overhead (~50 requests/day)
**Status**: Production ready
-160
View File
@@ -1,160 +0,0 @@
# TJ-56-654 Weather Clock - v1.9.0 Release Notes
## Release Date
2026-01-03
## Overview
Version 1.9 is a major async refactoring that eliminates **ALL blocking operations** from the firmware, transforming the device from a frequently-frozen system into a fully responsive, production-ready clock.
## Performance Improvements
### Before (v1.8):
- **WiFi connection**: 10 seconds blocking (v1.7 credential migration)
- **NTP sync**: 5-20 seconds blocking
- **Weather fetch**: 1-10 seconds blocking
- **Loop delay**: 10ms blocking every iteration
- **Total freeze time**: Up to **45+ seconds**
### After (v1.9):
- **WiFi connection**: 0ms blocking (async state machine)
- **NTP sync**: 0ms blocking (async UDP)
- **Weather fetch**: 0ms blocking (AsyncHTTPRequest)
- **Loop delay**: 0ms (removed)
- **Total freeze time**: **0 seconds** ✅
**Loop responsiveness**: <1ms typical (was 10ms minimum)
## New Features
### 1. Async HTTP Weather Fetch (v1.9.2)
- **Library**: AsyncHTTPRequest_Generic v1.13.0
- **State machine**: IDLE → REQUESTING → SUCCESS/FAILED
- **Callback**: `onWeatherResponse()` processes data non-blocking
- **Result**: OTA updates work during weather fetch
### 2. Async NTP Implementation (v1.9.3)
- **Manual NTP**: Custom UDP packet building/parsing
- **Independent epoch tracking**: `syncedEpoch`, `syncedMillis`, `timeIsSynced`
- **Workaround**: NTPClient library is inherently blocking, so we bypass it
- **State machine**: IDLE → REQUEST_SENT → WAITING → SUCCESS/FAILED
- **Timeout**: 5 seconds non-blocking
### 3. Async WiFi Connection (v1.9.4)
- **v1.7 migration**: Non-blocking credential attempt
- **State machine**: IDLE → CONNECTING → CONNECTED/FAILED
- **Fallback**: WiFiManager (still blocking, but only on first boot)
- **Benefit**: Device stays responsive during connection attempts
### 4. Zero Blocking Delays (v1.9.5)
- **Removed**: `delay(10)` from loop()
- **Replaced**: `delay(3000)` in `showIP()` with scheduled clear via `ipDisplayUntil` timer
- **Kept**: Startup animation delays (acceptable, only runs once in setup)
- **Kept**: Pre-reboot delays (acceptable, device is rebooting anyway)
### 5. Exponential Backoff Retries (v1.9.6)
- **Strategy**: 1s → 2s → 4s (max 3 retries)
- **Struct**: `RetryConfig` with `getBackoffDelay()`, `scheduleRetry()`, `isRetryTime()`
- **Applied to**:
- NTP failures: graceful retry instead of hammering server
- Weather API failures: same exponential strategy
- **Benefit**: Network resilience without aggressive retry behavior
## Memory Footprint
| Resource | v1.8 (baseline) | v1.9.0 (final) | Increase |
|----------|----------------|----------------|----------|
| RAM | 36,980 bytes | 37,516 bytes | +536 bytes (1.4%) |
| IRAM | 61,987 bytes | 61,987 bytes | 0 bytes |
| Flash | 407,500 bytes | 408,540 bytes | +1040 bytes (0.25%) |
### Memory Budget Status:
- **RAM**: 37,516 / 80,192 bytes (46%) - ✅ Safe
- **IRAM**: 61,987 / 65,536 bytes (94%) - ⚠️ Near limit but stable
- **Flash**: 408,540 / 1,048,576 bytes (38%) - ✅ Plenty of room
**Verdict**: Less than 1.5% RAM increase for fully async operation - excellent ROI!
## Code Quality Improvements
### Line Count:
- **v1.8**: ~1,950 lines
- **v1.9**: 2,026 lines (+76 lines for async infrastructure)
### New Data Structures:
```cpp
enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED };
enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED };
enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED, SKIP_ASYNC };
struct RetryConfig {
uint8_t maxRetries = 3;
uint8_t currentRetry = 0;
unsigned long nextRetryTime = 0;
unsigned long getBackoffDelay();
void scheduleRetry();
bool isRetryTime();
void reset();
bool maxRetriesReached();
};
```
### Key Functions Added:
1. `onWeatherResponse()` - AsyncHTTPRequest callback
2. `fetchWeatherAsync()` - Non-blocking weather fetch
3. `sendNTPRequestAsync()` - Manual NTP packet send
4. `processNTPResponse()` - Non-blocking NTP response check
5. `processWiFiConnection()` - Async WiFi state handler
6. `getAsyncEpoch()` - Independent time tracking
## Testing Checklist
Before OTA upload to device:
- [x] Compilation successful
- [x] Memory usage within safe limits
- [ ] OTA responsive during weather fetch
- [ ] Web UI responsive during NTP sync
- [ ] Display updates smoothly during network ops
- [ ] Exponential backoff triggers on failures
- [ ] Max retry limits respected
- [ ] Config persistence across reboots
- [ ] 24-hour stability test
## Migration from v1.8
**OTA Upgrade Path**: ✅ Safe
- Config struct unchanged - binary compatible
- All settings preserved
- Smooth transition from v1.7 credentials
**Rollback**: Keep v1.8.bin for emergency rollback via web upload
## Known Limitations
1. **WiFiManager**: Still blocking on first boot (acceptable)
2. **IRAM**: At 94% - future features must use `ICACHE_FLASH_ATTR`
3. **Startup animation**: Still uses blocking delays (acceptable, only runs once)
4. **Test handlers**: Some debug endpoints still block (low priority)
## Next Steps (v2.0)
Future improvements planned for v2.0:
1. **Modular architecture**: Split into separate files
2. **ArduinoJson**: Replace manual JSON parsing
3. **Constants**: Eliminate remaining magic numbers
4. **Code deduplication**: Display helper refactoring
5. **Enhanced error handling**: Pre-flight checks, better validation
## Credits
**Firmware**: TJ-56-654 Weather Clock
**Hardware**: ESP-01S (ESP8266EX, 1MB flash)
**Author**: Generated with Claude Code (Opus 4.5)
**Repository**: clock/firmware/clock_ntp_ota_v1.9
## Conclusion
v1.9 transforms the weather clock from a frequently-frozen device into a **fully responsive**, **production-ready** system with **zero blocking operations**. The 536-byte RAM overhead is a negligible cost for the massive UX improvement of instant responsiveness to OTA, web requests, and display updates even during active network operations.
**Status**: ✅ Ready for OTA deployment
@@ -9,7 +9,7 @@
#include <Arduino.h> #include <Arduino.h>
// Firmware version // Firmware version
#define FIRMWARE_VERSION "1.9.3" #define FIRMWARE_VERSION "1.9.4"
// OLED I2C Configuration // OLED I2C Configuration
#define I2C_SDA 0 // GPIO0 (I2C Data) - SWAPPED! #define I2C_SDA 0 // GPIO0 (I2C Data) - SWAPPED!
@@ -54,7 +54,7 @@ void ICACHE_FLASH_ATTR updateDisplay() {
// === YELLOW ZONE (Y: 48-63): Date (size 2 = 16px height) === // === YELLOW ZONE (Y: 48-63): Date (size 2 = 16px height) ===
display.setTextSize(2); display.setTextSize(2);
time_t t = epochTime; time_t t = localTime;
struct tm *ptm = gmtime(&t); struct tm *ptm = gmtime(&t);
// Format: "Thu 02.01" or "! Thu 02.01" if no WiFi // Format: "Thu 02.01" or "! Thu 02.01" if no WiFi
@@ -168,7 +168,7 @@ void ICACHE_FLASH_ATTR displaySunTimes() {
int daylightHours = daylightMinutes / 60; int daylightHours = daylightMinutes / 60;
int daylightMins = daylightMinutes % 60; int daylightMins = daylightMinutes % 60;
char daylightStr[16]; char daylightStr[32];
sprintf(daylightStr, "Day %dh %dm", daylightHours, daylightMins); sprintf(daylightStr, "Day %dh %dm", daylightHours, daylightMins);
display.setTextSize(1); display.setTextSize(1);
@@ -16,7 +16,6 @@ bool ICACHE_FLASH_ATTR isDST(unsigned long epochTime) {
int month = timeinfo->tm_mon + 1; // 1-12 int month = timeinfo->tm_mon + 1; // 1-12
int day = timeinfo->tm_mday; // 1-31 int day = timeinfo->tm_mday; // 1-31
int weekday = timeinfo->tm_wday; // 0=Sunday
int hour = timeinfo->tm_hour; int hour = timeinfo->tm_hour;
// Not DST: November - February // Not DST: November - February
@@ -28,7 +28,7 @@ void ICACHE_FLASH_ATTR onWeatherResponse(void* optParm, AsyncHTTPRequest* reques
Serial.printf("Weather response: %d bytes\n", payload.length()); Serial.printf("Weather response: %d bytes\n", payload.length());
// Parse JSON response // Parse JSON response
StaticJsonDocument<1536> doc; JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload); DeserializationError error = deserializeJson(doc, payload);
if (!error) { if (!error) {
@@ -83,7 +83,7 @@ void ICACHE_FLASH_ATTR onWeatherResponse(void* optParm, AsyncHTTPRequest* reques
sunTimes.lastDay = ptm->tm_yday; sunTimes.lastDay = ptm->tm_yday;
} }
weatherState = WEATHER_SUCCESS; // weatherState stays WEATHER_IDLE (set at readyState==4 entry) — allows periodic refresh
weatherRetry.reset(); weatherRetry.reset();
Serial.printf("Weather: %.1f C, code %d, wind %.1f km/h\n", Serial.printf("Weather: %.1f C, code %d, wind %.1f km/h\n",
weather.temperature, weather.weathercode, weather.windspeed); weather.temperature, weather.weathercode, weather.windspeed);
@@ -166,7 +166,7 @@ void ICACHE_FLASH_ATTR handleDebug() {
if (weather.valid) { if (weather.valid) {
snprintf_P(buf, sizeof(buf), PSTR("Temperature: %.1f C\nWeather code: %d\nWind speed: %.1f km/h\nLast update: %lu sec ago\n"), snprintf_P(buf, sizeof(buf), PSTR("Temperature: %.1f C\nWeather code: %d\nWind speed: %.1f km/h\nLast update: %lu sec ago\n"),
weather.temperature, weather.weathercode, weather.windspeed, weather.lastUpdate/1000); weather.temperature, weather.weathercode, weather.windspeed, (millis() - weather.lastUpdate)/1000);
server.sendContent(buf); server.sendContent(buf);
} }
@@ -67,37 +67,42 @@ void ICACHE_FLASH_ATTR setupWiFi() {
WiFi.hostname(config.hostname); WiFi.hostname(config.hostname);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Try 1: Use WiFi.begin() without params - uses SDK stored credentials // Try 1: Use WiFi.begin() without params - only when no SSID is configured.
Serial.println("Trying SDK-stored credentials..."); // Skipped if user has a saved SSID: SDK-cached credentials may include open
WiFi.begin(); // networks (e.g. public hotspots) that would be preferred over the user's
// network, and a successful connect would overwrite config.ssid (issue #3).
if (strlen(config.ssid) == 0) {
Serial.println("No SSID configured, trying SDK-stored credentials...");
WiFi.begin();
// SYNCHRONOUS wait for connection (max 10 seconds) // SYNCHRONOUS wait for connection (max 10 seconds)
Serial.print("Connecting to WiFi"); Serial.print("Connecting to WiFi");
int attempts = 0; int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) { while (WiFi.status() != WL_CONNECTED && attempts < 20) {
showWiFiConnecting(attempts); showWiFiConnecting(attempts);
delay(500); delay(500);
Serial.print("."); Serial.print(".");
attempts++; attempts++;
} }
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!"); Serial.println("\nWiFi connected!");
Serial.print("SSID: "); Serial.print("SSID: ");
Serial.println(WiFi.SSID()); Serial.println(WiFi.SSID());
Serial.print("IP: "); Serial.print("IP: ");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
Serial.print("Gateway: "); Serial.print("Gateway: ");
Serial.println(WiFi.gatewayIP()); Serial.println(WiFi.gatewayIP());
Serial.print("DNS: "); Serial.print("DNS: ");
Serial.println(WiFi.dnsIP()); Serial.println(WiFi.dnsIP());
safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid)); safeStringCopy(WiFi.SSID(), config.ssid, sizeof(config.ssid));
saveConfig(); saveConfig();
showIP(); showIP();
wifiConnState = WIFI_CONN_CONNECTED; wifiConnState = WIFI_CONN_CONNECTED;
return; return;
}
} }
// Try 2: If we have EEPROM credentials, try those // Try 2: If we have EEPROM credentials, try those
@@ -105,7 +110,7 @@ void ICACHE_FLASH_ATTR setupWiFi() {
Serial.println("\nTrying EEPROM credentials..."); Serial.println("\nTrying EEPROM credentials...");
WiFi.begin(config.ssid, config.password); WiFi.begin(config.ssid, config.password);
attempts = 0; int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) { while (WiFi.status() != WL_CONNECTED && attempts < 20) {
showWiFiConnecting(attempts); showWiFiConnecting(attempts);
delay(500); delay(500);
File diff suppressed because it is too large Load Diff