mirror of
https://github.com/google/pebble
synced 2026-10-16 08:14:11 +02:00
641 lines
22 KiB
C
641 lines
22 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 "workout_service.h"
|
|
|
|
#include "activity_algorithm.h"
|
|
#include "activity_calculators.h"
|
|
#include "activity_insights.h"
|
|
#include "activity_private.h"
|
|
#include "hr_util.h"
|
|
|
|
#include "apps/system_apps/workout/workout_utils.h"
|
|
#include "applib/app.h"
|
|
#include "applib/health_service.h"
|
|
#include "kernel/events.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "services/common/evented_timer.h"
|
|
#include "services/common/regular_timer.h"
|
|
#include "system/passert.h"
|
|
#include "util/time/time.h"
|
|
#include "util/units.h"
|
|
|
|
#include <os/mutex.h>
|
|
|
|
#define WORKOUT_HR_READING_TS_EXPIRE (SECONDS_PER_MINUTE)
|
|
#define WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE (10 * SECONDS_PER_MINUTE)
|
|
#define WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE (SECONDS_PER_HOUR)
|
|
#define WORKOUT_ABANDONED_NOTIFICATION_TIMEOUT_MS (55 * MS_PER_MINUTE)
|
|
#define WORKOUT_ABANDON_WORKOUT_TIMEOUT_MS (5 * MS_PER_MINUTE)
|
|
|
|
//! Allocated when a Workout is started
|
|
typedef struct CurrentWorkoutData {
|
|
ActivitySessionType type;
|
|
|
|
time_t start_utc;
|
|
time_t last_paused_utc;
|
|
time_t duration_completed_pauses_s;
|
|
|
|
int32_t duration_s;
|
|
int32_t steps;
|
|
int32_t distance_m;
|
|
// Pace
|
|
int32_t active_calories;
|
|
int32_t current_bpm;
|
|
time_t current_bpm_timestamp_ts; // Time since boot
|
|
HRZone current_hr_zone;
|
|
int32_t hr_zone_time_s[HRZoneCount];
|
|
int32_t hr_samples_sum;
|
|
int32_t hr_samples_count;
|
|
|
|
// Step count total from the last HealthEventMovementUpdate
|
|
int32_t last_event_step_count;
|
|
time_t last_movement_event_time_ts;
|
|
|
|
// Whether or not the current workout is paused
|
|
bool paused;
|
|
|
|
EventedTimerID workout_abandoned_timer;
|
|
} CurrentWorkoutData;
|
|
|
|
//! Persisted statically in RAM
|
|
typedef struct WorkoutServiceData {
|
|
PebbleRecursiveMutex *s_workout_mutex;
|
|
RegularTimerInfo second_timer;
|
|
time_t last_workout_end_ts;
|
|
time_t frontend_last_opened_ts;
|
|
HRMSessionRef hrm_session;
|
|
|
|
CurrentWorkoutData *current_workout;
|
|
} WorkoutServiceData;
|
|
|
|
static WorkoutServiceData s_workout_data;
|
|
|
|
static void prv_lock(void) {
|
|
mutex_lock_recursive(s_workout_data.s_workout_mutex);
|
|
}
|
|
|
|
static void prv_unlock(void) {
|
|
mutex_unlock_recursive(s_workout_data.s_workout_mutex);
|
|
}
|
|
|
|
static void prv_put_event(PebbleWorkoutEventType e_type) {
|
|
PebbleEvent event = {
|
|
.type = PEBBLE_WORKOUT_EVENT,
|
|
.workout = {
|
|
.type = e_type,
|
|
}
|
|
};
|
|
event_put(&event);
|
|
}
|
|
|
|
static int32_t prv_get_avg_hr(void) {
|
|
if (!s_workout_data.current_workout->hr_samples_count) {
|
|
return 0;
|
|
}
|
|
|
|
return ROUND(s_workout_data.current_workout->hr_samples_sum,
|
|
s_workout_data.current_workout->hr_samples_count);
|
|
}
|
|
|
|
static void prv_update_duration(void) {
|
|
// We can't just increment the time on a second callback because of the inaccuracy of our timer
|
|
// system. PBL-32523
|
|
// Instead, we keep track of a start_utc, paused_time, and last_paused_utc. With these
|
|
// we can accurately keep track of the total duration of the workout.
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
return;
|
|
}
|
|
|
|
time_t now_utc = rtc_get_time();
|
|
|
|
time_t total_paused_time_s = s_workout_data.current_workout->duration_completed_pauses_s;
|
|
if (workout_service_is_paused()) {
|
|
const time_t duration_current_pause = now_utc - s_workout_data.current_workout->last_paused_utc;
|
|
total_paused_time_s += duration_current_pause;
|
|
}
|
|
|
|
s_workout_data.current_workout->duration_s =
|
|
now_utc - s_workout_data.current_workout->start_utc - total_paused_time_s;
|
|
}
|
|
|
|
static void prv_reset_hr_data(void) {
|
|
const time_t now_ts = time_get_uptime_seconds();
|
|
s_workout_data.current_workout->current_bpm = 0;
|
|
s_workout_data.current_workout->current_hr_zone = HRZone_Zone0;
|
|
s_workout_data.current_workout->current_bpm_timestamp_ts = now_ts;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
static void prv_handle_movement_update(HealthEventMovementUpdateData *event) {
|
|
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
|
|
|
|
const int32_t new_event_steps = event->steps;
|
|
const time_t now_ts = time_get_uptime_seconds();
|
|
if (new_event_steps < wrkt_data->last_event_step_count) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Working out through midnight, resetting last_event_step_count");
|
|
wrkt_data->last_event_step_count = 0;
|
|
}
|
|
|
|
if (!workout_service_is_paused()) {
|
|
// Calculate the step delta
|
|
const uint32_t delta_steps = new_event_steps - wrkt_data->last_event_step_count;
|
|
wrkt_data->steps += delta_steps;
|
|
|
|
// Calculate the distance delta
|
|
const time_t delta_ms = (now_ts - wrkt_data->last_movement_event_time_ts) * MS_PER_SECOND;
|
|
const int32_t delta_distance_mm = activity_private_compute_distance_mm(delta_steps, delta_ms);
|
|
wrkt_data->distance_m += (delta_distance_mm / MM_PER_METER);
|
|
|
|
// Calculate active calories
|
|
const int32_t active_calories = activity_private_compute_active_calories(delta_distance_mm,
|
|
delta_ms);
|
|
wrkt_data->active_calories += active_calories;
|
|
}
|
|
|
|
// Reset the last event count regardless of whether we are paused
|
|
wrkt_data->last_event_step_count = new_event_steps;
|
|
wrkt_data->last_movement_event_time_ts = now_ts;
|
|
}
|
|
|
|
static void prv_handle_heart_rate_update(HealthEventHeartRateUpdateData *event) {
|
|
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
|
|
|
|
if (event->is_filtered) {
|
|
// We don't care about median heart rate updates
|
|
return;
|
|
}
|
|
|
|
if (event->quality == HRMQuality_OffWrist) {
|
|
// Reset to zero for OffWrist readings
|
|
prv_reset_hr_data();
|
|
} else if (event->quality >= HRMQuality_Worst) {
|
|
const int prev_bpm_timestamp_ts = wrkt_data->current_bpm_timestamp_ts;
|
|
|
|
wrkt_data->current_bpm = event->current_bpm;
|
|
wrkt_data->current_hr_zone = hr_util_get_hr_zone(wrkt_data->current_bpm);
|
|
wrkt_data->current_bpm_timestamp_ts = time_get_uptime_seconds();
|
|
|
|
if (!workout_service_is_paused()) {
|
|
// TODO: Maybe apply smoothing
|
|
wrkt_data->hr_zone_time_s[wrkt_data->current_hr_zone] +=
|
|
wrkt_data->current_bpm_timestamp_ts - prev_bpm_timestamp_ts;
|
|
wrkt_data->hr_samples_count++;
|
|
wrkt_data->hr_samples_sum += event->current_bpm;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_is_workout_type_supported(ActivitySessionType type) {
|
|
return type == ActivitySessionType_Walk ||
|
|
type == ActivitySessionType_Run ||
|
|
type == ActivitySessionType_Open;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
T_STATIC void prv_abandon_workout_timer_callback(void *unused) {
|
|
workout_service_stop_workout();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
T_STATIC void prv_abandoned_notification_timer_callback(void *unused) {
|
|
workout_utils_send_abandoned_workout_notification();
|
|
|
|
s_workout_data.current_workout->workout_abandoned_timer =
|
|
evented_timer_register(WORKOUT_ABANDON_WORKOUT_TIMEOUT_MS, false,
|
|
prv_abandon_workout_timer_callback, NULL);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
T_STATIC void prv_workout_timer_cb(void *unused) {
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
return;
|
|
}
|
|
|
|
prv_lock();
|
|
{
|
|
// Update the duration
|
|
prv_update_duration();
|
|
|
|
// Check to make sure our HR sample is still valid
|
|
const time_t now_ts = time_get_uptime_seconds();
|
|
const time_t age_hr_s = now_ts - s_workout_data.current_workout->current_bpm_timestamp_ts;
|
|
if (s_workout_data.current_workout->current_bpm != 0 &&
|
|
age_hr_s >= WORKOUT_HR_READING_TS_EXPIRE) {
|
|
// Reset HR reading. It has expired
|
|
prv_reset_hr_data();
|
|
}
|
|
}
|
|
prv_unlock();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void workout_service_health_event_handler(PebbleHealthEvent *event) {
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
return;
|
|
}
|
|
|
|
prv_lock();
|
|
{
|
|
if (event->type == HealthEventMovementUpdate) {
|
|
prv_handle_movement_update(&event->data.movement_update);
|
|
} else if (event->type == HealthEventHeartRateUpdate) {
|
|
prv_handle_heart_rate_update(&event->data.heart_rate_update);
|
|
}
|
|
}
|
|
prv_unlock();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void workout_service_activity_event_handler(PebbleActivityEvent *event) {
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
return;
|
|
}
|
|
|
|
if (event->type == PebbleActivityEvent_TrackingStopped) {
|
|
workout_service_pause_workout(true);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void workout_service_workout_event_handler(PebbleWorkoutEvent *event) {
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
return;
|
|
}
|
|
|
|
// Handling this with an event because the timer needs to be called from KernelMain
|
|
if (event->type == PebbleWorkoutEvent_FrontendOpened) {
|
|
evented_timer_cancel(s_workout_data.current_workout->workout_abandoned_timer);
|
|
} else if (event->type == PebbleWorkoutEvent_FrontendClosed) {
|
|
s_workout_data.current_workout->workout_abandoned_timer =
|
|
evented_timer_register(WORKOUT_ABANDONED_NOTIFICATION_TIMEOUT_MS, false,
|
|
prv_abandoned_notification_timer_callback, NULL);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
void workout_service_init(void) {
|
|
s_workout_data.s_workout_mutex = mutex_create_recursive();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// FIXME: We should probably handle this on KernelBG and not use the official app subscription
|
|
void workout_service_frontend_opened(void) {
|
|
PBL_ASSERT_TASK(PebbleTask_App);
|
|
prv_lock();
|
|
{
|
|
#if CAPABILITY_HAS_BUILTIN_HRM
|
|
s_workout_data.hrm_session =
|
|
sys_hrm_manager_app_subscribe(app_get_app_id(), 1, 0, HRMFeature_BPM);
|
|
#endif // CAPABILITY_HAS_BUILTIN_HRM
|
|
s_workout_data.frontend_last_opened_ts = time_get_uptime_seconds();
|
|
prv_put_event(PebbleWorkoutEvent_FrontendOpened);
|
|
}
|
|
prv_unlock();
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// FIXME: We should probably handle this on KernelBG and not use the official app subscription
|
|
void workout_service_frontend_closed(void) {
|
|
PBL_ASSERT_TASK(PebbleTask_App);
|
|
prv_lock();
|
|
{
|
|
int32_t hr_time_left;
|
|
|
|
if (workout_service_is_workout_ongoing()) {
|
|
// The workout app can be closed without stopping the workout. In this scenario keep
|
|
// collecting HR data until so much time has passed that it is assumed the user has forgotten
|
|
// about the workout
|
|
hr_time_left = WORKOUT_ACTIVE_HR_SUBSCRIPTION_TS_EXPIRE;
|
|
} else if (s_workout_data.frontend_last_opened_ts >= s_workout_data.last_workout_end_ts) {
|
|
// If the app was opened and closed without starting a workout, turn the HR sensor off
|
|
hr_time_left = 0;
|
|
} else {
|
|
// We have ended a workout while the app was open. Make sure to keep the HR sensor on for at
|
|
// least a little bit after the workout is finished
|
|
const time_t now_ts = time_get_uptime_seconds();
|
|
const time_t time_since_workout = (now_ts - s_workout_data.last_workout_end_ts);
|
|
|
|
// After a workout has finished, keep the HR sensor on for a bit to capture the user's HR
|
|
// returning to a normal level.
|
|
hr_time_left = WORKOUT_ENDED_HR_SUBSCRIPTION_TS_EXPIRE - time_since_workout;
|
|
}
|
|
|
|
#if CAPABILITY_HAS_BUILTIN_HRM
|
|
if (hr_time_left > 0) {
|
|
// Still some time left. Set a subscription with an expiration
|
|
s_workout_data.hrm_session =
|
|
sys_hrm_manager_app_subscribe(app_get_app_id(), 1, hr_time_left, HRMFeature_BPM);
|
|
} else {
|
|
// No time left. Kill the subscription
|
|
sys_hrm_manager_unsubscribe(s_workout_data.hrm_session);
|
|
}
|
|
#endif // CAPABILITY_HAS_BUILTIN_HRM
|
|
|
|
prv_put_event(PebbleWorkoutEvent_FrontendClosed);
|
|
}
|
|
prv_unlock();
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_start_workout(ActivitySessionType type) {
|
|
bool rv = true;
|
|
prv_lock();
|
|
{
|
|
if (!workout_service_is_workout_type_supported(type)) {
|
|
rv = false;
|
|
goto unlock;
|
|
}
|
|
|
|
if (workout_service_is_workout_ongoing()) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Only 1 workout at a time is supported");
|
|
rv = false;
|
|
goto unlock;
|
|
}
|
|
|
|
// Before starting this new session we need to deal with any in progress sessions
|
|
uint32_t num_sessions = 0;
|
|
ActivitySession *sessions = kernel_zalloc_check(sizeof(ActivitySession) *
|
|
ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT);
|
|
activity_get_sessions(&num_sessions, sessions);
|
|
for (unsigned i = 0; i < num_sessions; i++) {
|
|
// End and save any automatically detected ongoing sessions
|
|
if (sessions[i].ongoing) {
|
|
sessions[i].ongoing = false;
|
|
activity_sessions_prv_add_activity_session(&sessions[i]);
|
|
}
|
|
}
|
|
kernel_free(sessions);
|
|
|
|
s_workout_data.current_workout = kernel_zalloc_check(sizeof(CurrentWorkoutData));
|
|
s_workout_data.current_workout->type = type;
|
|
s_workout_data.current_workout->start_utc = rtc_get_time();
|
|
s_workout_data.current_workout->current_bpm_timestamp_ts = time_get_uptime_seconds();
|
|
// FIXME: This probably doesn't need to be on a timer. We can just flush out a new time on each
|
|
// API function call
|
|
s_workout_data.second_timer = (RegularTimerInfo) {
|
|
.cb = prv_workout_timer_cb,
|
|
};
|
|
|
|
// Initialize all of our initial values for keeping track of metrics
|
|
activity_get_metric(ActivityMetricStepCount, 1,
|
|
&s_workout_data.current_workout->last_event_step_count);
|
|
s_workout_data.current_workout->last_movement_event_time_ts = time_get_uptime_seconds();
|
|
|
|
regular_timer_add_seconds_callback(&s_workout_data.second_timer);
|
|
|
|
// Finally tell our algorithm it should stop automatically tracking activities
|
|
activity_algorithm_enable_activity_tracking(false /* disable */);
|
|
|
|
PBL_LOG(LOG_LEVEL_INFO, "Starting a workout with type: %d", type);
|
|
prv_put_event(PebbleWorkoutEvent_Started);
|
|
}
|
|
unlock:
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_pause_workout(bool should_be_paused) {
|
|
if (workout_service_is_paused() == should_be_paused) {
|
|
// If no change in state, return early and successful
|
|
return true;
|
|
}
|
|
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "Workout (un)pause requested but no workout in progress");
|
|
return false;
|
|
}
|
|
|
|
prv_lock();
|
|
{
|
|
CurrentWorkoutData *wrkt_data = s_workout_data.current_workout;
|
|
|
|
if (workout_service_is_paused()) {
|
|
// We are paused and want to unpause. Add the in progress pause time to the total
|
|
wrkt_data->duration_completed_pauses_s += (rtc_get_time() - wrkt_data->last_paused_utc);
|
|
} else {
|
|
// We are unpaused and want to pause. Set the last_paused_utc timestamp
|
|
wrkt_data->last_paused_utc = rtc_get_time();
|
|
}
|
|
|
|
s_workout_data.current_workout->paused = should_be_paused;
|
|
|
|
// Update the global duration since we have changed the pause state
|
|
prv_update_duration();
|
|
PBL_LOG(LOG_LEVEL_INFO, "Paused a workout with type: %d", wrkt_data->type);
|
|
prv_put_event(PebbleWorkoutEvent_Paused);
|
|
}
|
|
prv_unlock();
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_stop_workout(void) {
|
|
bool rv = true;
|
|
prv_lock();
|
|
{
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
PBL_LOG(LOG_LEVEL_WARNING, "No workout in progress");
|
|
rv = false;
|
|
goto unlock;
|
|
}
|
|
|
|
// Create an activity session for this workout if it was long enough
|
|
if (s_workout_data.current_workout->duration_s >= SECONDS_PER_MINUTE) {
|
|
const time_t len_min =
|
|
MIN(ACTIVITY_SESSION_MAX_LENGTH_MIN,
|
|
s_workout_data.current_workout->duration_s / SECONDS_PER_MINUTE);
|
|
|
|
ActivitySession session = {
|
|
.type = s_workout_data.current_workout->type,
|
|
.start_utc = s_workout_data.current_workout->start_utc,
|
|
.length_min = len_min,
|
|
.ongoing = false,
|
|
.manual = true,
|
|
.step_data.steps = s_workout_data.current_workout->steps,
|
|
.step_data.distance_meters = s_workout_data.current_workout->distance_m,
|
|
.step_data.active_kcalories = ROUND(s_workout_data.current_workout->active_calories,
|
|
ACTIVITY_CALORIES_PER_KCAL),
|
|
.step_data.resting_kcalories = ROUND(activity_private_compute_resting_calories(len_min),
|
|
ACTIVITY_CALORIES_PER_KCAL),
|
|
};
|
|
activity_sessions_prv_add_activity_session(&session);
|
|
|
|
activity_insights_push_activity_session_notification(rtc_get_time(), &session,
|
|
prv_get_avg_hr(), s_workout_data.current_workout->hr_zone_time_s);
|
|
|
|
s_workout_data.last_workout_end_ts = time_get_uptime_seconds();
|
|
}
|
|
|
|
regular_timer_remove_callback(&s_workout_data.second_timer);
|
|
|
|
// Re-enable automatic activity tracking
|
|
activity_algorithm_enable_activity_tracking(true /* enable */);
|
|
|
|
PBL_LOG(LOG_LEVEL_INFO, "Stopping a workout with type: %d",
|
|
s_workout_data.current_workout->type);
|
|
prv_put_event(PebbleWorkoutEvent_Stopped);
|
|
|
|
kernel_free(s_workout_data.current_workout);
|
|
s_workout_data.current_workout = NULL;
|
|
}
|
|
unlock:
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_is_workout_ongoing(void) {
|
|
prv_lock();
|
|
bool rv = (s_workout_data.current_workout != NULL);
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_takeover_activity_session(ActivitySession *session) {
|
|
bool rv = true;
|
|
prv_lock();
|
|
{
|
|
if (!workout_service_is_workout_type_supported(session->type)) {
|
|
rv = false;
|
|
goto unlock;
|
|
}
|
|
|
|
ActivitySession session_copy = *session;
|
|
|
|
// Remove the session from out list of sessions so it doesn't get counted twice
|
|
activity_sessions_prv_delete_activity_session(session);
|
|
|
|
// Start a new workout
|
|
if (!workout_service_start_workout(session_copy.type)) {
|
|
rv = false;
|
|
goto unlock;
|
|
}
|
|
|
|
// Update the new workout to mirror the session we took over
|
|
s_workout_data.current_workout->start_utc = session_copy.start_utc;
|
|
s_workout_data.current_workout->duration_s = session_copy.length_min * SECONDS_PER_MINUTE;
|
|
s_workout_data.current_workout->steps = session_copy.step_data.steps;
|
|
s_workout_data.current_workout->distance_m = session_copy.step_data.distance_meters;
|
|
s_workout_data.current_workout->active_calories =
|
|
session_copy.step_data.active_kcalories * ACTIVITY_CALORIES_PER_KCAL;
|
|
}
|
|
unlock:
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_is_paused(void) {
|
|
prv_lock();
|
|
bool rv = (workout_service_is_workout_ongoing() && s_workout_data.current_workout->paused);
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_get_current_workout_type(ActivitySessionType *type_out) {
|
|
bool rv = true;
|
|
prv_lock();
|
|
if (!type_out || !workout_service_is_workout_ongoing()) {
|
|
rv = false;
|
|
} else {
|
|
if (type_out) {
|
|
*type_out = s_workout_data.current_workout->type;
|
|
}
|
|
}
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
bool workout_service_get_current_workout_info(int32_t *steps_out, int32_t *duration_s_out,
|
|
int32_t *distance_m_out, int32_t *current_bpm_out,
|
|
HRZone *current_hr_zone_out) {
|
|
bool rv = true;
|
|
prv_lock();
|
|
{
|
|
if (!workout_service_is_workout_ongoing()) {
|
|
rv = false;
|
|
} else {
|
|
if (steps_out) {
|
|
*steps_out = s_workout_data.current_workout->steps;
|
|
}
|
|
if (duration_s_out) {
|
|
*duration_s_out = s_workout_data.current_workout->duration_s;
|
|
}
|
|
if (distance_m_out) {
|
|
*distance_m_out = s_workout_data.current_workout->distance_m;
|
|
}
|
|
if (current_bpm_out) {
|
|
*current_bpm_out = s_workout_data.current_workout->current_bpm;
|
|
}
|
|
if (current_hr_zone_out) {
|
|
*current_hr_zone_out = s_workout_data.current_workout->current_hr_zone;
|
|
}
|
|
}
|
|
}
|
|
prv_unlock();
|
|
return rv;
|
|
}
|
|
|
|
#if UNITTEST
|
|
bool workout_service_get_avg_hr(int32_t *avg_hr_out) {
|
|
if (!avg_hr_out || !workout_service_is_workout_ongoing()) {
|
|
return false;
|
|
}
|
|
|
|
*avg_hr_out = prv_get_avg_hr();
|
|
return true;
|
|
}
|
|
|
|
bool workout_service_get_current_workout_hr_zone_time(int32_t *hr_zone_time_s_out) {
|
|
if (!hr_zone_time_s_out || !workout_service_is_workout_ongoing()) {
|
|
return false;
|
|
}
|
|
|
|
prv_lock();
|
|
{
|
|
hr_zone_time_s_out[HRZone_Zone0] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone0];
|
|
hr_zone_time_s_out[HRZone_Zone1] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone1];
|
|
hr_zone_time_s_out[HRZone_Zone2] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone2];
|
|
hr_zone_time_s_out[HRZone_Zone3] = s_workout_data.current_workout->hr_zone_time_s[HRZone_Zone3];
|
|
}
|
|
prv_unlock();
|
|
return true;
|
|
}
|
|
|
|
void workout_service_get_active_kcalories(int32_t *active) {
|
|
if (workout_service_is_workout_ongoing()) {
|
|
*active = ROUND(s_workout_data.current_workout->active_calories, ACTIVITY_CALORIES_PER_KCAL);
|
|
}
|
|
}
|
|
|
|
void workout_service_reset(void) {
|
|
if (s_workout_data.current_workout) {
|
|
kernel_free(s_workout_data.current_workout);
|
|
}
|
|
s_workout_data = (WorkoutServiceData) {};
|
|
}
|
|
#endif
|