Battery: subscription now scoped to guard hours + a lead-in
HealthService now subscribes only during the guard window itself, plus
a 2-hour lead-in before the window opens (WARM_LEAD_HOURS) β enough
to warm the awake baseline before guarding actually starts.
Outside both, the worker is fully unsubscribed β no more always-on
health pipeline running all day.
The 5-minute fallback timer now only runs while subscribed (window or
lead-in), instead of waking the CPU every 5 minutes, 24 hours a day.
The 120-second boosted HR sample period is unchanged β still only active
during the guard window itself, same as v2.0.0/v2.1.0.
Net effect: for a typical guard window this cuts the "always-on background
cost" from 24h/day down to roughly (window length + 2h), which should
meaningfully reduce the drain reported on v2.0.0+.
1. Up the HR drop differential for doze triggers at all levels
2. Add ability to cancel a snooze and resume guarding immediately
A full overhaul of the Tier-1 (HR + VMC) detection engine. Tier-1 was structurally unable to fire the full alarm on real hardware, and spammed nudges after a dismissal β this release fixes both, plus the appβworker dismiss desync.
Root causes fixed
- Worker never owned its HR cadence. The OS's default background HR sampling is ~10 min, and gets slower during long stillness β precisely when a nap is happening. The worker now requests a 120-second HR sample period while the guard window is open (health_service_set_heart_rate_sample_period), cancelled the moment the window closes, on relevant settings changes, and on worker shutdown. This is the app's one real battery spend β everything else still piggybacks on free OS samples.
- Missing data used to reset the detection streak instead of freezing it. peek_current_value(HeartRateBPM) returns 0 once a sample is >15 min old, and that 0 was silently zeroing progress mid-nap β the single biggest reason the full alarm almost never fired. Missing HR/VMC data now just skips the cycle; only real awake evidence resets the streak.
- Two-stage wake is now time-based, not raw event-count-based: nudge fires after β₯2 positive cycles sustained β₯4 min, full alarm after β₯3 cycles sustained β₯10 min. Same latency whether HR events arrive every 2 minutes (boost accepted) or every 10 (boost rejected by the OS).
- Dismiss now actually notifies the worker. stop_alarm() previously never sent APP_MSG_DISMISS (only snooze did), so the worker's streak rebuilt from the same low-HR state and re-fired minutes later. Dismiss now messages the worker and applies a 10-minute re-fire cooldown; nudges get their own 10-minute cooldown.
- Anchored awake baseline is now asymmetric: updates upward freely (unless exercising, which would inflate it), downward only with awake-zone movement (VMC β₯ 50) β so it can't follow you down into a nap. Seeding requires movement evidence and is clamped to a sane 40β120 BPM.
- Misc hardening: a single restless minute mid-nap no longer discards the streak (stillness gate checks the VMC trend too); HR capability probe self-heals every minute instead of only checking once at boot; active_days == 0 (possible from a stale persist key) is treated as corrupt and defaults to every day instead of silently never guarding.
Two-stage wake
Instead of jumping straight to a full repeating alarm, NapBuster now gives you a gentle heads-up first:
x1 streak (~10 min) β quiet double-pulse buzz. Watch opens to the home screen. If this stirs you, great β no alarm needed.
x2 streak (~20 min) β full repeating alarm as before, with dismiss/snooze buttons.
This is less jarring if you're only lightly dozing and responds faster to the nap onset signal.
Anchored awake HR baseline
Replaces the rolling average with a baseline that is anchored to your actual awake resting HR, and only updates when you're demonstrably awake and active at a resting pace.
The problem with rolling average: During a gradual nap onset, HR drifts down slowly β and the rolling average chases it down step by step. The threshold was never crossed because the average and the current HR moved together.
The fix: The baseline only updates in the resting-awake VMC zone (50 β€ VMC < 400)
Motion detection now uses HealthMinuteData.vmc instead of raw accel_service_peek().
Previously: the worker called accel_service_peek() on every HR event, computed β(xΒ²+yΒ²+zΒ²) by hand, and compared against an EMA baseline. This had two problems:
accel_service_peek() requires the accelerometer to be active β extra battery
A single instantaneous sample is noisy and unreliable as a stillness measure
Now: the worker reads HealthMinuteData.vmc (Vector Magnitude Count) via health_service_get_minute_history(). VMC is:
Pre-computed by the OS health subsystem β zero extra sensor power
A per-minute aggregate of all accelerometer movement β much more stable than a single sample
The same signal used by Pebble Health itself for activity tracking
The stillness threshold (VMC_STILL_THRESH = 100) maps to the documented VMC ranges: 0β100 = very still, 100β500 = light movement, 500+ = active.
Tune NapBuster to your own body
Debug telemetry
The GUARDING screen now shows a live readout of what the detection algorithm sees:
HR:68 avg:74 d:142 x1
HR β current heart rate BPM
avg β your rolling average over the last ~40β80 min
d β accelerometer deviation (lower = stiller)
x1 β consecutive trigger streak (fires alarm at x2+)
Use this to understand why it's triggering (or not), then adjust the sensitivity setting accordingly.
Detection sensitivity setting
New setting in the settings screen: Detection
Level HR drop required Best for
Sensitive 8% below average Light sleepers, hard to trigger
Balanced 13% below average Default β works for most people
Conservative 20% below average Frequent false positives
Alarm screen button labels
The alarm screen now shows labelled hints next to each button on the right edge β no more guessing which button does what at 2pm when you've just been woken up.
β² UP = snooze 10 min
β SELECT = dismiss
βΌ DOWN = snooze 30 min
Instant detection at guard window open
Previously the Tier 1 HR buffer was reset when the guard window opened, meaning NapBuster needed 3+ HR readings inside the window before it could evaluate a drop β potentially a 30β50 min blind spot at window start.
Now the HR buffer stays warm all the time (on Pebble Time 2 / Pebble 2). When the window opens, the existing baseline carries in and detection can fire on the very first reading if conditions are met.
Battery optimisation: Tier 1 analysis now piggybacks on OS HR events.
Previously the worker ran a 5-minute timer that always peeked HR. Now:
If your watch fires a HealthEventHeartRateUpdate, Tier 1 analysis runs at that moment for free (OS was already waking the CPU for the sample)
The 5-min timer checks whether a HR event arrived recently β if yes, it skips; if no (slow/disabled background sampling), it peeks HR itself as a fallback
Net result: on the default 10-min HR sampling setting, the timer almost never does any work. Behaviour is identical, battery usage is lower.
Two-tier sleep detection: HR+accel early warning (~10-15 min) on Pebble Time 2 and Pebble 2, with HealthService sleep event fallback on all platforms.
Stop accidentally falling asleep when you don't mean to.
NapBuster runs silently in the background and triggers a vibration alarm if your Pebble detects sleep during your configured guard window.
Features
π’ Background sleep monitoring β works even when the app is closed
β° Configurable guard window β set the hours you want protected (default 11amβ11pm)
π
Day picker β choose which days to guard (any combination)
π€ Smart snooze β snooze the alarm for 15/30/60 minutes via Wakeup API
π³ Vibration strength β Gentle, Medium, or Strong pulse patterns
π 4 visual states β GUARDING / OFF-HOURS / SNOOZED / DISABLED with ASCII faces
π Battery-conscious β HealthService only active during guard window