pebble/devsite/source/_guides/events-and-services/health.md
2025-02-24 18:58:29 -08:00

469 lines
19 KiB
Markdown

---
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
title: Pebble Health
description: |
Information on using the HealthService API to incorporate multiple types of
health data into your apps.
guide_group: events-and-services
order: 6
platforms:
- basalt
- chalk
- diorite
- emery
related_docs:
- HealthService
related_examples:
- title: Simple Health Example
url: https://github.com/pebble-examples/simple-health-example
---
[Pebble Health](https://blog.getpebble.com/2015/12/15/health/) provides builtin
health data tracking to allow users to improve their activity and sleep habits.
With SDK 3.9, the ``HealthService`` API opens this data up to developers to
include and use within their apps. For example, a watchface could display a
brief summary of the user's activity for the day.
## API Availability
In order to use the ``HealthService`` (and indeed Pebble Health), the user must
enable the 'Pebble Health' app in the 'Apps/Timeline' view of the official
Pebble mobile app. If this is not enabled health data will not be available to
apps, and API calls will return values to reflect this.
In addition, any app using the ``HealthService`` API must declare the 'health'
capability in order to be accepted by the
[developer portal](https://dev-portal.getpebble.com/). This can be done in
CloudPebble 'Settings', or in `package.json` in the local SDK:
```js
"capabilities": [ "health" ]
```
Since Pebble Health is not available on the Aplite platform, developers should
check the API return values and hence the lack of ``HealthService`` on that
platform gracefully. In addition, the `PBL_HEALTH` define and
`PBL_IF_HEALTH_ELSE()` macro can be used to selectively omit affected code.
## Available Metrics
The ``HealthMetric`` `enum` lists the types of data (or 'metrics') that can be
read using the API. These are described below:
| Metric | Description |
|--------|-------------|
| `HealthMetricStepCount` | The user's step count. |
| `HealthMetricActiveSeconds` | Duration of time the user was considered 'active'. |
| `HealthMetricWalkedDistanceMeters` | Estimation of the distance travelled in meters. |
| `HealthMetricSleepSeconds` | Duration of time the user was considered asleep. |
| `HealthMetricSleepRestfulSeconds` | Duration of time the user was considered in deep restful sleep. |
| `HealthMetricRestingKCalories` | The number of kcal (thousand calories) burned due to resting metabolism. |
| `HealthMetricActiveKCalories` | The number of kcal (thousand calories) burned due to activity. |
| `HealthMetricHeartRateBPM` | The heart rate, in beats per minute. |
## Subscribing to HealthService Events
Like other Event Services, an app can subscribe a handler function to receive a
callback when new health data is available. This is useful for showing
near-realtime activity updates. The handler must be a suitable implementation of
``HealthEventHandler``. The `event` parameter describes the type of each update,
and is one of the following from the ``HealthEventType`` `enum`:
| Event Type | Value | Description |
|------------|-------|-------------|
| `HealthEventSignificantUpdate` | `0` | All data is considered as outdated, apps should re-read all health data. This can happen on a change of the day or in other cases that significantly change the underlying data. |
| `HealthEventMovementUpdate` | `1` | Recent values around `HealthMetricStepCount`, `HealthMetricActiveSeconds`, `HealthMetricWalkedDistanceMeters`, and `HealthActivityMask` changed. |
| `HealthEventSleepUpdate` | `2` | Recent values around `HealthMetricSleepSeconds`, `HealthMetricSleepRestfulSeconds`, `HealthActivitySleep`, and `HealthActivityRestfulSleep` changed. |
| `HealthEventHeartRateUpdate` | `4` | The value of `HealthMetricHeartRateBPM` has changed. |
A simple example handler is shown below, which outputs to app logs the type of
event that fired the callback:
```c
static void health_handler(HealthEventType event, void *context) {
// Which type of event occurred?
switch(event) {
case HealthEventSignificantUpdate:
APP_LOG(APP_LOG_LEVEL_INFO,
"New HealthService HealthEventSignificantUpdate event");
break;
case HealthEventMovementUpdate:
APP_LOG(APP_LOG_LEVEL_INFO,
"New HealthService HealthEventMovementUpdate event");
break;
case HealthEventSleepUpdate:
APP_LOG(APP_LOG_LEVEL_INFO,
"New HealthService HealthEventSleepUpdate event");
break;
case HealthEventHeartRateUpdate:
APP_LOG(APP_LOG_LEVEL_INFO,
"New HealthService HealthEventHeartRateUpdate event");
break;
}
}
```
The subscription is then registered in the usual way, optionally providing a
`context` parameter that is relayed to each event callback. The return value
should be used to determine whether the subscription was successful:
```c
#if defined(PBL_HEALTH)
// Attempt to subscribe
if(!health_service_events_subscribe(health_handler, NULL)) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Health not available!");
}
#else
APP_LOG(APP_LOG_LEVEL_ERROR, "Health not available!");
#endif
```
## Reading Health Data
Health data is collected in the background as part of Pebble Health regardless
of the state of the app using the ``HealthService`` API, and is available to
apps through various ``HealthService`` API functions.
Before reading any health data, it is recommended to check that data is
available for the desired time range, if applicable. In addition to the
``HealthServiceAccessibilityMask`` value, health-related code can be
conditionally compiled using `PBL_HEALTH`. For example, to check whether
any data is available for a given time range:
```c
#if defined(PBL_HEALTH)
// Use the step count metric
HealthMetric metric = HealthMetricStepCount;
// Create timestamps for midnight (the start time) and now (the end time)
time_t start = time_start_of_today();
time_t end = time(NULL);
// Check step data is available
HealthServiceAccessibilityMask mask = health_service_metric_accessible(metric,
start, end);
bool any_data_available = mask & HealthServiceAccessibilityMaskAvailable;
#else
// Health data is not available here
bool any_data_available = false;
#endif
```
Most applications will want to read the sum of a metric for the current day's
activity. This is the simplest method for accessing summaries of users' health
data, and is shown in the example below:
```c
HealthMetric metric = HealthMetricStepCount;
time_t start = time_start_of_today();
time_t end = time(NULL);
// Check the metric has data available for today
HealthServiceAccessibilityMask mask = health_service_metric_accessible(metric,
start, end);
if(mask & HealthServiceAccessibilityMaskAvailable) {
// Data is available!
APP_LOG(APP_LOG_LEVEL_INFO, "Steps today: %d",
(int)health_service_sum_today(metric));
} else {
// No data recorded yet today
APP_LOG(APP_LOG_LEVEL_ERROR, "Data unavailable!");
}
```
For more specific data queries, the API also allows developers to request data
records and sums of metrics from a specific time range. If data is available, it
can be read as a sum of all values recorded between that time range. You can use
the convenience constants from ``Time``, such as ``SECONDS_PER_HOUR`` to adjust
a timestamp relative to the current moment returned by ``time()``.
> Note: The value returned will be an average since midnight, weighted for the
> length of the specified time range. This may change in the future.
An example of this process is shown below:
```c
// Make a timestamp for now
time_t end = time(NULL);
// Make a timestamp for the last hour's worth of data
time_t start = end - SECONDS_PER_HOUR;
// Check data is available
HealthServiceAccessibilityMask result =
health_service_metric_accessible(HealthMetricStepCount, start, end);
if(result & HealthServiceAccessibilityMaskAvailable) {
// Data is available! Read it
HealthValue steps = health_service_sum(HealthMetricStepCount, start, end);
APP_LOG(APP_LOG_LEVEL_INFO, "Steps in the last hour: %d", (int)steps);
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, "No data available!");
}
```
## Representing Health Data
Depending on the locale of the user, the conventional measurement system used to
represent distances may vary between metric and imperial. For this reason it is
recommended to query the user's preferred ``MeasurementSystem`` before
formatting distance data from the ``HealthService``:
> Note: This API is currently only meaningful when querying the
> ``HealthMetricWalkedDistanceMeters`` metric. ``MeasurementSystemUnknown`` will
> be returned for all other queries.
```c
const HealthMetric metric = HealthMetricWalkedDistanceMeters;
const HealthValue distance = health_service_sum_today(metric);
// Get the preferred measurement system
MeasurementSystem system = health_service_get_measurement_system_for_display(
metric);
// Format accordingly
static char s_buffer[32];
switch(system) {
case MeasurementSystemMetric:
snprintf(s_buffer, sizeof(s_buffer), "Walked %d meters", (int)distance);
break;
case MeasurementSystemImperial: {
// Convert to imperial first
int feet = (int)((float)distance * 3.28F);
snprintf(s_buffer, sizeof(s_buffer), "Walked %d feet", (int)feet);
} break;
case MeasurementSystemUnknown:
default:
APP_LOG(APP_LOG_LEVEL_INFO, "MeasurementSystem unknown or does not apply");
}
// Display to user in correct units
text_layer_set_text(s_some_layer, s_buffer);
```
## Obtaining Averaged Data
The ``HealthService`` also allows developers to read average values of a
particular ``HealthMetric`` with varying degrees of scope. This is useful for
apps that wish to display an average value (e.g.: as a goal for the user)
alongside a summed value.
In this context, the `start` and `end` parameters specify the time period to be
used for the daily average calculation. For example, a start time of midnight
and an end time ten hours later will return the average value for the specified
metric measured until 10 AM on average across the days specified by the scope.
The ``HealthServiceTimeScope`` specified when querying for averaged data over a
given time range determines how the average is calculated, as detailed in the
table below:
| Scope Type | Description |
|------------|-------------|
| `HealthServiceTimeScopeOnce` | No average computed. The result is the same as calling ``health_service_sum()``. |
| `HealthServiceTimeScopeWeekly` | Compute average using the same day from each week (up to four weeks). For example, every Monday if the provided time range falls on a Monday. |
| `HealthServiceTimeScopeDailyWeekdayOrWeekend` | Compute average using either weekdays (Monday to Friday) or weekends (Saturday and Sunday), depending on which day the provided time range falls. |
| `HealthServiceTimeScopeDaily` | Compute average across all days of the week. |
> Note: If the difference between the start and end times is greater than one
> day, an average will be returned that takes both days into account. Similarly,
> if the time range crosses between scopes (such as including weekend days and
> weekdays with ``HealthServiceTimeScopeDailyWeekdayOrWeekend``), the start time
> will be used to determine which days are used.
Reading averaged data values works in a similar way to reading sums. The example
below shows how to read an average step count across all days of the week for a
given time range:
```c
// Define query parameters
const HealthMetric metric = HealthMetricStepCount;
const HealthServiceTimeScope scope = HealthServiceTimeScopeDaily;
// Use the average daily value from midnight to the current time
const time_t start = time_start_of_today();
const time_t end = time(NULL);
// Check that an averaged value is accessible
HealthServiceAccessibilityMask mask =
health_service_metric_averaged_accessible(metric, start, end, scope);
if(mask & HealthServiceAccessibilityMaskAvailable) {
// Average is available, read it
HealthValue average = health_service_sum_averaged(metric, start, end, scope);
APP_LOG(APP_LOG_LEVEL_INFO, "Average step count: %d steps", (int)average);
}
```
## Detecting Activities
It is possible to detect when the user is sleeping using a
``HealthActivityMask`` value. A useful application of this information could be
to disable a watchface's animations or tick at a reduced rate once the user is
asleep. This is done by checking certain bits of the returned value:
```c
// Get an activities mask
HealthActivityMask activities = health_service_peek_current_activities();
// Determine which bits are set, and hence which activity is active
if(activities & HealthActivitySleep) {
APP_LOG(APP_LOG_LEVEL_INFO, "The user is sleeping.");
} else if(activities & HealthActivityRestfulSleep) {
APP_LOG(APP_LOG_LEVEL_INFO, "The user is sleeping peacefully.");
} else {
APP_LOG(APP_LOG_LEVEL_INFO, "The user is not currently sleeping.");
}
```
## Read Per-Minute History
The ``HealthMinuteData`` structure contains multiple types of activity-related
data that are recorded in a minute-by-minute fashion. This style of data access
is best suited to those applications requiring more granular detail (such as
creating a new fitness algorithm). Up to seven days worth of data is available
with this API.
> See [*Notes on Minute-level Data*](#notes-on-minute-level-data) below for more
> information on minute-level data.
The data items contained in the ``HealthMinuteData`` structure are summarized
below:
| Item | Type | Description |
|------|------|-------------|
| `steps` | `uint8_t` | Number of steps taken in this minute. |
| `orientation` | `uint8_t` | Quantized average orientation, encoding the x-y plane (the "yaw") in the lower 4 bits (360 degrees linearly mapped to 1 of 16 values) and the z axis (the "pitch") in the upper 4 bits. |
| `vmc` | `uint16_t` | Vector Magnitude Counts (VMC). This is a measure of the total amount of movement seen by the watch. More vigorous movement yields higher VMC values. |
| `is_invalid` | `bool` | `true` if the item doesn't represent actual data, and should be ignored. |
| `heart_rate_bpm` | `uint8_t` | Heart rate in beats per minute (if available). |
These data items can be obtained in the following manner, similar to obtaining a
sum.
```c
// Create an array to store data
const uint32_t max_records = 60;
HealthMinuteData *minute_data = (HealthMinuteData*)
malloc(max_records * sizeof(HealthMinuteData));
// Make a timestamp for 15 minutes ago and an hour before that
time_t end = time(NULL) - (15 * SECONDS_PER_MINUTE);
time_t start = end - SECONDS_PER_HOUR;
// Obtain the minute-by-minute records
uint32_t num_records = health_service_get_minute_history(minute_data,
max_records, &start, &end);
APP_LOG(APP_LOG_LEVEL_INFO, "num_records: %d", (int)num_records);
// Print the number of steps for each minute
for(uint32_t i = 0; i < num_records; i++) {
APP_LOG(APP_LOG_LEVEL_INFO, "Item %d steps: %d", (int)i,
(int)minute_data[i].steps);
}
```
Don't forget to free the array once the data is finished with:
```c
// Free the array
free(minute_data);
```
### Notes on Minute-level Data
Missing minute-level records can occur if the watch is reset, goes into low
power (watch-only) mode due to critically low battery, or Pebble Health is
disabled during the time period requested.
``health_service_get_minute_history()`` will return as many **consecutive**
minute-level records that are available after the provided `start` timestamp,
skipping any missing records until one is found. This API behavior enables one
to easily continue reading data after a previous query encountered a missing
minute. If there are some minutes with missing data, the API will return all
available records up to the last available minute, and no further. Conversely,
records returned will begin with the first available record after the provided
`start` timestamp, skipping any missing records until one is found. This can
be used to continue reading data after a previous query encountered a missing
minute.
The code snippet below shows an example function that packs a provided
``HealthMinuteData`` array with all available values in a time range, up to an
arbitrary maximum number. Any missing minutes are collapsed, so that as much
data can be returned as is possible for the allocated array size and time range
requested.
> This example shows querying up to 60 records. More can be obtained, but this
> increases the heap allocation required as well as the time taken to process
> the query.
```c
static uint32_t get_available_records(HealthMinuteData *array, time_t query_start,
time_t query_end, uint32_t max_records) {
time_t next_start = query_start;
time_t next_end = query_end;
uint32_t num_records_found = 0;
// Find more records until no more are returned
while (num_records_found < max_records) {
int ask_num_records = max_records - num_records_found;
uint32_t ret_val = health_service_get_minute_history(&array[num_records_found],
ask_num_records, &next_start, &next_end);
if (ret_val == 0) {
// a 0 return value means no more data is available
return num_records_found;
}
num_records_found += ret_val;
next_start = next_end;
next_end = query_end;
}
return num_records_found;
}
static void print_last_hours_steps() {
// Query for the last hour, max 60 minute-level records
// (except the last 15 minutes)
const time_t query_end = time(NULL) - (15 * SECONDS_PER_MINUTE);
const time_t query_start = query_end - SECONDS_PER_HOUR;
const uint32_t max_records = (query_end - query_start) / SECONDS_PER_MINUTE;
HealthMinuteData *data =
(HealthMinuteData*)malloc(max_records * sizeof(HealthMinuteData));
// Populate the array
max_records = get_available_records(data, query_start, query_end, max_records);
// Print the results
for(uint32_t i = 0; i < max_records; i++) {
if(!data[i].is_invalid) {
APP_LOG(APP_LOG_LEVEL_INFO, "Record %d contains %d steps.", (int)i,
(int)data[i].steps);
} else {
APP_LOG(APP_LOG_LEVEL_INFO, "Record %d was not valid.", (int)i);
}
}
free(data);
}
```