mirror of
https://github.com/google/pebble.git
synced 2025-03-22 03:32:20 +00:00
469 lines
19 KiB
Markdown
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);
|
|
}
|
|
```
|