pebble/src/fw/services/common/hrm/hrm_manager.c

829 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 "hrm_manager.h"
#include "hrm_manager_private.h"
#include "console/prompt.h"
#include "drivers/hrm.h"
#include "kernel/pbl_malloc.h"
#include "mfg/mfg_info.h"
#include "os/tick.h"
#include "process_management/app_manager.h"
#include "process_management/worker_manager.h"
#include "services/common/analytics/analytics.h"
#include "services/common/system_task.h"
#include "syscall/syscall_internal.h"
#include "system/hexdump.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/math.h"
#include "util/size.h"
#include "FreeRTOS.h"
#include "queue.h"
#include <stddef.h>
#define HRM_DEBUG 0
#if HRM_DEBUG
#define HRM_LOG(fmt, ...) \
do { \
PBL_LOG(LOG_LEVEL_DEBUG, fmt, ## __VA_ARGS__); \
} while (0)
#define HRM_HEXDUMP(data, length) \
do { \
PBL_HEXDUMP(LOG_LEVEL_DEBUG, (uint8_t *)data, length); \
} while (0)
#else
#define HRM_LOG(fmt, ...)
#define HRM_HEXDUMP(data, length)
#endif
static struct HRMManagerState s_manager_state;
static bool s_hrm_present = false;
// Forward declarations
static void prv_update_enable_timer_cb(void *context);
static bool prv_match_session_ref(ListNode *found_node, void *data) {
const HRMSubscriberState *state = (HRMSubscriberState *)found_node;
return (state->session_ref == (HRMSessionRef)data);
}
T_STATIC HRMSubscriberState * prv_get_subscriber_state_from_ref(HRMSessionRef session) {
ListNode *node = list_find(s_manager_state.subscribers, prv_match_session_ref,
(void *)(uintptr_t)session);
return (HRMSubscriberState *)node;
}
typedef struct {
AppInstallId app_id;
PebbleTask task;
} HRMAppIdAndTask;
static bool prv_match_app_id(ListNode *found_node, void *data) {
HRMAppIdAndTask *context = (HRMAppIdAndTask *)data;
const HRMSubscriberState *state = (HRMSubscriberState *)found_node;
return ((state->app_id == context->app_id) && (state->task == context->task));
}
T_STATIC HRMSubscriberState * prv_get_subscriber_state_from_app_id(PebbleTask task,
AppInstallId app_id) {
HRMAppIdAndTask context = {
.app_id = app_id,
.task = task,
};
ListNode *node = list_find(s_manager_state.subscribers, prv_match_app_id, &context);
return (HRMSubscriberState *)node;
}
// Return true if this subscriber needs to be sent a HRMEvent_SubscriptionExpiring event
static bool prv_needs_expiring_event(HRMSubscriberState *state, time_t utc_now) {
if (state->sent_expiration_event) {
return false;
}
return (state->expire_utc && (utc_now >= state->expire_utc
- MAX(HRM_SUBSCRIPTION_EXPIRING_WARNING_SEC, (int)state->update_interval_s)));
}
T_STATIC void prv_read_event_from_buffer_and_consume(CircularBuffer *buffer,
PebbleHRMEvent *event) {
const uint16_t total_size = sizeof(*event);
uint16_t remaining = total_size;
uint8_t *out_buf = (uint8_t *)event;
while (remaining > 0) {
const uint8_t *data_out;
uint16_t length_out;
const bool success = circular_buffer_read(buffer, remaining, &data_out, &length_out);
PBL_ASSERTN(success);
memcpy(out_buf, data_out, length_out);
out_buf += length_out;
remaining -= length_out;
}
PBL_ASSERTN(remaining == 0);
circular_buffer_consume(buffer, sizeof(*event));
}
static void prv_remove_and_free_subscription(HRMSubscriberState *state) {
list_remove((ListNode *)state, &s_manager_state.subscribers, NULL);
kernel_free(state);
}
#if UNITTEST
// Used by unit tests
T_STATIC TimerID prv_get_timer_id(void) {
return s_manager_state.update_enable_timer_id;
}
// Used by unit tests
T_STATIC uint32_t prv_num_system_task_events_queued(void) {
uint16_t avail_bytes = circular_buffer_get_read_space_remaining(
&s_manager_state.system_task_event_buffer);
return avail_bytes / sizeof(PebbleHRMEvent);
}
#endif
static void prv_handle_accel_data(void * data) {
PBL_ASSERT_RUNNING_FROM_EXPECTED_TASK(PebbleTask_NewTimers);
uint64_t timestamp_ms;
uint32_t num_new_samples = sys_accel_manager_get_num_samples(
s_manager_state.accel_state, &timestamp_ms);
mutex_lock(s_manager_state.accel_data_lock);
// Only read as many as we have space to store
const size_t MAX_BUFFERED_SAMPLES = ARRAY_LENGTH(s_manager_state.accel_data.data);
if ((s_manager_state.accel_data.num_samples + num_new_samples) > MAX_BUFFERED_SAMPLES) {
analytics_inc(ANALYTICS_DEVICE_METRIC_HRM_ACCEL_DATA_MISSING, AnalyticsClient_System);
num_new_samples = MAX_BUFFERED_SAMPLES - s_manager_state.accel_data.num_samples;
}
void *write_ptr = &s_manager_state.accel_data.data[s_manager_state.accel_data.num_samples];
memcpy(write_ptr, s_manager_state.accel_manager_buffer, num_new_samples * sizeof(AccelRawData));
s_manager_state.accel_data.num_samples += num_new_samples;
mutex_unlock(s_manager_state.accel_data_lock);
sys_accel_manager_consume_samples(s_manager_state.accel_state, num_new_samples);
}
// Return true if this is a stable BPM reading. This is called each time we power the sensor off
// or receive a new HRMData update from the sensor driver. It returns true if we should trust the
// BPMData hrm_bpm and hrm_quality fields or not.
//
// In the current rev of the sensor FW, we need to take the following approach to filter out
// good readings:
// 1.) After first turning on the sensor, wait until the quality is "Good" or better, but wait
// no more than HRM_SENSOR_SPIN_UP_SEC seconds.
// 2.) During sensor startup, the sensor will occasionally send erroneous "Excellent" readings.
// We can tell they are erroneous because the BPM will be 0. These erroneous readings need to
// be ignored. We ignore any reading where the BPM is below HRM_SENSOR_MIN_VALID_BPM_READING.
// 3.) Once the quality is "Good", we have to ignore all other quality readings (except off-wrist)
// because they don't mean anything in this version of the sensor FW.
// 4.) If we suddenly go "off-wrist", wait for another "Good" or better.
//
// So, for the first 0 to HRM_SENSOR_SPIN_UP_SEC seconds after turning the sensor on or first
// contacting the wrist after being off-wrist, the readings can be unstable and this method will
// return false during that time.
//
// @param[in] data pointer to last received HRMdata or NULL if sensor powered off
// @return true if sensor is stable
static bool prv_is_sensor_stable(const HRMData *data) {
// Passing a NULL data pointer means reset our state
if (!data) {
s_manager_state.sensor_stable = false;
s_manager_state.sensor_start_ticks = 0;
return false;
}
// Ignore the "no accel" quality reading samples. We seem to get these occasionally
// and don't want them to mess up our state.
if (data->hrm_quality == HRMQuality_NoAccel) {
return s_manager_state.sensor_stable;
}
// If we were stable before, just make sure we are still stable
if (s_manager_state.sensor_stable) {
// If we just went on-wrist or off-wrist, reset the stable state
bool off_wrist_now = (data->hrm_quality == HRMQuality_OffWrist);
if (off_wrist_now != s_manager_state.off_wrist_when_stable) {
s_manager_state.sensor_stable = false;
s_manager_state.sensor_start_ticks = 0;
return false;
}
return true;
}
// Start the tick counter if this is the first reading since power-on or off-wrist
if (s_manager_state.sensor_start_ticks == 0) {
s_manager_state.sensor_start_ticks = rtc_get_ticks();
}
// When first powering up, we can get "Excellent" quality readings the first few seconds, even
// though the BPM is 0. Let's fix the quality if the BPM is too low to be valid
HRMQuality quality = data->hrm_quality;
if ((data->hrm_bpm < HRM_SENSOR_MIN_VALID_BPM_READING)
&& (data->hrm_quality > HRMQuality_NoSignal)) {
quality = HRMQuality_NoSignal;
}
// Update our state
if (quality >= HRMQuality_Good) {
// Once we receive at least one good reading, we are stable
s_manager_state.sensor_stable = true;
s_manager_state.off_wrist_when_stable = false;
} else {
// We haven't yet received a good reading yet. Wait for a timeout...
RtcTicks elapsed_ticks = rtc_get_ticks() - s_manager_state.sensor_start_ticks;
RtcTicks max_startup_time = milliseconds_to_ticks(HRM_SENSOR_SPIN_UP_SEC * MS_PER_SECOND);
if (elapsed_ticks >= max_startup_time) {
// If it's been past the tolerable startup time, we have a valid reading - even though it
// may indicate off-wrist.
s_manager_state.sensor_stable = true;
s_manager_state.off_wrist_when_stable = (quality == HRMQuality_OffWrist);
}
}
return s_manager_state.sensor_stable;
}
T_STATIC bool prv_can_turn_sensor_on(void) {
#if IS_BIGBOARD || RECOVERY_FW
return true;
#endif
return s_manager_state.enabled_run_level &&
s_manager_state.enabled_charging_state &&
activity_prefs_heart_rate_is_enabled();
}
// Figure out if we should enable the HR sensor or not based on all subscribers and their
// desired sampling periods. Must be called from the KernelBG task.
static void prv_update_hrm_enable_system_cb(void *unused) {
const time_t utc_now = rtc_get_time();
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
mutex_lock_recursive(s_manager_state.lock);
{
bool turn_sensor_on = false;
// How many ms until we need the sensor on again. INT32_MAX means we don't need to turn it on
// again
int32_t remaining_ms = INT32_MAX;
if (prv_can_turn_sensor_on()) {
RtcTicks cur_ticks = rtc_get_ticks();
int32_t remaining_ticks = INT32_MAX;
const int32_t spin_up_ticks = (int32_t)milliseconds_to_ticks(
HRM_SENSOR_SPIN_UP_SEC * MS_PER_SECOND);
// Loop through each of the subscribers and figure out when the next one needs an update
HRMSubscriberState *state = (HRMSubscriberState *) s_manager_state.subscribers;
for (; state != NULL; state = (HRMSubscriberState *) state->list_node.next) {
if (state->expire_utc && (utc_now >= state->expire_utc)) {
// Ignore expired subscriptions
continue;
}
int64_t subscriber_age_ticks;
if (state->last_valid_ticks) {
subscriber_age_ticks = cur_ticks - state->last_valid_ticks;
} else {
// Never got an update yet
subscriber_age_ticks = milliseconds_to_ticks(state->update_interval_s * MS_PER_SECOND);
}
int64_t subscriber_remaining_ticks =
(int64_t)milliseconds_to_ticks(state->update_interval_s * MS_PER_SECOND)
- subscriber_age_ticks - spin_up_ticks;
subscriber_remaining_ticks = MAX(0, subscriber_remaining_ticks);
remaining_ticks = MIN(remaining_ticks, subscriber_remaining_ticks);
}
// How many milliseconds till we need to send the next sensor reading
remaining_ms = ticks_to_milliseconds(remaining_ticks);
HRM_LOG("Need sensor on again in %"PRIu32" sec", remaining_ms / MS_PER_SECOND);
turn_sensor_on = (remaining_ms <= 0);
}
if (turn_sensor_on && !hrm_is_enabled(HRM)) {
// Turn on the sensor now
HRM_LOG("Turning on HR sensor");
s_manager_state.accel_state = sys_accel_manager_data_subscribe(
ACCEL_SAMPLING_25HZ, prv_handle_accel_data, NULL, PebbleTask_NewTimers);
accel_manager_set_jitterfree_sampling_rate(s_manager_state.accel_state,
HRM_MANAGER_ACCEL_RATE_MILLIHZ);
sys_accel_manager_set_sample_buffer(
s_manager_state.accel_state, s_manager_state.accel_manager_buffer,
HRM_MANAGER_ACCEL_MANAGER_SAMPLES_PER_UPDATE);
hrm_enable(HRM);
// Don't need the re-enable timer to fire
new_timer_stop(s_manager_state.update_enable_timer_id);
} else if (!turn_sensor_on && hrm_is_enabled(HRM)) {
// Turn off the sensor now
HRM_LOG("Turning off HR sensor");
hrm_disable(HRM);
sys_accel_manager_data_unsubscribe(s_manager_state.accel_state);
s_manager_state.accel_state = NULL;
prv_is_sensor_stable(NULL); // inform state machine that sensor got powered off
// If we need the sensor on again later, turn on a timer to re-enable the HRM in enough time
// to get a good reading for the next subscriber that needs one
if (remaining_ms < INT32_MAX) {
new_timer_start(s_manager_state.update_enable_timer_id, remaining_ms,
prv_update_enable_timer_cb, NULL /*context*/, 0 /*flags*/);
} else {
new_timer_stop(s_manager_state.update_enable_timer_id);
}
}
}
mutex_unlock_recursive(s_manager_state.lock);
}
// Timer callback that we use to re-enable the HR sensor in case we turned it off for a while
static void prv_update_enable_timer_cb(void *context) {
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
}
//! The system task needs its own handler for HRM data since we can't queue up generic events.
static void prv_system_task_hrm_handler(void *context) {
time_t utc_now = rtc_get_time();
mutex_lock_recursive(s_manager_state.lock);
PebbleHRMEvent event;
prv_read_event_from_buffer_and_consume(&s_manager_state.system_task_event_buffer, &event);
// Send event to all KernelBG subscribers that asked for this feature
HRMSubscriberState *state = (HRMSubscriberState *)s_manager_state.subscribers;
for (; state != NULL; state = (HRMSubscriberState *)state->list_node.next) {
if (!state->callback_handler) {
// Not a KernelBG subscriber
continue;
}
// If this subscription is ready to expire, send an "expiring" event
if (prv_needs_expiring_event(state, utc_now)) {
PebbleHRMEvent expiring_event = (PebbleHRMEvent) {
.event_type = HRMEvent_SubscriptionExpiring,
.expiring.session_ref = state->session_ref,
};
state->callback_handler(&expiring_event, state->callback_context);
state->sent_expiration_event = true;
}
// See if this subscriber wants these types of events
switch (event.event_type) {
case HRMEvent_BPM:
if (!(state->features & HRMFeature_BPM)) {
continue;
}
break;
case HRMEvent_LEDCurrent:
if (!(state->features & HRMFeature_LEDCurrent)) {
continue;
}
break;
case HRMEvent_HRV:
if (!(state->features & HRMFeature_HRV)) {
continue;
}
break;
case HRMEvent_Diagnostics:
if (!(state->features & HRMFeature_Diagnostics)) {
continue;
}
break;
case HRMEvent_SubscriptionExpiring:
continue;
}
// Send the event to the subscriber
state->callback_handler(&event, state->callback_context);
}
mutex_unlock_recursive(s_manager_state.lock);
}
// Assumes that s_manager_state.lock is held
static void prv_queue_system_task_event(const PebbleHRMEvent *event) {
const uint16_t free_space =
circular_buffer_get_read_space_remaining(&s_manager_state.system_task_event_buffer);
if (free_space < sizeof(PebbleHRMEvent)) {
circular_buffer_consume(&s_manager_state.system_task_event_buffer, sizeof(PebbleHRMEvent));
++s_manager_state.dropped_events;
}
circular_buffer_write(&s_manager_state.system_task_event_buffer,
(const uint8_t *)event, sizeof(PebbleHRMEvent));
}
static void prv_populate_hrm_event(PebbleHRMEvent *event, HRMFeature feature, const HRMData *data) {
switch (feature) {
case HRMFeature_BPM:
*event = (PebbleHRMEvent) {
.event_type = HRMEvent_BPM,
.bpm = {
.bpm = data->hrm_bpm,
.quality = data->hrm_quality,
},
};
break;
case HRMFeature_HRV:
*event = (PebbleHRMEvent) {
.event_type = HRMEvent_HRV,
.hrv = {
.ppi_ms = data->hrv_ppi_ms,
.quality = data->hrv_quality,
},
};
break;
case HRMFeature_LEDCurrent:
*event = (PebbleHRMEvent) {
.event_type = HRMEvent_LEDCurrent,
.led = {
.current_ua = data->led_current_ua,
.tia = data->ppg_data.tia[0]
},
};
break;
case HRMFeature_Diagnostics:
{
HRMDiagnosticsData *debug = kernel_zalloc_check(sizeof(HRMDiagnosticsData));
debug->ppg_data = data->ppg_data;
debug->accel_data = data->accel_data;
*event = (PebbleHRMEvent) {
.event_type = HRMEvent_Diagnostics,
.debug = debug,
};
break;
}
default:
WTF;
}
}
static bool prv_event_put(HRMSubscriberState *state, PebbleHRMEvent *event) {
bool success;
if (state->queue) {
PebbleEvent e = {
.type = PEBBLE_HRM_EVENT,
.hrm = *event,
};
success = xQueueSendToBack(state->queue, &e, 0);
} else {
prv_queue_system_task_event(event);
success = system_task_add_callback(prv_system_task_hrm_handler, NULL);
}
return success;
}
T_STATIC void prv_charger_event_cb(PebbleEvent *e, void *context) {
const PebbleBatteryStateChangeEvent *evt = &e->battery_state;
mutex_lock_recursive(s_manager_state.lock);
{
s_manager_state.enabled_charging_state = !evt->new_state.is_plugged;
}
mutex_unlock_recursive(s_manager_state.lock);
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
}
// Accept new data from the HR device driver.
void hrm_manager_new_data_cb(const HRMData *data) {
mutex_lock_recursive(s_manager_state.lock);
if (!prv_can_turn_sensor_on() || s_manager_state.subscribers == NULL) {
// If the hrm manager should be disabled or we have no subscribers, this data is unwanted.
goto unlock;
}
// See if the sensor signal is stable or not
bool stable_sensor = prv_is_sensor_stable(data);
HRM_LOG("HRM Data:");
HRM_LOG("Status %"PRIx8, data->hrm_status);
HRM_LOG("HRM: %"PRIu8"bpm, Quality: %d, Stable: %d", data->hrm_bpm, data->hrm_quality,
(int)stable_sensor);
HRM_LOG("PPG samples: %d", data->ppg_data.num_samples);
HRM_HEXDUMP(data->ppg_data.ppg, data->ppg_data.num_samples * sizeof(uint16_t));
HRM_LOG("TIA samples: %d", data->ppg_data.num_samples);
HRM_HEXDUMP(data->ppg_data.tia, data->ppg_data.num_samples * sizeof(uint16_t));
HRM_LOG("Accel samples: %"PRIu32, data->accel_data.num_samples);
HRM_LOG("LED %"PRIu16"uA, TIA: %"PRIu16, data->led_current_ua, data->tia);
time_t utc_now = rtc_get_time();
RtcTicks cur_ticks = rtc_get_ticks();
HRMFeature kernel_bg_features_sent = 0;
HRMSubscriberState *state = (HRMSubscriberState *)s_manager_state.subscribers;
while (state) {
HRMSubscriberState *expired_state = NULL;
// Update the time stamp for when this subscriber last received an update if the sensor
// is currently stable
if (stable_sensor) {
state->last_valid_ticks = cur_ticks;
}
PebbleHRMEvent hrm_event;
for (uint8_t i = 0; i < HRMFeatureShiftMax; ++i) {
HRMFeature feature = (1 << i);
if (!(state->features & feature)) {
continue;
}
// Only send BPM and HRV events if the sensor is stable
if (!stable_sensor && (feature == HRMFeature_BPM || feature == HRMFeature_HRV)) {
continue;
}
if (state->callback_handler) {
// For kernel BG subscribers, we only queue one event of each type (which is then
// dispatched to all KernelBG subscribers from the KernelBG callback) so that we don't
// overfill our limited size circular buffer.
if (kernel_bg_features_sent & feature) {
continue;
}
kernel_bg_features_sent |= feature;
}
prv_populate_hrm_event(&hrm_event, feature, data);
PBL_ASSERTN(prv_event_put(state, &hrm_event));
}
// If this is an app subscription, see if we need to send an "expiring" event. We check
// KernelBG subscribers from the system callback function (prv_system_task_hrm_handler).
if (!state->callback_handler && prv_needs_expiring_event(state, utc_now)) {
hrm_event = (PebbleHRMEvent) {
.event_type = HRMEvent_SubscriptionExpiring,
.expiring.session_ref = state->session_ref,
};
PBL_ASSERTN(prv_event_put(state, &hrm_event));
state->sent_expiration_event = true;
}
if (state->expire_utc && (utc_now >= state->expire_utc)) {
// This subscription has expired
expired_state = state;
}
state = (HRMSubscriberState *)state->list_node.next;
// If the prior subscription expired, remove it now
if (expired_state) {
PBL_LOG(LOG_LEVEL_DEBUG, "Subscription %"PRIu32" expired", expired_state->session_ref);
prv_remove_and_free_subscription(expired_state);
}
}
// Update the HRM enable state. If no subscribers need an update for a while, we can turn off the
// HR sensor and set a timer to turn it on again later. To avoid this overhead on every callback,
// we only check it once every HRM_CHECK_SENSOR_DISABLE_COUNT times
if (++s_manager_state.check_disable_counter >= HRM_CHECK_SENSOR_DISABLE_COUNT) {
s_manager_state.check_disable_counter = 0;
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
}
unlock:
mutex_unlock_recursive(s_manager_state.lock);
}
void hrm_manager_handle_prefs_changed(void) {
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
}
void hrm_manager_init(void) {
s_hrm_present = mfg_info_is_hrm_present();
s_manager_state = (struct HRMManagerState) {
.lock = mutex_create_recursive(),
.accel_data_lock = mutex_create(),
.update_enable_timer_id = new_timer_create(),
.enabled_charging_state = !battery_is_usb_connected(),
.charger_subscription = (EventServiceInfo) {
.type = PEBBLE_BATTERY_STATE_CHANGE_EVENT,
.handler = prv_charger_event_cb,
},
};
circular_buffer_init(&s_manager_state.system_task_event_buffer,
s_manager_state.system_task_event_storage,
EVENT_STORAGE_SIZE);
event_service_client_subscribe(&s_manager_state.charger_subscription);
}
HRMSessionRef hrm_manager_subscribe_with_callback(AppInstallId app_id, uint32_t update_interval_s,
uint16_t expire_s, HRMFeature features,
HRMSubscriberCallback callback, void *context) {
if (!s_hrm_present) {
return HRM_INVALID_SESSION_REF;
}
const PebbleTask current_task = pebble_task_get_current();
bool is_app_subscription = false;
if (current_task == PebbleTask_KernelBackground) {
// KernelBG must provide a callback
PBL_ASSERTN(callback != NULL);
} else if (current_task == PebbleTask_KernelMain) {
// KernelMain clients can either set a callback, or use the event_service interface.
} else {
PBL_ASSERTN(current_task == PebbleTask_App || current_task == PebbleTask_Worker);
is_app_subscription = true;
}
mutex_lock_recursive(s_manager_state.lock);
HRMSessionRef session_ref = HRM_INVALID_SESSION_REF;
// If there is already an existing subscription for this app, remove the old one before we
// add another subscription for this app.
if (is_app_subscription) {
HRMSubscriberState * state = prv_get_subscriber_state_from_app_id(current_task, app_id);
if (state != NULL) {
session_ref = state->session_ref;
PBL_LOG(LOG_LEVEL_DEBUG, "Removing existing subscription for this app");
prv_remove_and_free_subscription(state);
}
}
// Get the session ref to use
if (session_ref == HRM_INVALID_SESSION_REF) {
session_ref = ++s_manager_state.next_session_ref;
}
HRMSubscriberState *state = kernel_malloc_check(sizeof(*state));
*state = (HRMSubscriberState) {
.session_ref = session_ref,
.app_id = app_id,
.task = current_task,
.queue = pebble_task_get_to_queue(current_task),
.callback_handler = callback,
.callback_context = context,
.update_interval_s = update_interval_s,
.expire_utc = (expire_s != 0) ? (rtc_get_time() + expire_s) : 0,
.features = features,
};
s_manager_state.subscribers =
list_insert_before(s_manager_state.subscribers, &state->list_node);
// Update the HR enablement state
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
mutex_unlock_recursive(s_manager_state.lock);
return state->session_ref;
}
DEFINE_SYSCALL(HRMSessionRef, sys_hrm_manager_app_subscribe,
AppInstallId app_id, uint32_t update_interval_s, uint16_t expire_sec, HRMFeature features) {
return hrm_manager_subscribe_with_callback(app_id, update_interval_s, expire_sec, features, NULL,
NULL);
}
DEFINE_SYSCALL(bool, sys_hrm_manager_unsubscribe, HRMSessionRef session) {
HRM_LOG("Unsubscribing");
bool success = false;
mutex_lock_recursive(s_manager_state.lock);
HRMSubscriberState *state = prv_get_subscriber_state_from_ref(session);
if (state) {
prv_remove_and_free_subscription(state);
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
success = true;
}
mutex_unlock_recursive(s_manager_state.lock);
return success;
}
DEFINE_SYSCALL(HRMSessionRef, sys_hrm_manager_get_app_subscription, AppInstallId app_id) {
mutex_lock_recursive(s_manager_state.lock);
HRMSessionRef ref = HRM_INVALID_SESSION_REF;
HRMSubscriberState *state = prv_get_subscriber_state_from_app_id(pebble_task_get_current(),
app_id);
if (state) {
ref = state->session_ref;
}
mutex_unlock_recursive(s_manager_state.lock);
return ref;
}
DEFINE_SYSCALL(bool, sys_hrm_manager_get_subscription_info, HRMSessionRef session,
AppInstallId *app_id, uint32_t *update_interval_s, uint16_t *expire_s,
HRMFeature *features) {
mutex_lock_recursive(s_manager_state.lock);
HRMSubscriberState *state = prv_get_subscriber_state_from_ref(session);
if (state) {
if (app_id) {
*app_id = state->app_id;
}
if (update_interval_s) {
*update_interval_s = state->update_interval_s;
}
if (expire_s) {
int16_t expire_in_s = 0;
if (state->expire_utc != 0) {
expire_in_s = MAX(0, state->expire_utc - rtc_get_time());
}
*expire_s = MAX(0, expire_in_s);
}
if (features) {
*features = state->features;
}
}
mutex_unlock_recursive(s_manager_state.lock);
return (state != NULL);
}
DEFINE_SYSCALL(bool, sys_hrm_manager_set_features, HRMSessionRef session, HRMFeature features) {
bool success = false;
mutex_lock_recursive(s_manager_state.lock);
HRMSubscriberState *state = prv_get_subscriber_state_from_ref(session);
if (state) {
state->features = features;
success = true;
}
mutex_unlock_recursive(s_manager_state.lock);
return success;
}
DEFINE_SYSCALL(bool, sys_hrm_manager_set_update_interval, HRMSessionRef session,
uint32_t update_interval_s, uint16_t expire_s) {
bool success = false;
mutex_lock_recursive(s_manager_state.lock);
HRMSubscriberState *state = prv_get_subscriber_state_from_ref(session);
if (state) {
state->update_interval_s = update_interval_s;
state->expire_utc = (expire_s != 0) ? (rtc_get_time() + expire_s) : 0;
state->sent_expiration_event = false;
success = true;
}
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
mutex_unlock_recursive(s_manager_state.lock);
return success;
}
DEFINE_SYSCALL(bool, sys_hrm_manager_is_hrm_present) {
return s_hrm_present;
}
void hrm_manager_enable(bool on) {
mutex_lock_recursive(s_manager_state.lock);
s_manager_state.enabled_run_level = on;
system_task_add_callback(prv_update_hrm_enable_system_cb, NULL);
mutex_unlock_recursive(s_manager_state.lock);
}
static HRMSessionRef s_console_session = HRM_INVALID_SESSION_REF;
static uint8_t s_tia_count = 0;
static void prv_console_unsubscribe_callback(void *data) {
sys_hrm_manager_unsubscribe(s_console_session);
s_console_session = HRM_INVALID_SESSION_REF;
prompt_command_finish();
}
static void prv_console_read_callback(PebbleHRMEvent *event, void *context) {
if (event->event_type == HRMEvent_LEDCurrent) {
if (s_tia_count++ == 5) { // Need to leave time for TIA to ramp up
system_task_add_callback(prv_console_unsubscribe_callback, NULL);
char buf[32];
prompt_send_response_fmt(buf, 32, "TIA: %"PRIu16, event->led.tia);
prompt_send_response_fmt(buf, 32, "LED: %"PRIu16"uA", event->led.current_ua);
}
}
}
void command_hrm_read(void) {
s_tia_count = 0;
sys_hrm_manager_unsubscribe(s_console_session);
s_console_session = hrm_manager_subscribe_with_callback(
INSTALL_ID_INVALID, 1 /*update_interval_s*/, 0 /*expire_s*/, HRMFeature_LEDCurrent,
prv_console_read_callback, NULL);
prompt_command_continues_after_returning();
}
HRMAccelData * hrm_manager_get_accel_data(void) {
mutex_lock(s_manager_state.accel_data_lock);
return &s_manager_state.accel_data;
}
void hrm_manager_release_accel_data(void) {
s_manager_state.accel_data.num_samples = 0; // Reset buffer
mutex_unlock(s_manager_state.accel_data_lock);
}
void hrm_manager_process_cleanup(PebbleTask task, AppInstallId app_id) {
if (task != PebbleTask_App && task != PebbleTask_Worker) {
return;
}
// For apps and workers, if they have a subscription still active, make sure it expires
HRMSubscriberState *state = prv_get_subscriber_state_from_app_id(task, app_id);
if (state == NULL) {
return;
}
// Set an expiration time now
PBL_LOG(LOG_LEVEL_DEBUG, "Setting expiration time on session for app_id %d", (int)app_id);
sys_hrm_manager_set_update_interval(state->session_ref, state->update_interval_s,
HRM_MANAGER_APP_EXIT_EXPIRATION_SEC);
}