The Focus Reminder system is a non-blocking scheduler that fires attention-grabbing NeoPixel animations at user-configured intervals during active hours on selected days. Designed for ADHD hyperfocus interruption and task switching cues.
- Location:
src/main.cpp(new in v2.0) - Scope: Application-level scheduler, independent of WebUI
- Loop cycle: Called once per main loop iteration (every ~1ms)
- State stored in:
ClockSettings.focusReminder_*fields (EEPROM-backed)
checkAndFire(now) {
1. Check master enable flag
2. Get current day-of-week (from the NTP-synced epoch; filter skipped if never synced)
3. Validate day matches bitmask
4. Validate current hour in window [startHour, endHour)
5. Validate interval elapsed: (now - lastFireMs) >= (intervalMinutes * 60000ms)
6. If all pass: trigger animation + update lastFireMs
}
Reminders can invoke existing ClockRenderer methods (or fire a dedicated nudge):
triggerQuarterAnimation(now)-- Slow Comet, Dual Orbit, or Bloom RippletriggerHalfHourAnimation(now)-- Unfurl, Three Comets, or BreathetriggerHourAnimation(now)-- Ceremony, Galaxy Spin, Supernova, Comet Relay, or Deep Breath
Six dedicated nudge animations also exist (modes 6-11: Gentle Pulse, Orbiting Orb, Ripple In, Heartbeat, Slow Bloom, Firefly). All reminder animations are non-blocking millis()-based state machines; the loop, web UI, and time-keeping run throughout.
struct ClockSettings {
// ... existing 180 bytes ...
// Focus Reminders (16 bytes total)
uint8_t focusReminder_enabled; // 1 byte: 0=off, 1=on
uint8_t focusReminder_startHour; // 1 byte: 0-23
uint8_t focusReminder_endHour; // 1 byte: 0-23
uint16_t focusReminder_intervalMinutes; // 2 bytes: 1-1440
uint8_t focusReminder_daysMask; // 1 byte: bitmask Sun(0)-Sat(6)
uint8_t focusReminder_animation; // 1 byte: 0-11 (0-5 reuse chime anims, 6-11 dedicated nudges)
uint8_t focusReminder_durationSeconds; // 1 byte: reserved for v2 (currently unused)
uint32_t focusReminder_lastFireMs; // 4 bytes: reserved; fire time is tracked in RAM only
// 3 bytes reserved for future expansion
};Validation: SettingsStore::valid() checks all fields are in legal range.
Defaults: Reminder disabled, 08:00-22:00 window, 60-min interval, no days selected, animation 0 ("Use quarter animation").
Persistence: Auto-saved to EEPROM on WebUI "Save reminder" button.
- Panel: "Focus Reminders (ADHD)" (new section in main settings page)
- Controls:
- Enable/disable toggle
- Start/end hour (number inputs, 0-23)
- Interval (number input, 1-1440 minutes)
- Days-of-week selector (7 checkboxes, Sun-Sat, stored as bitmask)
- Animation dropdown (9 options: reuse the quarter/half-hour/hour chime animation, or one of the six dedicated nudges)
- Save button (POSTs to
/settingsendpoint)
- Real-time sync: Reminder config loaded at page load, updates reflected after save
- Window is half-open:
[startHour, endHour)(e.g., 08:00-22:00 fires 08:00-21:59) - Wrapped windows supported: if
startHour > endHour, window crosses midnight (e.g., 22:00-08:00) - Day-of-week: 0=Sunday, 6=Saturday (bitmask, e.g., 0x7F = all days)
- Last fire timestamp stored as
millis()at time of animation trigger - Interval calculated as:
uint32_t(intervalMinutes) * 60000ULmilliseconds - Clock wraps at ~49 days; interval logic handles correctly via unsigned arithmetic
- Fire occurs when:
(now - lastFireMs) >= intervalMs
Each fire logs:
[FocusReminder] Fired at HH:MM (interval=N min)
Enable via Serial monitor at 115200 baud.
- Scheduler call is non-blocking (< 1ms)
- Animation trigger calls
renderer_.triggerReminderDirectAnimation(mode, now) - Reminder animations are non-blocking
millis()-based state machines; nodelay()
Computed from the NTP-synced system epoch via localtime_r() (tm_wday, 0=Sun).
If the epoch was never synced (offline boot, time set via buttons/web), the day
filter is skipped rather than silently never firing; the hour window still applies.
- Firmware compiles (pio build)
- Device boots, WebUI loads, new "Focus Reminders (ADHD)" panel visible
- Enable toggle works (setting persists after reboot)
- Start/end hour inputs work (0-23 range enforced)
- Interval input works (1-1440 range, clamped)
- Days-of-week toggles set bitmask correctly
- Animation dropdown selects animation type
- Save button POSTs to
/settingsand triggers reload - Disable reminder: animation does not fire (confirmed via console log)
- Set time to within window + matching day, wait interval: animation fires
- Interval fires multiple times: every 60 min (if set to 60)
- Outside time window: no fire
- Non-matching day: no fire (requires an NTP-synced clock)