Loading...
Searching...
No Matches
health_store.c File Reference

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.

Detailed Description

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.

Macro Definition Documentation

◆ HR_HISTORY_MINUTES

#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.

◆ STEP_CATCHUP_HOURS

#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.

Function Documentation

◆ cadence_poll()

void cadence_poll ( void )
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.

◆ hour_start()

time_t hour_start ( const struct tm * today,
int hour )
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.

Parameters
todayToday's local time, broken apart.
hourThe hour of the day, 0 to 24.
Returns
The epoch the hour starts at.

◆ hr_history_advance()

void hr_history_advance ( time_t now_min)
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.

Parameters
now_minThe current wall-clock minute (time / SECONDS_PER_MINUTE).

◆ metric_available()

bool metric_available ( HealthMetric metric,
time_t start,
time_t end )
static

Whether the watch actually has a reading for a metric over a time span.

Parameters
metricThe health metric to check.
startStart of the span to check.
endEnd of the span to check.
Returns
True when the watch reports the metric available over that span.

◆ notify_if_moved()

void notify_if_moved ( uint32_t before)
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.

Parameters
beforeThe sum of the readings before the refresh.

◆ on_health_event()

void on_health_event ( HealthEventType event,
void * context )
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.

Parameters
eventThe health event type.
contextContext (unused).

◆ persist_save()

void persist_save ( void )
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.

◆ read_hr()

int read_hr ( void )
static

Peek the current heart rate.

Returns
The reading in beats per minute, or 0 when there is none.

◆ read_hr_history()

void read_hr_history ( time_t now_min)
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.

Parameters
now_minThe minute the window's last slot holds, the same one the live readings write.

◆ read_step_hourly()

void read_step_hourly ( void )
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.

◆ read_sum_today()

int read_sum_today ( HealthMetric metric,
int fallback )
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.

Parameters
metricThe health metric to sum.
fallbackReturned when the metric is unavailable.
Returns
The sum for today, or fallback.

◆ refresh_activity()

void refresh_activity ( bool force)
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.

Parameters
forceRead now regardless of the minute gate, for the seed read and for the significant update that calls every number stale.

◆ refresh_hr()

void refresh_hr ( void )
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.

◆ scratch_take()

HealthMinuteData * scratch_take ( int records)
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.

Parameters
recordsHow many minute records to make room for.
Returns
The scratch buffer, or NULL if the allocation failed.

◆ steps_first_read()

void steps_first_read ( void * data)
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.

Parameters
dataThe timer context (unused).