20 Commits
Author SHA1 Message Date
T fe77b61813 Add platformio 2026-09-27 15:39:38 +02:00
Alex PetrochenkoandClaude Fable 5.1 aee1d3e0ef fix(wifi): persist password alongside SSID after SDK/WiFiManager connect (v1.9.10)
After a successful connect via SDK-cached credentials (Try 1) or the
WiFiManager captive portal, only the SSID was saved to EEPROM. On the next
boot config.password was empty, so setupWiFi() and the reconnect loop called
WiFi.begin(ssid) as if the network were open and failed with
WL_WRONG_PASSWORD.

v1.9.6 masked this: with an empty password the retry loop fell back to a
bare WiFi.begin(), which reuses the SDK-stored credentials. Commit 02834ba
(v1.9.7) replaced that fallback with WiFi.begin(config.ssid), and v1.9.9
(M2) added the same call to setupWiFi(), so every version after 1.9.6
never reconnects after a reboot when the device was set up through the
portal.

Read the password back with WiFi.psk() and store it next to the SSID in
all three places that sync the SSID. Devices already in this state recover
by entering the password once in the web UI (fallback AP after ~2.5 min).

Verified: builds with PlatformIO (espressif8266 core 3.1.2, esp01_1m).

Fixes #12

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:40:46 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 259eda6663 fix: post-review issues + close M2/M3/M4 from internal backlog (v1.9.9)
From Sonnet pre-release code review (3 real issues):
- F1 (high): factory reset atomicity — save cleared creds BEFORE zeroing reset
  counter, so a power loss between commits still results in WiFiManager AP boot
  instead of inconsistent "counter=0 + stale creds" state
