The active health store: reads the watch's health service and holds the numbers. It owns the service accessors too so it is the single point of truth for health. More...
#include "io/stores/health_store.h"#include "math/scale.h"#include "io/stores/store_cadence.h"#include "io/stores/store_persist.h"#include "health/minute_window.h"#include "health/step_hours.h"#include <stdlib.h>#include <string.h>#include <time.h>Data Structures | |
| struct | HealthSaved |
| The only part worth keeping across a relaunch. More... | |
| struct | [struct].s_state |
Macros | |
| #define | HR_HISTORY_MINUTES 60 |
| How many minutes of heart rate the graph shows. | |
| #define | STEP_CATCHUP_HOURS 6 |
| How many hours the catch-up asks for at a time. | |
Functions | |
| static bool | metric_available (HealthMetric metric, time_t start, time_t end) |
| Whether the watch actually has a reading for a metric over a time span. | |
| static int | read_hr (void) |
| Peek the current heart rate. | |
| static int | read_sum_today (HealthMetric metric, int fallback) |
| Sum a metric over today, or return the fallback when it is unavailable. | |
| static HealthMinuteData * | scratch_take (int records) |
| Allocate scratch room for a minute-history read, off the heap for the length of the read rather than sitting in the app's fixed footprint. | |
| static void | read_hr_history (time_t now_min) |
| Fill the empty minutes of the heart rate window from the watch's own minute log. | |
| static time_t | hour_start (const struct tm *today, int hour) |
| When a local hour of today starts. | |
| static void | read_step_hourly (void) |
| Fill the per-hour step buckets from midnight up to the current hour. | |
| static void | persist_save (void) |
| Stash the heart rate graph so a relaunch can restore it. | |
| static void | hr_history_advance (time_t now_min) |
| Slide the rolling heart rate window forward to now_min, zeroing any minutes that passed with no reading. The last slot always holds the current minute, so the chart reads oldest on the left and newest on the right. | |
| static void | refresh_hr (void) |
| Refresh the heart rate, and append the live reading to the rolling window when the face asked for one. A heart rate is never really 0, so 0 is kept as "no reading yet". | |
| static void | refresh_activity (bool force) |
| Refresh the day's activity totals, steps, distance, calories, sleep, and active time, and the hourly step counts. | |
| static void | steps_first_read (void *data) |
| The first read of the hourly buckets, once the face is up. | |
| static void | notify_if_moved (uint32_t before) |
| Asks the face for a repaint only when a reading really moved. | |
| static void | on_health_event (HealthEventType event, void *context) |
| Health event handler: refresh only the metrics the event touched, then notify. | |
| static void | cadence_poll (void) |
| The store's turn on the face's cadence, registered at init so no face wires it by hand. | |
| void | health_store_subscribe (void(*cb)(void)) |
| Hand it the function to call when the numbers change (the screen redraw). | |
| void | health_store_init (HealthConfig cfg, const HealthSeed *seed) |
| Start the store with its rules. Pass seed = NULL for normal use. | |
| uint8_t * | health_store_hr_history (void) |
| The last hour of heart rate readings, one per minute (a 60 slot buffer). The store fills it on each update. The little charts read it back out. | |
| const uint16_t * | health_store_step_hourly (void) |
| Today's steps split into per-hour buckets, one slot per hour from midnight (24 slots). The bar chart reads it back out. | |
| int | health_store_step_hours (void) |
| How many of the hourly step buckets hold a real value (0 to 24). Steps count 0 as a real reading (a quiet hour), so a chart needs the count rather than a no-data marker. | |
| int | health_store_hr (void) |
| The heart rate in beats per minute, or -1 if we have not got one yet. | |
| int | health_store_steps (void) |
| The steps taken so far today, or -1 if we have not got one yet. | |
| int | health_store_calories (void) |
| The active calories so far today, or -1 if we have not got one yet. | |
| int | health_store_sleep_min (void) |
| The minutes of sleep, or -1 if we have not got one yet. | |
| int | health_store_active_min (void) |
| The active minutes so far today, or -1 if we have not got one yet. | |
| int | health_store_distance_m (void) |
| Today's walked distance in meters, or 0 if we cannot read it. | |
Variables | |
| static struct | s_state |
| Every health number the store holds. | |
| static void(*) | s_cb (void) |
| Called whenever a reading changes, so the face can redraw. | |
| static bool | s_live |
| True once subscribed to the live health service, so the minute poll does nothing in seed mode. | |
| static bool | s_steps_pending |
| True between init and the first bucket read, which is held back until the face has painted. | |
| static uint32_t | s_persist_key |
| The persist slot the face handed us for the saved history. | |
History series | |
The two history series cost real work, so only a face that graphs one asks for it. The current readings are cheap and every face shows one, so those are always tracked. | |
| static bool | s_hr_history |
| Whether the face graphs heart rate history. | |
| static bool | s_step_history |
| Whether the face graphs steps by the hour. | |
Costly readings | |
These four cost a flash read every time they are asked for, so only a face that shows one pays. | |
| static bool | s_sleep |
| Whether the face shows sleep. | |
| static bool | s_active |
| Whether the face shows active minutes. | |
| static bool | s_calories |
| Whether the face shows calories. | |
| static bool | s_distance |
| Whether the face shows distance. | |
Settled step hours | |
Finished hours can never gain another step, so they are read once and kept. These live out here because init clears the buckets, and stale beliefs about them would strand the cleared ones. | |
| static time_t | s_cached_day |
| The day the kept hours belong to. | |
| static int | s_settled_hours |
| How many finished hours are already read and kept. | |
The active health store: reads the watch's health service and holds the numbers. It owns the service accessors too so it is the single point of truth for health.
| #define HR_HISTORY_MINUTES 60 |
How many minutes of heart rate the graph shows.
It has nothing to do with an hour having 60 minutes, even though it lands on the same number.
| #define STEP_CATCHUP_HOURS 6 |
How many hours the catch-up asks for at a time.
A read costs the same whatever window it asks for, so a wider one gets most of a day's catch-up done in four goes instead of twenty-four.
|
static |
The store's turn on the face's cadence, registered at init so no face wires it by hand.
The events alone are not enough. Heart rate events are sparse, so the graph would be dots not a line, and movement events stop the moment the wearer goes still, stranding the step count. The gates inside the two refreshes make this free on a turn an event already covered.
|
static |
When a local hour of today starts.
Counted on the wall clock rather than as hours since midnight, because on the days the clocks change the two part ways by an hour. An hour past the end of the day rolls onto the next midnight, and the hour a spring change skips starts where the one after it does.
| today | Today's local time, broken apart. |
| hour | The hour of the day, 0 to 24. |
|
static |
Slide the rolling heart rate window forward to now_min, zeroing any minutes that passed with no reading. The last slot always holds the current minute, so the chart reads oldest on the left and newest on the right.
| now_min | The current wall-clock minute (time / SECONDS_PER_MINUTE). |
|
static |
Whether the watch actually has a reading for a metric over a time span.
| metric | The health metric to check. |
| start | Start of the span to check. |
| end | End of the span to check. |
|
static |
Asks the face for a repaint only when a reading really moved.
The minute tick and most movement events bring back the same numbers, and a repaint for nothing still reruns every health panel. A face graphing the heart rate window still gets one a minute, since the window slides. It compares sums rather than a copy of every reading, which keeps the readings off the stack.
| before | The sum of the readings before the refresh. |
|
static |
Health event handler: refresh only the metrics the event touched, then notify.
A movement event does not change the heart rate history and an hr event does not change the step count, so each reads only its own group. The minute gate inside refresh_activity does the rest, turning most movement events into nothing at all.
| event | The health event type. |
| context | Context (unused). |
|
static |
Stash the heart rate graph so a relaunch can restore it.
Only a live face writes, so seed mode never touches storage. A burst can land a reading a second and a flash write blocks, so the saves are held to one a minute, which is all the graph gains anyway.
One a minute is the rate to keep, not one to trim. The save runs from whichever comes first in a new minute, the face's cadence turn or a health event that refreshes the heart rate. Either way the CPU is already awake for it, so no extra wake is spent. The face is closed whenever the wearer opens a watchapp, even for a moment, and a graph saved less often comes back missing its last minutes every time.
|
static |
Peek the current heart rate.
|
static |
Fill the empty minutes of the heart rate window from the watch's own minute log.
Used at launch, on an empty window or on a restored one slid forward over the time the face was away, so the minutes spent in a watchapp show what the watch logged. A minute that already holds a reading keeps it, since the live readings can be more frequent than the log. Each reading lands in the slot for its own minute.
| now_min | The minute the window's last slot holds, the same one the live readings write. |
|
static |
Fill the per-hour step buckets from midnight up to the current hour.
step_hours records how many buckets are real so the chart can tell a quiet hour from one that has not happened yet.
An hour is read as it rolls over and once more the minute after, then never again. The hour in progress is today's total less the settled ones, which is free. Each real read blocks on a scan of the watch's whole minute log, so they stay off the minute path.
|
static |
Sum a metric over today, or return the fallback when it is unavailable.
Distance passes 0 as its fallback, and a panel tells no data from a real zero by the steps count. The rest, steps included, pass -1 so a panel can tell "no data yet" apart from a real zero and show a placeholder.
| metric | The health metric to sum. |
| fallback | Returned when the metric is unavailable. |
fallback.
|
static |
Refresh the day's activity totals, steps, distance, calories, sleep, and active time, and the hourly step counts.
These are whole-day sums that move once a minute at most, but movement events arrive every few seconds while walking, and every metric bar steps costs a blocking flash read. So the reads are held to one round a minute, and the four a face has to ask for are skipped unless it has.
| force | Read now regardless of the minute gate, for the seed read and for the significant update that calls every number stale. |
|
static |
Refresh the heart rate, and append the live reading to the rolling window when the face asked for one. A heart rate is never really 0, so 0 is kept as "no reading yet".
The chart reads from this window rather than the minute history: the watch only logs a per-minute heart_rate_bpm now and then in the background, so the minute history is mostly empty, while peeking the live value on each event fills the window with real readings.
|
static |
Allocate scratch room for a minute-history read, off the heap for the length of the read rather than sitting in the app's fixed footprint.
What a Pebble app runs out of first is its image, and the heap does not count towards that.
| records | How many minute records to make room for. |
|
static |
The first read of the hourly buckets, once the face is up.
Launching is the one time the buckets have a whole day to catch up on, which is several blocking scans even batched. Doing that inside init leaves the old screen frozen until it finishes.
| data | The timer context (unused). |