pebble/src/fw/services/normal/activity/activity_metrics.c
2025-01-27 11:38:16 -08:00

783 lines
30 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.
*/
#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);
}