انتقل إلى المحتوى الرئيسي

Favorites, Scheduler, Time, and the Boot Report

state_manager collects the features that are pure state plus a little policy: favorites, the scheduler, time synchronization, and the boot report. None is individually large; together they define how trustworthy the device feels.

Favorites

A favorite is a small record (type: surah+reciter or station; identifiers; a play count), stored as one NVS blob, capped at a fixed count. The API (favorites_add/remove/toggle/is_favorite/increment_play_count) is called from the REST layer, and every mutation persists immediately (human-rate writes, per the NVS policies).

Play counts ride along in the same record and are incremented by the player on playback start of a matching item. The design accepts an honest limitation: counts are best-effort (a power cut between play and save loses one increment) because making them transactional would cost complexity for a sort key.

The scheduler

Schedules are records (time of day, weekday mask or one-shot, target, optional volume, enabled flag) in NVS, evaluated by a once-a-minute check.

The interesting design point is what happens when the clock is not trustworthy:

if (now < TIME_SANITY_FLOOR) { // epoch ~2023: "we have never synced"
log_rate_limited("Clock not synchronized yet, scheduler idle");
return;
}

An unsynced ESP32 boots believing it is 1970. Without the gate, every schedule whose time-of-day happens to match the fictional clock fires; the gate makes "no real time" mean "no time-based behavior", which is the only defensible semantic for a device that plays audio into a quiet room. This is the firmware's clearest example of failing safe over failing functional.

Fire semantics, chosen and documented rather than accidental: a matching minute fires once (the check remembers the last fired minute); missed minutes while powered off are skipped, not replayed; one-shot schedules disable themselves after firing.

Two entry points exist beyond the periodic tick: scheduler_check_now() runs one evaluation immediately (used after the RTC seeds the clock at boot, so an alarm wake fires its schedule within seconds instead of waiting for the next tick), and scheduler_next_wake() computes the soonest enabled occurrence, which is what arms the DS3231 alarm before deep sleep.

Time: SNTP and timezones

time_sync.c starts an SNTP client when EVENT_WIFI_CONNECTED arrives (subscribing before WiFi init so the first connection cannot be missed), against a configurable server (default pool.ntp.org). On sync it publishes EVENT_TIME_SYNCED, flips the flag the scheduler and the UI clock key off, and re-syncs periodically thereafter.

Timezones are stored and applied as POSIX TZ strings (EET-2EEST,M4.5.5/0,M10.5.4/24), because that is what newlib's localtime natively consumes: zero lookup tables in flash, full DST rule support, settable at runtime from the dashboard. The cost is the format's notorious inverted sign convention (UTC-2 means east of Greenwich), which the settings guide warns about. Two guards soften it: normalize_offset_tz() rewrites naive human entries (UTC+3, GMT-5, a bare UTC) into correct POSIX form before they are stored, and time_sync_init() migrates any legacy naive value still in NVS back to the configured default at boot; the Settings page still shows the resulting local time so a bad rule is caught by eye.

The module also tracks an honest time source (none, rtc, sntp, manual) for the API and the dashboard. The optional DS3231 seeds the clock at boot and receives SNTP corrections back; a manual set-time endpoint covers installs with no network at all. SNTP is authoritative: an RTC seed never masks a later sync. The original "no RTC, SNTP is enough" decision is preserved in that the RTC stays optional and probed at runtime; what changed the calculus was the scheduler's offline dead zone after power cuts. Separately, whether the clock reads 12-hour or 24-hour is a persisted flag (time_fmt in NVS, default 12-hour) that the idle clock reads when it renders the time.

The boot report

A tiny fixed array (component name + esp_err code, eight slots) that system_init appends to whenever a peripheral fails to initialize. The splash screen prints it in red; GET /api/system serves it as JSON; nothing clears it until reboot.

It earns a chapter mention because of what it changed culturally: failures that used to be one scrolled-past log line became something a user photographs and files an issue about, and the missing-otadata saga demonstrated exactly why warnings need a surface with dwell time. The implementation is deliberately too simple to fail: no allocation, no locking beyond a mutex, no formatting until render time.