Skip to content

PR-08: SNTP time, task watchdog & event logging#14

Merged
cryptotomte merged 7 commits into
mainfrom
008-sntp-watchdog-logging
Jul 5, 2026
Merged

PR-08: SNTP time, task watchdog & event logging#14
cryptotomte merged 7 commits into
mainfrom
008-sntp-watchdog-logging

Conversation

@cryptotomte

Copy link
Copy Markdown
Owner

PR-08 — SNTP time, task watchdog & event logging

Completes the phase-2 exit criteria (FR10 + reliability/logging NFRs). Spec-kit artifacts in
specs/008-sntp-watchdog-logging/. Depends on PR-06 (littlefs event log) and PR-07 (WiFi), both merged.

What it does

  • SNTP wall-clock time (FR10): esp_netif_sntp against the Swedish pool (se.pool.ntp.org, Kconfig)
    starts once on the first WiFi Connected; Europe/Stockholm CET/CEST (TZ=CET-1CEST,M3.5.0,M10.5.0/3);
    an explicit time-not-set state (year < 2020) that PR-11's scheduler gates on. Non-fatal if offline.
  • Task watchdog: esp_task_wdt (reboot on hang) subscribes ONLY the watering-critical tasks — the
    10 Hz main loop (drives pump update()) and the sensor task — plus the per-core idle tasks. The WiFi
    task and console REPL are deliberately excluded
    (a network stall must never reboot the device — PR-07
    isolation). Replaces the ineffective legacy software watchdog (QUIRK 3). After a watchdog reset,
    pumps_force_off() (unchanged, first action) keeps pumps OFF and the reset reason is persisted.
  • Event logging: a pure, host-tested EventLogger wires producers (reset reason at boot, WiFi state
    changes, pump start/stop) into the existing PR-06 rotating event log; readable via storage events.

Architecture

  • New time component: pure TimeService (plausibility + CET/CEST formatting) behind the new
    IWallClock seam; target-only SntpClient (the sole IDF touchpoint) + SystemWallClock.
  • New events component: pure EventLogger (depends only on interfaces; primitive args; a failed
    store is counted, never throws — FR-014), reuses PR-06 IDataStorage (no interface change).
  • main/ glue: task_watchdog, system_observer (edge-detects WiFi/pump events, starts SNTP), boot
    reset-reason logging. pumps_force_off() stays the first app_main action.

Verification

  • Host tests: 191/0 — CET/CEST + DST spring & autumn boundaries, time-not-set, EventLogger producers
    (incl. failsafe/ota), reset-reason total mapping (0..15), store-failure/dropped counter, epoch-stamped-
    at-emit clock-step.
  • Both board targets build + board-config verified; app ~1.03 MB (34 % free in the 1.5 MiB OTA slot).
    dependencies.lock + both esp-modbus pins unchanged (only IDF built-ins added). No frozen-legacy edits.

Review (CP3)

Five specialist agents; all core invariants PASS. Findings F1–F6 + polish fixed and re-verified (see
docs/checkpoints/cp3-008-sntp-watchdog-logging.md): dropped-event surfacing, watchdog idle-mask, a
mismatch-proof logReset, SNTP retry/SyncStatus cleanup, full reset-reason mapping, and a boot serial
trace.

Deferred (by design)

  • An ISntp seam to host-test SystemObserver's edge detection → a later PR.
  • HIL (specs/008-sntp-watchdog-logging/checklists/hil.md) runs on the rev1 rig: correct Swedish time
    • sync status, a starved task → reboot with pumps OFF + reset=TASK_WDT logged, event-log power-cycle
      persistence, WiFi-outage-does-not-reboot, non-fatal SNTP offline.

🤖 Generated with Claude Code

cryptotomte and others added 7 commits July 4, 2026 18:34
Spec-kit artifacts for PR-08 (SNTP time + task watchdog + event logging),
approved at Checkpoint 2. Records: reuse of PR-06 IDataStorage event log,
the new IWallClock seam, CET/CEST TZ, watchdog excludes the WiFi task,
WS_TASK_WDT_TIMEOUT_S=20s and se.pool.ntp.org defaults. No implementation yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent, watchdog config

Phase 1+2 of PR-08:
- time component skeleton (linux-guarded CMake; header-only until US1 adds SRCS)
- IWallClock pure interface (nowEpoch/isTimeSet) + FakeWallClock host fake + SyncStatus
- Kconfig WS_SNTP_SERVER (se.pool.ntp.org) + WS_TASK_WDT_TIMEOUT_S (20s); sdkconfig
  CONFIG_ESP_TASK_WDT_INIT/PANIC (timeout reconfigured from Kconfig at init)
- host time + event_logger suites registered (stubs)

Verified: host tests 178/0, both board targets build + board-config verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t state

Phase 3 / US1:
- TimeService (pure): isPlausibleEpoch (>=2020) + formatLocal (localtime_r/strftime),
  host-tested for CET(+0100)/CEST(+0200) incl. the spring DST boundary
- SystemWallClock (target): IWallClock over time(nullptr)
- SntpClient (target): esp_netif_sntp against CONFIG_WS_SNTP_SERVER, step-set,
  sync callback updates SyncStatus; idempotent, non-blocking, non-fatal; TZ applied
- diag console 'time' command + diag_console_register_time (app_main wiring deferred to US3)

Verified: host 181/0 (+3), both board targets build + verified; SNTP links with
PRIV_REQUIRES esp_netif.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot reset reason

Phase 4 / US2:
- events component: EventLogger (pure, over IDataStorage + IWallClock) with typed
  producers (reset/wifi/pump start-stop/failsafe/ota), droppedEvents counter,
  resetReasonName mapping; reuses the PR-06 rotating event log (no interface change)
- app_main: SystemWallClock + EventLogger; esp_reset_reason() -> logReset at boot
- system_observer (main/): polls WifiManager snapshot + pump isRunning() from the
  10 Hz loop, logs WiFi state changes + pump start/stop; null-guarded
- 6 host tests (EventLogger producers/failure/resetReasonName) vs MockDataStorage
- TODO(US3/T018): construct SntpClient + register the time console

Verified: host 187/0 (+6), both board targets build + verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… SNTP wiring

Phase 5 / US3 + polish:
- task_watchdog helper: reconfigure to WS_TASK_WDT_TIMEOUT_S (20s, panic->reboot),
  subscribe/feed. Subscribes ONLY the 10 Hz main loop + the 5 s sensor task; the
  WiFi task and console REPL are deliberately NOT subscribed (a network stall must
  not reboot the device - FR-014). Idle-task WDT from sdkconfig retained.
- SNTP wiring: app_main constructs SntpClient + applyTimezone(); SystemObserver
  starts SNTP once on the first WifiState::Connected; diag console 'time' registered.
- boot order verified: pumps_force_off() first -> esp_reset_reason()->logReset ->
  watchdog_init() -> tasks subscribe (a prior TASK_WDT reset shows in storage events).
- firmware/CLAUDE.md feature-008 section; HIL checklist.

Verified: host 187/0; rev1 1034400B (34% slot free) + rev2 build, both config-verified;
dependencies.lock + esp-modbus pins unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses CP3 findings F1-F6 + polish (host 187/0, both boards green):
- F1: SystemObserver surfaces EventLogger droppedEvents() via ESP_LOGW (was a
  dead counter — a failing event store went silent)
- F2: task watchdog preserves idle-task watching on all cores
  (idle_core_mask = (1<<portNUM_PROCESSORS)-1) + corrected comment
- F3: EventLogger::logReset(int) maps reason internally (dropped the dead,
  mismatchable reasonName param)
- F4: SyncStatus.synced() derived from lastSyncEpoch (no redundant bool);
  SntpClient::start() returns bool; observer retries SNTP if init failed
- F5: resetReasonName extended to IDF v6 values 11-15 (USB/JTAG/EFUSE/
  PWR_GLITCH/CPU_LOCKUP) + corrected comment
- F6: reset reason also ESP_LOGI'd at boot (serial trace independent of store)
- polish: FakeWallClock threshold references TimeService::kMinPlausibleEpoch;
  SntpClient single-instance guard + benign-cross-task-read note; formatLocal
  guards localtime_r; corrected SystemWallClock 'IDF touchpoint' docs; softened
  reset-ordering comment

Host tests (new cases) land in the next commit. pumps_force_off() still first;
pure components remain esp-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…autumn, clock-step

- logFailsafe/logOta producer tests (category + verbatim detail + epoch)
- store-failure test extended to all six producers (droppedEvents==6)
- resetReasonName total mapping 0..15 + unknown->UNKNOWN
- multi-event clock-step: epoch stamped at emit time (not construction), getEvents newest-first
- DST autumn (fall-back) boundary: CEST->CET flip at the last-Sunday-of-October instant

Verified: host tests 191/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cryptotomte cryptotomte merged commit 1de46ad into main Jul 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant