mirror of
https://github.com/google/pebble.git
synced 2025-05-09 19:11:41 -04:00
783 lines
30 KiB
C
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);
|
|
}
|