diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6695172..bbe645d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,14 +73,42 @@ Feature requests are welcome! Please include: arduino-cli compile --fqbn esp8266:esp8266:generic firmware/weather_clock ``` -### Testing +### Flashing ```bash -# Upload via FTDI (first time) -arduino-cli upload -p /dev/cu.usbserial* --fqbn esp8266:esp8266:generic - -# Upload via OTA (subsequent) +# OTA upload (preferred, when device is on the network) curl -u admin:admin -F "file=@build/*.bin" http://192.168.x.x/update + +# Initial flash via FTDI (3.3V! ESP-01S in socket — no soldering) +# 1. Pull ESP-01S from socket on TJ-56-654 PCB +# 2. Connect: FTDI 3V3→3V3, GND→GND, TX↔RX crossed, GND→GPIO0 +# 3. Power on with GPIO0 grounded → bootloader mode +esptool.py --port /dev/cu.usbserial-0001 --baud 115200 write_flash \ + --flash_size 1MB --flash_mode dout 0x0 firmware.bin +``` + +### Running the test suite + +Hardware-in-the-loop tests verify functionality and resilience: + +```bash +python3 tests/test_device.py +``` + +73 test cases cover REST API, config validation, fuzz testing, and heap stability. +Safe to run repeatedly — validation rejects garbage, only WiFi changes reboot the device. + +### Recovery (bricked device) + +Triple power-cycle (≤10s apart, 3 times) triggers factory reset — clears WiFi +credentials and shows AP info on the OLED. Connect to `TJ56654-Setup` / +`12345678` and reconfigure via `http://192.168.4.1/config`. + +If that fails, full reset via FTDI: + +```bash +esptool.py --port /dev/cu.usbserial-0001 erase_flash +esptool.py --port /dev/cu.usbserial-0001 write_flash 0x0 firmware.bin ``` ## Project Structure diff --git a/README.md b/README.md index cce1cf8..40baf7b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ I bought a cute weather clock kit from AliExpress ([TJ-56-654](https://pt.aliexp - [The Investigation](#the-investigation) - [The Solution: Custom Firmware](#the-solution-custom-firmware) - [Technical Deep Dive](#technical-deep-dive) -- [The Journey: v1.7 → v1.9.4](#the-journey-v17--v194) +- [The Journey: v1.7 → v1.9.8](#the-journey-v17--v198) - [What's Next: Home Assistant Integration](#whats-next-home-assistant-integration) - [How to Flash This Firmware](#how-to-flash-this-firmware) - [Web Interface](#web-interface) @@ -395,7 +395,7 @@ Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2 --- -## The Journey: v1.7 → v1.9.4 +## The Journey: v1.7 → v1.9.8 ### v1.7: Display Discovery ✅ @@ -558,7 +558,7 @@ Split monolithic 2,100-line `.ino` into focused modules: | `web_server.cpp` | Web UI and REST API | | `wifi_manager.cpp` | WiFi connection and resilience | -### v1.9.4: Bug Fixes & Cleanup ✅ (Current) +### v1.9.4: Community Bug Fixes 🐛 Community-reported bugs fixed: @@ -568,6 +568,33 @@ Community-reported bugs fixed: - **ArduinoJson v7**: Updated `StaticJsonDocument` → `JsonDocument` for library compatibility - **Compiler warnings**: Removed unused variable, fixed sprintf buffer size +### v1.9.5: Weather Recovery 🌦 + +- **Permanent weather lockup after 3 API failures** (#9): state machine no longer + deadlocks; resets to IDLE after max retries so periodic refresh resumes + +### v1.9.6: Long-Running Stability ⏱ + +Six bugs found via dual AI code review (Sonnet + Gemini): + +- **WEATHER_REQUESTING timeout watchdog** (C1): 15s timeout prevents TCP-hang deadlock +- **millis() rollover at 49.7 days** (C2): all retry timers now use subtraction-safe comparison +- **DST formula mathematically wrong** (H1): now uses tm_wday-based last-Sunday calculation +- **String heap fragmentation** (H2): API handlers use `snprintf+sendContent` instead of `String +=` +- **Volatile shared state** (H3): `weatherState`/`ntpState` written from callbacks, read in loop +- **Triple power-cycle factory reset**: 3 quick power cycles within 10s clears WiFi creds (no FTDI needed) + +### v1.9.7: Config Validation 🛡 + +- **`/config` accepted any input, could brick device**: now rejects empty/all-same/oversized + SSIDs (HTTP 400), clamps numeric fields to safe ranges. Closes M1 from internal backlog. + +### v1.9.8: Live Config Updates ⚡ (Current) + +- **Restart only on WiFi/network changes**: brightness, timezone, intervals, coordinates, + display options apply live. Eliminates display blackouts on minor config tweaks. +- **73/73 automated tests pass** on hardware including full fuzz suite + ### Memory Evolution | Version | RAM Usage | IRAM Usage | Flash Usage | Notes | @@ -577,6 +604,8 @@ Community-reported bugs fixed: | 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.2 | 37,800 (47%) | **61,987 (94%)** | 409,100 (39%) | WiFi resilience | +| v1.9.6 | 37,268 (46%) | **61,987 (94%)** | 410,436 (39%) | String → snprintf | +| v1.9.8 | 37,664 (46%) | **61,987 (94%)** | 411,380 (39%) | Live config updates | **Verdict**: Stable memory usage, no leaks detected after 24h+ uptime tests. @@ -996,8 +1025,17 @@ Device reboots immediately. ``` esp8266-weather-clock/ ├── firmware/ -│ └── weather_clock/ -│ └── weather_clock.ino # Main firmware (~2,100 lines) +│ └── weather_clock/ # Modular firmware +│ ├── weather_clock.ino # setup() and loop() +│ ├── config.h # Config struct, EEPROM layout +│ ├── globals.h # Shared state +│ ├── display.cpp # OLED rendering +│ ├── ntp_client.cpp # Async NTP + DST +│ ├── weather.cpp # Open-Meteo API +│ ├── web_server.cpp # HTTP UI + REST API +│ └── wifi_manager.cpp # WiFi resilience +├── tests/ +│ └── test_device.py # HW-in-the-loop test suite (73 cases) ├── docs/ │ ├── HARDWARE.md # Hardware specifications │ └── INSTALLATION.md # Flashing guide @@ -1005,6 +1043,21 @@ esp8266-weather-clock/ └── README.md # This file ``` +## Testing + +```bash +# Hardware-in-the-loop test suite — verifies API, fuzzes config endpoint, +# checks heap stability over 5 samples +python3 tests/test_device.py 192.168.x.x +``` + +The test suite covers: + +- All REST API endpoints (functional validation) +- Boundary fuzz against `/config` (oversized SSID, invalid intervals, etc.) +- Malformed JSON fuzz against `/api/config` import +- Heap stability check (must stay >20KB, drift <2KB across runs) + --- ## License @@ -1038,4 +1091,4 @@ Now go make something cool. 🚀 **Author**: apetrochenko **Date**: 2026-01-06 -**Firmware Version**: v1.9.4 (Production Ready) +**Firmware Version**: v1.9.8 (Production Ready) diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 8463937..f11c229 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -499,6 +499,34 @@ To save memory, disable unused features: --- +## Factory Reset (no tools needed) + +Since v1.9.6, three quick power cycles within 10 seconds trigger a factory reset: + +1. Power off the device +2. Power on (briefly, < 10 sec) +3. Power off, power on +4. Power off, power on + +On the third boot the OLED shows `FACTORY RESET! WiFi: TJ56654-Setup Pass: 12345678`. +Connect your phone to that AP and reconfigure WiFi at `http://192.168.4.1/config`. + +Use this if: + +- Forgot configured WiFi credentials +- Device stuck in "No WiFi" loop after router change +- Tests/fuzzing corrupted config + +## Verifying Installation + +Run the test suite against your device: + +```bash +python3 tests/test_device.py 192.168.x.x +``` + +A healthy device passes all 73 tests, with heap drift under 1 KB across the run. + ## Getting Help If you're still stuck: