Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,547 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "applib/accel_service_private.h"
#include "applib/health_service.h"
#include "util/attributes.h"
#include "util/time/time.h"
// Max # of days of history we store
#define ACTIVITY_HISTORY_DAYS 30
// The max number of activity sessions we collect and cache at a time. Usually, there will only be
// about 4 or 5 sleep sessions (1 container and a handful of restful periods) in a night and
// a handful of walk and/or run sessions. Allocating space for 32 to should be more than enough.
#define ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT 32
// Number of calories in a kcalorie
#define ACTIVITY_CALORIES_PER_KCAL 1000
// Values for ActivitySettingGender
typedef enum {
ActivityGenderFemale = 0,
ActivityGenderMale = 1,
ActivityGenderOther = 2
} ActivityGender;
// Activity Settings Struct, for storing to prefs
typedef struct PACKED ActivitySettings {
int16_t height_mm;
int16_t weight_dag;
bool tracking_enabled;
bool activity_insights_enabled;
bool sleep_insights_enabled;
int8_t age_years;
int8_t gender;
} ActivitySettings;
// Heart Rate Preferences Struct, for storing to prefs
typedef struct PACKED HeartRatePreferences {
uint8_t resting_hr;
uint8_t elevated_hr;
uint8_t max_hr;
uint8_t zone1_threshold;
uint8_t zone2_threshold;
uint8_t zone3_threshold;
} HeartRatePreferences;
// Activity HRM Settings Struct, for storing to prefs
typedef struct PACKED ActivityHRMSettings {
bool enabled;
} ActivityHRMSettings;
// Default values, taken from http://www.cdc.gov/nchs/fastats/body-measurements.htm
#define ACTIVITY_DEFAULT_HEIGHT_MM 1620 // 5'3.8"
// dag - decagram (10 g)
#define ACTIVITY_DEFAULT_WEIGHT_DAG 7539 // 166.2 lbs
#define ACTIVITY_DEFAULT_GENDER ActivityGenderFemale
#define ACTIVITY_DEFAULT_AGE_YEARS 30
#define ACTIVITY_DEFAULT_PREFERENCES { \
.tracking_enabled = false, \
.activity_insights_enabled = false, \
.sleep_insights_enabled = false, \
.age_years = ACTIVITY_DEFAULT_AGE_YEARS, \
.gender = ACTIVITY_DEFAULT_GENDER, \
.height_mm = ACTIVITY_DEFAULT_HEIGHT_MM, \
.weight_dag = ACTIVITY_DEFAULT_WEIGHT_DAG, \
}
#define ACTIVITY_HEART_RATE_DEFAULT_PREFERENCES { \
.resting_hr = 70, \
.elevated_hr = 100, \
.max_hr = 220 - ACTIVITY_DEFAULT_AGE_YEARS, \
.zone1_threshold = 130 /* 50% of HRR */, \
.zone2_threshold = 154 /* 70% of HRR */, \
.zone3_threshold = 172 /* 85% of HRR */, \
}
#define ACTIVITY_HRM_DEFAULT_PREFERENCES { \
.enabled = true, \
}
// We consider values outside of this range to be invalid
// In the future we could pick these values based on user history
#define ACTIVITY_DEFAULT_MIN_HR 40
#define ACTIVITY_DEFAULT_MAX_HR 200
// Activity metric enums, accepted by activity_get_metric()
typedef enum {
ActivityMetricFirst = 0,
ActivityMetricStepCount = ActivityMetricFirst,
ActivityMetricActiveSeconds,
ActivityMetricRestingKCalories,
ActivityMetricActiveKCalories,
ActivityMetricDistanceMeters,
ActivityMetricSleepTotalSeconds,
ActivityMetricSleepRestfulSeconds,
ActivityMetricSleepEnterAtSeconds, // What time the user fell asleep. Measured in
// seconds after midnight.
ActivityMetricSleepExitAtSeconds, // What time the user woke up. Measured in
// seconds after midnight
ActivityMetricSleepState, // returns an ActivitySleepState enum value
ActivityMetricSleepStateSeconds, // how many seconds we've been in the
// ActivityMetricSleepState state
ActivityMetricLastVMC,
ActivityMetricHeartRateRawBPM, // Most recent heart rate reading
ActivityMetricHeartRateRawQuality, // Heart rate signal quality
ActivityMetricHeartRateRawUpdatedTimeUTC, // UTC of last heart rate update
ActivityMetricHeartRateFilteredBPM, // Most recent "Stable (median)" HR reading
ActivityMetricHeartRateFilteredUpdatedTimeUTC, // UTC of last stable HR reading
ActivityMetricHeartRateZone1Minutes,
ActivityMetricHeartRateZone2Minutes,
ActivityMetricHeartRateZone3Minutes,
// KEEP THIS AT THE END
ActivityMetricNumMetrics,
ActivityMetricInvalid = ActivityMetricNumMetrics,
} ActivityMetric;
// Activity session types, used in ActivitySession struct
typedef enum {
ActivitySessionType_None = 0,
// ActivityType_Sleep encapsulates an entire sleep session from sleep entry to wake, and
// contains both light and deep sleep periods. An ActivityType_DeepSleep session identifies
// a restful period and its start and end times will always be inside of a ActivityType_Sleep
// session.
ActivitySessionType_Sleep = 1,
// A restful period, these will always be inside of a ActivityType_Sleep session
ActivitySessionType_RestfulSleep = 2,
// Like ActivityType_Sleep, but labeled as a nap because of its duration and time (as
// compared to the assumed nightly sleep).
ActivitySessionType_Nap = 3,
// A restful period that was part of a nap, these will always be inside of a
// ActivityType_Nap session
ActivitySessionType_RestfulNap = 4,
// A "significant" length walk
ActivitySessionType_Walk = 5,
// A run
ActivitySessionType_Run = 6,
// Open workout. Basically a catch all / generic activity type
ActivitySessionType_Open = 7,
// Leave at end
ActivitySessionTypeCount,
ActivitySessionType_Invalid = ActivitySessionTypeCount,
} ActivitySessionType;
// Sleep state, used in AlgorithmStateMinuteData and to express possible values of
// ActivityMetricSleepState when calling activity_get_metric().
typedef enum {
ActivitySleepStateAwake = 0,
ActivitySleepStateRestfulSleep,
ActivitySleepStateLightSleep,
ActivitySleepStateUnknown,
} ActivitySleepState;
// Data included for stepping related activities.
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
// an update to documentation on this wiki page:
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
typedef struct PACKED {
uint16_t steps; // number of steps
uint16_t active_kcalories; // number of active kcalories
uint16_t resting_kcalories; // number of resting kcalories
uint16_t distance_meters; // distance covered
} ActivitySessionDataStepping;
// Data included for sleep related activities
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
// an update to documentation on this wiki page:
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
typedef struct {
} ActivitySessionDataSleeping;
#define ACTIVITY_SESSION_MAX_LENGTH_MIN MINUTES_PER_DAY
typedef struct PACKED {
time_t start_utc; // session start time
uint16_t length_min; // length of session in minutes
ActivitySessionType type:8; // type of activity
union {
struct {
uint8_t ongoing:1; // activity still ongoing
uint8_t manual:1; // activity is a manual one
uint8_t reserved:6;
};
uint8_t flags;
};
union {
ActivitySessionDataStepping step_data;
ActivitySessionDataSleeping sleep_data;
};
} ActivitySession;
// Structure of data logging records generated by raw sample collection
// Each of the 32bit samples in the record is encoded as follows:
// Each axis is encoded into 10 bits, by shifting the 16-bit raw value right by 3 bits and
// masking with 0x3FF. This is done because the max dynamic range of an axis is +/- 4000 and
// the least significant 3 bits are more or less noise.
// 0bxx 10bits_x 10bits_y 10bits_z The accel sensor generated a run of 0bxx samples with
// the given x, y, and z values
#define ACTIVITY_RAW_SAMPLES_VERSION 2
#define ACTIVITY_RAW_SAMPLES_MAX_ENTRIES 25
// Utilities for the encoded samples collected by raw sample collection.
#define ACTIVITY_RAW_SAMPLE_VALUE_BITS (10)
#define ACTIVITY_RAW_SAMPLE_VALUE_MASK (0x03FF) // 10 bits per axis
// We throw away the least significant 3 bits and keep only 10 bits per axix. The + 4 is used
// so that we round to nearest instead of rounding down as a result of the shift right
#define ACTIVITY_RAW_SAMPLE_SHIFT 3
#define ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(x) ((((x) + 4) >> ACTIVITY_RAW_SAMPLE_SHIFT) \
& ACTIVITY_RAW_SAMPLE_VALUE_MASK)
#define ACTIVITY_RAW_SAMPLE_MAX_RUN_SIZE 3
#define ACTIVITY_RAW_SAMPLE_GET_RUN_SIZE(s) ((s) >> (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS))
#define ACTIVITY_RAW_SAMPLE_SET_RUN_SIZE(s, r) (s |= (r) << (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS))
#define ACTIVITY_RAW_SAMPLE_SIGN_EXTEND(x) ((x) & 0x1000 ? -1 * (0x2000 - (x)) : (x))
#define ACTIVITY_RAW_SAMPLE_GET_X(s) \
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND((((uint32_t)s >> (2 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
#define ACTIVITY_RAW_SAMPLE_GET_Y(s) \
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND(((s >> ACTIVITY_RAW_SAMPLE_VALUE_BITS) \
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
#define ACTIVITY_RAW_SAMPLE_GET_Z(s) \
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND((s \
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
#define ACTIVITY_RAW_SAMPLE_ENCODE(run_size, x, y, z) \
((run_size) << (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
| (ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(x) << (2 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
| (ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(y) << ACTIVITY_RAW_SAMPLE_VALUE_BITS) \
| ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(z)
#define ACTIVITY_RAW_SAMPLE_FLAG_FIRST_RECORD 0x01 // Set for first record of session
#define ACTIVITY_RAW_SAMPLE_FLAG_LAST_RECORD 0x02 // set for last record of session
typedef struct __attribute__((__packed__)) {
uint16_t version; // Set to ACTIVITY_RAW_SAMPLE_VERSION
uint16_t session_id; // raw sample session id
uint32_t time_local; // local time
uint8_t flags; // one or more of ACTIVITY_RAW_SAMPLE_FLAG_.*
uint8_t len; // length of this blob, including this entire header
uint8_t num_samples; // number of uncompressed samples that this blob represents
uint8_t num_entries; // number of elements in the entries array below
uint32_t entries[ACTIVITY_RAW_SAMPLES_MAX_ENTRIES];
// array of entries, each entry can represent multiple samples
// if we detect run lengths
} ActivityRawSamplesRecord;
//! Init the activity tracking service. This does not start it up - to start it up call
//! activity_start_tracking();
//! @return true if successfully initialized
bool activity_init(void);
//! Start the activity tracking service. This starts sampling of the accelerometer
//! @param test_mode if true, samples must be fed in using activity_feed_samples()
//! @return true if successfully started
bool activity_start_tracking(bool test_mode);
//! Stop the activity tracking service.
//! @return true if successfully stopped
bool activity_stop_tracking(void);
//! Return true if activity tracking is currently running
//! @return true if activity tracking is currently running
bool activity_tracking_on(void);
//! Enable/disable the activity service. This callback is ONLY for use by the service manager's
//! services_set_runlevel() method. If false gets passed to this method, then tracking is
//! turned off regardless of the state as set by activity_start_tracking/activity_stop_tracking.
void activity_set_enabled(bool enable);
// Functions for getting and setting the activity preferences (defined in shell/normal/prefs.c)
//! Enable/disable activity tracking and store new setting in prefs for the next reboot
//! @param enable if true, enable activity tracking
void activity_prefs_tracking_set_enabled(bool enable);
//! Returns true if activity tracking is enabled
bool activity_prefs_tracking_is_enabled(void);
//! Records the current time when called. Used to determine when activity was first used
// so that we can send insights X days after activation
void activity_prefs_set_activated(void);
//! @return The utc timestamp of the first call to activity_prefs_set_activated()
//! returns 0 if activity_prefs_set_activated() has never been called
time_t activity_prefs_get_activation_time(void);
typedef enum ActivationDelayInsightType ActivationDelayInsightType;
//! @return True if the activation delay insight has fired
bool activity_prefs_has_activation_delay_insight_fired(ActivationDelayInsightType type);
//! @return Mark an activation delay insight as having fired
void activity_prefs_set_activation_delay_insight_fired(ActivationDelayInsightType type);
//! @return Which version of the health app was last opened
//! @note 0 is "never opened"
uint8_t activity_prefs_get_health_app_opened_version(void);
//! @return Record that the health app has been opened at a given version
void activity_prefs_set_health_app_opened_version(uint8_t version);
//! @return Which version of the workout app was last opened
//! @note 0 is "never opened"
uint8_t activity_prefs_get_workout_app_opened_version(void);
//! @return Record that the workout app has been opened at a given version
void activity_prefs_set_workout_app_opened_version(uint8_t version);
//! Enable/disable activity insights
//! @param enable if true, enable activity insights
void activity_prefs_activity_insights_set_enabled(bool enable);
//! Returns true if activity insights are enabled
bool activity_prefs_activity_insights_are_enabled(void);
//! Enable/disable sleep insights
//! @param enable if true, enable sleep insights
void activity_prefs_sleep_insights_set_enabled(bool enable);
//! Returns true if sleep insights are enabled
bool activity_prefs_sleep_insights_are_enabled(void);
//! Set the user height
//! @param height_mm the height in mm
void activity_prefs_set_height_mm(uint16_t height_mm);
//! Get the user height
//! @return the user's height in mm
uint16_t activity_prefs_get_height_mm(void);
//! Set the user weight
//! @param weight_dag the weight in dag (decagrams)
void activity_prefs_set_weight_dag(uint16_t weight_dag);
//! Get the user weight
//! @return the user's weight in dag
uint16_t activity_prefs_get_weight_dag(void);
//! Set the user's gender
//! @param gender the new gender
void activity_prefs_set_gender(ActivityGender gender);
//! Get the user's gender
//! @return the user's set gender
ActivityGender activity_prefs_get_gender(void);
//! Set the user's age
//! @param age_years the user's age in years
void activity_prefs_set_age_years(uint8_t age_years);
//! Get the user's age in years
//! @return the user's age in years
uint8_t activity_prefs_get_age_years(void);
//! Get the user's resting heart rate
uint8_t activity_prefs_heart_get_resting_hr(void);
//! Get the user's elevated heart rate
uint8_t activity_prefs_heart_get_elevated_hr(void);
//! Get the user's max heart rate
uint8_t activity_prefs_heart_get_max_hr(void);
//! Get the user's hr zone1 threshold (lowest HR in zone 1)
uint8_t activity_prefs_heart_get_zone1_threshold(void);
//! Get the user's hr zone2 threshold (lowest HR in zone 2)
uint8_t activity_prefs_heart_get_zone2_threshold(void);
//! Get the user's hr zone3 threshold (lowest HR in zone 3)
uint8_t activity_prefs_heart_get_zone3_threshold(void);
//! Return true if the HRM is enabled, false if not
bool activity_prefs_heart_rate_is_enabled(void);
//! Get the current and (optionally) historical values for a given metric. The caller passes
//! in a pointer to an array that will be filled in with the results (current value for today at
//! index 0, yesterday's at index 1, etc.)
//! @param[in] metric which metric to fetch
//! @param[in] history_len This must contain the length of the history array being passed in (as
//! number of entries). To determine a max size for this array, call
//! health_service_max_days_history().
//! @param[out] history pointer to int32_t array that will contain the returned metric. The current
//! value will be at index 0, yesterday's at index 1, etc. For days where no history is
//! available, -1 will be written. For some metrics, like HealthMetricActiveDayID and
//! HealthMetricSleepDayID, history is not applicable, so all entries past entry 0 will
//! always be filled in with -1.
//! @return true on success, false on failure
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history);
//! Get the typical value for a metric on a given day of the week
bool activity_get_metric_typical(ActivityMetric metric, DayInWeek day, int32_t *value_out);
//! Get the value for a metric over the last 4 weeks
bool activity_get_metric_monthly_avg(ActivityMetric metric, int32_t *value_out);
//! Get detailed info about activity sessions. This fills in an array with info on all of the
//! activity sessions that ended after 12am (midnight) of the current day. The caller must allocate
//! space for the array and tell this method how many entries the array can hold
//! ("session_entries"). This call returns the actual number of entries required, which may be
//! greater or less than the passed in size. If it is greater, only the first session_entries are
//! filled in.
//! @param[in,out] *session_entries size of sessions array (as number of elements) on entry.
//! On exit, this is set to the number of entries required to hold all sessions.
//! @param[out] sessions this array is filled in with the list of sessions.
//! @return true on success, false on failure
bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions);
//! Return historical minute data.
//! IMPORTANT: This call will block on KernelBG, so it can only be called from the app or
//! worker task.
//! @param[in] minute_data pointer to an array of HealthMinuteData records that will be filled
//! in with the historical minute data
//! @param[in,out] num_records On entry, the number of records the minute_data array can hold.
//! On exit, the number of records in the minute data array that were written, including
//! any missing minutes between 0 and *num_records. To check to see if a specific minute is
//! missing, compare the vmc value of that record to HEALTH_MINUTE_DATA_MISSING_VMC_VALUE.
//! @param[in,out] utc_start on entry, the UTC time of the first requested record. On exit,
//! the UTC time of the first record returned.
//! @return true on success, false on failure
bool activity_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
time_t *utc_start);
// Metric averages, returned by activity_get_step_averages()
#define ACTIVITY_NUM_METRIC_AVERAGES (4 * 24) //!< one average for each 15 minute interval of a day
#define ACTIVITY_METRIC_AVERAGES_UNKNOWN 0xFFFF //!< indicates the average is unknown
typedef struct {
uint16_t average[ACTIVITY_NUM_METRIC_AVERAGES];
} ActivityMetricAverages;
//! Return step averages.
//! @param[in] day_of_week day of the week to get averages for. Sunday: 0, Monday: 1, etc.
//! @param[out] averages pointer to ActivityStepAverages structure that will be filled
//! in with the step averages.
//! @return true on success, false on failure
bool activity_get_step_averages(DayInWeek day_of_week, ActivityMetricAverages *averages);
//! Control raw accel sample collection. This method can be used to start and stop raw
//! accel sample collection. The samples are sent to data logging with tag
//! ACTIVITY_DLS_TAG_RAW_SAMPLES and also PBL_LOG messages are generated by base64 encoding the
//! data (so that it can be sent in a support request). Every time raw sample collection is
//! enabled, a new raw sample session id is created. This session id is saved along with the
//! samples and can be displayed to the user in the watch UI to help later identify specific
//! sessions.
//! @param[in] enable if true, enable sample collection
//! @param[in] disable if true, disable sample collection
//! @param[out] *enabled true if sample collection is currently enabled
//! @param[out] *session_id the current raw sample session id. If sampling is currently disabled,
//! this is the session id of the most recently ended session.
//! @param[out] *num_samples the number of samples collected for the current session. If sampling is
//! currently disabled, this is the number of samples collected in the most recently
//! ended session.
//! @param[out] *seconds the number of seconds of data collected for the current session. If
//! sampling is currently disabled, this is the number of seconds of data in the most recently
//! ended session.
//! @return true on success, false on error
bool activity_raw_sample_collection(bool enable, bool disable, bool *enabled,
uint32_t *session_id, uint32_t *num_samples, uint32_t *seconds);
//! Dump the current sleep data using PBL_LOG. We write out base64 encoded data using PBL_LOG
//! so that it can be extracted using a support request.
//! @return true on success, false on error
//! IMPORTANT: This call will block on KernelBG, so it can only be called from the app or
//! worker task.
bool activity_dump_sleep_log(void);
//! Used by test apps (running on firmware): feed in samples, bypassing the accelerometer.
//! In order to use this, you must have called activity_start_tracking(test_mode = true);
//! @param[in] data array of samples to feed in
//! @param[in] num_samples number of samples in the data array
//! @return true on success, false on error
bool activity_test_feed_samples(AccelRawData *data, uint32_t num_samples);
//! Used by test apps (running on firmware): call the periodic minute callback. This can be used to
//! accelerate tests, to run in non-real time.
//! @return true on success, false on error
bool activity_test_run_minute_callback(void);
//! Used by test apps (running on firmware): Get info on the minute data file
//! @param[in] compact_first if true, compact the file first before getting info
//! @param[out] *num_records how many records it contains
//! @param[out] *data_bytes how many bytes of data it contains
//! @param[out] *minutes how many minutes of data it contains
//! @return true on success, false on error
bool activity_test_minute_file_info(bool compact_first, uint32_t *num_records, uint32_t *data_bytes,
uint32_t *minutes);
//! Used by test apps (running on firmware): Fill up the minute data file with as much data as
//! possible. Used for testing performance of compaction and checking for watchdog timeouts when
//! the file gets very large.
//! @return true on success, false on error
bool activity_test_fill_minute_file(void);
//! Used by test apps (running on firmware): Send fake records to data logging. This sends the
//! following records: AlgMinuteDLSRecord, ActivityLegacySleepSessionDataLoggingRecord,
//! ActivitySessionDataLoggingRecord (one for each activity type).
//! Useful for mobile app testing
//! @return true if success
bool activity_test_send_fake_dls_records(void);
//! Used by test apps (running on firmware): Set the current step count
//! Useful for testing the health app
//! @param[in] new_steps the number of steps to set the current steps to
void activity_test_set_steps_and_avg(int32_t new_steps, int32_t current_avg, int32_t daily_avg);
//! Used by test apps (running on firmware): Set the past seven days of history
//! Useful for testing the health app
void activity_test_set_steps_history();
//! Used by test apps (running on firmware): Set the past seven days of history
//! Useful for testing the health app
void activity_test_set_sleep_history();

View File

@@ -0,0 +1,257 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "applib/accel_service.h"
#include "services/normal/activity/activity.h"
#define ACTIVITY_ALGORITHM_MAX_SAMPLES 25
// Version of our minute file minute records
// Version history:
// 4: Initial version
// 5: Added the flags field and the plugged_in bit
// 5 (3/1/16): Added the active bit to flags
// 6: Added heart rate bpm
#define ALG_MINUTE_FILE_RECORD_VERSION 6
// Format of each minute in our minute file. In the minute file, which is stored as a settings file
// on the watch, we store a subset of what we send to data logging since we only need the
// information required by the sleep algorithm and the information that could be returned by
// the health_service_get_minute_history() API call.
typedef struct __attribute__((__packed__)) {
// Base fields, present in versions 4 and 5
uint8_t steps; // # of steps in this minute
uint8_t orientation; // average orientation of the watch
uint16_t vmc; // VMC (Vector Magnitude Counts) for this minute
uint8_t light; // light sensor reading divided by
// ALG_RAW_LIGHT_SENSOR_DIVIDE_BY
// New fields added in version 5
union {
struct {
uint8_t plugged_in:1;
uint8_t active:1; // This is an "active" minute
uint8_t reserved:6;
};
uint8_t flags;
};
} AlgMinuteFileSampleV5;
typedef struct __attribute__((__packed__)) {
// Base fields, present in versions <= 5
AlgMinuteFileSampleV5 v5_fields;
// New fields added in version 6
uint8_t heart_rate_bpm;
} AlgMinuteFileSample;
// Version of our minute data logging records.
// NOTE: AlgDlsMinuteData and the mobile app will continue to assume it can parse the blob,
// only appending more properties is allowed.
// Android 3.10-4.0 requires bit 2 to be set, while iOS requires the value to be <= 255.
// Available versions are: 4, 5, 6, 7, 12, 13, 14, 15, 20, ...
// Version history:
// 4: Initial version
// 5: Added the bases.flags field
// 6: Added based.flags.active, resting_calories, active_calories, and distance_cm
// 7: Added heart rate bpm
// 12: Added total heart rate weight
// 13: Added heart rate zone
// 14: ... (NYI, you decide!)
#define ALG_DLS_MINUTES_RECORD_VERSION 13
_Static_assert((ALG_DLS_MINUTES_RECORD_VERSION & (1 << 2)) > 0,
"Android 3.10-4.0 requires bit 2 to be set");
_Static_assert(ALG_DLS_MINUTES_RECORD_VERSION <= 225,
"iOS requires version less that 255");
// Format of each minute in our data logging minute records.
typedef struct __attribute__((__packed__)) {
// Base fields, which are also stored in the minute file on the watch. These are
// present in versions 4 and 5.
AlgMinuteFileSampleV5 base;
// New fields added in version 6
uint16_t resting_calories; // number of resting calories burned in this minute
uint16_t active_calories; // number of active calories burned in this minute
uint16_t distance_cm; // distance in centimeters traveled in this minute
// New fields added in version 7
uint8_t heart_rate_bpm; // weighted median hr value in this minute
// New fields added in version 12
uint16_t heart_rate_total_weight_x100; // total weight of all HR values multiplied by 100
// New fields added in version 13
uint8_t heart_rate_zone; // the hr zone for this minute
} AlgMinuteDLSSample;
// We store minute data in this struct into a circular buffer and then transfer from there to
// data logging and to the minute file in PFS as we get a batch big enough.
typedef struct {
time_t utc_sec;
AlgMinuteDLSSample data;
} AlgMinuteRecord;
// Record header. The same header is used for minute file records and minute data logging records
typedef struct __attribute__((__packed__)) {
uint16_t version; // Set to ALG_DLS_MINUTES_RECORD_VERSION or
// ALG_MINUTE_FILE_RECORD_VERSION
uint32_t time_utc; // UTC time
int8_t time_local_offset_15_min; // add this many 15 minute intervals to UTC to get local time.
uint8_t sample_size; // size in bytes of each sample
uint8_t num_samples; // # of samples included (ALG_MINUTES_PER_RECORD)
} AlgMinuteRecordHdr;
// Format of each data logging minute data record
#define ALG_MINUTES_PER_DLS_RECORD 15
typedef struct __attribute__((__packed__)) {
AlgMinuteRecordHdr hdr;
AlgMinuteDLSSample samples[ALG_MINUTES_PER_DLS_RECORD];
} AlgMinuteDLSRecord;
// Format of each minute file record
#define ALG_MINUTES_PER_FILE_RECORD 15
typedef struct __attribute__((__packed__)) {
AlgMinuteRecordHdr hdr;
AlgMinuteFileSample samples[ALG_MINUTES_PER_FILE_RECORD];
} AlgMinuteFileRecord;
// Size quota for the minute file
#define ALG_MINUTE_DATA_FILE_LEN 0x20000
// Max possible number of entries we can fit in our settings file if there was no overhead to
// the settings file at all. The actual number we can fit is less than this.
#define ALG_MINUTE_FILE_MAX_ENTRIES (ALG_MINUTE_DATA_FILE_LEN / sizeof(AlgMinuteFileRecord))
//! Init the algorithm
//! @param[out] sampling_rate the required sampling rate is returned in this variable
//! @return true if success
bool activity_algorithm_init(AccelSamplingRate *sampling_rate);
//! Called at the start of the activity teardown process
void activity_algorithm_early_deinit(void);
//! Deinit the algorithm
//! @return true if success
bool activity_algorithm_deinit(void);
//! Set the user metrics. These are used for the calorie calculation today, and possibly other
//! calculations in the future.
//! @return true if success
bool activity_algorithm_set_user(uint32_t height_mm, uint32_t weight_g, ActivityGender gender,
uint32_t age_years);
//! Process accel samples
//! @param[in] data pointer to the accel samples
//! @param[in] num_samples number of samples to process
//! @param[in] timestamp timestamp of the first sample in ms
void activity_algorithm_handle_accel(AccelRawData *data, uint32_t num_samples,
uint64_t timestamp_ms);
//! Called once per minute so the algorithm can collect minute stats and log them. This is
//! usually the data that gets used to compute sleep.
//! @param[in] utc_sec the UTC timestamp when the minute handler was first triggered
//! @param[out] record_out an AlgMinuteRecord that will be filled in
void activity_algorithm_minute_handler(time_t utc_sec, AlgMinuteRecord *record_out);
//! Return the current number of steps computed
//! @param[out] steps the number of steps is returned in this variable
//! @return true if success
bool activity_algorithm_get_steps(uint16_t *steps);
//! Tells the activity algorithm whether or not it should automatically track activities
//! @param enable true to start tracking, false to stop tracking
void activity_algorithm_enable_activity_tracking(bool enable);
//! Return the most recent stepping rate computed. This rate is returned as a number of steps
//! and an elapsed time.
//! @param[out] steps the number of steps taken during the last 'elapsed_sec' is returned in this
//! variable.
//! @param[out] elapsed_ms the number of elapsed milliseconds is returned in this variable
//! @param[out] end_sec the UTC timestamp of the last time rate was computed is returned in this
//! variable.
//! @return true if success
bool activity_algorithm_get_step_rate(uint16_t *steps, uint32_t *elapsed_ms, time_t *end_sec);
//! Reset all metrics that the algorithm tracks. Used at midnight to reset all metrics for a new
//! day and whenever new values are written into healthDB
//! @return true if success
bool activity_algorithm_metrics_changed_notification(void);
//! Set the algorithm steps to the given value. Used when first starting up the algorithm after
//! a watch reboot.
//! @param[in] steps set the number of steps to this
//! @return true if success
bool activity_algorithm_set_steps(uint16_t steps);
//! Return the timestamp of the last minute that was processed by the sleep detector.
time_t activity_algorithm_get_last_sleep_utc(void);
//! Send current minute data right away
void activity_algorithm_send_minutes(void);
//! Scan the list of activity sessions for sleep sessions and relabel the ones that should be
//! labeled as naps.
//! @param[in] num_sessions number of activity sessions
//! @param[in] sessions pointer to array of activity sessions
void activity_algorithm_post_process_sleep_sessions(uint16_t num_sessions,
ActivitySession *sessions);
//! Retrieve minute history
//! @param[in] minute_data pointer to an array of HealthMinuteData records that will be filled
//! in with the historical minute data
//! @param[in,out] num_records On entry, the number of records the minute_data array can hold.
//! On exit, the number of records in the minute data array that were written, including
//! any missing minutes between 0 and *num_records. To check to see if a specific minute is
//! missing, compare the vmc value of that record to HEALTH_MINUTE_DATA_MISSING_VMC_VALUE.
//! @param[in,out] utc_start on entry, the UTC time of the first requested record. On exit,
//! the UTC time of the first record returned.
//! @return true if success
bool activity_algorithm_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
time_t *utc_start);
//! Dump the current sleep file to PBL_LOG. We write out base64 encoded data using PBL_LOG
//! so that it can be extracted using a support request.
//! @return true if success
bool activity_algorithm_dump_minute_data_to_log(void);
//! Get info on the sleep file
//! @param[in] compact_first if true, compact the file first
//! @param[out] *num_records number of records in file
//! @param[out] *data_bytes bytes of data it contains
//! @param[out] *minutes how many minutes of data it contains
//! @return true if success
bool activity_algorithm_minute_file_info(bool compact_first, uint32_t *num_records,
uint32_t *data_bytes, uint32_t *minutes);
//! Fill the sleep file
//! @return true if success
bool activity_algorithm_test_fill_minute_file(void);
//! Send a fake minute logging record to data logging. Useful for mobile app testing
//! @return true if success
bool activity_algorithm_test_send_fake_minute_data_dls_record(void);

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2024 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.
*/
#include "activity_calculators.h"
#include "services/normal/activity/activity.h"
#include "services/normal/activity/activity_private.h"
#include "util/units.h"
#include <util/math.h>
#include <stdint.h>
#include <stdbool.h>
// ------------------------------------------------------------------------------------------------
// Compute distance (in millimeters) covered by the taking the given number of steps in the given
// amount of time.
//
// This function first computes a stride length based on the user's height, gender, and
// rate of stepping. It then multiplies the stride length by the number of steps taken to get the
// distance covered.
//
// Generally, the faster you go, the longer your stride length, and stride length is roughly
// linearly proportional to cadence. The proportionality factor though depends on height, and
// shorter users will have a steeper slope than taller users.
// The general equation for stride length is:
// stride_len = (a * steps/minute + b) * height
// where a and b depend on height and gender
//
// @param[in] steps How many steps were taken
// @param[in] ms How many milliseconds elapsed while the steps were taken
// @param[out] distance covered (in millimeters)
uint32_t activity_private_compute_distance_mm(uint32_t steps, uint32_t ms) {
if ((steps == 0) || (ms == 0)) {
return 0;
}
// For a rough ballpack figure, according to
// http://livehealthy.chron.com/determine-stride-pedometer-height-weight-4518.html
// The average stride length in mm is:
// men: 0.415 * height(mm)
// women: 0.413 * height(mm)
// An average cadence would be about 100 steps/min, so plugging in that cadence into the
// computations below should generate a stride length roughly around 0.414 * height.
//
const uint64_t steps_64 = steps;
const uint64_t ms_64 = ms;
const uint64_t height_mm_64 = activity_prefs_get_height_mm();
// Generate the 'a' factor. Eventually, this will be based on height and/or gender. For now,
// set it to .003129
const uint64_t k_a_x10000 = 31;
// Generate the 'b' factor. Eventually, this may be based on height and/or gender. For now,
// set it to 0.14485
const uint64_t k_b_x10000 = 1449;
// The factor we use to avoid fractional arithmetic
const uint64_t k_x10000 = 10000;
// We want: stride_len = (a * steps/minute + b) * height
// Since we have cadence in steps and milliseconds, this becomes:
// stride_len = (a * steps * 1000 * 60 / milliseconds + b) * height
// Compute the "(a * steps * 1000 * 60 / milliseconds + b)" component:
uint64_t stride_len_component = ROUND(k_a_x10000 * steps_64 * MS_PER_SECOND * SECONDS_PER_MINUTE,
ms_64) + k_b_x10000;
// Multiply by height to get stride_len, then by steps to get distance, then factor out our
// constant multiplier at the very end to minimize rounding errors.
uint32_t distance_mm = ROUND(stride_len_component * height_mm_64 * steps, k_x10000);
// Return distance in mm
ACTIVITY_LOG_DEBUG("Got delta distance of %"PRIu32" mm", distance_mm);
return distance_mm;
}
// ------------------------------------------------------------------------------------------------
// Compute active calories (in calories, not kcalories) covered by going the given distance in
// the given amount of time.
//
// This method uses a formula for active calories as presented in this paper:
// https://www.researchgate.net/profile/Glen_Duncan2/publication/
// 221568418_Validated_caloric_expenditure_estimation_using_a_single_body-worn_sensor/
// links/0912f4fb562b675d63000000.pdf
//
// In the paper, the formulas for walking and running compute energy in ml:
// walking:
// active_ml = 0.1 * speed_m_per_min * minutes * weight_kg
// running:
// active_ml = 0.2 * speed_m_per_min * minutes * weight_kg
//
// Converting to calories (5.01 calories per ml) and plugging in distance for speed * time, we get
// the following. We will define walking as less then 4.5MPH (120 meters/minute)
// for walking:
// active_cal = 0.1 * distance_m * weight_kg * 5.01
// = 0.501 * distance_m * weight_kg
// for running:
// active_cal = 0.2 * distance_m * weight_kg * 5.01
// = 1.002 * distance_m * weight_kg
//
// For a rough ballpack figure, a 73kg person walking 80 meters in a minute burns about
// 2925 active calories (2.9 kcalories)
// That same 73kg person running 140 meters in a minute burns about 10,240 active calories
// (10.2 kcalories)
//
// @param[in] distance_mm distance covered in millimeters
// @param[in] ms How many milliseconds elapsed while the distance was covered
// @param[out] active calories
uint32_t activity_private_compute_active_calories(uint32_t distance_mm, uint32_t ms) {
if ((distance_mm == 0) || (ms == 0)) {
return 0;
}
uint64_t distance_mm_64 = distance_mm;
uint64_t ms_64 = ms;
// Figure out the rate and see if it's walking or running. We set the walking threshold at
// 120 m/min. This is 2m/s or 2 mm/ms
const unsigned int k_max_walking_rate_mm_per_min = 120 * MM_PER_METER;
uint64_t rate_mm_per_min = distance_mm_64 * MS_PER_SECOND * SECONDS_PER_MINUTE / ms_64;
bool walking = (rate_mm_per_min <= k_max_walking_rate_mm_per_min);
uint64_t k_constant_x1000;
if (walking) {
k_constant_x1000 = 501;
} else {
k_constant_x1000 = 1002;
}
uint64_t weight_dag = activity_prefs_get_weight_dag(); // 10 grams = 1 dag
uint32_t calories = ROUND(k_constant_x1000 * (uint64_t)distance_mm * weight_dag,
1000 * MM_PER_METER * ACTIVITY_DAG_PER_KG);
// Return calories
ACTIVITY_LOG_DEBUG("Got delta active calories of %"PRIu32" ", calories);
return calories;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_private_compute_resting_calories(uint32_t elapsed_minutes) {
// This computes resting metabolic rate in calories based on the MD Mifflin and ST St jeor
// formula. This formula gives the number of kcalories expended per day
uint32_t calories_per_day;
ActivityGender gender = activity_prefs_get_gender();
uint64_t weight_dag = activity_prefs_get_weight_dag();
uint64_t height_mm = activity_prefs_get_height_mm();
uint64_t age_years = activity_prefs_get_age_years();
// For men: kcalories = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) + 5
// For women: kcalories = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) - 161
calories_per_day = (100 * weight_dag)
+ (625 * height_mm)
- (5000 * age_years);
if (gender == ActivityGenderMale) {
calories_per_day += 5000;
} else if (gender == ActivityGenderFemale) {
calories_per_day -= 161000;
} else {
// midpoint of 5000 and -161000
calories_per_day -= 78000;
}
// Scale by the requested number of minutes
uint32_t resting_calories = ROUND(calories_per_day * elapsed_minutes, MINUTES_PER_DAY);
ACTIVITY_LOG_DEBUG("resting_calories: %"PRIu32"", resting_calories);
return resting_calories;
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include <stdint.h>
// ------------------------------------------------------------------------------------------------
// Compute distance (in millimeters) covered by the taking the given number of steps in the given
// amount of time.
uint32_t activity_private_compute_distance_mm(uint32_t steps, uint32_t ms);
// ------------------------------------------------------------------------------------------------
// Compute active calories (in calories, not kcalories) covered by going the given distance in
// the given amount of time.
uint32_t activity_private_compute_active_calories(uint32_t distance_mm, uint32_t ms);
// ------------------------------------------------------------------------------------------------
// Compute resting calories (in calories, not kcalories) within the elapsed time given
uint32_t activity_private_compute_resting_calories(uint32_t elapsed_minutes);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "activity_private.h"
#include "util/time/time.h"
#include <stdint.h>
typedef enum PercentTier {
PercentTier_AboveAverage = 0,
PercentTier_OnAverage,
PercentTier_BelowAverage,
PercentTier_Fail,
PercentTierCount
} PercentTier;
// Insight types (for analytics)
typedef enum ActivityInsightType {
ActivityInsightType_Unknown = 0,
ActivityInsightType_SleepReward,
ActivityInsightType_ActivityReward,
ActivityInsightType_SleepSummary,
ActivityInsightType_ActivitySummary,
ActivityInsightType_Day1,
ActivityInsightType_Day4,
ActivityInsightType_Day10,
ActivityInsightType_ActivitySessionSleep,
ActivityInsightType_ActivitySessionNap,
ActivityInsightType_ActivitySessionWalk,
ActivityInsightType_ActivitySessionRun,
ActivityInsightType_ActivitySessionOpen,
} ActivityInsightType;
// Insight response types (for analytics)
typedef enum ActivityInsightResponseType {
ActivityInsightResponseTypePositive = 0,
ActivityInsightResponseTypeNeutral,
ActivityInsightResponseTypeNegative,
ActivityInsightResponseTypeClassified,
ActivityInsightResponseTypeMisclassified,
} ActivityInsightResponseType;
typedef enum ActivationDelayInsightType {
// New vals must be added on the end. These are used in a prefs bitfield
ActivationDelayInsightType_Day1,
ActivationDelayInsightType_Day4,
ActivationDelayInsightType_Day10,
ActivationDelayInsightTypeCount,
} ActivationDelayInsightType;
// Various stats for metrics that are used to determine when it's ok to trigger an insight
typedef struct ActivityInsightMetricHistoryStats {
uint8_t total_days;
uint8_t consecutive_days;
ActivityScalarStore median;
ActivityScalarStore mean;
ActivityMetric metric;
} ActivityInsightMetricHistoryStats;
//! Called at midnight rollover to recalculate medians/totals for metric history
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
//! s_activity_state.mutex
void activity_insights_recalculate_stats(void);
//! Init activity insights
//! IMPORTANT: This call is not thread safe and should only be called from activity_init (since it
//! is called during boot when no other task might use an activity service call)
//! @param[in] now_utc Current time
void activity_insights_init(time_t now_utc);
//! Called by prv_minute_system_task_cb whenever it updates sleep metrics
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
//! s_activity_state.mutex
//! @param[in] now_utc Current time
void activity_insights_process_sleep_data(time_t now_utc);
//! Called once per minute by prv_minute_system_task_cb to check step insights
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
//! s_activity_state.mutex
//! @param[in] now_utc Current time
void activity_insights_process_minute_data(time_t now_utc);
void activity_insights_push_activity_session_notification(time_t notif_time,
ActivitySession *session,
int32_t avg_hr,
int32_t *hr_zone_time_s);
//! Used by test apps: Pushes the 3 variants of each summary pin to the timeline and a notification
//! for the last variant of each
void activity_insights_test_push_summary_pins(void);
//! Used by test apps: Pushes the 2 rewards to the watch
void activity_insights_test_push_rewards(void);
//! Used by test apps: Pushes the day 1, 4 and 10 insights
void activity_insights_test_push_day_insights(void);
//! Used by test apps: Pushes a run and a walk notification
void activity_insights_test_push_walk_run_sessions(void);
//! Used by test apps: Pushes a nap pin and notification
void activity_insights_test_push_nap_session(void);

View File

@@ -0,0 +1,783 @@
/*
* Copyright 2024 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.
*/
#include "applib/data_logging.h"
#include "applib/health_service.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "os/tick.h"
#include "popups/health_tracking_ui.h"
#include "services/common/analytics/analytics_event.h"
#include "services/normal/protobuf_log/protobuf_log.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/base64.h"
#include "util/math.h"
#include "util/size.h"
#include "util/stats.h"
#include "util/units.h"
#include "activity.h"
#include "activity_algorithm.h"
#include "activity_calculators.h"
#include "activity_insights.h"
#include "activity_private.h"
// ---------------------------------------------------------------------------------------
// Storage converters. These convert metrics from their storage type (ActivityScalarStore,
// which is only 16-bits) into the uint32_t value returned by activity_get_metric. For example,
// we might convert minutes to seconds.
static uint32_t prv_convert_none(ActivityScalarStore in) {
return in;
}
static uint32_t prv_convert_minutes_to_seconds(ActivityScalarStore in) {
return (uint32_t)in * SECONDS_PER_MINUTE;
}
// ------------------------------------------------------------------------------------------------
// Returns info about each metric we capture
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info) {
ActivityState *state = activity_private_state();
*info = (ActivityMetricInfo) {
.converter = prv_convert_none,
};
switch (metric) {
case ActivityMetricStepCount:
info->value_p = &state->step_data.steps;
info->settings_key = ActivitySettingsKeyStepCountHistory;
info->has_history = true;
break;
case ActivityMetricActiveSeconds:
info->value_p = &state->step_data.step_minutes;
info->settings_key = ActivitySettingsKeyStepMinutesHistory;
info->has_history = true;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricDistanceMeters:
info->value_p = &state->step_data.distance_meters;
info->settings_key = ActivitySettingsKeyDistanceMetersHistory;
info->has_history = true;
break;
case ActivityMetricRestingKCalories:
info->value_p = &state->step_data.resting_kcalories;
info->settings_key = ActivitySettingsKeyRestingKCaloriesHistory;
info->has_history = true;
break;
case ActivityMetricActiveKCalories:
info->value_p = &state->step_data.active_kcalories;
info->settings_key = ActivitySettingsKeyActiveKCaloriesHistory;
info->has_history = true;
break;
case ActivityMetricSleepTotalSeconds:
info->value_p = &state->sleep_data.total_minutes;
info->settings_key = ActivitySettingsKeySleepTotalMinutesHistory;
info->has_history = true;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricSleepRestfulSeconds:
info->value_p = &state->sleep_data.restful_minutes;
info->settings_key = ActivitySettingsKeySleepDeepMinutesHistory;
info->has_history = true;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricSleepEnterAtSeconds:
info->value_p = &state->sleep_data.enter_at_minute;
info->settings_key = ActivitySettingsKeySleepEnterAtHistory;
info->has_history = true;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricSleepExitAtSeconds:
info->value_p = &state->sleep_data.exit_at_minute;
info->settings_key = ActivitySettingsKeySleepExitAtHistory;
info->has_history = true;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricSleepState:
info->value_p = &state->sleep_data.cur_state;
info->settings_key = ActivitySettingsKeySleepState;
break;
case ActivityMetricSleepStateSeconds:
info->value_p = &state->sleep_data.cur_state_elapsed_minutes;
info->settings_key = ActivitySettingsKeySleepStateMinutes;
info->converter = prv_convert_minutes_to_seconds;
break;
case ActivityMetricLastVMC:
info->value_p = &state->last_vmc;
info->settings_key = ActivitySettingsKeyLastVMC;
break;
case ActivityMetricHeartRateRawBPM:
info->value_p = &state->hr.metrics.current_bpm;
break;
case ActivityMetricHeartRateRawQuality:
info->value_p = &state->hr.metrics.current_quality;
break;
case ActivityMetricHeartRateRawUpdatedTimeUTC:
info->value_u32p = &state->hr.metrics.current_update_time_utc;
break;
case ActivityMetricHeartRateFilteredBPM:
info->value_p = &state->hr.metrics.last_stable_bpm;
break;
case ActivityMetricHeartRateFilteredUpdatedTimeUTC:
info->value_u32p = &state->hr.metrics.last_stable_bpm_update_time_utc;
break;
case ActivityMetricHeartRateZone1Minutes:
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone1];
info->settings_key = ActivitySettingsKeyHeartRateZone1Minutes;
info->has_history = false;
break;
case ActivityMetricHeartRateZone2Minutes:
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone2];
info->settings_key = ActivitySettingsKeyHeartRateZone2Minutes;
info->has_history = false;
break;
case ActivityMetricHeartRateZone3Minutes:
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone3];
info->settings_key = ActivitySettingsKeyHeartRateZone3Minutes;
info->has_history = false;
break;
case ActivityMetricNumMetrics:
WTF;
break;
}
}
// ----------------------------------------------------------------------------------------------
// Set the value of a given metric
// The current value will only be overridden if the new value is higher
// Historical values can be overridden with any value
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek wday, int32_t value) {
if (!activity_tracking_on()) {
return;
}
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
switch (metric) {
case ActivityMetricActiveSeconds:
case ActivityMetricSleepTotalSeconds:
case ActivityMetricSleepRestfulSeconds:
case ActivityMetricSleepEnterAtSeconds:
case ActivityMetricSleepExitAtSeconds:
// We only store minutes for these metrics. Convert before saving
value /= SECONDS_PER_MINUTE;
break;
default:
break;
}
ActivityMetricInfo m_info = {};
activity_metrics_prv_get_metric_info(metric, &m_info);
const DayInWeek cur_wday = time_util_get_day_in_week(rtc_get_time());
bool current_value_updated = false;
if (cur_wday == wday) {
// Update our cached copy of the value if it is larger than what we currently have
if (m_info.value_p && value > *m_info.value_p) {
*m_info.value_p = value;
current_value_updated = true;
} else if (m_info.value_u32p && (uint32_t)value > *m_info.value_u32p) {
*m_info.value_u32p = value;
current_value_updated = true;
}
} else if (m_info.has_history) {
// This update is for a day in the past. Modify the copy stored in the settings file
SettingsFile *file = activity_private_settings_open();
if (!file) {
goto unlock;
}
ActivitySettingsValueHistory history;
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key),
&history, sizeof(history));
int day = positive_modulo(cur_wday - wday, DAYS_PER_WEEK);
if (history.values[day] != value) {
history.values[day] = value;
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
&history, sizeof(history));
}
activity_private_settings_close(file);
}
if (current_value_updated) {
if (metric == ActivityMetricStepCount) {
PebbleEvent e = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventMovementUpdate,
.data.movement_update = {
.steps = value,
},
},
};
event_put(&e);
} else if (metric == ActivityMetricDistanceMeters) {
state->distance_mm = state->step_data.distance_meters * MM_PER_METER;
} else if (metric == ActivityMetricActiveKCalories) {
state->active_calories = state->step_data.active_kcalories * ACTIVITY_CALORIES_PER_KCAL;
} else if (metric == ActivityMetricRestingKCalories) {
state->resting_calories = state->step_data.resting_kcalories * ACTIVITY_CALORIES_PER_KCAL;
}
activity_algorithm_metrics_changed_notification();
}
unlock:
mutex_unlock_recursive(state->mutex);
}
// ----------------------------------------------------------------------------------------------
// Shift the history back one day and reset the current day's stats.
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
static void NOINLINE prv_shift_history(time_t utc_now) {
ActivityState *state = activity_private_state();
PBL_LOG(LOG_LEVEL_INFO, "resetting metrics for new day");
mutex_lock_recursive(state->mutex);
{
SettingsFile *file = activity_private_settings_open();
if (!file) {
goto unlock;
}
ActivitySettingsValueHistory history;
ActivityMetricInfo m_info;
for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
metric++) {
activity_metrics_prv_get_metric_info(metric, &m_info);
// Shift the history
if (m_info.has_history) {
PBL_ASSERTN(m_info.value_p);
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
sizeof(history));
for (int i = ACTIVITY_HISTORY_DAYS - 1; i >= 1; i--) {
history.values[i] = history.values[i - 1];
}
// We just wrapped up yesterday
history.values[1] = *m_info.value_p;
// Reset stats for today
history.values[0] = 0;
history.utc_sec = utc_now;
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
sizeof(history));
}
}
activity_private_settings_close(file);
}
unlock:
mutex_unlock_recursive(state->mutex);
}
// --------------------------------------------------------------------------------------------
// Called from activity_get_metric() every time a client asks for a metric. Also called
// periodically from the minute handler before we save current metrics to setting.
static void prv_update_real_time_derived_metrics(void) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
state->step_data.distance_meters = ROUND(state->distance_mm,
MM_PER_METER);
ACTIVITY_LOG_DEBUG("new distance: %"PRIu16"", state->step_data.distance_meters);
state->step_data.active_kcalories = ROUND(state->active_calories,
ACTIVITY_CALORIES_PER_KCAL);
ACTIVITY_LOG_DEBUG("new active kcal: %"PRIu16"", state->step_data.active_kcalories);
}
mutex_unlock_recursive(state->mutex);
}
// --------------------------------------------------------------------------------------------
// Called periodically from the minute handler to update step derived metrics that do not have to
// be updated in real time.
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
static void NOINLINE prv_update_step_derived_metrics(time_t utc_sec) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
int minute_of_day = time_util_get_minute_of_day(utc_sec);
// The "no-steps-during-sleep" logic can introduce negative steps, so make sure we clip
// negative steps to 0 when computing the metrics below
uint16_t steps_in_minute = 0;
if (state->step_data.steps >= state->steps_per_minute_last_steps) {
steps_in_minute = state->step_data.steps
- state->steps_per_minute_last_steps;
}
// Update the walking rate
state->steps_per_minute = steps_in_minute;
state->steps_per_minute_last_steps = state->step_data.steps;
ACTIVITY_LOG_DEBUG("new steps/minute: %"PRIu16"", state->steps_per_minute);
// Update the number of stepping minutes and the last active minute
if (state->steps_per_minute >= ACTIVITY_ACTIVE_MINUTE_MIN_STEPS) {
state->step_data.step_minutes++;
ACTIVITY_LOG_DEBUG("new step minutes: %"PRIu16"", state->step_data.step_minutes);
// The prior minute was the most recent active one
state->last_active_minute = time_util_minute_of_day_adjust(minute_of_day, -1);
ACTIVITY_LOG_DEBUG("last active minute: %"PRIu16"", state->last_active_minute);
}
// Update the resting calories
state->resting_calories = activity_private_compute_resting_calories(minute_of_day);
state->step_data.resting_kcalories = ROUND(state->resting_calories,
ACTIVITY_CALORIES_PER_KCAL);
ACTIVITY_LOG_DEBUG("resting kcalories: %"PRIu16"",
state->step_data.resting_kcalories);
}
mutex_unlock_recursive(state->mutex);
}
// ------------------------------------------------------------------------------------------
// Pushes an HR Median/Filtered/LastStable event.
static void prv_push_median_hr_event(uint8_t median_hr) {
if (median_hr > 0) {
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventHeartRateUpdate,
.data.heart_rate_update = {
.current_bpm = median_hr,
.is_filtered = true,
}
}
};
event_put(&event);
}
}
// ------------------------------------------------------------------------------------------
// Calculates and stores the most recent minutes median heart rate value.
// Used for the health_service and the minute level data.
static void prv_update_median_hr_bpm(ActivityState *state) {
const ActivityHRSupport *hr = &state->hr;
const uint16_t num_hr_samples = hr->num_samples;
if (num_hr_samples > 0) {
int32_t median, total_weight;
// Stats requires an int32_t array and we need one for both the samples and the weights
int32_t *sample_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
int32_t *weight_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
for (size_t i = 0; i < num_hr_samples; i++) {
sample_buf[i] = hr->samples[i];
weight_buf[i] = hr->weights[i];
}
// Calculate the total weight
stats_calculate_basic(StatsBasicOp_Sum, weight_buf, hr->num_samples, NULL, NULL,
&total_weight);
// Calculate the weighted median
median = stats_calculate_weighted_median(sample_buf, weight_buf, num_hr_samples);
task_free(sample_buf);
task_free(weight_buf);
state->hr.metrics.last_stable_bpm = (uint8_t)median;
state->hr.metrics.last_stable_bpm_update_time_utc = rtc_get_time();
state->hr.metrics.previous_median_bpm = (uint8_t)median;
state->hr.metrics.previous_median_total_weight_x100 = total_weight;
prv_push_median_hr_event(state->hr.metrics.previous_median_bpm);
}
}
// ------------------------------------------------------------------------------------------
static void prv_write_hr_zone_info_to_flash(HRZone zone) {
ActivityMetric metric;
if (zone == HRZone_Zone1) {
metric = ActivityMetricHeartRateZone1Minutes;
} else if (zone == HRZone_Zone2) {
metric = ActivityMetricHeartRateZone2Minutes;
} else if (zone == HRZone_Zone3) {
metric = ActivityMetricHeartRateZone3Minutes;
} else {
// Don't store data for Zone 0
return;
}
SettingsFile *file = activity_private_settings_open();
if (!file) {
return;
}
ActivityMetricInfo m_info;
activity_metrics_prv_get_metric_info(metric, &m_info);
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
m_info.value_p, sizeof(*m_info.value_p));
activity_private_settings_close(file);
}
// ------------------------------------------------------------------------------------------
// The median HR should get updated before calling this
static void prv_update_current_hr_zone(ActivityState *state) {
int32_t hr_median;
activity_metrics_prv_get_median_hr_bpm(&hr_median, NULL);
HRZone new_hr_zone = hr_util_get_hr_zone(hr_median);
if (new_hr_zone != HRZone_Zone0 && state->hr.num_samples < ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE) {
// There wasn't enough data in the past minute to give us confidence that
// the new HR zone will represents that minute, default to Zone0
new_hr_zone = HRZone_Zone0;
}
bool new_hr_elevated = hr_util_is_elevated(hr_median);
// Before changing the zone make sure the user has an elevated heart rate.
// This prevents erroneous HRM readings accumulating minutes in zone 1.
// Then only go up/down 1 zone per minute.
// This prevents erroneous HRM readings accumulating minutes in higher zones.
if (!state->hr.metrics.is_hr_elevated && new_hr_elevated) {
state->hr.metrics.is_hr_elevated = new_hr_elevated;
} else if (new_hr_zone > state->hr.metrics.current_hr_zone) {
state->hr.metrics.current_hr_zone++;
} else if (new_hr_zone < state->hr.metrics.current_hr_zone) {
state->hr.metrics.current_hr_zone--;
} else if (!new_hr_elevated) {
state->hr.metrics.is_hr_elevated = new_hr_elevated;
}
state->hr.metrics.minutes_in_zone[state->hr.metrics.current_hr_zone]++;
prv_write_hr_zone_info_to_flash(state->hr.metrics.current_hr_zone);
}
// ------------------------------------------------------------------------------------------
// Called periodically from the minute handler to update the median HR and time spent in HR zones
static void prv_update_hr_derived_metrics(void) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
// Update the median HR / HR weight for the minute
prv_update_median_hr_bpm(state);
// Update our current HR zone (based on the median which is calculated above)
prv_update_current_hr_zone(state);
}
mutex_unlock_recursive(state->mutex);
}
// ------------------------------------------------------------------------------------------
// The metrics minute handler
void activity_metrics_prv_minute_handler(time_t utc_sec) {
ActivityState *state = activity_private_state();
uint16_t cur_day_index = time_util_get_day(utc_sec);
if (cur_day_index != state->cur_day_index) {
// If we've just encountered a midnight rollover, shift history to the new day
// before we compute metrics for the new day
prv_shift_history(utc_sec);
}
// Update the derived metrics
prv_update_real_time_derived_metrics();
prv_update_step_derived_metrics(utc_sec);
prv_update_hr_derived_metrics();
}
// --------------------------------------------------------------------------------------------
ActivityScalarStore activity_metrics_prv_steps_per_minute(void) {
ActivityState *state = activity_private_state();
return state->steps_per_minute;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_distance_mm(void) {
ActivityState *state = activity_private_state();
return state->distance_mm;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_resting_calories(void) {
ActivityState *state = activity_private_state();
return state->resting_calories;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_active_calories(void) {
ActivityState *state = activity_private_state();
return state->active_calories;
}
// --------------------------------------------------------------------------------------------
uint32_t activity_metrics_prv_get_steps(void) {
ActivityState *state = activity_private_state();
return state->step_data.steps;
}
static uint8_t prv_get_hr_quality_weight(HRMQuality quality) {
static const struct {
HRMQuality quality;
uint8_t weight_x100;
} s_hr_quality_weights_x100[] = {
{HRMQuality_NoAccel, 0 },
{HRMQuality_OffWrist, 0 },
{HRMQuality_NoSignal, 0 },
{HRMQuality_Worst, 1 },
{HRMQuality_Poor, 1 },
{HRMQuality_Acceptable, 60 },
{HRMQuality_Good, 65 },
{HRMQuality_Excellent, 85 },
};
for (size_t i = 0; i < ARRAY_LENGTH(s_hr_quality_weights_x100); i++) {
if (quality == s_hr_quality_weights_x100[i].quality) {
return s_hr_quality_weights_x100[i].weight_x100;
}
}
return 0;
}
// --------------------------------------------------------------------------------------------
HRZone activity_metrics_prv_get_hr_zone(void) {
ActivityState *state = activity_private_state();
return state->hr.metrics.current_hr_zone;
}
// --------------------------------------------------------------------------------------------
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
int32_t *heart_rate_total_weight_x100_out) {
ActivityState *state = activity_private_state();
if (median_out) {
*median_out = state->hr.metrics.previous_median_bpm;
}
if (heart_rate_total_weight_x100_out) {
*heart_rate_total_weight_x100_out = state->hr.metrics.previous_median_total_weight_x100;
}
}
// --------------------------------------------------------------------------------------------
void activity_metrics_prv_reset_hr_stats(void) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
state->hr.num_samples = 0;
state->hr.num_quality_samples = 0;
memset(state->hr.samples, 0, sizeof(state->hr.samples));
memset(state->hr.weights, 0, sizeof(state->hr.weights));
state->hr.metrics.previous_median_bpm = 0;
state->hr.metrics.previous_median_total_weight_x100 = 0;
}
mutex_unlock_recursive(state->mutex);
}
// --------------------------------------------------------------------------------------------
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
time_t now_uptime) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
// Update stats used for computing the average
if (hrm_event->bpm.bpm > 0) {
// This should get reset about once a minute, so X minutes worth of samples means something
// is terribly wrong.
PBL_ASSERT(state->hr.num_samples <= ACTIVITY_MAX_HR_SAMPLES, "Too many samples");
state->hr.samples[state->hr.num_samples] = hrm_event->bpm.bpm;
state->hr.weights[state->hr.num_samples] =
prv_get_hr_quality_weight(hrm_event->bpm.quality);
if (hrm_event->bpm.quality >= ACTIVITY_MIN_HR_QUALITY_THRESH) {
state->hr.num_quality_samples++;
}
state->hr.num_samples++;
}
// Update the timestamp used for figuring out when we should change the sampling period.
// This is based on uptime so that it doesn't get messed up if the mobile changes the
// UTC time on us.
state->hr.last_sample_ts = now_uptime;
// Save the BPM, quality, and update time (UTC) of the last reading for activity_get_metric()
state->hr.metrics.current_bpm = hrm_event->bpm.bpm;
state->hr.metrics.current_quality = hrm_event->bpm.quality;
state->hr.metrics.current_update_time_utc = now_utc;
}
mutex_unlock_recursive(state->mutex);
}
// ------------------------------------------------------------------------------------------------
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now) {
ActivityState *state = activity_private_state();
// Roll back the history if needed and init each of the metrics for today
for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
metric++) {
ActivityMetricInfo m_info;
activity_metrics_prv_get_metric_info(metric, &m_info);
if (m_info.has_history) {
PBL_ASSERTN(m_info.value_p);
ActivitySettingsValueHistory old_history = { 0 };
ActivitySettingsValueHistory new_history = { 0 };
// In case we change the length of the history, fetch the old size
int fetch_size = sizeof(old_history);
fetch_size = MIN(fetch_size, settings_file_get_len(file, &m_info.settings_key,
sizeof(m_info.settings_key)));
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &old_history,
fetch_size);
uint16_t day = time_util_get_day(old_history.utc_sec);
int old_age = state->cur_day_index - day;
// If this is resting kcalories, the default for each day is not 0
if (metric == ActivityMetricRestingKCalories) {
uint32_t full_day_resting_calories =
activity_private_compute_resting_calories(MINUTES_PER_DAY);
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
if (i == 0) {
uint32_t elapsed_minutes = time_util_get_minute_of_day(utc_now);
uint32_t cur_day_resting_calories =
activity_private_compute_resting_calories(elapsed_minutes);
new_history.values[i] = ROUND(cur_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
} else {
new_history.values[i] = ROUND(full_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
}
}
}
// Copy values from old history into correct slot in new history
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
int new_index = i + old_age;
if (new_index >= 0 && new_index < ACTIVITY_HISTORY_DAYS) {
new_history.values[new_index] = old_history.values[i];
}
}
// init the time stamp if not initialized yet
if (new_history.utc_sec == 0) {
new_history.utc_sec = utc_now;
}
// Init current value
*m_info.value_p = new_history.values[0];
// Only write to flash if the values change or this is a new day (to update the timestamp)
if (memcmp(old_history.values, new_history.values, sizeof(old_history.values)) != 0
|| old_age != 0) {
// Write out the updated history
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &new_history,
sizeof(new_history));
}
} else if (m_info.settings_key != ActivitySettingsKeyInvalid) {
// Metric with no history, just init current value
PBL_ASSERTN(m_info.value_p);
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), m_info.value_p,
sizeof(*m_info.value_p));
}
}
}
// ------------------------------------------------------------------------------------------------
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
ActivityState *state = activity_private_state();
bool success = true;
// Default results
for (uint32_t i = 0; i < history_len; i++) {
history[i] = -1;
}
mutex_lock_recursive(state->mutex);
{
if (!activity_prefs_tracking_is_enabled() && pebble_task_get_current() == PebbleTask_App) {
health_tracking_ui_app_show_disabled();
}
// Update derived metrics
prv_update_real_time_derived_metrics();
ActivityMetricInfo m_info;
activity_metrics_prv_get_metric_info(metric, &m_info);
if (history_len == 0) {
goto unlock;
}
// Clip history length
history_len = MIN(history_len, ACTIVITY_HISTORY_DAYS);
if (!m_info.has_history) {
history_len = 1;
}
// Fill in current value
if (m_info.value_p) {
history[0] = m_info.converter(*m_info.value_p);
} else {
PBL_ASSERTN(m_info.value_u32p && (m_info.converter == prv_convert_none));
history[0] = *m_info.value_u32p;
}
ACTIVITY_LOG_DEBUG("get current metric %"PRIi32" : %"PRIi32"", (int32_t)metric, history[0]);
// Look up historical values
if (history_len > 1) {
// Read from the history stored in settings
ActivitySettingsValueHistory setting_history = {};
SettingsFile *file = activity_private_settings_open();
if (!file) {
PBL_LOG(LOG_LEVEL_ERROR, "Settings file DNE. No need to continue getting metric");
success = false;
goto unlock;
}
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &setting_history,
sizeof(setting_history));
for (uint32_t i = 1; i < history_len; i++) {
history[i] = m_info.converter(setting_history.values[i]);
ACTIVITY_LOG_DEBUG("get metric %"PRIi32" %"PRIu32" days ago: %"PRIi32"", (int32_t)metric,
i, history[i]);
}
activity_private_settings_close(file);
}
}
unlock:
mutex_unlock_recursive(state->mutex);
return success;
}
// ------------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, sys_activity_get_metric, ActivityMetric metric,
uint32_t history_len, int32_t *history) {
if (PRIVILEGE_WAS_ELEVATED) {
if (history) {
syscall_assert_userspace_buffer(history, history_len * sizeof(*history));
}
}
return activity_get_metric(metric, history_len, history);
}

View File

@@ -0,0 +1,534 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "activity.h"
#include "hr_util.h"
#include "applib/event_service_client.h"
#include "kernel/events.h"
#include "os/mutex.h"
#include "services/normal/data_logging/data_logging_service.h"
#include "services/normal/settings/settings_file.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "util/attributes.h"
#include <stdbool.h>
#include <stdint.h>
#define ACTIVITY_LOG_DEBUG(fmt, args...) \
PBL_LOG_D(LOG_DOMAIN_ACTIVITY, LOG_LEVEL_DEBUG, fmt, ## args)
#define ACTIVITY_HEXDUMP(data, length) \
PBL_HEXDUMP_D(LOG_DOMAIN_DATA_ACTIVITY, LOG_LEVEL_DEBUG, data, length)
// How often we update settings with the current step/sleep stats for today.
#define ACTIVITY_SETTINGS_UPDATE_MIN 15
// How often we recompute the activity sessions (like sleep, walks, runs). This has significant
// enough CPU requirements to warrant only recomputing occasionally
#define ACTIVITY_SESSION_UPDATE_MIN 15
// Every scalar metric and setting is stored in globals and in the settings file using this
// typedef
typedef uint16_t ActivityScalarStore;
#define ACTIVITY_SCALAR_MAX UINT16_MAX
// Each step average interval covers this many minutes
#define ACTIVITY_STEP_AVERAGES_MINUTES (MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES)
// flash vs. the most amount of data we could lose if we reset.
#define ACTIVITY_STEP_AVERAGES_PER_KEY 4
#define ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY \
(ACTIVITY_NUM_METRIC_AVERAGES / ACTIVITY_STEP_AVERAGES_PER_KEY)
// If we see at least this many steps in a minute, it was an "active minute"
#define ACTIVITY_ACTIVE_MINUTE_MIN_STEPS 40
// We consider any sleep session that ends after this minute of the day (representing 9pm) as
// part of the next day's sleep
#define ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (21 * MINUTES_PER_HOUR)
// Default HeartRate sampling period (Must take a sample every X seconds by default)
#define ACTIVITY_DEFAULT_HR_PERIOD_SEC (10 * SECONDS_PER_MINUTE)
// Default HeartRate sampling ON time (Stays on for X seconds every
// ACTIVITY_DEFAULT_HR_PERIOD_SEC seconds)
#define ACTIVITY_DEFAULT_HR_ON_TIME_SEC (SECONDS_PER_MINUTE)
// Turn off the HR device after we've received X number of thresholded samples
#define ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT (15)
// The minimum number of samples needed before we can approximate the user's HR zone
#define ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE (10)
#define ACTIVITY_MIN_HR_QUALITY_THRESH (HRMQuality_Good)
// HRM Subscription values during ON and OFF periods
#define ACTIVITY_HRM_SUBSCRIPTION_ON_PERIOD_SEC (1)
#define ACTIVITY_HRM_SUBSCRIPTION_OFF_PERIOD_SEC (SECONDS_PER_DAY)
// Max number of stored HR samples to compute the median
#define ACTIVITY_MAX_HR_SAMPLES (3 * SECONDS_PER_MINUTE)
// Conversion factors
#define ACTIVITY_DAG_PER_KG 100
// -----------------------------------------------------------------------------------------
// Settings file info and keys
#define ACTIVITY_SETTINGS_FILE_NAME "activity"
#define ACTIVITY_SETTINGS_FILE_LEN 0x4000
// The version of our settings file
// Version 1 - ActivitySettingsKeyVersion didn't exist
// Version 2 - Changed file size from 2k to 16k
#define ACTIVITY_SETTINGS_CURRENT_VERSION 2
typedef struct {
uint32_t utc_sec; // timestamp of first entry in list
// One entry per day. The most recent day (today) is stored at index 0
ActivityScalarStore values[ACTIVITY_HISTORY_DAYS];
} ActivitySettingsValueHistory;
// Keys of the settings we save in our settings file.
typedef enum {
ActivitySettingsKeyInvalid = 0, // Used for error discovery
ActivitySettingsKeyVersion, // uint16_t: ACTIVITY_SETTINGS_CURRENT_VERSION
ActivitySettingsKeyUnused0, // Unused
ActivitySettingsKeyUnused1, // Unused
ActivitySettingsKeyUnused2, // Unused
ActivitySettingsKeyUnused3, // Unused
ActivitySettingsKeyStepCountHistory, // ActivitySettingsValueHistory
ActivitySettingsKeyStepMinutesHistory, // ActivitySettingsValueHistory
ActivitySettingsKeyUnused4, // Unused
ActivitySettingsKeyDistanceMetersHistory, // ActivitySettingsValueHistory
ActivitySettingsKeySleepTotalMinutesHistory, // ActivitySettingsValueHistory
ActivitySettingsKeySleepDeepMinutesHistory, // ActivitySettingsValueHistory
ActivitySettingsKeySleepEntryMinutesHistory, // ActivitySettingsValueHistory
// How long it took to fall asleep
ActivitySettingsKeySleepEnterAtHistory, // ActivitySettingsValueHistory
// What time the user fell asleep. Measured in
// minutes after midnight.
ActivitySettingsKeySleepExitAtHistory, // ActivitySettingsValueHistory
// What time the user woke up. Measured in
// minutes after midnight
ActivitySettingsKeySleepState, // uint16_t
ActivitySettingsKeySleepStateMinutes, // uint16_t
ActivitySettingsKeyStepAveragesWeekdayFirst, // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
ActivitySettingsKeyStepAveragesWeekdayLast =
ActivitySettingsKeyStepAveragesWeekdayFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,
ActivitySettingsKeyStepAveragesWeekendFirst, // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
ActivitySettingsKeyStepAveragesWeekendLast =
ActivitySettingsKeyStepAveragesWeekendFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,
ActivitySettingsKeyAgeYears, // uint16_t: age in years
ActivitySettingsKeyUnused5, // Unused
ActivitySettingsKeyInsightSleepRewardTime, // time_t: time we last showed the sleep reward
// This will be 0 if we haven't triggered one yet
ActivitySettingsKeyInsightActivityRewardTime, // time_t: time we last showed the activity reward
// This will be 0 if we haven't triggered one yet
ActivitySettingsKeyInsightActivitySummaryState, // SummaryPinLastState: the UUID and last time the
// pin was added
ActivitySettingsKeyInsightSleepSummaryState, // SummaryPinLastState: the UUID and last time the
// pin was added
ActivitySettingsKeyRestingKCaloriesHistory, // ActivitySettingsValueHistory
ActivitySettingsKeyActiveKCaloriesHistory, // ActivitySettingsValueHistory
ActivitySettingsKeyLastSleepActivityUTC, // time_t: UTC timestamp of the last sleep related
// activity we logged to analytics
ActivitySettingsKeyLastRestfulSleepActivityUTC, // time_t: UTC timestamp of the last restful sleep
// related activity we logged to analytics
ActivitySettingsKeyLastStepActivityUTC, // time_t: UTC timestamp of the last step related
// activity we logged to analytics
ActivitySettingsKeyStoredActivities, // ActivitySession[
// ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]
ActivitySettingsKeyInsightNapSessionTime, // time_t: time we last showed the nap pin
ActivitySettingsKeyInsightActivitySessionTime, // time_t: time we last showed the activity pin
ActivitySettingsKeyLastVMC, // uint16_t: the VMC at the last processed minute
ActivitySettingsKeyRestingHeartRate, // ActivitySettingsValueHistory
ActivitySettingsKeyHeartRateZone1Minutes,
ActivitySettingsKeyHeartRateZone2Minutes,
ActivitySettingsKeyHeartRateZone3Minutes,
} ActivitySettingsKey;
// -----------------------------------------------------------------------------------------
// Internal structs
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivityStepData
// is an ActivityScalarStore
typedef struct {
ActivityScalarStore steps;
ActivityScalarStore step_minutes;
ActivityScalarStore distance_meters;
ActivityScalarStore resting_kcalories;
ActivityScalarStore active_kcalories;
} ActivityStepData;
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivitySleepData
// is an ActivityScalarStore
typedef struct {
ActivityScalarStore total_minutes;
ActivityScalarStore restful_minutes;
ActivityScalarStore enter_at_minute; // minutes after midnight
ActivityScalarStore exit_at_minute; // minutes after midnight
ActivityScalarStore cur_state; // HealthActivity
ActivityScalarStore cur_state_elapsed_minutes;
} ActivitySleepData;
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that elements of
// ActivityHeartRateData are ActivityScalarStore by default. The update_time_utc is
// specially coded as a 32-bit metric and is allowed to be because we don't persist it in
// the settings file and it has no history
typedef struct {
ActivityScalarStore current_bpm; // Most current reading
uint32_t current_update_time_utc; // Timestamp of the current HR reading
ActivityScalarStore current_hr_zone;
ActivityScalarStore resting_bpm;
ActivityScalarStore current_quality; // HRMQuality
ActivityScalarStore last_stable_bpm;
uint32_t last_stable_bpm_update_time_utc; // Timestamp of the last stable BPM
ActivityScalarStore previous_median_bpm; // Most recently calculated median HR in a minute
int32_t previous_median_total_weight_x100;
ActivityScalarStore minutes_in_zone[HRZoneCount];
bool is_hr_elevated;
} ActivityHeartRateData;
// This callback used to convert a metric from the storage format (as a ActivityScalarStore) into
// the return format (uint32_t) returned by activity_get_metric. It might convert minutes to
// seconds, etc.
typedef uint32_t (*ActivityMetricConverter)(ActivityScalarStore storage_value);
// Filled in by activity_metrics_prv_get_metric_info()
typedef struct {
ActivityScalarStore *value_p; // pointer to storage in globals
uint32_t *value_u32p; // alternate value pointer for 32-bit metrics. These
// can NOT have history and settings_key MUST be
// ActivitySettingsKeyInvalid.
bool has_history; // True if this metric has history. This determines the
// size of the value as stored in settings
ActivitySettingsKey settings_key; // Settings key for this value
ActivityMetricConverter converter; // convert from storage value to return value.
} ActivityMetricInfo;
// Used by activity_feed_samples
typedef struct {
uint16_t num_samples;
AccelRawData data[];
} ActivityFeedSamples;
// Version of our legacy sleep session logging records (prior to FW 3.11). NOTE: The version
// field is treated as a bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set,
// we are free to add more fields to the end of ActivityLegacySleepSessionDataLoggingRecord and the
// mobile app will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app
// will know that it has no chance of parsing the blob (until the mobile app is updated of course).
#define ACTIVITY_SLEEP_SESSION_LOGGING_VERSION 1
// Data logging record used to send sleep sessions to the phone
typedef struct PACKED {
uint16_t version; // set to ACTIVITY_SLEEP_SESSION_LOGGING_VERSION
int32_t utc_to_local; // Add this to UTC to get local time
uint32_t start_utc; // The start time in UTC
uint32_t end_utc; // The end time in UTC
uint32_t restful_secs;
} ActivityLegacySleepSessionDataLoggingRecord;
// Version of our activity session logging records. NOTE: The version field is treated as a
// bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set, we are free to
// add more fields to the end of ActivitySessionDataLoggingRecord and the mobile app
// will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app will know that
// it has no chance of parsing the blob (until the mobile app is updated of course).
#define ACTIVITY_SESSION_LOGGING_VERSION 3
// Data logging record used to send activity sessions to the phone
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
// an update to documentation on this wiki page:
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
typedef struct PACKED {
uint16_t version; // set to ACTIVITY_SESSION_LOGGING_VERSION
uint16_t size; // size of this structure
uint16_t activity; // ActivitySessionType: the type of activity
int32_t utc_to_local; // Add this to UTC to get local time
uint32_t start_utc; // The start time in UTC
uint32_t elapsed_sec; // Elapsed time in seconds
// New fields add in version 3
union {
ActivitySessionDataStepping step_data;
ActivitySessionDataSleeping sleep_data;
};
} ActivitySessionDataLoggingRecord;
// -----------------------------------------------------------------------------------------
// Globals
// Support for raw accel sample collection
typedef struct {
// The data logging session for the current sample collection session
DataLoggingSession *dls_session;
// Most recently encoded accel sample value. Used for detecting and encoding runs of the same
// value
uint32_t prev_sample; // See comments in ActivityRawSamplesRecord for encoding
uint8_t run_size; // run size of prev_sample
// The currently forming record
ActivityRawSamplesRecord record;
// large enough to base64 encode half of the record at once.
char base64_buf[sizeof(ActivityRawSamplesRecord)];
// True if we are forming the first record
bool first_record;
} ActivitySampleCollectionData;
// This type is defined in measurements_log.h but we can't include measurements_log.h in this header
// because of build issues with the auto-generated SDK files.
typedef void *ProtobufLogRef;
// Support for heart rate
typedef struct {
ActivityHeartRateData metrics; // ActivityMetrics for heart rate
HRMSessionRef hrm_session; // The HRM session we use
ProtobufLogRef log_session; // The measurements log we send data to
bool currently_sampling; // Are we activity sampling the HR
uint32_t toggled_sampling_at_ts; // When we last toggled our sampling rate
// (from time_get_uptime_seconds)
uint32_t last_sample_ts; // When we last received a HR sample
// (from time_get_uptime_seconds)
uint16_t num_samples; // number of samples in the past minute
uint16_t num_quality_samples; // number of samples in the past minute that have met our
// quality threshold ACTIVITY_MIN_HR_QUALITY_THRESH
// NOTE: Used to short circuit
// our HR polling when enough samples have been taken
uint8_t samples[ACTIVITY_MAX_HR_SAMPLES]; // HR Samples stored
uint8_t weights[ACTIVITY_MAX_HR_SAMPLES]; // HR Sample Weights
} ActivityHRSupport;
typedef struct {
// Mutex for serializing access to these globals
PebbleRecursiveMutex *mutex;
// Semaphore used for waiting for KernelBG to finish a callback
SemaphoreHandle_t bg_wait_semaphore;
// Accel session ref
AccelServiceState *accel_session;
// Event Service to keep track of whether the charger is connected
EventServiceInfo charger_subscription;
// Cumulative stats for today
ActivityStepData step_data;
ActivitySleepData sleep_data;
// We accumulate distance in mm to and active/resting calories in calories (not kcalories) to
// minimize rounding errors since we increment them every time we get a new rate reading from the
// algorithm (every 5 seconds).
uint32_t distance_mm;
uint32_t active_calories;
uint32_t resting_calories;
ActivityScalarStore last_vmc;
uint8_t last_orientation;
time_t rate_last_update_time;
// Most recently calculated minute average walking rate
ActivityScalarStore steps_per_minute;
ActivityScalarStore steps_per_minute_last_steps;
// The most recent minute that had any significant step activity. Used for computing
// amount of time it takes to fall asleep
uint16_t last_active_minute;
// Heart rate support
ActivityHRSupport hr;
// Most recent values from prv_get_day()
uint16_t cur_day_index;
// Modulo counter used to periodically update settings file
int8_t update_settings_counter;
// Captured activity sessions
uint16_t activity_sessions_count; // how many sessions we have captured
ActivitySession activity_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
bool need_activities_saved; // true if activities need to be persisted
// Set to true when a new sleep session is registered
bool sleep_sessions_modified;
// Exit time for the last sleep/step activities we logged. Used to prevent logging the same event
// more than once.
time_t logged_sleep_activity_exit_at_utc;
time_t logged_restful_sleep_activity_exit_at_utc;
time_t logged_step_activity_exit_at_utc;
// Data logging session used for sending activity sessions (introduced in v3.11)
DataLoggingSession *activity_dls_session;
// Variables used for detecting "significant activity" events
time_t activity_event_start_utc; // UTC of first active minute, 0 if none detected
// True if service has been enabled via services_set_runlevel.
bool enabled_run_level;
// True if the current state of charging allows the service to run.
bool enabled_charging_state;
// True if activity tracking should be started. If enabled is false, this can still be true
// and will tell us that we should re-start tracking once enabled gets set again.
bool should_be_started;
// True if tracking has actually been started. This will only ever be set if enabled is also
// true.
bool started;
// Support for raw accel sample collection
bool sample_collection_enabled;
uint16_t sample_collection_session_id; // raw sample collection session id
time_t sample_collection_seconds; // if enabled is true, the UTC when sample
// collection started, else the # of seconds of
// of data in recently ended session
uint16_t sample_collection_num_samples; // number of samples collected so far
ActivitySampleCollectionData *sample_collection_data;
// True if activity_start_tracking was called with test_mode = true
bool test_mode;
bool pending_test_cb;
} ActivityState;
//! Get pointer to the activity state
ActivityState *activity_private_state(void);
//! Get whether HRM is present
bool activity_is_hrm_present(void);
//! Shared with activity_insights.c - opens the activity settings file
//! IMPORTANT: This function must only be called during activity init routines or while holding
//! the activity mutex
SettingsFile *activity_private_settings_open(void);
//! Shared with activity_insights.c - closes the activity settings file
//! IMPORTANT: This function must only be called during activity init routines or while holding
//! the activity mutex
void activity_private_settings_close(SettingsFile *file);
//! Used by test apps (running on firmware): Re-initialize activity service. If reset_settings is
//! true, all persistent data is cleared
//! @param[in] reset_settings if true, reset all stored settings
//! @param[in] tracking_on if true, turn on tracking if not already on. Otherwise, preserve
//! the current tracking status
//! @param[in] sleep_history if not NULL, rewrite sleep history to these values
//! @param[in] step_history if not NULL, rewrite step history to these values
bool activity_test_reset(bool reset_settings, bool tracking_on,
const ActivitySettingsValueHistory *sleep_history,
const ActivitySettingsValueHistory *step_history);
// --------------------------------------------------------------------------------
// Activity Sessions
// Load in the stored activities from our settings file
void activity_sessions_prv_init(SettingsFile *file, time_t utc_now);
// Get the UTC time bounds for the current day
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc,
time_t *exit_utc);
// Remove all activity sessions that are older than "today", those that are invalid because they
// are in the future, and optionally those that are still ongoing.
void activity_sessions_prv_remove_out_of_range_activity_sessions(time_t utc_sec,
bool remove_ongoing);
//! Return true if the given activity type is sleep related
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type);
//! Return true if the given activity type has session that is currently ongoing.
bool activity_sessions_is_session_type_ongoing(ActivitySessionType activity_type);
//! Register a new activity session. This is called by the algorithm logic when it detects a new
//! activity.
void activity_sessions_prv_add_activity_session(ActivitySession *session);
//! Delete an activity session. This is called by the algorithm logic when it decides to not
//! register a sleep session after all. Only sessions that are still 'ongoing' are allowed to be
//! deleted.
void activity_sessions_prv_delete_activity_session(ActivitySession *session);
//! Perform our once a minute activity session maintenance logic
void activity_sessions_prv_minute_handler(time_t utc_sec);
//! Send an activity session to data logging
void activity_sessions_prv_send_activity_session_to_data_logging(ActivitySession *session);
// ---------------------------------------------------------------------------
// Activity Metrics
//! Init all metrics
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now);
//! Returns info about each metric we capture
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info);
//! Perform our once a minute metrics maintenance logic
void activity_metrics_prv_minute_handler(time_t utc_sec);
//! Returns the number of millimeters the user has walked so far today (since midnight)
uint32_t activity_metrics_prv_get_distance_mm(void);
//! Returns the number of resting calories the user has consumed so far today (since midnight)
uint32_t activity_metrics_prv_get_resting_calories(void);
//! Returns the number of active calories the user has consumed so far today (since midnight)
uint32_t activity_metrics_prv_get_active_calories(void);
//! Retrieve the median heart rate and the total weight x100 since it was last reset.
//! If no readings were recorded since it was reset, it will return 0.
//! This median can be reset using activity_metrics_prv_reset_hr_stats().
//! It is by default reset once a minute.
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
int32_t *heart_rate_total_weight_x100_out);
//! Retrieve the current HR zone since it was last reset.
//! If no readings were recorded since it was reset, it will return 0.
//! This HR zone can be reset using activity_metrics_prv_reset_hr_stats().
//! It is by default reset once a minute.
HRZone activity_metrics_prv_get_hr_zone(void);
//! Reset the average / median heart rate and hr zone
void activity_metrics_prv_reset_hr_stats(void);
//! Feed in a new heart rate sample that will be used to update the median. This updates
//! the value returned by activity_metrics_prv_get_median_hr_bpm().
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
time_t now_uptime);
//! Returns the number of steps the user has taken so far today (since midnight)
uint32_t activity_metrics_prv_get_steps(void);
//! Returns the number of steps the user has walked in the past minute
ActivityScalarStore activity_metrics_prv_steps_per_minute(void);
//! Set a metric's value. Used from BlobDB to honor requests from the phone
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek day, int32_t value);

