Import of the watch repository from Pebble
1516
src/fw/services/normal/activity/activity.c
Normal file
547
src/fw/services/normal/activity/activity.h
Normal 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();
|
||||
257
src/fw/services/normal/activity/activity_algorithm.h
Normal 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);
|
||||
|
||||
183
src/fw/services/normal/activity/activity_calculators.c
Normal 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;
|
||||
}
|
||||
34
src/fw/services/normal/activity/activity_calculators.h
Normal 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);
|
||||
2471
src/fw/services/normal/activity/activity_insights.c
Normal file
117
src/fw/services/normal/activity/activity_insights.h
Normal 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);
|
||||
783
src/fw/services/normal/activity/activity_metrics.c
Normal 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);
|
||||
}
|
||||
534
src/fw/services/normal/activity/activity_private.h
Normal 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);
|
||||
733
src/fw/services/normal/activity/activity_sessions.c
Normal 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, ¶ms->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);
|
||||
}
|
||||
BIN
src/fw/services/normal/activity/docs/fft_arm_swing.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/fw/services/normal/activity/docs/fft_driving.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/fw/services/normal/activity/docs/fft_non_walk.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/fw/services/normal/activity/docs/fft_walking.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
260
src/fw/services/normal/activity/docs/index.md
Normal 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 0G’s 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 watch’s 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 user’s 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.
|
||||
|
||||

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

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

|
||||
|
||||
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 second’s 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):
|
||||
|
||||

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

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

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

|
||||
|
||||
#### 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.c’s` accel service callback is called, it simply passes the raw accel data onto the underlying algorithm’s 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.
|
||||
BIN
src/fw/services/normal/activity/docs/raw_accel_5s.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/fw/services/normal/activity/docs/spectial_density.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/fw/services/normal/activity/docs/vmc_formula.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
195
src/fw/services/normal/activity/health_util.c
Normal 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);
|
||||
}
|
||||
136
src/fw/services/normal/activity/health_util.h
Normal 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);
|
||||
40
src/fw/services/normal/activity/hr_util.c
Normal 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();
|
||||
}
|
||||
35
src/fw/services/normal/activity/hr_util.h
Normal 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);
|
||||
263
src/fw/services/normal/activity/insights_settings.c
Normal 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);
|
||||
}
|
||||
136
src/fw/services/normal/activity/insights_settings.h
Normal 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);
|
||||
@@ -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
|
||||
640
src/fw/services/normal/activity/workout_service.c
Normal 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
|
||||
81
src/fw/services/normal/activity/workout_service.h
Normal 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);
|
||||