mirror of
https://github.com/google/pebble.git
synced 2026-02-21 12:36:50 -05:00
Import of the watch repository from Pebble
This commit is contained in:
783
src/fw/services/normal/activity/activity_metrics.c
Normal file
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);
|
||||
}
|
||||
Reference in New Issue
Block a user