View File

@@ -0,0 +1,733 @@
/*
* Copyright 2024 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.
*/
#include "applib/data_logging.h"
#include "applib/health_service.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "os/tick.h"
#include "services/common/analytics/analytics_event.h"
#include "services/normal/alarms/alarm.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/passert.h"
#include "util/size.h"
#include <pebbleos/cron.h>
#include "activity.h"
#include "activity_algorithm.h"
#include "activity_insights.h"
#include "activity_private.h"
// ------------------------------------------------------------------------------------
// Figure out the cutoff times for sleep and step activities for today given the current time
static void prv_get_earliest_end_times_utc(time_t utc_sec, time_t *sleep_earliest_end_utc,
time_t *step_earliest_end_utc) {
time_t start_of_today_utc = time_util_get_midnight_of(utc_sec);
int last_sleep_second_of_day = ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
*sleep_earliest_end_utc = start_of_today_utc - (SECONDS_PER_DAY - last_sleep_second_of_day);
*step_earliest_end_utc = start_of_today_utc;
}
// ------------------------------------------------------------------------------------
// Remove all activity sessions that are older than "today", those that are invalid because they
// are in the future, and optionally those that are still ongoing.
void activity_sessions_prv_remove_out_of_range_activity_sessions(time_t utc_sec,
bool remove_ongoing) {
ActivityState *state = activity_private_state();
uint16_t num_sessions_to_clear = 0;
uint16_t *session_entries = &state->activity_sessions_count;
ActivitySession *sessions = state->activity_sessions;
// Figure out the cutoff times for sleep and step activities
time_t sleep_earliest_end_utc;
time_t step_earliest_end_utc;
prv_get_earliest_end_times_utc(utc_sec, &sleep_earliest_end_utc, &step_earliest_end_utc);
for (uint32_t i = 0; i < *session_entries; i++) {
time_t end_utc;
if (activity_sessions_prv_is_sleep_activity(sessions[i].type)) {
end_utc = sleep_earliest_end_utc;
} else {
end_utc = step_earliest_end_utc;
}
// See if we should keep this activity
time_t end_time = sessions[i].start_utc + (sessions[i].length_min * SECONDS_PER_MINUTE);
if ((end_time >= end_utc) && (end_time <= utc_sec)
&& (!remove_ongoing || !sessions[i].ongoing)) {
// Keep it
continue;
}
// This one needs to be removed
uint32_t remaining = *session_entries - i - 1;
memcpy(&sessions[i], &sessions[i + 1], remaining * sizeof(*sessions));
(*session_entries)--;
num_sessions_to_clear++;
i--;
}
// Zero out unused sessions at end. This is important because when we re-init from stored
// settings, we detect the number of sessions we have by checking for non-zero ones
memset(&sessions[*session_entries], 0, num_sessions_to_clear * sizeof(ActivitySession));
}
// ------------------------------------------------------------------------------------
// Return true if the given activity type is a sleep activity
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type) {
switch (activity_type) {
case ActivitySessionType_Sleep:
case ActivitySessionType_RestfulSleep:
case ActivitySessionType_Nap:
case ActivitySessionType_RestfulNap:
return true;
case ActivitySessionType_Walk:
case ActivitySessionType_Run:
case ActivitySessionType_Open:
return false;
case ActivitySessionType_None:
case ActivitySessionTypeCount:
break;
}
WTF;
}
// ------------------------------------------------------------------------------------
// Return true if this is a valid activity session
static bool prv_is_valid_activity_session(ActivitySession *session) {
// Make sure the type is valid
switch (session->type) {
case ActivitySessionType_Sleep:
case ActivitySessionType_RestfulSleep:
case ActivitySessionType_Nap:
case ActivitySessionType_RestfulNap:
case ActivitySessionType_Walk:
case ActivitySessionType_Run:
case ActivitySessionType_Open:
break;
case ActivitySessionType_None:
case ActivitySessionTypeCount:
PBL_LOG(LOG_LEVEL_WARNING, "Invalid activity type: %d", (int)session->type);
return false;
}
// The length must be reasonable
if (session->length_min > ACTIVITY_SESSION_MAX_LENGTH_MIN) {
PBL_LOG(LOG_LEVEL_WARNING, "Invalid duration: %"PRIu16" ", session->length_min);
return false;
}
// The flags must be valid
if (session->reserved != 0) {
PBL_LOG(LOG_LEVEL_WARNING, "Invalid flags: %d", (int)session->reserved);
return false;
}
return true;
}
// ------------------------------------------------------------------------------------
// Return true if two activity sessions are equal in their type and start time
// @param[in] session_a ptr to first session
// @param[in] session_b ptr to second session
// @param[in] any_sleep if true, a match occurs if session_a and session_b are both sleep
// activities, even if they are different types of sleep
static bool prv_activity_sessions_equal(ActivitySession *session_a, ActivitySession *session_b,
bool any_sleep) {
bool type_matches;
const bool a_is_sleep = activity_sessions_prv_is_sleep_activity(session_a->type);
const bool b_is_sleep = activity_sessions_prv_is_sleep_activity(session_b->type);
if (any_sleep && a_is_sleep && b_is_sleep) {
type_matches = true;
} else {
type_matches = (session_a->type == session_b->type);
}
return type_matches && (session_a->start_utc == session_b->start_utc);
}
// ------------------------------------------------------------------------------------
// Register a new activity. Called by the algorithm code when it detects a new activity.
// If we already have this activity registered, it is updated.
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
if (!session->ongoing) {
state->need_activities_saved = true;
}
// Modifying a sleep session?
if (activity_sessions_prv_is_sleep_activity(session->type)) {
state->sleep_sessions_modified = true;
}
// If this is an existing activity, update it
ActivitySession *stored_session = state->activity_sessions;
for (uint16_t i = 0; i < state->activity_sessions_count; i++, stored_session++) {
if (prv_activity_sessions_equal(session, stored_session, true /*any_sleep*/)) {
state->activity_sessions[i] = *session;
goto unlock;
}
}
// If no more room, fail
if (state->activity_sessions_count >= ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT) {
PBL_LOG(LOG_LEVEL_WARNING, "No more room for additional activities");
goto unlock;
}
// Add this activity in
PBL_LOG(LOG_LEVEL_INFO, "Adding activity session %d, start_time: %"PRIu32,
(int)session->type, (uint32_t)session->start_utc);
state->activity_sessions[state->activity_sessions_count++] = *session;
}
unlock:
mutex_unlock_recursive(state->mutex);
}
// ------------------------------------------------------------------------------------
// Delete an ongoing activity. Called by the algorithm code when it decides that an activity
// that was previously ongoing should not be registered after all.
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
// Look for this activity
int found_session_idx = -1;
ActivitySession *stored_session = state->activity_sessions;
for (uint16_t i = 0; i < state->activity_sessions_count; i++, stored_session++) {
if (prv_activity_sessions_equal(session, stored_session, false /*any_sleep*/)) {
found_session_idx = i;
break;
}
}
// If session not found, do nothing
if (found_session_idx < 0) {
PBL_LOG(LOG_LEVEL_WARNING, "Session to delete not found");
goto unlock;
}
// The session we are deleting must be ongoing
PBL_ASSERT(stored_session->ongoing, "Only ongoing sessions can be deleted");
// Remove this session
int num_to_move = state->activity_sessions_count - found_session_idx - 1;
PBL_ASSERTN(num_to_move >= 0);
if (num_to_move == 0) {
memset(&state->activity_sessions[found_session_idx], 0, sizeof(ActivitySession));
} else {
memmove(&state->activity_sessions[found_session_idx],
&state->activity_sessions[found_session_idx + 1],
num_to_move * sizeof(ActivitySession));
}
state->activity_sessions_count--;
}
unlock:
mutex_unlock_recursive(state->mutex);
}
// --------------------------------------------------------------------------------------------
// Compute the total number of restful sleep seconds within a range of time
static uint32_t prv_sleep_restful_seconds(uint32_t num_sessions, ActivitySession *sessions,
time_t start_utc, time_t end_utc) {
// Iterate through the sleep sessions, accumulating the total restful seconds seen between
// start_utc and end_utc
ActivitySession *session = sessions;
uint32_t restful_sec = 0;
for (uint32_t i = 0; i < num_sessions; i++, session++) {
if ((session->type != ActivitySessionType_RestfulSleep)
&& (session->type != ActivitySessionType_RestfulNap)) {
continue;
}
if ((session->start_utc >= start_utc)
&& ((time_t)(session->start_utc + (session->length_min * SECONDS_PER_MINUTE)) <= end_utc)) {
restful_sec += session->length_min * SECONDS_PER_MINUTE;
}
}
return restful_sec;
}
// --------------------------------------------------------------------------------------------
// Send an activity session (including sleep sessions) to data logging
void activity_sessions_prv_send_activity_session_to_data_logging(ActivitySession *session) {
ActivityState *state = activity_private_state();
time_t start_local = time_utc_to_local(session->start_utc);
ActivitySessionDataLoggingRecord dls_record = {
.version = ACTIVITY_SESSION_LOGGING_VERSION,
.size = sizeof(ActivitySessionDataLoggingRecord),
.activity = session->type,
.utc_to_local = start_local - session->start_utc,
.start_utc = (uint32_t)session->start_utc,
.elapsed_sec = session->length_min * SECONDS_PER_MINUTE,
};
if (activity_sessions_prv_is_sleep_activity(session->type)) {
dls_record.sleep_data = session->sleep_data;
} else {
dls_record.step_data = session->step_data;
}
if (state->activity_dls_session == NULL) {
// We don't need to be buffered since we are logging from the KernelBG task and this
// saves having to allocate another buffer from the kernel heap.
const bool buffered = false;
const bool resume = false;
Uuid system_uuid = UUID_SYSTEM;
state->activity_dls_session = dls_create(
DlsSystemTagActivitySession, DATA_LOGGING_BYTE_ARRAY, sizeof(dls_record),
buffered, resume, &system_uuid);
if (!state->activity_dls_session) {
PBL_LOG(LOG_LEVEL_WARNING, "Error creating activity DLS session");
return;
}
}
// Log the record
DataLoggingResult result = dls_log(state->activity_dls_session, &dls_record, 1);
if (result != DATA_LOGGING_SUCCESS) {
PBL_LOG(LOG_LEVEL_WARNING, "Error %"PRIi32" while logging activity to DLS", (int32_t)result);
}
PBL_LOG(LOG_LEVEL_INFO, "Logging activity event %d, start_time: %"PRIu32", "
"elapsed_min: %"PRIu16", end_time: %"PRIu32" ",
(int)session->type, (uint32_t)session->start_utc, session->length_min,
(uint32_t)session->start_utc + (session->length_min * SECONDS_PER_MINUTE));
}
// This structre holds stats we collected from going through a list of sleep sessions. It is
// filled in by prv_compute_sleep_stats
typedef struct {
ActivityScalarStore total_minutes;
ActivityScalarStore restful_minutes;
time_t enter_utc; // When we entered sleep
time_t today_exit_utc; // last exit time for today, for regular sleep only
time_t last_exit_utc; // last exit time (sleep or nap, ignoring "today" boundary)
time_t last_deep_exit_utc; // last deep sleep exit time (sleep or nap, ignoring "today" boundary)
uint32_t last_session_len_sec;
} ActivitySleepStats;
// --------------------------------------------------------------------------------------------
// Goes through a list of activity sessions and collect sleep stats
// @param[in] now_utc the UTC time when the activity sessions were computed
// @param[in] min_end_utc Only include sleep sessions that end AFTER this time
// @param[in] max_end_utc Only include sleep sessions that end BEFORE this time
// @param[in] last_processed_utc When activity sessions were computed, this is the UTC of the
// most recent minute we had access to when activities were computed.
// @param[out] stats this structure is filled in with the sleep stats
// @return True if there were sleep session, False if not
static bool prv_compute_sleep_stats(time_t now_utc, time_t min_end_utc, time_t max_end_utc,
ActivitySleepStats *stats) {
ActivityState *state = activity_private_state();
*stats = (ActivitySleepStats) { };
bool rv = false;
// Iterate through the sleep sessions, accumulating the total sleep minutes, total
// restful minutes, sleep enter time, and sleep exit time.
ActivitySession *session = state->activity_sessions;
for (uint32_t i = 0; i < state->activity_sessions_count; i++, session++) {
// Get info on this session
stats->last_session_len_sec = session->length_min * SECONDS_PER_MINUTE;
time_t session_exit_utc = session->start_utc + stats->last_session_len_sec;
// Skip if it ended too early
if (session_exit_utc < min_end_utc) {
continue;
}
if ((session->type == ActivitySessionType_Sleep)
|| (session->type == ActivitySessionType_Nap)) {
rv = true;
// Accumulate sleep container stats
if (session_exit_utc <= max_end_utc) {
stats->total_minutes += session->length_min;
}
// Only regular sleep (not naps) should affect the enter and exit times
if (session->type == ActivitySessionType_Sleep) {
stats->enter_utc = (stats->enter_utc != 0) ? MIN(session->start_utc, stats->enter_utc)
: session->start_utc;
if ((session_exit_utc > stats->today_exit_utc) && (session_exit_utc <= max_end_utc)) {
stats->today_exit_utc = session_exit_utc;
}
}
stats->last_exit_utc = MAX(session_exit_utc, stats->last_exit_utc);
} else if ((session->type == ActivitySessionType_RestfulSleep)
|| (session->type == ActivitySessionType_RestfulNap)) {
if (session_exit_utc <= max_end_utc) {
// Accumulate restful sleep stats
stats->restful_minutes += session->length_min;
}
stats->last_deep_exit_utc = MAX(stats->last_deep_exit_utc, session_exit_utc);
}
}
return rv;
}
// --------------------------------------------------------------------------------------------
// Goes through a list of activity sessions and updates our sleep totals in the metrics
// accordingly. We also take this opportunity to post a sleep metric changed event for the SDK
// if the sleep totals have changed.
// @param num_sessions the number of sessions in the sessions array
// @param sessions array of activity sessions
// @param now_utc the UTC time when the activity sessions were computed
// @param max_end_utc Only include sleep sessions that end BEFORE this time
// @param last_processed_utc When activity sessions were computed, this is the UTC of the
// most recent minute we had access to when activities were computed.
static void prv_update_sleep_metrics(time_t now_utc, time_t max_end_utc,
time_t last_processed_utc) {
ActivityState *state = activity_private_state();
mutex_lock_recursive(state->mutex);
{
// We will be filling in this structure based on the sleep sessions
ActivitySleepData *sleep_data = &state->sleep_data;
// If we detect a change in the sleep metrics, we want to post a health event
ActivitySleepData prev_sleep_data = *sleep_data;
// Collect stats on sleep
ActivitySleepStats stats;
if (!prv_compute_sleep_stats(now_utc, 0 /*min_end_utc*/, max_end_utc, &stats)) {
// We didn't have any sleep data exit early
goto unlock;
}
// Update our sleep metrics
sleep_data->total_minutes = stats.total_minutes;
sleep_data->restful_minutes = stats.restful_minutes;
// Fill in the enter and exit minute
uint16_t enter_minute = time_util_get_minute_of_day(stats.enter_utc);
uint16_t exit_minute = time_util_get_minute_of_day(stats.today_exit_utc);
sleep_data->enter_at_minute = enter_minute;
sleep_data->exit_at_minute = exit_minute;
// Fill in the rest of the sleep data metrics: the current state, and how long we have been
// in the current state
uint32_t delta_min = abs((int32_t)(last_processed_utc - stats.last_exit_utc))
/ SECONDS_PER_MINUTE;
// Figure out our current state
if (delta_min > 1) {
// We are awake
sleep_data->cur_state = ActivitySleepStateAwake;
if (stats.last_exit_utc != 0) {
sleep_data->cur_state_elapsed_minutes = (now_utc - stats.last_exit_utc)
/ SECONDS_PER_MINUTE;
} else {
sleep_data->cur_state_elapsed_minutes = MINUTES_PER_DAY;
}
} else {
// We are still sleeping
if (stats.last_deep_exit_utc == stats.last_exit_utc) {
sleep_data->cur_state = ActivitySleepStateRestfulSleep;
} else {
sleep_data->cur_state = ActivitySleepStateLightSleep;
}
sleep_data->cur_state_elapsed_minutes = (stats.last_session_len_sec + now_utc
- stats.last_exit_utc) / SECONDS_PER_MINUTE;
}
// If the info that is part of a health sleep event has changed, send out a notification event
if ((sleep_data->total_minutes != prev_sleep_data.total_minutes)
|| (sleep_data->restful_minutes != prev_sleep_data.restful_minutes)) {
// Post a sleep changed event
PebbleEvent e = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventSleepUpdate,
.data.sleep_update = {
.total_seconds = sleep_data->total_minutes * SECONDS_PER_MINUTE,
.total_restful_seconds = sleep_data->restful_minutes * SECONDS_PER_MINUTE,
},
},
};
event_put(&e);
}
if (sleep_data->cur_state != prev_sleep_data.cur_state) {
// Debug logging
ACTIVITY_LOG_DEBUG("total_min: %"PRIu16", deep_min: %"PRIu16", state: %"PRIu16", "
"state_min: %"PRIu16"",
sleep_data->total_minutes,
sleep_data->restful_minutes,
sleep_data->cur_state,
sleep_data->cur_state_elapsed_minutes);
}
}
unlock:
mutex_unlock_recursive(state->mutex);
}
// --------------------------------------------------------------------------------------------
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc,
time_t *exit_utc) {
// Get useful UTC times
time_t start_of_today_utc = time_util_get_midnight_of(now_utc);
int minute_of_day = time_util_get_minute_of_day(now_utc);
int last_sleep_second_of_day = ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
int first_sleep_utc;
if (minute_of_day < ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY) {
// It is before the ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (currently 9pm) cutoff, so use
// the previou day's cutoff
first_sleep_utc = start_of_today_utc - (SECONDS_PER_DAY - last_sleep_second_of_day);
} else {
// It is after 9pm, so use the 9pm cutoff
first_sleep_utc = start_of_today_utc + last_sleep_second_of_day;
}
// Compute stats for today
ActivitySleepStats stats;
prv_compute_sleep_stats(now_utc, first_sleep_utc /*min_utc*/, now_utc /*max_utc*/, &stats);
*enter_utc = stats.enter_utc;
*exit_utc = stats.today_exit_utc;
}
// --------------------------------------------------------------------------------------------
// Goes through a list of activity sessions and logs new ones to data logging
static void prv_log_activities(time_t now_utc) {
ActivityState *state = activity_private_state();
// Activity classes. All of the activities in a class share the same "_exit_at_utc" state in
// the globals and the same settings key to persist it.
enum {
// for ActivitySessionType_Sleep, ActivitySessionType_Nap
ActivityClass_Sleep = 0,
// for ActivitySessionType_RestfulSleep, ActivitySessionType_RestfulNap
ActivityClass_RestfulSleep = 1,
// for ActivitySessionType_Walk, ActivitySessionType_Run, ActivitySessionType_Open
ActivityClass_Step = 2,
// Leave at end
ActivityClassCount,
};
// List of event classes and info on each
typedef struct {
ActivitySettingsKey key; // settings key used to store last UTC time for this activity class
time_t *exit_utc; // pointer to last UTC time in our globals
bool modified; // true if we need to update it.
} ActivityClassParams;
ActivityClassParams class_settings[ActivityClassCount] = {
{ActivitySettingsKeyLastSleepActivityUTC,
&state->logged_sleep_activity_exit_at_utc, false},
{ActivitySettingsKeyLastRestfulSleepActivityUTC,
&state->logged_restful_sleep_activity_exit_at_utc, false},
{ActivitySettingsKeyLastStepActivityUTC,
&state->logged_step_activity_exit_at_utc, false},
};
bool logged_event = false;
ActivitySession *session = state->activity_sessions;
for (uint32_t i = 0; i < state->activity_sessions_count; i++, session++) {
// Get info on this activity
uint32_t session_len_sec = session->length_min * SECONDS_PER_MINUTE;
time_t session_exit_utc = session->start_utc + session_len_sec;
ActivityClassParams *params = NULL;
switch (session->type) {
case ActivitySessionType_Sleep:
case ActivitySessionType_Nap:
params = &class_settings[ActivityClass_Sleep];
break;
case ActivitySessionType_RestfulSleep:
case ActivitySessionType_RestfulNap:
params = &class_settings[ActivityClass_RestfulSleep];
break;
case ActivitySessionType_Walk:
case ActivitySessionType_Run:
case ActivitySessionType_Open:
params = &class_settings[ActivityClass_Step];
break;
case ActivitySessionType_None:
case ActivitySessionTypeCount:
WTF;
break;
}
PBL_ASSERTN(params);
// If this is an event we already logged, or it's still onging, don't log it
if (session->ongoing || (session_exit_utc <= *params->exit_utc)) {
continue;
}
// Don't log *any* sleep events until we know for sure we are awake. For restful sessions
// in particular, even if the session ended, it might later be converted to a restful nap
// session (after the container sleep session it is in finally ends).
if (activity_sessions_prv_is_sleep_activity(session->type)) {
if (state->sleep_data.cur_state != ActivitySleepStateAwake) {
continue;
}
}
// Log this event
activity_sessions_prv_send_activity_session_to_data_logging(session);
*params->exit_utc = session_exit_utc;
params->modified = true;
logged_event = true;
}
// Update settings file if any events were logged
if (logged_event) {
mutex_lock_recursive(state->mutex);
SettingsFile *file = activity_private_settings_open();
if (file) {
for (int i = 0; i < ActivityClassCount; i++) {
ActivityClassParams *params = &class_settings[i];
status_t result = settings_file_set(file, &params->key, sizeof(params->key),
params->exit_utc, sizeof(*params->exit_utc));
if (result != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_ERROR, "Error saving last event time");
}
}
activity_private_settings_close(file);
}
mutex_unlock_recursive(state->mutex);
}
}
// ------------------------------------------------------------------------------------------------
// Load in the stored activities from our settings file
void activity_sessions_prv_init(SettingsFile *file, time_t utc_now) {
ActivityState *state = activity_private_state();
ActivitySettingsKey key = ActivitySettingsKeyStoredActivities;
// Check the length first. The settings_file_get() call will not return an error if we ask
// for less than the value size
int stored_len = settings_file_get_len(file, &key, sizeof(key));
if (stored_len != sizeof(state->activity_sessions)) {
PBL_LOG(LOG_LEVEL_WARNING, "Stored activities not found or incompatible");
return;
}
// Read in the stored activities
status_t result = settings_file_get(file, &key, sizeof(key), state->activity_sessions,
sizeof(state->activity_sessions));
if (result != S_SUCCESS) {
return;
}
// Scan to see how many valid activities we have.
ActivitySession *session = state->activity_sessions;
ActivitySession null_session = {};
for (unsigned i = 0; i < ARRAY_LENGTH(state->activity_sessions); i++, session++) {
if (!memcmp(session, &null_session, sizeof(null_session))) {
// Empty session detected, we are done
break;
}
if (!prv_is_valid_activity_session(session)) {
// NOTE: We check for full validity as well as we can (rather than just checking for a
// non-null activity start time for example) because there have been cases where
// flash got corrupted, as in PBL-37848
PBL_HEXDUMP(LOG_LEVEL_INFO, (void *)state->activity_sessions,
sizeof(state->activity_sessions));
PBL_LOG(LOG_LEVEL_ERROR, "Invalid activity session detected - could be flash corrruption");
// Zero out flash so that we don't get into a reboot loop
memset(state->activity_sessions, 0, sizeof(state->activity_sessions));
settings_file_set(file, &key, sizeof(key), state->activity_sessions,
sizeof(state->activity_sessions));
WTF;
}
state->activity_sessions_count++;
}
// Remove any activities that don't belong to "today" or that are ongoing
activity_sessions_prv_remove_out_of_range_activity_sessions(utc_now, true /*remove_ongoing*/);
PBL_LOG(LOG_LEVEL_INFO, "Restored %"PRIu16" activities from storage",
state->activity_sessions_count);
}
// --------------------------------------------------------------------------------------
void NOINLINE activity_sessions_prv_minute_handler(time_t utc_sec) {
ActivityState *state = activity_private_state();
time_t last_sleep_processed_utc = activity_algorithm_get_last_sleep_utc();
// Post process sleep sessions if we got any new sleep sessions that showed up
if (state->sleep_sessions_modified) {
// Post-process the sleep activities. This is where we relabel sleep sessions as nap
// sessions, depending on time and length heuristics.
activity_algorithm_post_process_sleep_sessions(state->activity_sessions_count,
state->activity_sessions);
state->sleep_sessions_modified = false;
}
// Update sleep metrics
// For today's metrics, we include sleep sessions that end between
// ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY the previous day and ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY
// today. activity_algorithm_get_activity_sessions() insures that we only get sessions
// that end after ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY the previous day, so we just need to insure
// that the end BEFORE ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY today.
int last_sleep_utc_of_day = time_util_get_midnight_of(utc_sec)
+ ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
prv_update_sleep_metrics(utc_sec, last_sleep_utc_of_day,
last_sleep_processed_utc);
// Log any new activites we detected to the phone
prv_log_activities(utc_sec);
}
// ------------------------------------------------------------------------------------------------
bool activity_sessions_is_session_type_ongoing(ActivitySessionType type) {
ActivityState *state = activity_private_state();
bool rv = false;
mutex_lock_recursive(state->mutex);
{
for (int i = 0; i < state->activity_sessions_count; i++) {
const ActivitySession *session = &state->activity_sessions[i];
if (session->type == type && session->ongoing) {
rv = true;
break;
}
}
}
mutex_unlock_recursive(state->mutex);
return rv;
}
// ------------------------------------------------------------------------------------------------
DEFINE_SYSCALL(bool, sys_activity_sessions_is_session_type_ongoing, ActivitySessionType type) {
return activity_sessions_is_session_type_ongoing(type);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,260 @@
# Health Algorithms
## Step Counting
The step counting algorithm uses input from the accelerometer sensor to detect when the user is walking and how many steps have been taken. The accelerometer measures acceleration in each of 3 axes: x, y, and z. A perfectly still watch resting flat on a table will have 1G (1 “Gravity”) of acceleration in the z direction (due to gravity) and 0Gs in both the x and y axes. If you tilt the watch on its side for example, the z reading will go to 0 and then either x or y will show +/-1G (depending on which of the 4 sides you tilt it to). During watch movement, the x, y, and z readings will vary over time due to the watchs changing orientation to gravity as well as the acceleration of the watch when it changes direction or speed. The pattern of these variations in the accelerometer readings over time can be used to detect if, and how fast, the user is stepping.
There are generally two dominant signals that show up in the accelerometer readings when a person is walking or running. The first is the signal due to your feet hitting the ground. This signal shows up as a spike in the accelerometer readings each time a foot hits the ground and will be more or less pronounced depending on the cushioning of your shoes, the type of flooring, etc. Another signal that can show up is from the arm swinging motion, and the strength of this will vary depending on the users walking style, whether their hand is in their pocket or not, whether they are carrying something, etc.
Of these two signals, the foot fall one is the most reliable since a user will not always be swinging their arms when walking. The goal of the step tracking algorithm is to isolate and detect this foot fall signal, while not getting confused by other signals (arm swings, random arm movements, etc.).
An overall outline of the approach taken by the stepping algorithm (glossing over the details for now) is as follows:
1. Separate the accelerometer sensor readings into 5 second epochs.
2. For each 5 second epoch, compute an FFT (Fast Fourier Transform) to get the energy of the signal at different frequencies (called the _spectral density_)
3. Examine the FFT output using a set of heuristics to identify the foot fall signal (if present) and its frequency.
4. The frequency of the foot fall signal (if present) is outputted as the number of steps taken in that epoch.
As an example, if the FFT of a 5 second epoch shows a significant amount of foot fall signal at a frequency of 2Hz, we can assume the person has walked 10 steps (2Hz x 5 seconds) in that epoch.
### Example Data
The following figure shows an example of the raw accelerometer data of a five-second epoch when a user is walking 10 steps. The x, y, and z axis signals are each shown in a different color. In this plot, there is a fairly evident five-cycle rhythm in the red and green axes, which happens to be the arm swing signal (for every 2 steps taken, only 1 full arm swing cycle occurs). The ten-cycle foot fall signal however is difficult to see in this particular sample because the arm swing is so strong.
![Raw accelerometer data](raw_accel_5s.png)
The spectral density of that same walk sample, showing the amount of energy present at different frequencies, is shown in the following figure (this particular plot was generated from a sample longer than 5 seconds, so will be less noisy than any individual 5 second epoch). Here, the spectral density of the x, y, and z axes as well as the combined signal are each plotted in a different color. The _combined_ signal is computed as the magnitude of the x, y, and z spectral density at each frequency:
combined[f] = sqrt(x[f]^2 + y[f]^2 + z[f]^2)
_Note that the y axis on this plot is not simply Power, but rather “Power / Average Power”, where “Average Power” is the average power of that particular signal._
![Spectral Density](spectial_density.png)
You can see in the above spectral density plot that the dominant frequency in this example is 1Hz, corresponding to the 5 arm swings that occurred in these 5 seconds.
There are also several smaller peaks at the following frequencies:
- ~.25Hz: non-stepping signal, most likely random arm movements
- 2Hz: stepping frequency + 2nd harmonic of arm swing
- 3Hz: 3rd harmonic of arm swing
- 4Hz: 2nd harmonic of steps + 4th harmonic of arm swing
- 5Hz: 5th harmonic of arm swing
- 8Hz: 4th harmonic of steps
The logic used to pull out and identify the stepping signal from the spectral density output will be described later, but first we have to introduce the concept of VMC, or Vector Magnitude Counts.
### VMC
VMC, or Vector Magnitude Counts, is a measure of the overall amount of movement in the watch over time. When the watch is perfectly still, the VMC will be 0 and greater amounts of movement result in higher VMC numbers. Running, for example results in a higher VMC than walking.
The VMC computation in Pebble Health was developed in conjunction with the Stanford Wearables lab and has been calibrated to match the VMC numbers produced by the [Actigraph](http://www.actigraphcorp.com/product-category/activity-monitors/) wrist-worn device. The Actigraph is commonly used today for medical research studies. The Stanford Wearables lab will be publishing the VMC computation used in the Pebble Health algorithm and this transparency of the algorithm will enable the Pebble to be used for medical research studies as well.
VMC is computed using the formula below. Before the accelerometer readings are incorporated into this computation however, each axis signal is run through a bandpass filter with a design of 0.25Hz to 1.75Hz.
![](vmc_formula.png)
The following pseudo code summarizes the VMC calculation for N samples worth of data in each axis. The accelerometer is sampled at 25 samples per second in the Health algorithm, so the VMC calculation for 1 seconds worth of data would process 25 samples from each axis. The _bandpass\_filter_ method referenced in this pseudo code is a convolution filter with a frequency response of 0.25 to 1.75Hz:
for each axis in x, y, z:
axis_sum[axis] = 0
for each sample in axis from 0 to N:
filtered_sample = bandpass_filter(sample,
filter_state)
axis_sum[axis] += abs(filtered_sample)
VMC = scaling_factor * sqrt(axis_sum[x]^2
+ axis_sum[y]^2
+ axis_sum[z]^2)
The step algorithm makes use of a VMC that is computed over each 5-second epoch. In addition to this 5-second VMC, the algorithm also computes a VMC over each minute of data. It saves this 1-minute VMC to persistent storage and sends it to data logging as well so that it will eventually get pushed to the phone and saved to a server. The 1-minute VMC values stored in persistent storage can be accessed by 3rd party apps through the Health API. It is these 1-minute VMC values that are designed to match the Actigraph computations and are most useful to medical researcher studies.
### Step Identification
As mentioned above, accelerometer data is processed in chunks of 5 seconds (one epoch) at a time. For each epoch, we use the combined spectral density (FFT output) and the 5-second VMC as inputs to the step identification logic.
#### Generating the FFT output
The accelerometer is sampled at 25Hz, so each 5 second epoch comprises 125 samples in each axis. An FFT must have an input width which is a power of 2, so for each axis, we subtract the mean and then 0-extend to get 128 samples before computing the FFT for that axis.
An FFT of a real signal with 128 samples produces 128 outputs that represent 64 different frequencies. For each frequency, the FFT produces a real and an imaginary component (thus the 128 outputs for 64 different frequencies). The absolute value of the real and imaginary part denote the amplitude at a particular frequency, while the angle represents the phase of that frequency. It is the amplitude of each frequency that we are interested in, so we compute sqrt(real^2 + imag^2) of each frequency to end up with just 64 outputs.
Once the 64 values for each of the 3 axes have been computed, we combine them to get the overall energy at each frequency as follows:
for i = 0 to 63:
energy[i] = sqrt(amp_x[i]^2 + amp_y[i]^2 + amp_z[i]^2)
In this final array of 64 elements, element 0 represents the DC component (0 Hz), element 1 represents a frequency of 1 cycle per epoch (1 / 5s = 0.2Hz), element 2 represents a frequency of 2 cycles per epoch (2 / 5s = 0.4Hz), etc. If the user is walking at a rate of 9 steps every 5 seconds, then a spike will appear at index 9 in this array (9 / 5s = 1.8Hz).
As an example, the following shows the FFT output of a user walking approximately 9 steps in an epoch (with very little arm swing):
![FFT of stepping epoch, 9 steps](fft_walking.png)
#### Determining the stepping frequency
Once the FFT output and VMC have been obtained, we search for the most likely stepping frequency. The naive approach is to simply locate the frequency with the highest amplitude among all possible stepping frequencies. That would work fine for the example just shown above where there is a clear peak at index 9 of the FFT, which happens to be the stepping frequency.
However, for some users the arm swinging signal can be as large or larger than the stepping signal, and happens to be at half the stepping frequency. If a user is walking at a quick pace, the arm swinging signal could easily be misinterpreted as the stepping signal of a slow walk. The following is the FFT of such an example. The stepping signal shows up at indices 9 and 10, but there is a larger peak at the arm-swing frequency at index 5.
![Stepping epoch with large arm-swing component](fft_arm_swing.png "FFT of arm-swing walk")
To deal with these possible confusions between arm-swing and stepping signals, the VMC is used to narrow down which range of frequencies the stepping is likely to fall in. Based on the VMC level, we search one of three different ranges of frequencies to find which frequency has the most energy and is the likely stepping frequency. When the VMC is very low, we search through a range of frequencies that represent a slow walk, and for higher VMCs we search through ranges of frequencies that represent faster walks or runs.
Once we find a stepping frequency candidate within the expected range, we further refine the choice by factoring in the harmonics of the stepping/arm swinging. Occasionally, a max signal in the stepping range does not represent the actual stepping rate - it might be off by one or two indices due to noise in the signal, or it might be very close in value to the neighboring frequency, making it hard to determine which is the optimal one to use. This is evident in the arm-swinging output shown above where the energy at index 9 is very close to the energy at index 10.
As mentioned earlier, we often see significant energy at the harmonics of both the arm-swinging and the stepping frequency. A harmonic is an integer multiple of the fundamental frequency (i.e. a stepping frequency of 2 Hz will result in harmonics at 4Hz, 6Hz, 8Hz, etc.). To further refine the stepping frequency choice, we evaluate all possible stepping frequencies near the first candidate (+/- 2 indices on each side) and add in the energy of the harmonics for each. For each evaluation, we add up the energy of that stepping frequency, the arm energy that would correspond to that stepping frequency (the energy at half the stepping frequency), and the 2nd thru 5th harmonics of both the stepping and arm-swinging frequencies. Among these 5 different candidate stepping frequencies, we then choose the one that ended up with the most energy overall.
At the end of this process, we have the most likely stepping frequency, **if** the user is indeed walking. The next step is to determine whether or not the user is in fact walking or not.
#### Classifying step vs non-step epochs
In order to classify an epoch as walking or non-walking, we compute and check a number of metrics from the FFT output.
The first such metric is the _walking score_ which is the sum of the energy in the stepping related frequencies (signal energy) divided by the sum of energy of all frequencies (total energy). The signal energy includes the stepping frequency, arm-swing frequency, and each of their harmonics. If a person is indeed walking, the majority of the signal will appear at these signal frequencies, yielding a high walking score.
The second constraint that the epoch must pass is that the VMC must be above a _minimum stepping VMC_ threshold. A higher threshold is used if the detected stepping rate is higher.
The third constraint that the epoch must pass is that the amount of energy in the very low frequency components must be relatively low. To evaluate this constraint, the amount of energy in the low frequency components (indices 0 through 4) is summed and then divided by the signal energy (computed above). If this ratio is below a set _low frequency ratio_ threshold, the constraint is satisfied. The example below is typical of many epochs that are non-stepping epochs - a large amount of the energy appears in the very low frequency area.
![Non-stepping epoch](fft_non_walk.png)
The fourth and final constraint that the epoch must pass is that the energy in the high frequencies must be relatively low. To evaluate this constraint, the amount of energy in the high frequency components (index 50 and above) is summed and then divided by the signal energy. If this ratio is below a set _high frequency ratio_ threshold, the constraint is satisfied. This helps to avoid counting driving epochs as stepping epochs. In many instances, the vibration of the engine in a car will show up as energy at these high frequencies as shown in the following diagram.
![Driving epoch](fft_driving.png)
#### Partial Epochs
If the user starts or ends a walk in the middle of an epoch, the epoch will likely not pass the checks for a full fledged stepping epoch and these steps will therefore not get counted. To adjust for this undercounting, the algorithm introduces the concept of _partial epochs_.
The required _walking score_ and _minimum VMC_ are lower for a partial epoch vs. a normal epoch and there are no constraints on the low or high frequency signal ratios. To detect if an epoch is a _partial epoch_ we only check that the _walking score_ is above the _partial epoch walking score_ threshold and that the VMC is above the _partial epoch minimum VMC_ threshold.
If we detect a partial epoch, and either the prior or next epoch were classified as a stepping epoch, we add in half the number of steps that were detected in the adjacent stepping epoch. This helps to average out the undercounting that would normally occur at the start and end of a walk. For a very short walk that is less than 2 epochs long though, there is still a chance that no steps at all would be counted.
----
## Sleep Tracking
The sleep tracking algorithm uses the minute-level VMC values and minute-level average orientation of the watch to determine if/when the user is sleeping and whether or not the user is in “restful” sleep.
The minute-level VMC was described above. It gives a measure of the overall amount of movement seen by the watch in each minute.
The average orientation is a quantized (currently 8 bits) indication of the 3-D angle of the watch. It is computed once per minute based on the average accelerometer reading seen in each of the 3 axes. The angle of the watch in the X-Y plane is computed and quantized into the lower 4 bits and the angle of that vector with the Z-axis is then quantized and stored in the upper 4 bits.
### Sleep detection
The following discussion uses the term _sleep minute_. To determine if a minute is a _sleep minute_, we perform a convolution of the VMC values around that minute (using the 4 minutes immediately before and after the given minute) to generate a _filtered VMC_ and compare the _filtered VMC_ value to a threshold. If the result is below a determined sleep threshold, we count it as a _sleep minute_.
A rough outline of the sleep algorithm is as follows.
1. Sleep is entered if there are at least 5 _sleep minutes_ in a row.
2. Sleep continues until there are at least 11 non-_sleep minutes_ in a row.
3. If there were at least 60 minutes between the above sleep enter and sleep exit times, it is counted as a valid sleep session.
There are some exceptions to the above rules however:
- After sleep has been entered, if we see any minute with an exceptionally high _filtered VMC_, we end the sleep session immediately.
- If it is early in the sleep session (the first 60 minutes), we require 14 non-_sleep minutes_ in a row to consider the user as awake instead of 11.
- If at least 80% of the minutes have slight movement in them (even if each one is not high enough to make it a non-_sleep minute_), we consider the user awake.
- If we detect that the watch was not being worn during the above time (see below), we invalidate the sleep session.
#### Restful sleep
Once we detect a sleep session using the above logic, we make another pass through that same data to see if there are any periods within that session that might be considered as _restful sleep_.
A _restful sleep minute_ is a minute where the _filtered VMC_ is below the _restful sleep minute_ threshold (this is lower than the normal _sleep minute_ threshold).
1. Restful sleep is entered if there are at least 20 _restful sleep minutes_ in a row.
2. Restful sleep continues until there is at least 1 minute that is not a _restful sleep minute_.
### Detecting not-worn
Without some additional logic in place, the above rules would think a user is in a sleep session if the watch is not being worn. This is because there would be no movement and the VMC values would all be 0, or at least very low.
Once we detect a possible sleep session, we run that same data through the “not-worn” detection logic to determine if the watch was not being worn during that time. This is a set of heuristics that are designed to distinguish not-worn from sleep.
The following description uses the term _not worn minute_. A _not worn minute_ is a minute where **either** of the following is true:
- The VMC (the raw VMC, not _filtered VMC_) is below the _not worn_ threshold and the average orientation is same as it was the prior minute
- The watch is charging
If we see **both** of the following, we assume the watch is not being worn:
1. There are at least 100 _not worn_ minutes in a row in the sleep session
2. The _not worn_ section from #1 starts within 20 minutes of the start of the candidate sleep session and ends within 10 minutes of the end of the candidate sleep session.
The 100 minute required run length for _not worn_ might seem long, but it is not uncommon to see valid restful sleep sessions for a user that approach 100 minutes in length.
The orientation check is useful for situations where a watch is resting on a table, but encounters an occasional vibration due to floor or table shaking. This vibration shows up as a non-zero VMC and can look like the occasional movements that are normal during sleep. During actual sleep however, it is more likely that the user will change positions and end up at a different orientation on the next minute.
----
## System Integration
The following sections discuss how the step and sleep tracking algorithms are integrated into the firmware.
### Code organization
The core of the Health support logic is implemented in the activity service, which is in the `src/fw/services/normal/activity` directory. The 3rd party API, which calls into the activity service, is implemented in `src/fw/applib/health_service.c.`
The activity service implements the step and sleep algorithms and all of the supporting logic required to integrate the algorithms into the system. It has the following directory structure:
src/fw/services/normal/activity
activity.c
activity_insights.c
kraepelin/
kraepelin_algorithm.c
activity_algorithm_kraepelin.c
- **activity.c** This is the main module for the activity service. It implements the API for the activity service and the high level glue layer around the underlying step and sleep algorithms. This module contains only algorithm agnostic code and should require minimal changes if an alternative implementation for step or sleep tracking is incorporated in the future.
- **activity\_insights.c** This module implements the logic for generating Health timeline pins and notifications.
- **kraepelin** This subdirectory contains the code for the Kraepelin step and sleep algorithm, which is the name given to the current set of algorithms described in this document. This logic is broken out from the generic interface code in activity.c to make it easier to substitute in alternative algorithm implementations in the future if need be.
- **kraepelin\_algorithm.c** The core step and sleep algorithm code. This module is intended to be operating system agnostic and contains minimal calls to external functions. This module originated from open source code provided by the Stanford Wearables Lab.
- **kraepelin/activity\_algorightm\_kraepelin.c** This module wraps the core algorithm code found in `kraepelin_algorithm.c` to make it conform to the internal activity service algorithm API expected by activity.c. An alternative algorithm implementation would just need to implement this same API in order for it to be accessible from `activity.c`. This modules handles all memory allocations, persistent storage management, and other system integration functions for the raw algorithm code found in kraepelin\_algorithm.c.
The 3rd party Health API is implemented in `src/fw/applib/health_service.c`. The `health_service.c` module implements the “user land” logic for the Health API and makes calls into the activity service (which runs in privileged mode) to access the raw step and sleep data.
### Step Counting
The `activity.c` module asks the algorithm implementation `activity_algorithm_kraepelin.c` what accel sampling rate it requires and handles all of the logic required to subscribe to the accel service with that sampling rate. All algorithmic processing (both step and sleep) in the activity service is always done from the KernelBG task, so `activity.c` subscribes to the accel service from a KernelBG callback and provides the accel service a callback method which is implemented in `activity.c`.
When `activity.cs` accel service callback is called, it simply passes the raw accel data onto the underlying algorithms accel data handler implemented `activity_algorithm_kraepelin.c`. This handler in turn calls into the core algorithm code in `kraepelin_algorithm.c` to execute the raw step algorithm code and increments total steps by the number of steps returned by that method. Since the step algorithm in `kraepelin_algorithm.c` is based on five-second epochs and the accel service callback gets called once a second (25 samples per second), the call into `kraepelin_algorithm.c` will only return a non-zero step count value once every 5 times it is called.
Whenever a call is made to `activity.c` to get the total number of steps accumulated so far, `activity.c` will ask the `activity_algorithm_kraepelin.c` module for that count. The `activity_algorithm_kraepelin.c` module maintains that running count directly and returns it without needing to call into the raw algorithm code.
At midnight of each day, `activity.c` will make a call into `activity_algorithm_kraeplin.c` to reset the running count of steps back to 0.
### Sleep processing
For sleep processing, the `activity_algorithm_kraepelin.c` module has a much bigger role than it does for step processing. The core sleep algorithm in `kraepelin_algorithm.c` simply expects an array of VMC and average orientation values (one each per minute) and from that it identifies where the sleep sessions are. It is the role of `activity_algorithm_kraepelin.c` to build up this array of VMC values for the core algorithm and it does this by fetching the stored VMC and orientation values from persistent storage. The `activity_algorithm_kraepelin.c` module includes logic that periodically captures the VMC and orientation for each minute from the core algorithm module and saves those values to persistent storage for this purpose as well as for retrieval by the 3rd party API call that can be used by an app or worker to fetch historical minute-level values.
Currently, `activity.c` asks `activity_algorithm_kraepelin.c` to recompute sleep every 15 minutes. When asked to recompute sleep, `activity_algorithm_kraepelin.c` fetches the last 36 hours of VMC and orientation data from persistent storage and passes that array of values to the core sleep algorithm. When we compute sleep for the current day, we include all sleep sessions that *end* after midnight of the current day, so they may have started sometime before midnight. Including 36 hours of minute data means that, if asked to compute sleep at 11:59pm for example, we can go as far back as a sleep session that started at 6pm the prior day.
To keep memory requirements to a minimum, we encode each minute VMC value into a single byte for purposes of recomputing sleep. The raw VMC values that we store in persistent storage are 16-bit values, so we take the square root of each 16-bit value to compress it into a single byte. The average orientation is also encoded as a single byte. The 36 hours of minute data therefore requires that an array of 36 \* 60 \* 2 (4320) bytes be temporarily allocated and passed to the core sleep algorithm logic.
The core sleep logic in `kraepelin_algorithm.c` does not have any concept of what timestamp corresponds to each VMC value in the array, it only needs to describe the sleep sessions in terms of indices into the array. It is the role of `activity_algorithm_kraepelin.c` to translate these indices into actual UTC time stamps for use by the activity service.
## Algorithm Development and Testing
There is a full set of unit tests in `tests/fw/services/activity` for testing the step and sleep algorithms. These tests run captured sample data through the `kraepelin_algorithm.c` algorithm code to verify the expected number of steps or sleep sessions.
### Step Algorithm Testing
For testing the step algorithm, raw accel data is fed into the step algorithm. This raw accel data is stored in files as raw tuples of x, y z, accelerometer readings and can be found in the `tests/fixtures/activity/step_samples` directory.
Although these files have the C syntax, they are not compiled but are read in and parsed by the unit tests at run-time. Each sample in each file contains meta-data that tells the unit test the expected number of steps for that sample, which is used to determine if the test passes or not.
To capture these samples, the activity service has a special mode that can be turned on for raw sample capture and the `Activity Demo` app has an item in its debug menu for turning on this mode. When this mode is turned on, the activity service saves raw samples to data logging, and at the same time, also captures the raw sample data to the Pebble logs as base64 encoded binary data. Capturing the accel data to the logs makes it super convenient to pull that data out of the watch simply by issuing a support request from the mobile app.
The `tools/activity/parse_activity_data_logging_records.py` script can be used to parse the raw accel samples out of a log file that was captured as part of a support request or from a binary file containing the data logging records captured via data logging. This tool outputs a text file, in C syntax, that can be used directly by the step tracking unit tests.
The unit test that processes all of the step samples in `tests/fixtures/activity/step_samples` insures that the number of steps computed by the algorithm for each sample is within the allowed minimum and maximum for that sample (as defined by the meta data included in each sample file). It also computes an overall error amount across all sample files and generates a nice summary report for reference purposes. When tuning the algorithm, these summary reports can be used to easily compare results for various potential changes.
### Sleep Algorithm Testing
For testing the sleep algorithm, minute-by-minute VMC values are fed into the algorithm code. The set of sample sleep files used by the unit tests are found in the `tests/fixtures/activity/sleep_samples` directory. As is the case for the step samples, these files are parsed by the unit tests at run-time even though they are in C syntax.
To capture these samples, the activity service has a special call that will result in a dump of the contents of the last 36 hours of minute data to the Pebble logs. The `Activity Demo` app has an item in its debug menu for triggering this call. When this call is made, the activity service will fetch the last 36 hours of minute data from persistent storage, base64 encode it, and put it into the Pebble logs so that it can be easily retrieved using a support request from the mobile app.
As is the case for step data, the `tools/activity/parse_activity_data_logging_records.py` script can also be used to extract the minute data out of a support request log file and will in turn generate a text file that can be directly parsed by the sleep algorithm unit tests.
Each sleep sample file contains meta data in it that provides upper and lower bounds for each of the sleep metrics that can be computed by the algorithm (total amount of sleep, total amount of restful sleep, sleep start time, sleep end time, etc.). These metrics are checked by the unit tests to determine if each sample passes.
Note that the minute-by-minute VMC values can always be captured up to 36 hours **after** a sleep issue has been discovered on the watch since the watch is always storing these minute statistics in persistent storage. In contrast, turning on capture of raw accel data for a step algorithm issue must be done before the user starts the activity since capturing raw accel data is too expensive (memory and power-wise) to leave on all the time.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2024 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.
*/
#include "health_util.h"
#include "services/common/i18n/i18n.h"
#include "services/normal/activity/activity.h"
#include "shell/prefs.h"
#include "util/time/time.h"
#include "util/units.h"
#include "util/string.h"
#include <limits.h>
#include <stdio.h>
#include <string.h>
static void prv_convert_duration_to_hours_and_minutes(int duration_s, int *hours, int *minutes) {
*hours = (duration_s / SECONDS_PER_HOUR) ?: INT_MIN;
*minutes = ((duration_s % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) ?: INT_MIN;
if (*minutes == INT_MIN && *hours == INT_MIN) {
*hours = 0;
}
}
int health_util_format_hours_and_minutes(char *buffer, size_t buffer_size, int duration_s,
void *i18n_owner) {
int hours;
int minutes;
prv_convert_duration_to_hours_and_minutes(duration_s, &hours, &minutes);
int pos = 0;
if (hours != INT_MIN) {
pos += snprintf(buffer + pos, buffer_size - pos, i18n_get("%dH", i18n_owner), hours);
if (minutes != INT_MIN && pos < (int)buffer_size - 1) {
buffer[pos++] = ' ';
}
}
if (minutes != INT_MIN) {
pos += snprintf(buffer + pos, buffer_size - pos, i18n_get("%dM", i18n_owner), minutes);
}
return pos;
}
int health_util_format_hours_minutes_seconds(char *buffer, size_t buffer_size, int duration_s,
bool leading_zero, void *i18n_owner) {
const int hours = duration_s / SECONDS_PER_HOUR;
const int minutes = (duration_s % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
const int seconds = (duration_s % SECONDS_PER_HOUR) % SECONDS_PER_MINUTE;
if (hours > 0) {
const char *fmt = leading_zero ? "%02d:%02d:%02d" : "%d:%02d:%02d";
return snprintf(buffer, buffer_size, i18n_get(fmt, i18n_owner), hours, minutes, seconds);
} else {
const char *fmt = leading_zero ? "%02d:%02d" : "%d:%02d";
return snprintf(buffer, buffer_size, i18n_get(fmt, i18n_owner), minutes, seconds);
}
}
int health_util_format_minutes_and_seconds(char *buffer, size_t buffer_size, int duration_s,
void *i18n_owner) {
int minutes = duration_s / SECONDS_PER_MINUTE;
int seconds = duration_s % SECONDS_PER_MINUTE;
return snprintf(buffer, buffer_size, i18n_get("%d:%d", i18n_owner), minutes, seconds);
}
GTextNodeText *health_util_create_text_node(int buffer_size, GFont font, GColor color,
GTextNodeContainer *container) {
GTextNodeText *text_node = graphics_text_node_create_text(buffer_size);
if (container) {
graphics_text_node_container_add_child(container, &text_node->node);
}
text_node->font = font;
text_node->color = color;
return text_node;
}
GTextNodeText *health_util_create_text_node_with_text(const char *text, GFont font, GColor color,
GTextNodeContainer *container) {
GTextNodeText *text_node = health_util_create_text_node(0, font, color, container);
text_node->text = text;
return text_node;
}
void health_util_duration_to_hours_and_minutes_text_node(int duration_s, void *i18n_owner,
GFont number_font, GFont units_font,
GColor color,
GTextNodeContainer *container) {
int hours;
int minutes;
prv_convert_duration_to_hours_and_minutes(duration_s, &hours, &minutes);
const int units_offset_y = fonts_get_font_height(number_font) - fonts_get_font_height(units_font);
const int hours_and_minutes_buffer_size = sizeof("00");
if (hours != INT_MIN) {
GTextNodeText *hours_text_node = health_util_create_text_node(hours_and_minutes_buffer_size,
number_font, color, container);
snprintf((char *) hours_text_node->text, hours_and_minutes_buffer_size,
i18n_get("%d", i18n_owner), hours);
GTextNodeText *hours_units_text_node = health_util_create_text_node_with_text(
i18n_get("H", i18n_owner), units_font, color, container);
hours_units_text_node->node.offset.y = units_offset_y;
}
if (hours != INT_MIN && minutes != INT_MIN) {
// add a space between the H and the number of minutes
health_util_create_text_node_with_text(i18n_get(" ", i18n_owner), units_font, color, container);
}
if (minutes != INT_MIN) {
GTextNodeText *minutes_text_node = health_util_create_text_node(hours_and_minutes_buffer_size,
number_font, color, container);
snprintf((char *) minutes_text_node->text, hours_and_minutes_buffer_size,
i18n_get("%d", i18n_owner), minutes);
GTextNodeText *minutes_units_text_node = health_util_create_text_node_with_text(
i18n_get("M", i18n_owner), units_font, color, container);
minutes_units_text_node->node.offset.y = units_offset_y;
}
}
void health_util_convert_fraction_to_whole_and_decimal_part(int numerator, int denominator,
int* whole_part, int *decimal_part) {
const int figure = ROUND(numerator * 100, denominator * 10);
*whole_part = figure / 10;
*decimal_part = figure % 10;
}
int health_util_format_whole_and_decimal(char *buffer, size_t buffer_size, int numerator,
int denominator) {
int converted_distance_whole_part = 0;
int converted_distance_decimal_part = 0;
health_util_convert_fraction_to_whole_and_decimal_part(numerator, denominator,
&converted_distance_whole_part,
&converted_distance_decimal_part);
const char *fmt_i18n = i18n_noop("%d.%d");
const int rv = snprintf(buffer, buffer_size, i18n_get(fmt_i18n, buffer),
converted_distance_whole_part, converted_distance_decimal_part);
i18n_free(fmt_i18n, buffer);
return rv;
}
int health_util_get_distance_factor(void) {
switch (shell_prefs_get_units_distance()) {
case UnitsDistance_Miles:
return METERS_PER_MILE;
case UnitsDistance_KM:
return METERS_PER_KM;
case UnitsDistanceCount:
break;
}
return 1;
}
const char *health_util_get_distance_string(const char *miles_string, const char *km_string) {
switch (shell_prefs_get_units_distance()) {
case UnitsDistance_Miles:
return miles_string;
case UnitsDistance_KM:
return km_string;
case UnitsDistanceCount:
break;
}
return "";
}
int health_util_format_distance(char *buffer, size_t buffer_size, uint32_t distance_m) {
return health_util_format_whole_and_decimal(buffer, buffer_size, distance_m,
health_util_get_distance_factor());
}
void health_util_convert_distance_to_whole_and_decimal_part(int distance_m, int *whole_part,
int *decimal_part) {
const int conversion_factor = health_util_get_distance_factor();
health_util_convert_fraction_to_whole_and_decimal_part(distance_m, conversion_factor,
whole_part, decimal_part);
}
time_t health_util_get_pace(int time_s, int distance_meter) {
if (!distance_meter) {
return 0;
}
return ROUND(time_s * health_util_get_distance_factor(), distance_meter);
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "applib/ui/layer.h"
#include "apps/system_apps/timeline/text_node.h"
#include <stddef.h>
#include <stdint.h>
//! The maximum number of text nodes needed in a text node container
#define MAX_TEXT_NODES 5
//! Extra 4 bytes is for i18n purposes
#define HEALTH_WHOLE_AND_DECIMAL_LENGTH (sizeof("00.0") + 4)
//! Format a duration in seconds to hours and minutes, e.g. "12H 59M"
//! If duration is less than an hour, the format of "59M" is used.
//! If duration is a multiple of an hour, the format of "12H" is used.
//! If duration is 0, the string "0H" is used.
//! @param[in,out] buffer the string buffer to write to
//! @param buffer_size the size of the string buffer
//! @param duration_s the duration is seconds
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
//! @return snprintf-style number of bytes needed to be written not including the null terminator
int health_util_format_hours_and_minutes(char *buffer, size_t buffer_size, int duration_s,
void *i18n_owner);
//! Create a text node and add it to the container and set the font and color
//! @param buffer_size the size of the string buffer
//! @param font GFont to be used for the text node
//! @param color GColor to be used fot the text node
//! @param container GTextNodeContainer that the text node will be added to
GTextNodeText *health_util_create_text_node(int buffer_size, GFont font, GColor color,
GTextNodeContainer *container);
//! Create a text node with text and add it to the container and set the font and color
//! @param text the text string to be used for the text node
//! @param font GFont to be used for the text node
//! @param color GColor to be used fot the text node
//! @param container GTextNodeContainer that the text node will be added to
GTextNodeText *health_util_create_text_node_with_text(const char *text, GFont font, GColor color,
GTextNodeContainer *container);
//! Format a duration in seconds to hours, minutes and seconds, e.g. "1:15:32"
//! @param[in,out] buffer the string buffer to write to
//! @param buffer_size the size of the string buffer
//! @param duration_s the duration is seconds
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
//! @return snprintf-style number of bytes needed to be written not including the null terminator
int health_util_format_hours_minutes_seconds(char *buffer, size_t buffer_size, int duration_s,
bool leading_zero, void *i18n_owner);
//! Format a duration in seconds to minutes and seconds, e.g. "5:32"
//! @param[in,out] buffer the string buffer to write to
//! @param buffer_size the size of the string buffer
//! @param duration_s the duration is seconds
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
//! @return snprintf-style number of bytes needed to be written not including the null terminator
int health_util_format_minutes_and_seconds(char *buffer, size_t buffer_size, int duration_s,
void *i18n_owner);
//! Format a duration in seconds to hours and minutes, e.g. "12H 59M", using text node
//! number_font will be used for the nodes with hours and minutes,
//! units_font will be used for the "H" and "M"
//! If duration is less than an hour, the format of "59M" is used.
//! If duration is a multiple of an hour, the format of "12H" is used.
//! If duration is 0, the string "0H" is used.
//! @param duration_s the duration is seconds
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
//! @param number_font GFont to be used for the number text node
//! @param units_font GFont to be used for the units text node
//! @param color GColor to be used for the number and units text nodes
//! @param container GTextNodeContainer that will have the new number and units text nodes added to
void health_util_duration_to_hours_and_minutes_text_node(int duration_s, void *i18n_owner,
GFont number_font, GFont units_font,
GColor color,
GTextNodeContainer *container);
//! Convert a fraction into its whole and decimal parts
//! ex. 5/2 has a whole part of 2 and a decimal part of .5
//! @param numerator the numerator of the fraction
//! @param denominator the denominator of the fraction
//! @param[out] whole_part the whole part of the decimal representation
//! @param[out] decimal_part the decimal part of the decimal representation
void health_util_convert_fraction_to_whole_and_decimal_part(int numerator, int denominator,
int* whole_part, int *decimal_part);
//! Formats a fraction into its whole and decimal parts, e.g. "42.3"
//! @param[in,out] buffer the string buffer to write to
//! @param buffer_size the size of the string buffer
//! @param numerator the numerator of the fraction
//! @param denominator the denominator of the fraction
//! @return number of bytes written to buffer not including the null terminator
int health_util_format_whole_and_decimal(char *buffer, size_t buffer_size, int numerator,
int denominator);
//! @return meters conversion factor for the user's distance pref
int health_util_get_distance_factor(void);
//! @return the pace from a distance in meters and a time in seconds
time_t health_util_get_pace(int time_s, int distance_meter);
//! Get the meters units string for the user's distance pref
//! @param miles_string the units string to use if the user's preference is miles
//! @param km_string the units string to use if the user's preference is kilometers
//! @return meters units string matching the user's distance pref
const char *health_util_get_distance_string(const char *miles_string, const char *km_string);
//! Formats distance in meters based on the user's units preference, e.g. "42.3"
//! @param[in,out] buffer the string buffer to write to
//! @param buffer_size the size of the string buffer
//! @param distance_m the distance in meters
//! @return number of bytes written to buffer not including the null terminator
int health_util_format_distance(char *buffer, size_t buffer_size, uint32_t distance_m);
//! Convert distance in meters its whole and decimal parts in the user's distance pref
//! @param distance_m the distance in meters
//! @param[out] whole_part the whole part of the converted decimal representation
//! @param[out] decimal_part the decimal part of the converted decimal representation
void health_util_convert_distance_to_whole_and_decimal_part(int distance_m, int *whole_part,
int *decimal_part);

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2024 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.
*/
#include "hr_util.h"
#include "activity.h"
// ------------------------------------------------------------------------------------------------
HRZone hr_util_get_hr_zone(int bpm) {
const int zone_thresholds[HRZone_Max] = {
activity_prefs_heart_get_zone1_threshold(),
activity_prefs_heart_get_zone2_threshold(),
activity_prefs_heart_get_zone3_threshold(),
};
HRZone zone;
for (zone = HRZone_Zone0; zone < HRZone_Max; zone++) {
if (bpm < zone_thresholds[zone]) {
break;
}
}
return zone;
}
bool hr_util_is_elevated(int bpm) {
return bpm >= activity_prefs_heart_get_elevated_hr();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include <stdbool.h>
typedef enum HRZone {
HRZone_Zone0,
HRZone_Zone1,
HRZone_Zone2,
HRZone_Zone3,
HRZoneCount,
HRZone_Max = HRZone_Zone3,
} HRZone;
//! Returns the HR Zone for a given BPM
HRZone hr_util_get_hr_zone(int bpm);
//! Returns whether the BPM should be considered elevated
bool hr_util_is_elevated(int bpm);

View File

@@ -0,0 +1,263 @@
/*
* Copyright 2024 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.
*/
#include <inttypes.h>
#include <string.h>
#include "activity.h"
#include "insights_settings.h"
#include "os/mutex.h"
#include "services/normal/filesystem/pfs.h"
#include "services/normal/settings/settings_file.h"
#include "system/logging.h"
#include "util/size.h"
#define ACTIVITY_INSIGHTS_SETTINGS_FILENAME "insights"
#define ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE 4096
#define ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY "version"
#define ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION 0
#define ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION 4
static PebbleMutex *s_insight_settings_mutex;
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD_DEFAULT { \
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
.enabled = false, \
.reward = { \
.min_days_data = 6, \
.continuous_min_days_data = 2, \
.target_qualifying_days = 2, \
.target_percent_of_median = 120, \
.notif_min_interval_seconds = 7 * SECONDS_PER_DAY, \
.sleep.trigger_after_wakeup_seconds = 2 * SECONDS_PER_HOUR \
} \
}
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY_DEFAULT { \
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
.enabled = true, \
.summary = { \
.above_avg_threshold = 10, \
.below_avg_threshold = -10, \
.fail_threshold = -50, \
.sleep = { \
.max_fail_minutes = 7 * MINUTES_PER_HOUR, \
.trigger_notif_seconds = 30 * SECONDS_PER_MINUTE, \
.trigger_notif_activity = 20, \
.trigger_notif_active_minutes = 5 \
} \
} \
}
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD_DEFAULT { \
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
.enabled = false, \
.reward = {\
.min_days_data = 6, \
.continuous_min_days_data = 0, \
.target_qualifying_days = 0, \
.target_percent_of_median = 150, \
.notif_min_interval_seconds = 1 * SECONDS_PER_DAY, \
.activity = { \
.trigger_active_minutes = 2, \
.trigger_steps_per_minute = 50 \
} \
} \
}
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY_DEFAULT { \
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
.enabled = true, \
.summary = { \
.above_avg_threshold = 10, \
.below_avg_threshold = -10, \
.fail_threshold = -50, \
.activity = { \
.trigger_minute = (20 * MINUTES_PER_HOUR) + 30, \
.update_threshold_steps = 1000, \
.update_max_interval_seconds = 30 * SECONDS_PER_MINUTE, \
.show_notification = true, \
.max_fail_steps = 10000, \
} \
} \
}
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION_DEFAULT { \
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
.enabled = true, \
.session = { \
.show_notification = true, \
.activity = { \
.trigger_elapsed_minutes = 20, \
.trigger_cooldown_minutes = 10, \
}, \
} \
}
typedef struct {
const char *key;
ActivityInsightSettings default_val;
} AISDefault;
static const AISDefault AIS_DEFAULTS[] = {
{
.key = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD,
.default_val = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD_DEFAULT
},
{
.key = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY,
.default_val = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY_DEFAULT
},
{
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD,
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD_DEFAULT
},
{
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY,
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY_DEFAULT
},
{
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION,
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION_DEFAULT
},
};
// Return true if we successfully opened the file
static bool prv_open_settings_and_lock(SettingsFile *file) {
mutex_lock(s_insight_settings_mutex);
if (settings_file_open(file, ACTIVITY_INSIGHTS_SETTINGS_FILENAME,
ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE) == S_SUCCESS) {
return true;
} else {
mutex_unlock(s_insight_settings_mutex);
return false;
}
}
// Close the settings file and release the lock
static void prv_close_settings_and_unlock(SettingsFile *file) {
settings_file_close(file);
mutex_unlock(s_insight_settings_mutex);
}
void activity_insights_settings_init(void) {
// Create our mutex
s_insight_settings_mutex = mutex_create();
SettingsFile file;
if (settings_file_open(&file,
ACTIVITY_INSIGHTS_SETTINGS_FILENAME,
ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE) == S_SUCCESS) {
if (!settings_file_exists(&file,
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY))) {
// init version to 0
const uint16_t default_version = ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION;
settings_file_set(&file,
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY),
&default_version,
sizeof(uint16_t));
}
settings_file_close(&file);
return;
}
PBL_LOG(LOG_LEVEL_ERROR, "Failed to create activity insights settings file");
}
uint16_t activity_insights_settings_get_version(void) {
uint16_t version = ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION;
SettingsFile file;
if (prv_open_settings_and_lock(&file)) {
settings_file_get(&file,
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY),
&version,
sizeof(uint16_t));
prv_close_settings_and_unlock(&file);
}
return version;
}
bool activity_insights_settings_read(const char *insight_name,
ActivityInsightSettings *settings_out) {
bool rv = false;
*settings_out = (ActivityInsightSettings) {};
SettingsFile file;
if (prv_open_settings_and_lock(&file)) {
if (settings_file_get(&file,
insight_name, strlen(insight_name),
settings_out, sizeof(*settings_out)) != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_DEBUG, "Didn't find insight with key %s", insight_name);
goto close;
}
if (settings_out->version != ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION) {
// versions don't match, bail out!
PBL_LOG(LOG_LEVEL_WARNING, "activity insights struct version mismatch");
goto close;
}
rv = true;
close:
prv_close_settings_and_unlock(&file);
}
if (!rv) {
// Use default value if we didn't find anything else
for (unsigned i = 0; i < ARRAY_LENGTH(AIS_DEFAULTS); ++i) {
if (strcmp(insight_name, AIS_DEFAULTS[i].key) == 0) {
PBL_LOG(LOG_LEVEL_DEBUG, "Using default for insight %s", insight_name);
*settings_out = AIS_DEFAULTS[i].default_val;
rv = true;
}
}
}
return rv;
}
bool activity_insights_settings_write(const char *insight_name,
ActivityInsightSettings *settings) {
bool rv = false;
SettingsFile file;
if (prv_open_settings_and_lock(&file)) {
if (settings_file_set(&file,
insight_name, strlen(insight_name),
settings, sizeof(*settings)) != S_SUCCESS) {
PBL_LOG(LOG_LEVEL_WARNING, "Unable to save insight setting with key %s", insight_name);
} else {
rv = true;
}
prv_close_settings_and_unlock(&file);
}
return rv;
}
PFSCallbackHandle activity_insights_settings_watch(PFSFileChangedCallback callback) {
return pfs_watch_file(ACTIVITY_INSIGHTS_SETTINGS_FILENAME, callback, FILE_CHANGED_EVENT_CLOSED,
NULL);
}
void activity_insights_settings_unwatch(PFSCallbackHandle cb_handle) {
pfs_unwatch_file(cb_handle);
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "activity.h"
#include "services/normal/filesystem/pfs.h"
#include "util/attributes.h"
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD "sleep_reward"
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY "sleep_summary"
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD "activity_reward"
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY "activity_summary"
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION "activity_session"
typedef struct PACKED ActivityRewardSettings {
// Note: these parameters are the number of days in addition to 'today' that we want to look at
uint8_t min_days_data; //!< How many days of the metric's history we require
uint8_t continuous_min_days_data; //!< How many consecutive days of history we require
uint8_t target_qualifying_days; //!< Days that must be above target (on top of 'today')
uint16_t target_percent_of_median; //!< Percentage of median qualifying days must hit
uint32_t notif_min_interval_seconds; //!< How often we allow this insight to be shown
// Insight-specific values
union {
struct PACKED {
uint16_t trigger_after_wakeup_seconds; //!< Time we wait before showing sleep reward
} sleep;
struct PACKED {
uint8_t trigger_active_minutes; //!< Time we must be currently active before showing reward
uint8_t trigger_steps_per_minute; //!< Steps per minute required for an 'active' minute
} activity;
};
} ActivityRewardSettings;
typedef struct PACKED ActivitySummarySettings {
int8_t above_avg_threshold; //!< Values greater than this are counted as above avg
//!< In relation to 100% (eg 105% would be 5)
int8_t below_avg_threshold; //!< Values less than this are counted as above avg
//!< In relation to 100% (eg 93% would be -7)
int8_t fail_threshold; //!< Values less than this are counted as fail
//!< In releastion to 100% (e.g. 55% would be -45)
union {
struct PACKED {
uint16_t trigger_minute; //!< Minute of the day that we trigger the pin
uint16_t update_threshold_steps; //!< Step delta that will cause the pin to update
uint32_t update_max_interval_seconds; //!< Max time we'll go without updating the pin
bool show_notification; //!< Whether to show a notification
uint16_t max_fail_steps; //!< Don't show negative if walked more than X steps
} activity;
struct PACKED {
uint16_t max_fail_minutes; //!< Don't show negative if slept more than X minutes
uint16_t trigger_notif_seconds; //!< Time in seconds after wakeup to notify about sleep
uint16_t trigger_notif_activity; //!< Minimum amount of steps per minute to trigger the
//!< Sleep summary notification
uint8_t trigger_notif_active_minutes; //!< Minimum amount of active minutes to trigger the
//!< Sleep summary notification
} sleep;
};
} ActivitySummarySettings;
typedef struct PACKED ActivitySessionSettings {
bool show_notification; //!< Whether to show a notification
union {
struct PACKED {
uint16_t trigger_elapsed_minutes; //!< Minimum length of a walk to be given an insight
uint16_t trigger_cooldown_minutes; //!< Minutes wait after end of session before notifying
} activity;
};
} ActivitySessionSettings;
typedef struct PACKED ActivityInsightSettings {
// Common parameters
uint8_t version; //!< Current version of the struct - must be first
bool enabled; //!< Insight enabled
uint8_t unused; //!< Unused
union {
ActivityRewardSettings reward;
ActivitySummarySettings summary;
ActivitySessionSettings session;
};
} ActivityInsightSettings;
//! Read a setting from the insights settings
//! @param insights_name the name of the insight for which to get a setting
//! @param[out] settings out an ActivityInsightSettings struct to which the data will be written
//! @returns true if the setting was found and the data is valid, false otherwise
//! @note if this function returns false, settings_out will be zeroed out.
bool activity_insights_settings_read(const char *insight_name,
ActivityInsightSettings *settings_out);
//! Write a setting to the insights settings (used for testing)
//! @param insights_name the name of the insight for which to get a setting
//! @param settings an ActivityInsightSettings struct which contains the data to be written
//! @returns true if the setting was successfully saved
bool activity_insights_settings_write(const char *insight_name,
ActivityInsightSettings *settings);
//! Get the current version of the insights settings
//! @return the version number for the current insights settings
//! @note this is separate from the struct version
uint16_t activity_insights_settings_get_version(void);
//! Initialize insights settings
void activity_insights_settings_init(void);
//! Watch the insights settings file. The callback is called whenever the file is closed with
//! modifications or deleted
//! @param callback Function to call when the file has been modified
//! @return Callback handle for passing into \ref activity_insights_settings_unwatch
PFSCallbackHandle activity_insights_settings_watch(PFSFileChangedCallback callback);
//! Stop watching the settings file
//! @param cb_handle Callback handle which was returned by \ref activity_insights_settings_watch
void activity_insights_settings_unwatch(PFSCallbackHandle cb_handle);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "util/time/time.h"
#include "kraepelin_algorithm.h"
// We divide the raw light sensor reading by this factor before storing it into AlgDlsMinuteData
#define ALG_RAW_LIGHT_SENSOR_DIVIDE_BY 16
// Nap constraints, also used by unit tests
// A sleep session in this range is always considered "primary" (not nap) sleep
// ... if it ends after this minute in the evening
#define ALG_PRIMARY_EVENING_MINUTE (21 * MINUTES_PER_HOUR) // 9pm
// ... or starts before this minute in the morning
#define ALG_PRIMARY_MORNING_MINUTE (12 * MINUTES_PER_HOUR) // 12pm
// A sleep session outside of the primary range is considered a nap if it is less than
// this duration, otherwise it is considered a primary sleep session
#define ALG_MAX_NAP_MINUTES (3 * MINUTES_PER_HOUR)
// Max number of hours of past data we process to figure out sleep for "today". If a sleep
// cycle *ends* after midnight today, then we still count it as today's sleep. That means the
// start of the sleep cycle could have started more than 24 hours ago.
#define ALG_SLEEP_HISTORY_HOURS_FOR_TODAY 36

View File

@@ -0,0 +1,640 @@
/*
* Copyright 2024 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.
*/
#include "workout_service.h"
#include "activity_algorithm.h"
#include "activity_calculators.h"
#include "activity_insights.h"
#include "activity_private.h"
#include "hr_util.h"
#include "apps/system_apps/workout/workout_utils.h"
#include "applib/app.h"
#include "applib/health_service.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "services/common/evented_timer.h"
#include "services/common/regular_timer.h"
#include "system/passert.h"
#include "util/time/time.h"
#include "util/units.h"
#include <os/mutex.h>
#define WORKOUT_HR_READING_TS_EXPIRE (SECONDS_PER_MINUTE)
#define WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE (10 * SECONDS_PER_MINUTE)
#define WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE (SECONDS_PER_HOUR)
#define WORKOUT_ABANDONED_NOTIFICATION_TIMEOUT_MS (55 * MS_PER_MINUTE)
#define WORKOUT_ABANDON_WORKOUT_TIMEOUT_MS (5 * MS_PER_MINUTE)
//! Allocated when a Workout is started
typedef struct CurrentWorkoutData {
ActivitySessionType type;
time_t start_utc;
time_t last_paused_utc;
time_t duration_completed_pauses_s;
int32_t duration_s;
int32_t steps;
int32_t distance_m;
// Pace
int32_t active_calories;
int32_t current_bpm;
time_t current_bpm_timestamp_ts; // Time since boot
HRZone current_hr_zone;
int32_t hr_zone_time_s[HRZoneCount];
int32_t hr_samples_sum;
int32_t hr_samples_count;
// Step count total from the last HealthEventMovementUpdate
int32_t last_event_step_count;
time_t last_movement_event_time_ts;
// Whether or not the current workout is paused
bool paused;
EventedTimerID workout_abandoned_timer;
} CurrentWorkoutData;
//! Persisted statically in RAM
typedef struct WorkoutServiceData {
PebbleRecursiveMutex *s_workout_mutex;
RegularTimerInfo second_timer;
time_t last_workout_end_ts;
time_t frontend_last_opened_ts;
HRMSessionRef hrm_session;
CurrentWorkoutData *current_workout;
} WorkoutServiceData;
static WorkoutServiceData s_workout_data;
static void prv_lock(void) {
mutex_lock_recursive(s_workout_data.s_workout_mutex);
}
static void prv_unlock(void) {
mutex_unlock_recursive(s_workout_data.s_workout_mutex);
}
static void prv_put_event(PebbleWorkoutEventType e_type) {
PebbleEvent event = {
.type = PEBBLE_WORKOUT_EVENT,
.workout = {
.type = e_type,
}
};
event_put(&event);
}
static int32_t prv_get_avg_hr(void) {
if (!s_workout_data.current_workout->hr_samples_count) {
return 0;
}
return ROUND(s_workout_data.current_workout->hr_samples_sum,
s_workout_data.current_workout->hr_samples_count);
}
static void prv_update_duration(void) {
// We can't just increment the time on a second callback because of the inaccuracy of our timer
// system. PBL-32523
// Instead, we keep track of a start_utc, paused_time, and last_paused_utc. With these
// we can accurately keep track of the total duration of the workout.
if (!workout_service_is_workout_ongoing()) {
return;
}
time_t now_utc = rtc_get_time();
time_t total_paused_time_s = s_workout_data.current_workout->duration_completed_pauses_s;
if (workout_service_is_paused()) {
const time_t duration_current_pause = now_utc - s_workout_data.current_workout->last_paused_utc;
total_paused_time_s += duration_current_pause;
}
s_workout_data.current_workout->duration_s =
now_utc - s_workout_data.current_workout->start_utc - total_paused_time_s;
}
static void prv_reset_hr_data(void) {
const time_t now_ts = time_get_uptime_seconds();
s_workout_data.current_workout->current_bpm = 0;
s_workout_data.current_workout->current_hr_zone = HRZone_Zone0;
s_workout_data.current_workout->current_bpm_timestamp_ts = now_ts;
}
// ---------------------------------------------------------------------------------------
static void prv_handle_movement_update(HealthEventMovementUpdateData *event) {
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
const int32_t new_event_steps = event->steps;
const time_t now_ts = time_get_uptime_seconds();
if (new_event_steps < wrkt_data->last_event_step_count) {
PBL_LOG(LOG_LEVEL_WARNING, "Working out through midnight, resetting last_event_step_count");
wrkt_data->last_event_step_count = 0;
}
if (!workout_service_is_paused()) {
// Calculate the step delta
const uint32_t delta_steps = new_event_steps - wrkt_data->last_event_step_count;
wrkt_data->steps += delta_steps;
// Calculate the distance delta
const time_t delta_ms = (now_ts - wrkt_data->last_movement_event_time_ts) * MS_PER_SECOND;
const int32_t delta_distance_mm = activity_private_compute_distance_mm(delta_steps, delta_ms);
wrkt_data->distance_m += (delta_distance_mm / MM_PER_METER);
// Calculate active calories
const int32_t active_calories = activity_private_compute_active_calories(delta_distance_mm,
delta_ms);
wrkt_data->active_calories += active_calories;
}
// Reset the last event count regardless of whether we are paused
wrkt_data->last_event_step_count = new_event_steps;
wrkt_data->last_movement_event_time_ts = now_ts;
}
static void prv_handle_heart_rate_update(HealthEventHeartRateUpdateData *event) {
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
if (event->is_filtered) {
// We don't care about median heart rate updates
return;
}
if (event->quality == HRMQuality_OffWrist) {
// Reset to zero for OffWrist readings
prv_reset_hr_data();
} else if (event->quality >= HRMQuality_Worst) {
const int prev_bpm_timestamp_ts = wrkt_data->current_bpm_timestamp_ts;
wrkt_data->current_bpm = event->current_bpm;
wrkt_data->current_hr_zone = hr_util_get_hr_zone(wrkt_data->current_bpm);
wrkt_data->current_bpm_timestamp_ts = time_get_uptime_seconds();
if (!workout_service_is_paused()) {
// TODO: Maybe apply smoothing
wrkt_data->hr_zone_time_s[wrkt_data->current_hr_zone] +=
wrkt_data->current_bpm_timestamp_ts - prev_bpm_timestamp_ts;
wrkt_data->hr_samples_count++;
wrkt_data->hr_samples_sum += event->current_bpm;
}
}
return;
}
// ---------------------------------------------------------------------------------------
bool workout_service_is_workout_type_supported(ActivitySessionType type) {
return type == ActivitySessionType_Walk ||
type == ActivitySessionType_Run ||
type == ActivitySessionType_Open;
}
// ---------------------------------------------------------------------------------------
T_STATIC void prv_abandon_workout_timer_callback(void *unused) {
workout_service_stop_workout();
}
// ---------------------------------------------------------------------------------------
T_STATIC void prv_abandoned_notification_timer_callback(void *unused) {
workout_utils_send_abandoned_workout_notification();
s_workout_data.current_workout->workout_abandoned_timer =
evented_timer_register(WORKOUT_ABANDON_WORKOUT_TIMEOUT_MS, false,
prv_abandon_workout_timer_callback, NULL);
}
// ---------------------------------------------------------------------------------------
T_STATIC void prv_workout_timer_cb(void *unused) {
if (!workout_service_is_workout_ongoing()) {
return;
}
prv_lock();
{
// Update the duration
prv_update_duration();
// Check to make sure our HR sample is still valid
const time_t now_ts = time_get_uptime_seconds();
const time_t age_hr_s = now_ts - s_workout_data.current_workout->current_bpm_timestamp_ts;
if (s_workout_data.current_workout->current_bpm != 0 &&
age_hr_s >= WORKOUT_HR_READING_TS_EXPIRE) {
// Reset HR reading. It has expired
prv_reset_hr_data();
}
}
prv_unlock();
}
// ---------------------------------------------------------------------------------------
void workout_service_health_event_handler(PebbleHealthEvent *event) {
if (!workout_service_is_workout_ongoing()) {
return;
}
prv_lock();
{
if (event->type == HealthEventMovementUpdate) {
prv_handle_movement_update(&event->data.movement_update);
} else if (event->type == HealthEventHeartRateUpdate) {
prv_handle_heart_rate_update(&event->data.heart_rate_update);
}
}
prv_unlock();
}
// ---------------------------------------------------------------------------------------
void workout_service_activity_event_handler(PebbleActivityEvent *event) {
if (!workout_service_is_workout_ongoing()) {
return;
}
if (event->type == PebbleActivityEvent_TrackingStopped) {
workout_service_pause_workout(true);
}
}
// ---------------------------------------------------------------------------------------
void workout_service_workout_event_handler(PebbleWorkoutEvent *event) {
if (!workout_service_is_workout_ongoing()) {
return;
}
// Handling this with an event because the timer needs to be called from KernelMain
if (event->type == PebbleWorkoutEvent_FrontendOpened) {
evented_timer_cancel(s_workout_data.current_workout->workout_abandoned_timer);
} else if (event->type == PebbleWorkoutEvent_FrontendClosed) {
s_workout_data.current_workout->workout_abandoned_timer =
evented_timer_register(WORKOUT_ABANDONED_NOTIFICATION_TIMEOUT_MS, false,
prv_abandoned_notification_timer_callback, NULL);
}
}
// ---------------------------------------------------------------------------------------
void workout_service_init(void) {
s_workout_data.s_workout_mutex = mutex_create_recursive();
}
// ---------------------------------------------------------------------------------------
// FIXME: We should probably handle this on KernelBG and not use the official app subscription
void workout_service_frontend_opened(void) {
PBL_ASSERT_TASK(PebbleTask_App);
prv_lock();
{
#if CAPABILITY_HAS_BUILTIN_HRM
s_workout_data.hrm_session =
sys_hrm_manager_app_subscribe(app_get_app_id(), 1, 0, HRMFeature_BPM);
#endif // CAPABILITY_HAS_BUILTIN_HRM
s_workout_data.frontend_last_opened_ts = time_get_uptime_seconds();
prv_put_event(PebbleWorkoutEvent_FrontendOpened);
}
prv_unlock();
}
// ---------------------------------------------------------------------------------------
// FIXME: We should probably handle this on KernelBG and not use the official app subscription
void workout_service_frontend_closed(void) {
PBL_ASSERT_TASK(PebbleTask_App);
prv_lock();
{
int32_t hr_time_left;
if (workout_service_is_workout_ongoing()) {
// The workout app can be closed without stopping the workout. In this scenario keep
// collecting HR data until so much time has passed that it is assumed the user has forgotten
// about the workout
hr_time_left = WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE;
} else if (s_workout_data.frontend_last_opened_ts >= s_workout_data.last_workout_end_ts) {
// If the app was opened and closed without starting a workout, turn the HR sensor off
hr_time_left = 0;
} else {
// We have ended a workout while the app was open. Make sure to keep the HR sensor on for at
// least a little bit after the workout is finished
const time_t now_ts = time_get_uptime_seconds();
const time_t time_since_workout = (now_ts - s_workout_data.last_workout_end_ts);
// After a workout has finished, keep the HR sensor on for a bit to capture the user's HR
// returning to a normal level.
hr_time_left = WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE - time_since_workout;
}
#if CAPABILITY_HAS_BUILTIN_HRM
if (hr_time_left > 0) {
// Still some time left. Set a subscription with an expiration
s_workout_data.hrm_session =
sys_hrm_manager_app_subscribe(app_get_app_id(), 1, hr_time_left, HRMFeature_BPM);
} else {
// No time left. Kill the subscription
sys_hrm_manager_unsubscribe(s_workout_data.hrm_session);
}
#endif // CAPABILITY_HAS_BUILTIN_HRM
prv_put_event(PebbleWorkoutEvent_FrontendClosed);
}
prv_unlock();
}
// ---------------------------------------------------------------------------------------
bool workout_service_start_workout(ActivitySessionType type) {
bool rv = true;
prv_lock();
{
if (!workout_service_is_workout_type_supported(type)) {
rv = false;
goto unlock;
}
if (workout_service_is_workout_ongoing()) {
PBL_LOG(LOG_LEVEL_WARNING, "Only 1 workout at a time is supported");
rv = false;
goto unlock;
}
// Before starting this new session we need to deal with any in progress sessions
uint32_t num_sessions = 0;
ActivitySession *sessions = kernel_zalloc_check(sizeof(ActivitySession) *
ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT);
activity_get_sessions(&num_sessions, sessions);
for (unsigned i = 0; i < num_sessions; i++) {
// End and save any automatically detected ongoing sessions
if (sessions[i].ongoing) {
sessions[i].ongoing = false;
activity_sessions_prv_add_activity_session(&sessions[i]);
}
}
kernel_free(sessions);
s_workout_data.current_workout = kernel_zalloc_check(sizeof(CurrentWorkoutData));
s_workout_data.current_workout->type = type;
s_workout_data.current_workout->start_utc = rtc_get_time();
s_workout_data.current_workout->current_bpm_timestamp_ts = time_get_uptime_seconds();
// FIXME: This probably doesn't need to be on a timer. We can just flush out a new time on each
// API function call
s_workout_data.second_timer = (RegularTimerInfo) {
.cb = prv_workout_timer_cb,
};
// Initialize all of our initial values for keeping track of metrics
activity_get_metric(ActivityMetricStepCount, 1,
&s_workout_data.current_workout->last_event_step_count);
s_workout_data.current_workout->last_movement_event_time_ts = time_get_uptime_seconds();
regular_timer_add_seconds_callback(&s_workout_data.second_timer);
// Finally tell our algorithm it should stop automatically tracking activities
activity_algorithm_enable_activity_tracking(false /* disable */);
PBL_LOG(LOG_LEVEL_INFO, "Starting a workout with type: %d", type);
prv_put_event(PebbleWorkoutEvent_Started);
}
unlock:
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_pause_workout(bool should_be_paused) {
if (workout_service_is_paused() == should_be_paused) {
// If no change in state, return early and successful
return true;
}
if (!workout_service_is_workout_ongoing()) {
PBL_LOG(LOG_LEVEL_WARNING, "Workout (un)pause requested but no workout in progress");
return false;
}
prv_lock();
{
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
if (workout_service_is_paused()) {
// We are paused and want to unpause. Add the in progress pause time to the total
wrkt_data->duration_completed_pauses_s += (rtc_get_time() - wrkt_data->last_paused_utc);
} else {
// We are unpaused and want to pause. Set the last_paused_utc timestamp
wrkt_data->last_paused_utc = rtc_get_time();
}
s_workout_data.current_workout->paused = should_be_paused;
// Update the global duration since we have changed the pause state
prv_update_duration();
PBL_LOG(LOG_LEVEL_INFO, "Paused a workout with type: %d", wrkt_data->type);
prv_put_event(PebbleWorkoutEvent_Paused);
}
prv_unlock();
return true;
}
// ---------------------------------------------------------------------------------------
bool workout_service_stop_workout(void) {
bool rv = true;
prv_lock();
{
if (!workout_service_is_workout_ongoing()) {
PBL_LOG(LOG_LEVEL_WARNING, "No workout in progress");
rv = false;
goto unlock;
}
// Create an activity session for this workout if it was long enough
if (s_workout_data.current_workout->duration_s >= SECONDS_PER_MINUTE) {
const time_t len_min =
MIN(ACTIVITY_SESSION_MAX_LENGTH_MIN,
s_workout_data.current_workout->duration_s / SECONDS_PER_MINUTE);
ActivitySession session = {
.type = s_workout_data.current_workout->type,
.start_utc = s_workout_data.current_workout->start_utc,
.length_min = len_min,
.ongoing = false,
.manual = true,
.step_data.steps = s_workout_data.current_workout->steps,
.step_data.distance_meters = s_workout_data.current_workout->distance_m,
.step_data.active_kcalories = ROUND(s_workout_data.current_workout->active_calories,
ACTIVITY_CALORIES_PER_KCAL),
.step_data.resting_kcalories = ROUND(activity_private_compute_resting_calories(len_min),
ACTIVITY_CALORIES_PER_KCAL),
};
activity_sessions_prv_add_activity_session(&session);
activity_insights_push_activity_session_notification(rtc_get_time(), &session,
prv_get_avg_hr(), s_workout_data.current_workout->hr_zone_time_s);
s_workout_data.last_workout_end_ts = time_get_uptime_seconds();
}
regular_timer_remove_callback(&s_workout_data.second_timer);
// Re-enable automatic activity tracking
activity_algorithm_enable_activity_tracking(true /* enable */);
PBL_LOG(LOG_LEVEL_INFO, "Stopping a workout with type: %d",
s_workout_data.current_workout->type);
prv_put_event(PebbleWorkoutEvent_Stopped);
kernel_free(s_workout_data.current_workout);
s_workout_data.current_workout = NULL;
}
unlock:
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_is_workout_ongoing(void) {
prv_lock();
bool rv = (s_workout_data.current_workout != NULL);
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_takeover_activity_session(ActivitySession *session) {
bool rv = true;
prv_lock();
{
if (!workout_service_is_workout_type_supported(session->type)) {
rv = false;
goto unlock;
}
ActivitySession session_copy = *session;
// Remove the session from out list of sessions so it doesn't get counted twice
activity_sessions_prv_delete_activity_session(session);
// Start a new workout
if (!workout_service_start_workout(session_copy.type)) {
rv = false;
goto unlock;
}
// Update the new workout to mirror the session we took over
s_workout_data.current_workout->start_utc = session_copy.start_utc;
s_workout_data.current_workout->duration_s = session_copy.length_min * SECONDS_PER_MINUTE;
s_workout_data.current_workout->steps = session_copy.step_data.steps;
s_workout_data.current_workout->distance_m = session_copy.step_data.distance_meters;
s_workout_data.current_workout->active_calories =
session_copy.step_data.active_kcalories * ACTIVITY_CALORIES_PER_KCAL;
}
unlock:
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_is_paused(void) {
prv_lock();
bool rv = (workout_service_is_workout_ongoing() && s_workout_data.current_workout->paused);
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_get_current_workout_type(ActivitySessionType *type_out) {
bool rv = true;
prv_lock();
if (!type_out || !workout_service_is_workout_ongoing()) {
rv = false;
} else {
if (type_out) {
*type_out = s_workout_data.current_workout->type;
}
}
prv_unlock();
return rv;
}
// ---------------------------------------------------------------------------------------
bool workout_service_get_current_workout_info(int32_t *steps_out, int32_t *duration_s_out,
int32_t *distance_m_out, int32_t *current_bpm_out,
HRZone *current_hr_zone_out) {
bool rv = true;
prv_lock();
{
if (!workout_service_is_workout_ongoing()) {
rv = false;
} else {
if (steps_out) {
*steps_out = s_workout_data.current_workout->steps;
}
if (duration_s_out) {
*duration_s_out = s_workout_data.current_workout->duration_s;
}
if (distance_m_out) {
*distance_m_out = s_workout_data.current_workout->distance_m;
}
if (current_bpm_out) {
*current_bpm_out = s_workout_data.current_workout->current_bpm;
}
if (current_hr_zone_out) {
*current_hr_zone_out = s_workout_data.current_workout->current_hr_zone;
}
}
}
prv_unlock();
return rv;
}
#if UNITTEST
bool workout_service_get_avg_hr(int32_t *avg_hr_out) {
if (!avg_hr_out || !workout_service_is_workout_ongoing()) {
return false;
}
*avg_hr_out = prv_get_avg_hr();
return true;
}
bool workout_service_get_current_workout_hr_zone_time(int32_t *hr_zone_time_s_out) {
if (!hr_zone_time_s_out || !workout_service_is_workout_ongoing()) {
return false;
}
prv_lock();
{
hr_zone_time_s_out[HRZone_Zone0] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone0];
hr_zone_time_s_out[HRZone_Zone1] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone1];
hr_zone_time_s_out[HRZone_Zone2] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone2];
hr_zone_time_s_out[HRZone_Zone3] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone3];
}
prv_unlock();
return true;
}
void workout_service_get_active_kcalories(int32_t *active) {
if (workout_service_is_workout_ongoing()) {
*active = ROUND(s_workout_data.current_workout->active_calories, ACTIVITY_CALORIES_PER_KCAL);
}
}
void workout_service_reset(void) {
if (s_workout_data.current_workout) {
kernel_free(s_workout_data.current_workout);
}
s_workout_data = (WorkoutServiceData) {};
}
#endif

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2024 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.
*/
#pragma once
#include "activity.h"
#include "hr_util.h"
#include "kernel/events.h"
#include <stdbool.h>
//! Workouts are very similar to ActivitySessions, the only difference is that they are manually
//! started / stopped, and update more frequently than automatically detected activities.
//! Note: If a workout is in progress, then we disable automatic activity detection.
//! Note: Only 1 workout at a time is supported
void workout_service_init(void);
//! Called by the frontend application to signal that the app has been opened.
//! @note Must be called from PebbleTask_App
void workout_service_frontend_opened(void);
//! Called by the frontend application to signal that the app has been closed.
//! @note Must be called from PebbleTask_App
void workout_service_frontend_closed(void);
//! Event handler for Health events
void workout_service_health_event_handler(PebbleHealthEvent *event);
//! Event handler for Activity events
void workout_service_activity_event_handler(PebbleActivityEvent *event);
//! Event handler for Workout events
void workout_service_workout_event_handler(PebbleWorkoutEvent *event);
//! Returns true if there is an ongoing workout
bool workout_service_is_workout_ongoing(void);
//! Returns true if the activity type is a supported workout
bool workout_service_is_workout_type_supported(ActivitySessionType type);
//! Start a new workout
//! This stops / saves all onoing automatically detected activity sessions
//! All workouts must eventually get stopped
bool workout_service_start_workout(ActivitySessionType type);
//! Pause / unpause the currect workout
bool workout_service_pause_workout(bool should_be_paused);
//! Stops the current workout. Resumes automatic activity session detection
bool workout_service_stop_workout(void);
//! Starts a workout using the data from the given activity session
bool workout_service_takeover_activity_session(ActivitySession *session);
//! Returns true if there is a paused workout
bool workout_service_is_paused(void);
//! Get the current workout type
//! Returns true if a workout is going on
bool workout_service_get_current_workout_type(ActivitySessionType *type_out);
//! Dumps the current state of the workout
bool workout_service_get_current_workout_info(int32_t *steps_out, int32_t *duration_s_out,
int32_t *distance_m_out, int32_t *current_bpm_out,
HRZone *current_hr_zone_out);