- F2: isValidSSID now allows single-char SSIDs (per 802.11 spec)
- F3: ntp_interval changes now trigger needsRestart (NTPClient constructed once
  in setup() with this value, doesn't pick up runtime changes)

From internal backlog:
- M2: setupWiFi Try-2 now supports open networks (no password) — fixed the
  &&-condition that required both ssid and password
- M3: removed dead NTP_WAITING/NTP_SUCCESS/NTP_FAILED enum values
- M4: randomSeed() with ESP.getChipId() ^ micros() — dissolve pattern varies

Tested on hardware: 72/73 tests pass (single failure is expected — ntp_interval
fuzz cases now trigger restart due to F3, second case lands in reboot window).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:05:57 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 d0aa68e64f fix(web): only reboot on WiFi/network config changes (v1.9.8)
Previously every successful config save triggered ESP.restart() — meant rapid
config changes caused reboot cascades and made the test suite unable to run
multiple cases against /config. Now restarts only if ssid/password/hostname/
ntp_server changed; other settings (brightness, timezone, intervals, coords,
display options) apply live without reboot.

tests/test_device.py: fixed expected field names for /api/time
(time/hours/minutes/epoch instead of current/timezone_offset/ntp_synced).

Test suite: 73/73 passing on hardware. Heap drift <1KB across full fuzz run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:12:21 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 5b9285aeb9 fix(web): validate inputs in handleConfigSave to prevent fuzz bricking (M1)
Previously /config accepted any value and called ESP.restart() — fuzz tests
(or any malicious POST) could save garbage SSIDs and brick the device until
FTDI recovery. Now:

- SSID: rejected if empty, >31 chars, non-printable, or all-same-char (HTTP 400)
- Password: rejected if >63 chars
- Hostname/city_name/ntp_server: length-validated
- Numeric fields (timezone, brightness, intervals, lat/lon, display): clamped
  to safe ranges via constrain()

Tested on hardware: ssid="AAAA..." now correctly returns HTTP 400 and
preserves existing config. Device survives entire fuzz suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:40:43 +01:00
Alex Petrochenko 02834bada1 fix(wifi): preserve AP_STA mode after WiFi.begin() kills the AP
WiFi.begin() internally resets mode to STA, immediately destroying the
fallback AP that was just created. Fix: restore WIFI_AP_STA mode after
begin() when in AP mode, so TJ56654-Setup stays visible.

This was why the fallback AP was never visible despite being 'created'.
2026-05-18 20:52:14 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 5b8a58715e feat: triple power-cycle factory reset (no FTDI needed)
Power-cycle the device 3 times within 10 seconds to trigger factory reset:
- Clears WiFi credentials (SSID + password) in EEPROM
- Shows "FACTORY RESET / WiFi: TJ56654-Setup / Pass: 12345678" on display
- Reboots into WiFiManager AP mode for reconfiguration

Counter stored at EEPROM offset 480 (well past Config ~260 bytes).
Clears automatically after 10s of stable operation.

Prevents the need for FTDI/USB recovery when credentials are corrupted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:25:32 +01:00
Alex Petrochenko e9dc158a4e chore: bump version to 1.9.6, update CHANGELOG 2026-05-18 16:02:53 +01:00
Alex Petrochenko 7ebe341c06 fix(web): clamp lastError before snprintf to prevent malformed JSON (Gemini review) 2026-05-18 15:57:41 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 78bbd96f45 fix: C1 C2 H1 H2 H3 — stability fixes for long-running operation
C1 (critical): Add 15s watchdog for WEATHER_REQUESTING state — resets to
IDLE if TCP connection hangs silently, preventing permanent weather death

C2 (critical): Fix millis() rollover in all retry timers — replace unsafe
`millis() >= nextRetryTime` with subtraction-safe `(millis() - nextRetryTime)
< 0x80000000UL` in RetryConfig and WiFiRetryConfig; fix boot guard with
static flag instead of raw millis() comparison

H1 (high): Fix DST last-Sunday formula — was using incorrect year-only
heuristic; now derives weekday of the 31st from current day's tm_wday:
`weekdayOf31 = (tm_wday + (31 - day)) % 7`. Verified: March 2026 = 29th ✓

H2 (high): Replace String+= with snprintf+sendContent in handleAPITime,
handleAPIStatus, handleAPIDebug, handleAPIWeather — eliminates permanent
heap fragmentation from JS polling every second

H3 (high): Add volatile to weatherState and ntpState — shared between
ESPAsyncTCP callbacks and main loop; prevents stale register-cached reads

RAM: 37,268 bytes (46%) — reduced from 37,560 due to String elimination

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:53:39 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 d5d2b23129 chore: bump version to 1.9.5, update CHANGELOG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:53:15 +01:00
Alex PetrochenkoandClaude Sonnet 4.6 2f3814c225 fix(weather): recover from permanent lockup after max retries (closes #9)
After 3 consecutive API failures, weatherRetry.reset() was never called
and weatherState stayed WEATHER_FAILED permanently. Both the retry path
and periodic refresh path require WEATHER_IDLE, so no new requests were
ever made — even after the API recovered.

Fix: when maxRetriesReached(), reset weatherRetry and set weatherState
back to WEATHER_IDLE so the next periodic refresh interval (default 30min)
triggers a fresh attempt automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:52:42 +01:00
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
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
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
Alex PetrochenkoandClaude Opus 4.5 9c0f269ee5 chore: bump version to 1.9.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:44:57 +00:00
Alex PetrochenkoandClaude Opus 4.5 4a7e889052 refactor: split monolithic .ino into modular structure + improve boot UX
## Code Structure
- Split 2172-line .ino into 7 modules:
  - config.h - structs, enums, constants
  - globals.h - extern declarations, function prototypes
  - display.cpp - OLED display functions, Thanos dissolve effect
  - ntp_client.cpp - NTP sync, DST calculation
  - wifi_manager.cpp - WiFi connection management
  - weather.cpp - Open-Meteo API (async)
  - web_server.cpp - Web interface handlers
  - clock_ntp_ota_v1.9.ino - setup/loop, EEPROM, OTA (153 lines)

## Boot UX Improvements
- Remove meaningless "READY" screen
- Replace cryptic counter (0-7) with "WiFi..." + animated dots
- Show SSID + IP on connected screen
- Use yellow zone (Y 48-63) for status text
- Fix text centering on all boot screens
- Initialize lastBlinkTime/lastModeSwitch to prevent first-frame glitch

## Layout (dual-color display)
- Blue zone (Y 0-47): main content
- Yellow zone (Y 48-63): status/version
- Centering formula: width = n×5 + (n-1)×1 for size 1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:05:45 +00:00
Alex PetrochenkoandAlex Petrochenko c3bfaa26bc feat(wifi): v1.9.2 - WiFi resilience and credential persistence (#1)
## Problem
Previous versions would clear WiFi credentials on connection failure,
requiring manual reconfiguration via captive portal after each WiFi outage.

## Solution
- Never clear credentials on connection failure
- Infinite retry with exponential backoff (5s → 10s → 20s → ... → 5min max)
- Fallback AP ("TJ56654-Setup") enabled after ~5 min of failed attempts
- Dual STA+AP mode allows configuration while still retrying connection
- AP automatically disabled when WiFi reconnects

## Changes

### WiFi Credential Handling
- Try SDK-stored credentials first (from WiFiManager)
- Fall back to EEPROM config if SDK credentials fail
- Fixes OTA update credential loss issue

### User Experience
- Display shows "No WiFi" with retry countdown instead of cryptic numbers
- "!" indicator in date line when WiFi is disconnected
- Clock continues running with last synced time during outage

### Network Activity (~50 requests/day)
| Service | Interval | Endpoint |
|---------|----------|----------|
| NTP | 1 hour | pool.ntp.org:123 (UDP) |
| Weather | 30 min | api.open-meteo.com (HTTP) |
| mDNS | continuous | 224.0.0.251 (multicast) |

Co-authored-by: Alex Petrochenko <petrochenko@life-pay.ru>
2026-01-06 20:51:42 +00:00