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

19 KiB

title description guide_group order platforms related_docs related_examples
Pebble Health Information on using the HealthService API to incorporate multiple types of health data into your apps. events-and-services 6
basalt
chalk
diorite
emery
HealthService
title url
Simple Health Example https://github.com/pebble-examples/simple-health-example

Pebble 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. This can be done in CloudPebble 'Settings', or in package.json in the local SDK:

"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:

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:

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

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

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:

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

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:

// 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:

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

// 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:

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

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);
}