Files
pebble/src/fw/services/normal/activity/activity_private.h
2025-01-27 11:38:16 -08:00

535 lines
25 KiB
C

/*
* 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);