Import of the watch repository from Pebble
103
src/fw/services/normal/accessory/accessory_manager.c
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 "drivers/accessory.h"
|
||||
#include "services/normal/accessory/accessory_manager.h"
|
||||
#include "services/normal/accessory/smartstrap_attribute.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "system/logging.h"
|
||||
#include "os/mutex.h"
|
||||
|
||||
static AccessoryInputState s_input_state = AccessoryInputStateIdle;
|
||||
static PebbleMutex *s_state_mutex;
|
||||
|
||||
void accessory_manager_init(void) {
|
||||
s_state_mutex = mutex_create();
|
||||
|
||||
// initialize consumers of the accessory port
|
||||
smartstrap_attribute_init();
|
||||
smartstrap_comms_init();
|
||||
smartstrap_state_init();
|
||||
smartstrap_connection_init();
|
||||
smartstrap_profiles_init();
|
||||
}
|
||||
|
||||
bool accessory_manager_handle_character_from_isr(char c) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
switch (s_input_state) {
|
||||
case AccessoryInputStateSmartstrap:
|
||||
return smartstrap_handle_data_from_isr((uint8_t)c);
|
||||
case AccessoryInputStateIdle:
|
||||
case AccessoryInputStateMic:
|
||||
// fallthrough
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool accessory_manager_handle_break_from_isr(void) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
switch (s_input_state) {
|
||||
case AccessoryInputStateSmartstrap:
|
||||
return smartstrap_handle_break_from_isr();
|
||||
case AccessoryInputStateIdle:
|
||||
case AccessoryInputStateMic:
|
||||
// fallthrough
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The accessory state is used to differentiate between different consumers of the accessory port.
|
||||
// Before a consumer uses the accessory port, it must set its state and return the state to idle
|
||||
// once it has finished. No other consumer will be permitted to use the accessory port until the
|
||||
// state is returned to idle.
|
||||
bool accessory_manager_set_state(AccessoryInputState state) {
|
||||
mutex_lock(s_state_mutex);
|
||||
|
||||
// Setting the state is only allowed if we are currently in the Idle state or we are
|
||||
// moving to the Idle state.
|
||||
if (state != AccessoryInputStateIdle && s_input_state != AccessoryInputStateIdle) {
|
||||
// the state is already set by somebody else
|
||||
mutex_unlock(s_state_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
accessory_use_dma(false);
|
||||
s_input_state = state;
|
||||
switch (s_input_state) {
|
||||
case AccessoryInputStateIdle:
|
||||
// restore accessory to default state
|
||||
accessory_enable_input();
|
||||
accessory_set_baudrate(AccessoryBaud115200);
|
||||
accessory_set_power(false);
|
||||
break;
|
||||
case AccessoryInputStateSmartstrap:
|
||||
case AccessoryInputStateMic:
|
||||
// fallthrough
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mutex_unlock(s_state_mutex);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Setting accessory state to %u", state);
|
||||
return true;
|
||||
}
|
||||
26
src/fw/services/normal/accessory/accessory_manager.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
typedef enum {
|
||||
AccessoryInputStateIdle,
|
||||
AccessoryInputStateMic,
|
||||
AccessoryInputStateSmartstrap
|
||||
} AccessoryInputState;
|
||||
|
||||
void accessory_manager_init(void);
|
||||
bool accessory_manager_set_state(AccessoryInputState state);
|
||||
497
src/fw/services/normal/accessory/smartstrap_attribute.c
Normal file
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* 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/applib_malloc.auto.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/accessory/smartstrap_attribute.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "os/mutex.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/list.h"
|
||||
#include "util/mbuf.h"
|
||||
|
||||
//! Currently, we only support attributes being created by the App task
|
||||
#define CONSUMER_TASK PebbleTask_App
|
||||
//! Gets the next attribute in the list
|
||||
#define NEXT_ATTR(attr) ((SmartstrapAttributeInternal *)list_get_next(&attr->list_node))
|
||||
//! This macro allows easy iteration through attributes in the s_attr_head list which don't have the
|
||||
//! 'deferred_delete' field set. Example usage:
|
||||
//! SmartstrapAttributeInternal *attr;
|
||||
//! FOREACH_VALID_ATTR(attr) {
|
||||
//! // 'attr' will be the current item in the list
|
||||
//! }
|
||||
#define FOREACH_VALID_ATTR(item) \
|
||||
for (item = s_attr_head; item; item = NEXT_ATTR(item)) \
|
||||
if (!item->deferred_delete)
|
||||
|
||||
// This file relies on the ServiceId/AttributeId being uint16_t as the protocol defines it as such.
|
||||
_Static_assert(sizeof(SmartstrapServiceId) == sizeof(uint16_t),
|
||||
"SmartstrapServiceId MUST be two bytes in length!");
|
||||
_Static_assert(sizeof(SmartstrapAttributeId) == sizeof(uint16_t),
|
||||
"SmartstrapAttributeId MUST be two bytes in length!");
|
||||
|
||||
typedef struct {
|
||||
ListNode list_node;
|
||||
//! The ServiceId for this attribute
|
||||
uint16_t service_id;
|
||||
//! The AttributeId for this attribute
|
||||
uint16_t attribute_id;
|
||||
//! MBuf used for sending / receiving data for this attribute
|
||||
MBuf mbuf;
|
||||
//! The number of bytes to write from the buffer
|
||||
uint32_t write_length;
|
||||
//! The type of request which is currently pending
|
||||
SmartstrapRequestType request_type:8;
|
||||
//! The current state of this attribute
|
||||
SmartstrapAttributeState state:8;
|
||||
//! The timeout to use for the next request
|
||||
uint16_t timeout_ms;
|
||||
//! Whether or not writes are being blocked
|
||||
bool write_blocked;
|
||||
//! Whether or not this attribute has a deferred delete pending
|
||||
bool deferred_delete;
|
||||
} SmartstrapAttributeInternal;
|
||||
|
||||
typedef struct {
|
||||
uint16_t service_id;
|
||||
uint16_t attribute_id;
|
||||
} AttributeFilterContext;
|
||||
|
||||
typedef enum {
|
||||
AttributeTransactionRead,
|
||||
AttributeTransactionBeginWrite,
|
||||
AttributeTransactionEndWrite
|
||||
} AttributeTransaction;
|
||||
|
||||
static SmartstrapAttributeInternal *s_attr_head;
|
||||
static bool s_deferred_delete_queued;
|
||||
static PebbleMutex *s_attr_list_lock;
|
||||
|
||||
|
||||
// Init
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void smartstrap_attribute_init(void) {
|
||||
s_attr_list_lock = mutex_create();
|
||||
}
|
||||
|
||||
|
||||
// Attribute state functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static bool prv_is_valid_fsm_transition(SmartstrapAttributeInternal *attr,
|
||||
SmartstrapAttributeState new_state) {
|
||||
SmartstrapAttributeState current_state = attr->state;
|
||||
if ((current_state == SmartstrapAttributeStateIdle) &&
|
||||
(new_state == SmartstrapAttributeStateRequestPending)) {
|
||||
return pebble_task_get_current() == CONSUMER_TASK;
|
||||
} else if ((current_state == SmartstrapAttributeStateIdle) &&
|
||||
(new_state == SmartstrapAttributeStateWritePending)) {
|
||||
return pebble_task_get_current() == CONSUMER_TASK;
|
||||
} else if ((current_state == SmartstrapAttributeStateWritePending) &&
|
||||
(new_state == SmartstrapAttributeStateRequestPending)) {
|
||||
return pebble_task_get_current() == CONSUMER_TASK;
|
||||
} else if ((current_state == SmartstrapAttributeStateWritePending) &&
|
||||
(new_state == SmartstrapAttributeStateIdle)) {
|
||||
return pebble_task_get_current() == CONSUMER_TASK;
|
||||
} else if ((current_state == SmartstrapAttributeStateRequestPending) &&
|
||||
(new_state == SmartstrapAttributeStateIdle)) {
|
||||
return pebble_task_get_current() == PebbleTask_KernelBackground;
|
||||
} else if ((current_state == SmartstrapAttributeStateRequestPending) &&
|
||||
(new_state == SmartstrapAttributeStateRequestInProgress)) {
|
||||
return pebble_task_get_current() == PebbleTask_KernelBackground;
|
||||
} else if ((current_state == SmartstrapAttributeStateRequestInProgress) &&
|
||||
(new_state == SmartstrapAttributeStateIdle)) {
|
||||
return pebble_task_get_current() == PebbleTask_KernelBackground;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_set_attribute_state(SmartstrapAttributeInternal *attr,
|
||||
SmartstrapAttributeState new_state) {
|
||||
PBL_ASSERTN(prv_is_valid_fsm_transition(attr, new_state));
|
||||
attr->state = new_state;
|
||||
}
|
||||
|
||||
//! Note; This should only be called from the consumer task
|
||||
static bool prv_start_transaction(SmartstrapAttributeInternal *attr, AttributeTransaction txn) {
|
||||
PBL_ASSERT_TASK(CONSUMER_TASK);
|
||||
if ((txn == AttributeTransactionRead) && (attr->state == SmartstrapAttributeStateIdle)) {
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateRequestPending);
|
||||
return true;
|
||||
} else if ((txn == AttributeTransactionBeginWrite) &&
|
||||
(attr->state == SmartstrapAttributeStateIdle)) {
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateWritePending);
|
||||
return true;
|
||||
} else if ((txn == AttributeTransactionEndWrite) &&
|
||||
(attr->state == SmartstrapAttributeStateWritePending)) {
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateRequestPending);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//! Note; This should only be called from the consumer task
|
||||
static void prv_cancel_transaction(SmartstrapAttributeInternal *attr) {
|
||||
PBL_ASSERT_TASK(CONSUMER_TASK);
|
||||
if (attr->state == SmartstrapAttributeStateWritePending) {
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateIdle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// List searching functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//! NOTE: the caller must hold s_attr_list_lock
|
||||
static SmartstrapAttributeInternal *prv_find_by_ids(uint16_t service_id, uint16_t attribute_id) {
|
||||
mutex_assert_held_by_curr_task(s_attr_list_lock, true);
|
||||
SmartstrapAttributeInternal *attr;
|
||||
FOREACH_VALID_ATTR(attr) {
|
||||
if ((attr->service_id == service_id) && (attr->attribute_id == attribute_id)) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_attr_list_lock
|
||||
static SmartstrapAttributeInternal *prv_find_by_buffer(uint8_t *buffer) {
|
||||
mutex_assert_held_by_curr_task(s_attr_list_lock, true);
|
||||
SmartstrapAttributeInternal *attr;
|
||||
FOREACH_VALID_ATTR(attr) {
|
||||
if (mbuf_get_data(&attr->mbuf) == buffer) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_attr_list_lock
|
||||
static SmartstrapAttributeInternal *prv_find_by_state(SmartstrapAttributeState state) {
|
||||
mutex_assert_held_by_curr_task(s_attr_list_lock, true);
|
||||
SmartstrapAttributeInternal *attr;
|
||||
FOREACH_VALID_ATTR(attr) {
|
||||
if (attr->state == state) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Attribute processing / request functions
|
||||
// NOTE: These all run on KernelBG which moves attributes from the RequestPending to the Idle state
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool smartstrap_attribute_send_pending(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
mutex_lock(s_attr_list_lock);
|
||||
if (prv_find_by_state(SmartstrapAttributeStateRequestInProgress)) {
|
||||
// we already have a request in progress
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the next attribute which has a pending request
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_state(SmartstrapAttributeStateRequestPending);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (!attr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare the request
|
||||
PBL_ASSERTN(!mbuf_get_next(&attr->mbuf));
|
||||
MBuf write_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&write_mbuf, mbuf_get_data(&attr->mbuf), attr->write_length);
|
||||
SmartstrapRequest request = (SmartstrapRequest) {
|
||||
.service_id = attr->service_id,
|
||||
.attribute_id = attr->attribute_id,
|
||||
.write_mbuf = (attr->request_type == SmartstrapRequestTypeRead) ? NULL : &write_mbuf,
|
||||
.read_mbuf = (attr->request_type == SmartstrapRequestTypeWrite) ? NULL : &attr->mbuf,
|
||||
.timeout_ms = attr->timeout_ms
|
||||
};
|
||||
|
||||
// send the request
|
||||
SmartstrapResult result = smartstrap_profiles_handle_request(&request);
|
||||
if (result == SmartstrapResultBusy) {
|
||||
// there was another request in progress so we'll try again later
|
||||
return false;
|
||||
} else if ((result == SmartstrapResultOk) &&
|
||||
(smartstrap_fsm_state_get() != SmartstrapStateReadReady)) {
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateRequestInProgress);
|
||||
if (attr->request_type == SmartstrapRequestTypeWrite) {
|
||||
// This is a generic service write, which will be ACK'd by the smartstrap so we shouldn't
|
||||
// send the event yet.
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Either the request was not written successfully, or we are not waiting for a response for it.
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateIdle);
|
||||
}
|
||||
|
||||
// send an event now that we've completed the write
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_SMARTSTRAP_EVENT,
|
||||
.smartstrap = {
|
||||
.type = SmartstrapDataSentEvent,
|
||||
.result = result,
|
||||
.attribute = mbuf_get_data(&attr->mbuf)
|
||||
},
|
||||
};
|
||||
process_manager_send_event_to_process(CONSUMER_TASK, &event);
|
||||
return true;
|
||||
}
|
||||
|
||||
void smartstrap_attribute_send_event(SmartstrapEventType type, SmartstrapProfile profile,
|
||||
SmartstrapResult result, uint16_t service_id,
|
||||
uint16_t attribute_id, uint16_t read_length) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_SMARTSTRAP_EVENT,
|
||||
.smartstrap = {
|
||||
.type = type,
|
||||
.profile = profile,
|
||||
.result = result,
|
||||
.read_length = read_length,
|
||||
},
|
||||
};
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_ids(service_id, attribute_id);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (!attr) {
|
||||
// this attribute has likely since been destroyed
|
||||
return;
|
||||
}
|
||||
event.smartstrap.attribute = mbuf_get_data(&attr->mbuf);
|
||||
if (type == SmartstrapDataReceivedEvent) {
|
||||
PBL_ASSERTN(attr->state == SmartstrapAttributeStateRequestInProgress);
|
||||
prv_set_attribute_state(attr, SmartstrapAttributeStateIdle);
|
||||
if (attr->request_type == SmartstrapRequestTypeWrite) {
|
||||
// the data we got was the ACK of the write, so change the event type and don't block writes
|
||||
event.smartstrap.type = SmartstrapDataSentEvent;
|
||||
} else {
|
||||
// prevent writing to the attribute until the app handles the event, at which point applib
|
||||
// code will call sys_smartstrap_attribute_event_processed() to clear this flag
|
||||
attr->write_blocked = true;
|
||||
}
|
||||
process_manager_send_event_to_process(CONSUMER_TASK, &event);
|
||||
} else if (type == SmartstrapNotifyEvent) {
|
||||
process_manager_send_event_to_process(CONSUMER_TASK, &event);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_do_deferred_delete_cb(void *context) {
|
||||
s_deferred_delete_queued = false;
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = s_attr_head;
|
||||
while (attr) {
|
||||
SmartstrapAttributeInternal *next = NEXT_ATTR(attr);
|
||||
if (attr->deferred_delete) {
|
||||
list_remove(&attr->list_node, (ListNode **)&s_attr_head, NULL);
|
||||
kernel_free(attr);
|
||||
}
|
||||
attr = next;
|
||||
}
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
}
|
||||
|
||||
|
||||
// Syscalls
|
||||
// NOTE: These all run on the consumer task which moves attributes from Idle to RequestPending
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_smartstrap_attribute_register, uint16_t service_id,
|
||||
uint16_t attribute_id, uint8_t *buffer, size_t buffer_length) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(buffer, buffer_length);
|
||||
}
|
||||
if (buffer_length > SMARTSTRAP_ATTRIBUTE_LENGTH_MAXIMUM) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Attribute length of %"PRIu32" is too long", (uint32_t)buffer_length);
|
||||
return false;
|
||||
}
|
||||
|
||||
mutex_lock(s_attr_list_lock);
|
||||
const bool exists = (prv_find_by_ids(service_id, attribute_id) || prv_find_by_buffer(buffer));
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (exists) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Attribute already exists (0x%x,0x%x)", service_id, attribute_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
SmartstrapAttributeInternal *new_attr = kernel_zalloc(sizeof(SmartstrapAttributeInternal));
|
||||
if (!new_attr) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to allocate attribute");
|
||||
return false;
|
||||
}
|
||||
|
||||
*new_attr = (SmartstrapAttributeInternal) {
|
||||
.service_id = service_id,
|
||||
.attribute_id = attribute_id,
|
||||
.mbuf = MBUF_EMPTY
|
||||
};
|
||||
list_init(&new_attr->list_node);
|
||||
mbuf_set_data(&new_attr->mbuf, buffer, buffer_length);
|
||||
|
||||
// add the node to our list
|
||||
mutex_lock(s_attr_list_lock);
|
||||
s_attr_head = (SmartstrapAttributeInternal *)list_prepend(&s_attr_head->list_node,
|
||||
&new_attr->list_node);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_attr_list_lock
|
||||
static void prv_queue_deferred_delete(SmartstrapAttributeInternal *attr, bool do_free) {
|
||||
mutex_assert_held_by_curr_task(s_attr_list_lock, true);
|
||||
|
||||
if (attr->state != SmartstrapAttributeStateRequestInProgress) {
|
||||
// stop the in-progress request
|
||||
smartstrap_cancel_send();
|
||||
}
|
||||
|
||||
void *buffer = mbuf_get_data(&attr->mbuf);
|
||||
// clear out the mbuf just in-case
|
||||
attr->mbuf = MBUF_EMPTY;
|
||||
if (do_free) {
|
||||
applib_free(buffer);
|
||||
}
|
||||
attr->deferred_delete = true;
|
||||
|
||||
// queue the deferred delete callback on KernelBG
|
||||
if (!s_deferred_delete_queued) {
|
||||
system_task_add_callback(prv_do_deferred_delete_cb, NULL);
|
||||
s_deferred_delete_queued = true;
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_smartstrap_attribute_unregister, SmartstrapAttribute *app_attr) {
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_buffer((uint8_t *)app_attr);
|
||||
if (!attr) {
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
return;
|
||||
}
|
||||
prv_queue_deferred_delete(attr, true /* do_free */);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
}
|
||||
|
||||
void smartstrap_attribute_unregister_all(void) {
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr;
|
||||
FOREACH_VALID_ATTR(attr) {
|
||||
// At this point, the app is closing so there's no point in freeing the buffers, and doing so
|
||||
// will crash the watch if the app had crashed (and the heap has already been cleaned up).
|
||||
prv_queue_deferred_delete(attr, false /* !do_free */);
|
||||
}
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_smartstrap_attribute_get_info, SmartstrapAttribute *app_attr,
|
||||
uint16_t *service_id, uint16_t *attribute_id, size_t *length) {
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_buffer((uint8_t *)app_attr);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (!attr) {
|
||||
return;
|
||||
}
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
if (service_id) {
|
||||
syscall_assert_userspace_buffer(service_id, sizeof(uint16_t));
|
||||
}
|
||||
if (attribute_id) {
|
||||
syscall_assert_userspace_buffer(attribute_id, sizeof(uint16_t));
|
||||
}
|
||||
if (length) {
|
||||
syscall_assert_userspace_buffer(length, sizeof(size_t));
|
||||
}
|
||||
}
|
||||
if (service_id) {
|
||||
*service_id = attr->service_id;
|
||||
}
|
||||
if (attribute_id) {
|
||||
*attribute_id = attr->attribute_id;
|
||||
}
|
||||
if (length) {
|
||||
*length = mbuf_get_length(&attr->mbuf);
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(SmartstrapResult, sys_smartstrap_attribute_do_request, SmartstrapAttribute *app_attr,
|
||||
SmartstrapRequestType type, uint16_t timeout_ms, uint32_t write_length) {
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_buffer((uint8_t *)app_attr);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (!attr) {
|
||||
return SmartstrapResultInvalidArgs;
|
||||
} else if (!sys_smartstrap_is_service_connected(attr->service_id)) {
|
||||
// go back to idle if we had begun a write
|
||||
prv_cancel_transaction(attr);
|
||||
return SmartstrapResultServiceUnavailable;
|
||||
}
|
||||
|
||||
if (type == SmartstrapRequestTypeBeginWrite) {
|
||||
if (attr->write_blocked || !prv_start_transaction(attr, AttributeTransactionBeginWrite)) {
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
// clear the write buffer
|
||||
memset(mbuf_get_data(&attr->mbuf), 0, mbuf_get_length(&attr->mbuf));
|
||||
return SmartstrapResultOk;
|
||||
} else if (type == SmartstrapRequestTypeRead) {
|
||||
// handle read request
|
||||
if (!prv_start_transaction(attr, AttributeTransactionRead)) {
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
} else {
|
||||
// handle write request
|
||||
if (!write_length || (write_length > mbuf_get_length(&attr->mbuf))) {
|
||||
prv_cancel_transaction(attr);
|
||||
return SmartstrapResultInvalidArgs;
|
||||
} else if (!prv_start_transaction(attr, AttributeTransactionEndWrite)) {
|
||||
// they didn't call smartstrap_begin_write first
|
||||
return SmartstrapResultInvalidArgs;
|
||||
}
|
||||
}
|
||||
|
||||
attr->write_length = write_length;
|
||||
attr->request_type = type;
|
||||
attr->timeout_ms = timeout_ms;
|
||||
smartstrap_connection_kick_monitor();
|
||||
return SmartstrapResultOk;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_smartstrap_attribute_event_processed, SmartstrapAttribute *app_attr) {
|
||||
mutex_lock(s_attr_list_lock);
|
||||
SmartstrapAttributeInternal *attr = prv_find_by_buffer((uint8_t *)app_attr);
|
||||
mutex_unlock(s_attr_list_lock);
|
||||
if (!attr) {
|
||||
// the app might have destroyed the attribute
|
||||
return;
|
||||
}
|
||||
// clear the write_blocked flag after the event has been processed for an attribute
|
||||
attr->write_blocked = false;
|
||||
}
|
||||
101
src/fw/services/normal/accessory/smartstrap_attribute.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/app_smartstrap.h"
|
||||
#include "kernel/events.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* This module creates kernel-space structs to represent SmartstrapAttributes for the app. Because
|
||||
* it is dealing with app buffers, the kernel-space structs are kept within smartstrap_attribute.c
|
||||
* and they are referenced via the APIs using the user-space SmartstrapAttribute pointer. This
|
||||
* SmartstrapAttribute pointer is actually just the user-space buffer pointer, but this fact is
|
||||
* hidden from apps (and not particularly useful for them to know).
|
||||
*/
|
||||
|
||||
/*
|
||||
* FSM state transitions:
|
||||
* +-------------------+-------------------+----------+---------------------------+
|
||||
* | From State | To State | Task | Event |
|
||||
* +-------------------+-------------------+----------+---------------------------+
|
||||
* | Idle | RequestPending | App | *_read() |
|
||||
* | Idle | WritePending | App | *_begin_write() |
|
||||
* | WritePending | RequestPending | App | *_end_write() |
|
||||
* | WritePending | Idle | App | *_end_write() failed |
|
||||
* | RequestPending | Idle | KernelBG | Failed to send request |
|
||||
* | RequestPending | RequestInProgress | KernelBG | Request sent successfully |
|
||||
* | RequestInProgress | Idle | KernelBG | Got response to request |
|
||||
* +-------------------+-------------------+----------+---------------------------+
|
||||
* Notes:
|
||||
* - Only the App task can move out of the Idle and WritePending states
|
||||
* - Only KernelBG can move out of the RequestPending and RequestInProgress state
|
||||
* - The lower-level smartstrap sending APIs are called only from KernelBG so we don't block the App
|
||||
*/
|
||||
typedef enum {
|
||||
SmartstrapAttributeStateIdle = 0,
|
||||
SmartstrapAttributeStateWritePending,
|
||||
SmartstrapAttributeStateRequestPending,
|
||||
SmartstrapAttributeStateRequestInProgress,
|
||||
NumSmartstrapAttributeStates
|
||||
} SmartstrapAttributeState;
|
||||
|
||||
typedef enum {
|
||||
SmartstrapRequestTypeRead,
|
||||
SmartstrapRequestTypeBeginWrite,
|
||||
SmartstrapRequestTypeWrite,
|
||||
SmartstrapRequestTypeWriteRead
|
||||
} SmartstrapRequestType;
|
||||
|
||||
|
||||
//! Initializes the smartstrap attribute code
|
||||
void smartstrap_attribute_init(void);
|
||||
|
||||
//! Sends the next pending attribute request
|
||||
bool smartstrap_attribute_send_pending(void);
|
||||
|
||||
//! Called by one of the profiles to send an event for an attribute.
|
||||
void smartstrap_attribute_send_event(SmartstrapEventType type, SmartstrapProfile profile,
|
||||
SmartstrapResult result, uint16_t service_id,
|
||||
uint16_t attribute_id, uint16_t read_length);
|
||||
|
||||
//! Unregisters all attributes which the app has registered
|
||||
void smartstrap_attribute_unregister_all(void);
|
||||
|
||||
// syscalls
|
||||
|
||||
//! Registers a new attribute by creating a kernel-space struct to represent it
|
||||
bool sys_smartstrap_attribute_register(uint16_t service_id, uint16_t attribute_id, uint8_t *buffer,
|
||||
size_t buffer_length);
|
||||
|
||||
//! Unregisters an attribute
|
||||
void sys_smartstrap_attribute_unregister(SmartstrapAttribute *app_attr);
|
||||
|
||||
//! Gets information on the specified attribute which has previously been created
|
||||
void sys_smartstrap_attribute_get_info(SmartstrapAttribute *app_attr, uint16_t *service_id,
|
||||
uint16_t *attribute_id, size_t *length);
|
||||
|
||||
//! Queues up a request for the specified attribute
|
||||
SmartstrapResult sys_smartstrap_attribute_do_request(SmartstrapAttribute *app_attr,
|
||||
SmartstrapRequestType type,
|
||||
uint16_t timeout_ms, uint32_t write_length);
|
||||
|
||||
//! Called by app_smartstrap.c after the app's event callback is called for an attribute
|
||||
void sys_smartstrap_attribute_event_processed(SmartstrapAttribute *app_attr);
|
||||
516
src/fw/services/normal/accessory/smartstrap_comms.c
Normal file
@@ -0,0 +1,516 @@
|
||||
/*
|
||||
* 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/app_smartstrap.h"
|
||||
#include "drivers/accessory.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "process_management/worker_manager.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/accessory/accessory_manager.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "os/mutex.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/crc8.h"
|
||||
#include "util/hdlc.h"
|
||||
#include "util/math.h"
|
||||
#include "util/mbuf_iterator.h"
|
||||
|
||||
//! The timeout for receiving the context frame after the break characters in ms
|
||||
static const uint32_t NOTIFY_TIMEOUT = 100;
|
||||
static const uint16_t SMARTSTRAP_MAX_TIMEOUT = 1000;
|
||||
|
||||
//! The header contains the version (1 byte), flags (4 bytes), and profile (2 bytes) fields
|
||||
//! The footer contains the checksum (1 byte) field
|
||||
#define FRAME_FOOTER_LENGTH 1
|
||||
#define FRAME_MIN_LENGTH (sizeof(FrameHeader) + FRAME_FOOTER_LENGTH)
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t version;
|
||||
union {
|
||||
struct PACKED {
|
||||
uint8_t is_read:1;
|
||||
uint8_t is_master:1;
|
||||
uint8_t is_notify:1;
|
||||
uint32_t reserved:29;
|
||||
};
|
||||
uint32_t raw;
|
||||
} flags;
|
||||
uint16_t profile;
|
||||
} FrameHeader;
|
||||
|
||||
typedef struct {
|
||||
//! HDLC context
|
||||
HdlcStreamingContext hdlc_ctx;
|
||||
//! The total number of bytes we've read for this frame
|
||||
uint32_t length;
|
||||
//! A temporary buffer for storing the footer (checksum byte)
|
||||
uint8_t footer_byte;
|
||||
//! The checksum byte (comes after the payload in the frame)
|
||||
uint8_t checksum;
|
||||
//! Flag which is set if we find the frame is invalid
|
||||
bool should_drop;
|
||||
} ReadInfo;
|
||||
|
||||
typedef struct {
|
||||
//! The profile used for the request
|
||||
SmartstrapProfile profile;
|
||||
//! The MBufIterator to read data into
|
||||
MBufIterator mbuf_iter;
|
||||
} ReadConsumer;
|
||||
|
||||
typedef union {
|
||||
struct PACKED {
|
||||
bool success;
|
||||
bool is_notify;
|
||||
};
|
||||
void *context_ptr;
|
||||
} ReadCompleteContext;
|
||||
_Static_assert(sizeof(ReadCompleteContext) == sizeof(void *), "ReadCompleteContext too big");
|
||||
|
||||
typedef struct {
|
||||
MBufIterator mbuf_iter;
|
||||
bool is_read;
|
||||
bool sent_escape;
|
||||
uint8_t escaped_byte;
|
||||
} SendInfo;
|
||||
|
||||
//! Info on the current frame being read
|
||||
static ReadInfo s_read_info;
|
||||
//! The consumer of the next frame which is read
|
||||
static ReadConsumer s_read_consumer;
|
||||
//! MBuf for storing the header when receiving
|
||||
static MBuf s_header_mbuf;
|
||||
static uint8_t s_header_data[sizeof(FrameHeader)];
|
||||
//! Info on the current frame being sent
|
||||
static SendInfo s_send_info;
|
||||
|
||||
//! Timer used to enforce read timeouts
|
||||
static TimerID s_read_timer = TIMER_INVALID_ID;
|
||||
|
||||
|
||||
// Init
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void smartstrap_comms_init(void) {
|
||||
s_read_timer = new_timer_create();
|
||||
s_header_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&s_header_mbuf, s_header_data, sizeof(s_header_data));
|
||||
}
|
||||
|
||||
|
||||
// Helper functions for static variables
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void prv_reset_read_info(void) {
|
||||
s_read_info = (ReadInfo) { };
|
||||
hdlc_streaming_decode_reset(&s_read_info.hdlc_ctx);
|
||||
}
|
||||
|
||||
static void prv_reset_read_consumer(void) {
|
||||
s_read_consumer = (ReadConsumer) { 0 };
|
||||
mbuf_iterator_init(&s_read_consumer.mbuf_iter, NULL);
|
||||
}
|
||||
|
||||
void smartstrap_comms_set_enabled(bool enabled) {
|
||||
if (enabled) {
|
||||
prv_reset_read_info();
|
||||
prv_reset_read_consumer();
|
||||
} else {
|
||||
new_timer_stop(s_read_timer);
|
||||
}
|
||||
}
|
||||
|
||||
// Receive functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void prv_read_complete_system_task_cb(void *context_ptr) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
ReadCompleteContext context = { .context_ptr = context_ptr };
|
||||
|
||||
smartstrap_state_lock();
|
||||
if (smartstrap_fsm_state_get() != SmartstrapStateReadComplete) {
|
||||
// We could not be in a ReadComplete state if we got disconnected or if we got a complete frame
|
||||
// while the timeout was scheduled.
|
||||
mbuf_clear_next(&s_header_mbuf);
|
||||
smartstrap_state_unlock();
|
||||
return;
|
||||
}
|
||||
// All other tasks and ISRs will be blocked while we are in the ReadComplete state and while we
|
||||
// hold the state lock, so we're free to access / modify static variables until we transition
|
||||
// the state back to ReadReady.
|
||||
|
||||
SmartstrapProfile read_profile = s_read_consumer.profile;
|
||||
uint32_t read_length = 0;
|
||||
if (context.success) {
|
||||
if (context.is_notify) {
|
||||
// get the profile from the frame
|
||||
FrameHeader *header = mbuf_get_data(&s_header_mbuf);
|
||||
read_profile = header->profile;
|
||||
}
|
||||
PBL_ASSERTN(s_read_info.length >= FRAME_MIN_LENGTH);
|
||||
read_length = s_read_info.length - FRAME_MIN_LENGTH;
|
||||
// don't care if the timeout is alreay queued as the FSM state will make it a noop
|
||||
new_timer_stop(s_read_timer);
|
||||
}
|
||||
|
||||
accessory_use_dma(false);
|
||||
mbuf_clear_next(&s_header_mbuf);
|
||||
prv_reset_read_info();
|
||||
prv_reset_read_consumer();
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadReady);
|
||||
smartstrap_state_unlock();
|
||||
|
||||
if (context.is_notify) {
|
||||
smartstrap_profiles_handle_notification(context.success, read_profile);
|
||||
} else {
|
||||
smartstrap_profiles_handle_read(context.success, read_profile, read_length);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_read_timeout(void *context) {
|
||||
if (smartstrap_fsm_state_test_and_set(SmartstrapStateReadInProgress,
|
||||
SmartstrapStateReadComplete)) {
|
||||
// we need to handle the timeout from KernelBG
|
||||
ReadCompleteContext context = {
|
||||
.success = false,
|
||||
.is_notify = false
|
||||
};
|
||||
system_task_add_callback(prv_read_complete_system_task_cb, context.context_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_store_byte(const uint8_t data) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
// The checksum byte is the last byte in the frame. This byte could be the last byte we receive
|
||||
// (making it the checksum byte), so we always keep a 1 byte temporary buffer before storing the
|
||||
// byte in the MBuf. This avoids us potentially overrunning a conservatively sized payload buffer;
|
||||
if (s_read_info.length > 0) {
|
||||
// copy the previous byte from the footer_byte field into the payload
|
||||
if (!mbuf_iterator_write_byte(&s_read_consumer.mbuf_iter,
|
||||
s_read_info.footer_byte)) {
|
||||
// no room left to store this byte
|
||||
s_read_info.should_drop = true;
|
||||
}
|
||||
}
|
||||
// Store this byte in the footer_byte. Note that we will still calculate the checksum on this byte
|
||||
// and verify that the checksum is 0 at the end, so if this byte is the actual footer byte (aka.
|
||||
// the checksum), we will still include it in the checksum.
|
||||
s_read_info.footer_byte = data;
|
||||
|
||||
// increment the length and run the CRC calculation
|
||||
s_read_info.length++;
|
||||
crc8_calculate_bytes_streaming((uint8_t *)&data, sizeof(data), (uint8_t *)&s_read_info.checksum,
|
||||
false /* !big_endian */);
|
||||
}
|
||||
|
||||
static void prv_handle_complete_frame(bool *should_context_switch) {
|
||||
FrameHeader *header = mbuf_get_data(&s_header_mbuf);
|
||||
bool is_notify = header->flags.is_notify;
|
||||
if ((is_notify && (smartstrap_fsm_state_get() != SmartstrapStateNotifyInProgress)) ||
|
||||
(!is_notify && (s_read_consumer.profile != header->profile))) {
|
||||
// We weither got a notify frame in response to a normal read, or we got a response for a
|
||||
// different frame than we requested.
|
||||
s_read_info.should_drop = true;
|
||||
}
|
||||
if ((s_read_info.should_drop == false) &&
|
||||
(header->version > 0) &&
|
||||
(header->version <= SMARTSTRAP_PROTOCOL_VERSION) &&
|
||||
!header->flags.is_read &&
|
||||
!header->flags.is_master &&
|
||||
!header->flags.reserved &&
|
||||
(header->profile > SmartstrapProfileInvalid) &&
|
||||
(header->profile < NumSmartstrapProfiles) &&
|
||||
(s_read_info.length >= FRAME_MIN_LENGTH) &&
|
||||
!s_read_info.checksum) {
|
||||
// If this is a notification, we shouldn't have a read consumer set.
|
||||
PBL_ASSERTN(!is_notify || (s_read_consumer.profile == SmartstrapProfileInvalid));
|
||||
// this frame is valid - transition the FSM and queue up processing of it
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadComplete);
|
||||
ReadCompleteContext context = {
|
||||
.success = true,
|
||||
.is_notify = is_notify
|
||||
};
|
||||
system_task_add_callback_from_isr(prv_read_complete_system_task_cb, context.context_ptr,
|
||||
should_context_switch);
|
||||
} else {
|
||||
// Reset our context so we can try again to receive a frame in case we do happen to get a valid
|
||||
// one before the timeout occurs.
|
||||
prv_reset_read_info();
|
||||
mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf);
|
||||
}
|
||||
}
|
||||
|
||||
bool smartstrap_handle_data_from_isr(uint8_t data) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
if ((smartstrap_fsm_state_get() != SmartstrapStateReadInProgress) &&
|
||||
(smartstrap_fsm_state_get() != SmartstrapStateNotifyInProgress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool should_context_switch = false;
|
||||
bool hdlc_err;
|
||||
bool should_store;
|
||||
bool is_complete = hdlc_streaming_decode(&s_read_info.hdlc_ctx, &data, &should_store, &hdlc_err);
|
||||
if (hdlc_err) {
|
||||
// the rest of the frame is invalid
|
||||
s_read_info.should_drop = true;
|
||||
} else if (is_complete) {
|
||||
prv_handle_complete_frame(&should_context_switch);
|
||||
} else if (should_store && !s_read_info.should_drop) {
|
||||
prv_store_byte(data);
|
||||
}
|
||||
|
||||
return should_context_switch;
|
||||
}
|
||||
|
||||
void prv_notify_timeout(void *context) {
|
||||
if (smartstrap_fsm_state_test_and_set(SmartstrapStateNotifyInProgress,
|
||||
SmartstrapStateReadComplete)) {
|
||||
// we need to handle the timeout from KernelBG
|
||||
ReadCompleteContext context = {
|
||||
.success = false,
|
||||
.is_notify = true
|
||||
};
|
||||
system_task_add_callback(prv_read_complete_system_task_cb, context.context_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
void prv_schedule_notify_timeout(void *context) {
|
||||
// make sure there's still a notification pending
|
||||
if (smartstrap_fsm_state_get() == SmartstrapStateNotifyInProgress) {
|
||||
PBL_ASSERTN(new_timer_start(s_read_timer, NOTIFY_TIMEOUT, prv_notify_timeout, NULL, 0));
|
||||
}
|
||||
}
|
||||
|
||||
bool smartstrap_handle_break_from_isr(void) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
bool should_context_switch = false;
|
||||
// we should only accept notifications if we're in the ReadReady state
|
||||
if (smartstrap_fsm_state_test_and_set(SmartstrapStateReadReady,
|
||||
SmartstrapStateNotifyInProgress)) {
|
||||
// prepare to read notification context
|
||||
PBL_ASSERTN(!mbuf_get_next(&s_header_mbuf));
|
||||
mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf);
|
||||
system_task_add_callback_from_isr(prv_schedule_notify_timeout, NULL, &should_context_switch);
|
||||
}
|
||||
return should_context_switch;
|
||||
}
|
||||
|
||||
|
||||
// Sending functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static bool prv_send_byte_and_check(uint8_t data) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
accessory_send_byte(data);
|
||||
const bool bus_contention = accessory_bus_contention_detected();
|
||||
if (bus_contention) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Bus contention was detected!");
|
||||
}
|
||||
return !bus_contention;
|
||||
}
|
||||
|
||||
static bool prv_send_byte(uint8_t data) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
if (hdlc_encode(&data)) {
|
||||
PBL_ASSERTN(!s_send_info.sent_escape);
|
||||
s_send_info.sent_escape = true;
|
||||
s_send_info.escaped_byte = data;
|
||||
data = HDLC_ESCAPE;
|
||||
}
|
||||
return prv_send_byte_and_check(data);
|
||||
}
|
||||
|
||||
static bool prv_send_stream_callback(void *context) {
|
||||
// NOTE: THIS IS RUN WITHIN AN ISR
|
||||
if (smartstrap_fsm_state_get() != SmartstrapStateReadDisabled) {
|
||||
// we should no longer be sending
|
||||
return false;
|
||||
}
|
||||
|
||||
// handle escaped bytes first
|
||||
if (s_send_info.sent_escape) {
|
||||
s_send_info.sent_escape = false;
|
||||
return prv_send_byte_and_check(s_send_info.escaped_byte);
|
||||
}
|
||||
|
||||
// send the next byte
|
||||
bool result = true;
|
||||
MBufIterator *iter = &s_send_info.mbuf_iter;
|
||||
MBuf *mbuf = mbuf_iterator_get_current_mbuf(iter);
|
||||
uint8_t read_data;
|
||||
PBL_ASSERTN(mbuf_iterator_read_byte(iter, &read_data));
|
||||
if (mbuf_is_flag_set(mbuf, MBUF_FLAG_IS_FRAMING)) {
|
||||
result = prv_send_byte_and_check(read_data);
|
||||
} else {
|
||||
result = prv_send_byte(read_data);
|
||||
}
|
||||
|
||||
if (mbuf_iterator_is_finished(iter)) {
|
||||
// we just sent the last byte
|
||||
if (s_send_info.is_read) {
|
||||
// We just successfully sent a read request, so should move to ReadInProgress to prepare to
|
||||
// read the response. We do this here to ensure we don't miss any bytes of the response due to
|
||||
// KernelBG not getting scheduled quickly enough.
|
||||
PBL_ASSERTN(!mbuf_get_next(&s_header_mbuf));
|
||||
mbuf_append(&s_header_mbuf, context);
|
||||
mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf);
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadInProgress);
|
||||
}
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
accessory_enable_input();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SmartstrapResult smartstrap_send(SmartstrapProfile profile, MBuf *write_mbuf, MBuf *read_mbuf,
|
||||
uint16_t timeout_ms) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
smartstrap_state_assert_locked_by_current_task();
|
||||
// we expect the arguments to be valid
|
||||
const bool is_read = (read_mbuf != NULL);
|
||||
PBL_ASSERTN((profile > SmartstrapProfileInvalid) && (profile < NumSmartstrapProfiles));
|
||||
PBL_ASSERTN(!is_read || (mbuf_get_chain_length(read_mbuf) > 0));
|
||||
PBL_ASSERTN((!write_mbuf && !read_mbuf) || (write_mbuf != read_mbuf));
|
||||
timeout_ms = MIN(timeout_ms, SMARTSTRAP_MAX_TIMEOUT);
|
||||
|
||||
// transition the FSM state
|
||||
if (!smartstrap_fsm_state_test_and_set(SmartstrapStateReadReady, SmartstrapStateReadDisabled)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Failed to change smartstrap FSM state (%d)",
|
||||
smartstrap_fsm_state_get());
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
|
||||
// We are now be in a state which allows us to freely modify static variables as we can be sure
|
||||
// that no ISR or other tasks will be allowed to access or modify them while we are in this
|
||||
// state.
|
||||
accessory_disable_input();
|
||||
// NOTE: Accessory input will be re-enabled by the stream callback after we finish sending
|
||||
prv_reset_read_info();
|
||||
prv_reset_read_consumer();
|
||||
s_send_info = (SendInfo) { .is_read = is_read };
|
||||
|
||||
if (is_read) {
|
||||
// populate the read consumer info
|
||||
s_read_consumer.profile = profile;
|
||||
}
|
||||
|
||||
// Go through and build the frame: Start_Flag | Header | Payload | Checksum | End_Flag
|
||||
|
||||
// Start_Flag
|
||||
uint8_t flag_data = HDLC_FLAG;
|
||||
MBuf start_flag_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&start_flag_mbuf, &flag_data, sizeof(flag_data));
|
||||
mbuf_set_flag(&start_flag_mbuf, MBUF_FLAG_IS_FRAMING, true);
|
||||
|
||||
// Header
|
||||
FrameHeader header = (FrameHeader) {
|
||||
.version = SMARTSTRAP_PROTOCOL_VERSION,
|
||||
.flags = {
|
||||
.is_read = is_read,
|
||||
.is_master = true,
|
||||
},
|
||||
.profile = profile
|
||||
};
|
||||
MBuf header_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&header_mbuf, &header, sizeof(header));
|
||||
mbuf_append(&start_flag_mbuf, &header_mbuf);
|
||||
|
||||
// Payload
|
||||
mbuf_append(&start_flag_mbuf, write_mbuf);
|
||||
|
||||
// Checksum
|
||||
uint8_t checksum = 0;
|
||||
for (MBuf *m = &header_mbuf; m; m = mbuf_get_next(m)) {
|
||||
if (!mbuf_is_flag_set(m, MBUF_FLAG_IS_FRAMING)) {
|
||||
crc8_calculate_bytes_streaming(m->data, m->length, &checksum, false /* !big_endian */);
|
||||
}
|
||||
}
|
||||
MBuf footer_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&footer_mbuf, &checksum, sizeof(checksum));
|
||||
mbuf_append(&start_flag_mbuf, &footer_mbuf);
|
||||
|
||||
// End_Flag
|
||||
MBuf end_flag_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&end_flag_mbuf, &flag_data, sizeof(flag_data));
|
||||
mbuf_set_flag(&end_flag_mbuf, MBUF_FLAG_IS_FRAMING, true);
|
||||
mbuf_append(&start_flag_mbuf, &end_flag_mbuf);
|
||||
|
||||
// send off the frame
|
||||
mbuf_iterator_init(&s_send_info.mbuf_iter, &start_flag_mbuf);
|
||||
accessory_use_dma(true);
|
||||
if (!accessory_send_stream(prv_send_stream_callback, (void *)read_mbuf)) {
|
||||
accessory_enable_input();
|
||||
}
|
||||
|
||||
if (is_read) {
|
||||
// If we sent the request successfully, the send ISR will have transitioned us out of
|
||||
// ReadDisabled.
|
||||
const bool was_successful = (smartstrap_fsm_state_get() != SmartstrapStateReadDisabled);
|
||||
if (was_successful) {
|
||||
// start the timer for the read timeout
|
||||
PBL_ASSERTN(new_timer_start(s_read_timer, timeout_ms, prv_read_timeout, NULL, 0));
|
||||
} else {
|
||||
// clean up and return an error
|
||||
accessory_use_dma(false);
|
||||
prv_reset_read_consumer();
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadReady);
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
} else {
|
||||
accessory_use_dma(false);
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadReady);
|
||||
if (!mbuf_iterator_is_finished(&s_send_info.mbuf_iter)) {
|
||||
// The write was not successful, so return an error
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
}
|
||||
|
||||
return SmartstrapResultOk;
|
||||
}
|
||||
|
||||
void smartstrap_cancel_send(void) {
|
||||
// Enter a critical region to prevent anybody else changing the state.
|
||||
portENTER_CRITICAL();
|
||||
SmartstrapState state = smartstrap_fsm_state_get();
|
||||
if ((state != SmartstrapStateReadDisabled) && (state != SmartstrapStateReadInProgress) &&
|
||||
(state != SmartstrapStateReadComplete)) {
|
||||
// we aren't in a state where something is in progress, so there's nothing to do
|
||||
portEXIT_CRITICAL();
|
||||
return;
|
||||
}
|
||||
smartstrap_fsm_state_reset();
|
||||
new_timer_stop(s_read_timer);
|
||||
smartstrap_profiles_handle_read_aborted(s_read_consumer.profile);
|
||||
prv_reset_read_info();
|
||||
prv_reset_read_consumer();
|
||||
mbuf_clear_next(&s_header_mbuf);
|
||||
portEXIT_CRITICAL();
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Canceled an in-progress request. Was in state: %d", state);
|
||||
}
|
||||
58
src/fw/services/normal/accessory/smartstrap_comms.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/app_smartstrap.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "util/mbuf.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SMARTSTRAP_PROTOCOL_VERSION 1
|
||||
|
||||
//! Initialize the smartstrap manager
|
||||
void smartstrap_comms_init(void);
|
||||
|
||||
//! Called by accessory_manager when we receive a byte of data from the accessory port
|
||||
bool smartstrap_handle_data_from_isr(uint8_t c);
|
||||
|
||||
//! Called by accessory_manager when we receive a break character
|
||||
bool smartstrap_handle_break_from_isr(void);
|
||||
|
||||
//! Sends a message over the accessory port using the smartstrap protocol. The message will be sent
|
||||
//! synchronously and the response will be read asynchronously with an event being put on the
|
||||
//! calling task's queue when the response is read or a timeout occurs. A response will only be
|
||||
//! expected if read_data is non-NULL.
|
||||
//! @note The calling task must be subscribed first (@see sys_smartstrap_subscribe)
|
||||
//! @param[in] profile The profile of the frame
|
||||
//! @param[in] write_data The data to be written to the smartstrap
|
||||
//! @param[in] write_length The length of write_data
|
||||
//! @param[in] read_data The buffer to store the response in (asynchronously)
|
||||
//! @param[in] read_length The length of the read_data buffer
|
||||
//! @param[in] timeout_ms A timeout will occur if the response is not received after this amount of
|
||||
//! time
|
||||
//! @return The result of the send
|
||||
SmartstrapResult smartstrap_send(SmartstrapProfile profile, MBuf *write_mbuf, MBuf *read_mbuf,
|
||||
uint16_t timeout_ms);
|
||||
|
||||
|
||||
//! Enables or disables the smartstrap communications
|
||||
void smartstrap_comms_set_enabled(bool enabled);
|
||||
|
||||
//! Cancels any send (write or read) which is in progress
|
||||
void smartstrap_cancel_send(void);
|
||||
215
src/fw/services/normal/accessory/smartstrap_connection.c
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 "drivers/accessory.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/normal/accessory/accessory_manager.h"
|
||||
#include "services/normal/accessory/smartstrap_attribute.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_link_control.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
//! How long to wait after failing to acquire the accessory in ms before trying again
|
||||
static const uint32_t ACCESSORY_ACQUIRE_INTERVAL = 5000;
|
||||
//! The backoff before trying to detect a smartstrap again in ms
|
||||
static const uint32_t DETECTION_BACKOFF = 200;
|
||||
//! The maximum interval between detection attempts in ms
|
||||
static const uint32_t DETECTION_MAX_INTERVAL = 10000;
|
||||
//! When we expect something will kick us, we'll use this value as a timeout just in-case.
|
||||
static const uint32_t KICK_TIMEOUT_INTERVAL = 2000;
|
||||
//! If we hit bus contention during sending, we should wait this number of milliseconds.
|
||||
static const uint32_t BUS_CONTENTION_INTERVAL = 100;
|
||||
|
||||
//! Subscriber information
|
||||
static int s_subscriber_count;
|
||||
//! Timer used for monitoring the connection and sending pending requests
|
||||
static TimerID s_monitor_timer = TIMER_INVALID_ID;
|
||||
//! The last time we got valid data from the smartstrap
|
||||
static time_t s_last_data_time = 0;
|
||||
|
||||
|
||||
void smartstrap_connection_init(void) {
|
||||
s_monitor_timer = new_timer_create();
|
||||
}
|
||||
|
||||
static bool prv_acquire_accessory(void) {
|
||||
if (accessory_manager_set_state(AccessoryInputStateSmartstrap)) {
|
||||
// enable the accessory port
|
||||
accessory_set_baudrate(AccessoryBaud9600);
|
||||
accessory_set_power(true);
|
||||
smartstrap_comms_set_enabled(true);
|
||||
return true;
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "The accessory is already in use");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_release_accessory(void) {
|
||||
PBL_ASSERTN(!s_subscriber_count);
|
||||
|
||||
smartstrap_fsm_state_set(SmartstrapStateUnsubscribed);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Disconnecting from smartstrap");
|
||||
smartstrap_link_control_disconnect();
|
||||
smartstrap_comms_set_enabled(false);
|
||||
new_timer_stop(s_monitor_timer);
|
||||
// stop any in-progress write
|
||||
accessory_send_stream_stop();
|
||||
// release the accessory port
|
||||
PBL_ASSERTN(accessory_manager_set_state(AccessoryInputStateIdle));
|
||||
}
|
||||
|
||||
static void prv_monitor_timer_cb(void *context);
|
||||
|
||||
static void prv_monitor_system_task_cb(void *context) {
|
||||
static uint32_t s_monitor_interval = 0;
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
smartstrap_state_lock();
|
||||
if (s_subscriber_count == 0) {
|
||||
prv_release_accessory();
|
||||
smartstrap_state_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
if (smartstrap_fsm_state_get() == SmartstrapStateUnsubscribed) {
|
||||
if (prv_acquire_accessory()) {
|
||||
// we will now start to attempt to connect to the smartstrap
|
||||
smartstrap_fsm_state_set(SmartstrapStateReadReady);
|
||||
s_monitor_interval = 0;
|
||||
} else {
|
||||
// try again in a little while to acquire the accessory
|
||||
s_monitor_interval = ACCESSORY_ACQUIRE_INTERVAL;
|
||||
}
|
||||
}
|
||||
|
||||
bool can_send = false;
|
||||
if (smartstrap_fsm_state_get() == SmartstrapStateReadReady) {
|
||||
if (accessory_is_present() || smartstrap_is_connected()) {
|
||||
// If the accessory is present and we are connected then we can send data freely. If the
|
||||
// accessory is present but we're not connected, we'll try to connected. If we are connected
|
||||
// but the accessory is not present, we'll disconnect.
|
||||
can_send = true;
|
||||
} else {
|
||||
// back off a bit and check again for an accessory to be present
|
||||
s_monitor_interval = MIN(s_monitor_interval + DETECTION_BACKOFF, DETECTION_MAX_INTERVAL);
|
||||
}
|
||||
} else if (smartstrap_fsm_state_get() != SmartstrapStateUnsubscribed) {
|
||||
// There is a request in progress. We'll get kicked when it's completed.
|
||||
s_monitor_interval = KICK_TIMEOUT_INTERVAL;
|
||||
}
|
||||
|
||||
smartstrap_state_unlock();
|
||||
|
||||
if (can_send) {
|
||||
// We should attempt to send control messages first, followed by pending attributes.
|
||||
bool did_send = false;
|
||||
if (smartstrap_profiles_send_control()) {
|
||||
did_send = true;
|
||||
} else if (smartstrap_attribute_send_pending()) {
|
||||
did_send = true;
|
||||
}
|
||||
if (did_send && smartstrap_fsm_state_get() == SmartstrapStateReadReady) {
|
||||
if (accessory_bus_contention_detected()) {
|
||||
// There was bus contention during the send which caused it to fail. Set a short interval
|
||||
// before trying to send to allow the bus contention to clear.
|
||||
s_monitor_interval = BUS_CONTENTION_INTERVAL;
|
||||
} else {
|
||||
// We sent a write request, so are ready to send another request right away.
|
||||
s_monitor_interval = 0;
|
||||
}
|
||||
} else {
|
||||
// Either we are now waiting for a response, at which point we'll get kicked, or there was
|
||||
// nothing to send, in which case we'll get kicked when there is.
|
||||
s_monitor_interval = KICK_TIMEOUT_INTERVAL;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_monitor_interval) {
|
||||
// run the monitor again after the set interval
|
||||
new_timer_start(s_monitor_timer, s_monitor_interval, prv_monitor_timer_cb, NULL, 0);
|
||||
} else {
|
||||
// custom fast-path for 0ms timeout
|
||||
prv_monitor_timer_cb(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_monitor_timer_cb(void *context) {
|
||||
// we need to run from KernelBG so schedule a callback
|
||||
system_task_add_callback(prv_monitor_system_task_cb, NULL);
|
||||
}
|
||||
|
||||
void smartstrap_connection_kick_monitor(void) {
|
||||
// queue up the system task immediately
|
||||
prv_monitor_timer_cb(NULL);
|
||||
}
|
||||
|
||||
void smartstrap_connection_got_valid_data(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
s_last_data_time = rtc_get_time();
|
||||
}
|
||||
|
||||
time_t smartstrap_connection_get_time_since_valid_data(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
return rtc_get_time() - s_last_data_time;
|
||||
}
|
||||
|
||||
#if !RELEASE
|
||||
#include "console/prompt.h"
|
||||
|
||||
void command_smartstrap_status(void) {
|
||||
char buf[80];
|
||||
prompt_send_response_fmt(buf, sizeof(buf), "present=%d, connected=%d", accessory_is_present(),
|
||||
smartstrap_is_connected());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Subscription functions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool smartstrap_connection_has_subscriber(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
return s_subscriber_count > 0;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_smartstrap_subscribe, void) {
|
||||
smartstrap_state_lock();
|
||||
s_subscriber_count++;
|
||||
if (s_subscriber_count == 1) {
|
||||
// kick the connection monitor
|
||||
smartstrap_connection_kick_monitor();
|
||||
}
|
||||
smartstrap_state_unlock();
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_smartstrap_unsubscribe, void) {
|
||||
smartstrap_state_lock();
|
||||
s_subscriber_count--;
|
||||
if (s_subscriber_count == 0) {
|
||||
// Disconnect directly from here rather than waiting for the monitor in order to ensure it
|
||||
// happens synchronously.
|
||||
prv_release_accessory();
|
||||
}
|
||||
PBL_ASSERTN(s_subscriber_count >= 0);
|
||||
smartstrap_state_unlock();
|
||||
}
|
||||
46
src/fw/services/normal/accessory/smartstrap_connection.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/pebble_tasks.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
void smartstrap_connection_init(void);
|
||||
|
||||
//! Kicks the monitor which is responsible for detection of connected smartstraps and sending any
|
||||
//! pending requests.
|
||||
void smartstrap_connection_kick_monitor(void);
|
||||
|
||||
//! Called to indicate that we got valid data from the smartstrap (@see smartstrap_got_valid_data())
|
||||
void smartstrap_connection_got_valid_data(void);
|
||||
|
||||
//! Returns the number of milliseconds since smartstrap_got_valid_data() was last called.
|
||||
time_t smartstrap_connection_get_time_since_valid_data(void);
|
||||
|
||||
//! Returns whether or not we currently have any subscribers.
|
||||
bool smartstrap_connection_has_subscriber(void);
|
||||
|
||||
//! Subscribes to the smartstrap. When there is at least one subscriber, we will attempt to connect
|
||||
//! to the smartstrap.
|
||||
void sys_smartstrap_subscribe(void);
|
||||
|
||||
//! Unsubscribes from the smartstrap. When nobody is subscribed, we will disconnect from the
|
||||
//! smartstrap.
|
||||
void sys_smartstrap_unsubscribe(void);
|
||||
394
src/fw/services/normal/accessory/smartstrap_generic_service.c
Normal file
@@ -0,0 +1,394 @@
|
||||
/*
|
||||
* 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/app_smartstrap.h"
|
||||
#include "drivers/accessory.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "os/mutex.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "services/normal/accessory/smartstrap_attribute.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_link_control.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/mbuf.h"
|
||||
|
||||
#define MAX_SERVICES 10
|
||||
#define MIN_SERVICE_ID 0x100
|
||||
#define GENERIC_SERVICE_VERSION 1
|
||||
#define TIMEOUT_MS 100
|
||||
//! The largest message for attributes which we support internally (the app has its own buffer)
|
||||
#define BUFFER_LENGTH 20
|
||||
#define MIN_SERVICE_DISCOVERY_INTERVAL 1
|
||||
|
||||
typedef struct {
|
||||
uint16_t service_id;
|
||||
uint16_t attribute_id;
|
||||
} ReadInfo;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint8_t version;
|
||||
uint16_t service_id;
|
||||
uint16_t attribute_id;
|
||||
uint8_t type;
|
||||
uint8_t error;
|
||||
uint16_t length;
|
||||
} FrameInfo;
|
||||
|
||||
typedef struct PACKED {
|
||||
uint16_t service_id;
|
||||
uint16_t attribute_id;
|
||||
} NotificationInfoData;
|
||||
|
||||
typedef enum {
|
||||
GenericServiceResultOk = 0,
|
||||
GenericServiceResultNotSupported = 1,
|
||||
NumGenericServiceResults
|
||||
} GenericServiceResult;
|
||||
|
||||
typedef enum {
|
||||
GenericServiceTypeRead = 0,
|
||||
GenericServiceTypeWrite = 1,
|
||||
GenericServiceTypeWriteRead = 2,
|
||||
NumGenericServiceTypes
|
||||
} GenericServiceType;
|
||||
|
||||
typedef enum {
|
||||
ReservedServiceManagement = 0x0101,
|
||||
ReservedServiceControl = 0x0102,
|
||||
ReservedServiceMax = 0x0fff
|
||||
} ReservedService;
|
||||
|
||||
typedef enum {
|
||||
ManagementServiceAttributeServiceDiscovery = 0x0001,
|
||||
ManagementServiceAttributeNotificationInfo = 0x0002
|
||||
} ManagementServiceAttribute;
|
||||
|
||||
typedef enum {
|
||||
ControlServiceAttributeLaunchApp = 0x0001,
|
||||
ControlServiceAttributeButtonEvent = 0x0002
|
||||
} ControlServiceAttribute;
|
||||
|
||||
static MBuf *s_reserved_read_mbuf;
|
||||
static MBuf *s_read_header_mbuf;
|
||||
static uint8_t s_read_header[sizeof(FrameInfo)];
|
||||
static ReadInfo s_read_info;
|
||||
static PebbleMutex *s_read_lock;
|
||||
static uint8_t s_read_buffer[BUFFER_LENGTH];
|
||||
static bool s_has_done_service_discovery;
|
||||
|
||||
|
||||
static void prv_init(void) {
|
||||
s_read_lock = mutex_create();
|
||||
}
|
||||
|
||||
static SmartstrapResult prv_do_send(GenericServiceType type, uint16_t service_id,
|
||||
uint16_t attribute_id, MBuf *write_mbuf, MBuf *read_mbuf,
|
||||
uint16_t timeout_ms) {
|
||||
if (s_read_header_mbuf) {
|
||||
// already a read in progress
|
||||
return SmartstrapResultBusy;
|
||||
}
|
||||
|
||||
mutex_lock(s_read_lock);
|
||||
// populate the read info
|
||||
s_read_info = (ReadInfo) {
|
||||
.service_id = service_id,
|
||||
.attribute_id = attribute_id,
|
||||
};
|
||||
|
||||
// add the header
|
||||
FrameInfo header = (FrameInfo) {
|
||||
.version = GENERIC_SERVICE_VERSION,
|
||||
.type = type,
|
||||
.error = GenericServiceResultOk,
|
||||
.service_id = service_id,
|
||||
.attribute_id = attribute_id,
|
||||
.length = mbuf_get_chain_length(write_mbuf)
|
||||
};
|
||||
MBuf send_header_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&send_header_mbuf, &header, sizeof(header));
|
||||
mbuf_append(&send_header_mbuf, write_mbuf);
|
||||
|
||||
// setup the MBuf chain for reading
|
||||
s_read_header_mbuf = mbuf_get(&s_read_header, sizeof(s_read_header), MBufPoolSmartstrap);
|
||||
mbuf_append(s_read_header_mbuf, read_mbuf);
|
||||
|
||||
SmartstrapResult result = smartstrap_send(SmartstrapProfileGenericService, &send_header_mbuf,
|
||||
s_read_header_mbuf, timeout_ms);
|
||||
if (result != SmartstrapResultOk) {
|
||||
mbuf_free(s_read_header_mbuf);
|
||||
s_read_header_mbuf = NULL;
|
||||
}
|
||||
mutex_unlock(s_read_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void prv_send_service_discovery(void *context) {
|
||||
const uint16_t service_id = ReservedServiceManagement;
|
||||
const uint16_t attribute_id = ManagementServiceAttributeServiceDiscovery;
|
||||
if (s_reserved_read_mbuf) {
|
||||
// already a read in progress
|
||||
return;
|
||||
}
|
||||
s_reserved_read_mbuf = mbuf_get(s_read_buffer, sizeof(s_read_buffer), MBufPoolSmartstrap);
|
||||
SmartstrapResult result = prv_do_send(GenericServiceTypeRead, service_id, attribute_id, NULL,
|
||||
s_reserved_read_mbuf, TIMEOUT_MS);
|
||||
if (result != SmartstrapResultOk) {
|
||||
mbuf_free(s_reserved_read_mbuf);
|
||||
s_reserved_read_mbuf = NULL;
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Sent service discovery message (result=%d)", result);
|
||||
}
|
||||
|
||||
static void prv_set_connected(bool connected) {
|
||||
s_has_done_service_discovery = false;
|
||||
}
|
||||
|
||||
static bool prv_handle_management_attribute_read(bool success, ManagementServiceAttribute attr,
|
||||
void *data, uint32_t length) {
|
||||
if (!success) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Read of management attribute was not successful (0x%x)", attr);
|
||||
return false;
|
||||
}
|
||||
if (attr == ManagementServiceAttributeServiceDiscovery) {
|
||||
if (length % sizeof(uint16_t)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Service discovery response is invalid length: %"PRIu32, length);
|
||||
return false;
|
||||
}
|
||||
// validate and mark the services as connected
|
||||
uint32_t i;
|
||||
uint16_t *services = data;
|
||||
bool has_valid_service = false;
|
||||
for (i = 0; i < length / sizeof(uint16_t); i++) {
|
||||
if ((services[i] > ReservedServiceMax) || (services[i] == ReservedServiceControl)) {
|
||||
has_valid_service = true;
|
||||
smartstrap_connection_state_set_by_service(services[i], true);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Skipping invalid service_id 0x%x", services[i]);
|
||||
}
|
||||
}
|
||||
if (has_valid_service) {
|
||||
s_has_done_service_discovery = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (attr == ManagementServiceAttributeNotificationInfo) {
|
||||
if (length != sizeof(NotificationInfoData)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Notification info response is invalid length: %"PRIu32, length);
|
||||
return false;
|
||||
}
|
||||
const NotificationInfoData *notification_info = data;
|
||||
if (notification_info->service_id <= ReservedServiceMax) {
|
||||
// Currently we only support the control service and its launch app attribute
|
||||
if (notification_info->service_id == ReservedServiceControl &&
|
||||
notification_info->attribute_id == ControlServiceAttributeLaunchApp) {
|
||||
s_reserved_read_mbuf = mbuf_get(s_read_buffer, sizeof(s_read_buffer), MBufPoolSmartstrap);
|
||||
SmartstrapResult result = prv_do_send(GenericServiceTypeRead, notification_info->service_id,
|
||||
notification_info->attribute_id, NULL,
|
||||
s_reserved_read_mbuf, TIMEOUT_MS);
|
||||
if (result != SmartstrapResultOk) {
|
||||
mbuf_free(s_reserved_read_mbuf);
|
||||
s_reserved_read_mbuf = NULL;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// Unsupported reserved service and/or attribute
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Notification wasn't for a reserved service, send a notification event
|
||||
smartstrap_attribute_send_event(SmartstrapNotifyEvent, SmartstrapProfileGenericService,
|
||||
SmartstrapResultOk, notification_info->service_id,
|
||||
notification_info->attribute_id, 0);
|
||||
}
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool prv_handle_control_attribute_read(bool success, ControlServiceAttribute attr,
|
||||
void *data, uint32_t length) {
|
||||
if (!success) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Read of control attribute was not successful (0x%x)", attr);
|
||||
return false;
|
||||
}
|
||||
if (attr == ControlServiceAttributeLaunchApp) {
|
||||
if (length != UUID_SIZE) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Launch app response is invalid length: %"PRIu32, length);
|
||||
return false;
|
||||
}
|
||||
|
||||
Uuid *app_uuid = (Uuid *)data;
|
||||
AppInstallId app_id = app_install_get_id_for_uuid(app_uuid);
|
||||
if (app_id == INSTALL_ID_INVALID) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Attempting to launch an invalid app");
|
||||
return false;
|
||||
}
|
||||
app_manager_put_launch_app_event(&(AppLaunchEventConfig) {
|
||||
.id = app_id,
|
||||
.common.reason = APP_LAUNCH_SMARTSTRAP,
|
||||
});
|
||||
} else if (attr == ControlServiceAttributeButtonEvent) {
|
||||
// TODO: PBL-38311
|
||||
return false;
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool prv_read_complete(bool success, uint32_t length) {
|
||||
FrameInfo *header = mbuf_get_data(s_read_header_mbuf);
|
||||
// get the length of the data buffer(s) which is the max length of data we could have received
|
||||
const uint32_t data_length = mbuf_get_chain_length(mbuf_get_next(s_read_header_mbuf));
|
||||
mbuf_free(s_read_header_mbuf);
|
||||
s_read_header_mbuf = NULL;
|
||||
SmartstrapResult result = SmartstrapResultOk;
|
||||
if (!success) {
|
||||
result = SmartstrapResultTimeOut;
|
||||
} else if ((length < sizeof(FrameInfo)) ||
|
||||
(header->length != length - sizeof(FrameInfo)) ||
|
||||
(header->length > data_length) ||
|
||||
(header->version != GENERIC_SERVICE_VERSION) ||
|
||||
(header->error >= NumGenericServiceResults) ||
|
||||
(header->service_id != s_read_info.service_id) ||
|
||||
(header->attribute_id != s_read_info.attribute_id)) {
|
||||
success = false;
|
||||
// TODO: We just got a bad frame, but time-out is the best error we have right now. Ideally,
|
||||
// we could validate the generic service header and drop this frame before we stop the read
|
||||
// timeout and then keep looking for a valid frame until the timeout is hit.
|
||||
result = SmartstrapResultTimeOut;
|
||||
} else if (success && (header->error != GenericServiceResultOk)) {
|
||||
// The response was completely valid, but there was a non-Ok error returned
|
||||
success = false;
|
||||
// translate the error code to the appropraite SmartstrapResult
|
||||
if (header->error == GenericServiceResultNotSupported) {
|
||||
result = SmartstrapResultAttributeUnsupported;
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
}
|
||||
|
||||
const uint16_t service_id = s_read_info.service_id;
|
||||
const uint16_t attribute_id = s_read_info.attribute_id;
|
||||
if (success) {
|
||||
length = header->length;
|
||||
} else {
|
||||
length = 0;
|
||||
}
|
||||
|
||||
if (service_id <= ReservedServiceMax) {
|
||||
// This is a reserved service read which we should handle internally
|
||||
void *data = mbuf_get_data(s_reserved_read_mbuf);
|
||||
mbuf_free(s_reserved_read_mbuf);
|
||||
s_reserved_read_mbuf = NULL;
|
||||
if (service_id == ReservedServiceManagement) {
|
||||
success = prv_handle_management_attribute_read(success, attribute_id, data, length);
|
||||
} else if (service_id == ReservedServiceControl) {
|
||||
success = prv_handle_control_attribute_read(success, attribute_id, data, length);
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
} else {
|
||||
smartstrap_attribute_send_event(SmartstrapDataReceivedEvent, SmartstrapProfileGenericService,
|
||||
result, service_id, attribute_id, length);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static void prv_handle_notification(void) {
|
||||
// follow-up with a notification info frame
|
||||
const uint16_t service_id = ReservedServiceManagement;
|
||||
const uint16_t attribute_id = ManagementServiceAttributeNotificationInfo;
|
||||
if (s_reserved_read_mbuf) {
|
||||
// already a read in progress
|
||||
return;
|
||||
}
|
||||
s_reserved_read_mbuf = mbuf_get(s_read_buffer, sizeof(s_read_buffer), MBufPoolSmartstrap);
|
||||
SmartstrapResult result = prv_do_send(GenericServiceTypeRead, service_id, attribute_id, NULL,
|
||||
s_reserved_read_mbuf, TIMEOUT_MS);
|
||||
if (result != SmartstrapResultOk) {
|
||||
mbuf_free(s_reserved_read_mbuf);
|
||||
s_reserved_read_mbuf = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static SmartstrapResult prv_send(const SmartstrapRequest *request) {
|
||||
if (!s_has_done_service_discovery || !sys_smartstrap_is_service_connected(request->service_id)) {
|
||||
return SmartstrapResultServiceUnavailable;
|
||||
}
|
||||
|
||||
GenericServiceType type;
|
||||
if (request->write_mbuf && request->read_mbuf) {
|
||||
type = GenericServiceTypeWriteRead;
|
||||
} else if (request->write_mbuf) {
|
||||
type = GenericServiceTypeWrite;
|
||||
} else if (request->read_mbuf) {
|
||||
type = GenericServiceTypeRead;
|
||||
} else {
|
||||
type = GenericServiceTypeRead; // stop lint from complaining
|
||||
WTF;
|
||||
}
|
||||
return prv_do_send(type, request->service_id, request->attribute_id, request->write_mbuf,
|
||||
request->read_mbuf, request->timeout_ms);
|
||||
}
|
||||
|
||||
static bool prv_send_control(void) {
|
||||
// make sure we're not spamming the smartstrap with service discovery messages
|
||||
static time_t s_last_service_discovery_time = 0;
|
||||
const time_t current_time = rtc_get_time();
|
||||
if (!s_has_done_service_discovery &&
|
||||
(current_time > s_last_service_discovery_time + MIN_SERVICE_DISCOVERY_INTERVAL) &&
|
||||
smartstrap_link_control_is_profile_supported(SmartstrapProfileGenericService)) {
|
||||
prv_send_service_discovery(NULL);
|
||||
s_last_service_discovery_time = current_time;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void prv_read_aborted(void) {
|
||||
mbuf_free(s_read_header_mbuf);
|
||||
s_read_header_mbuf = NULL;
|
||||
mbuf_free(s_reserved_read_mbuf);
|
||||
s_reserved_read_mbuf = NULL;
|
||||
}
|
||||
|
||||
const SmartstrapProfileInfo *smartstrap_generic_service_get_info(void) {
|
||||
static const SmartstrapProfileInfo s_generic_service_info = {
|
||||
.profile = SmartstrapProfileGenericService,
|
||||
.max_services = MAX_SERVICES,
|
||||
.min_service_id = MIN_SERVICE_ID,
|
||||
.init = prv_init,
|
||||
.connected = prv_set_connected,
|
||||
.send = prv_send,
|
||||
.read_complete = prv_read_complete,
|
||||
.notify = prv_handle_notification,
|
||||
.control = prv_send_control,
|
||||
.read_aborted = prv_read_aborted,
|
||||
};
|
||||
return &s_generic_service_info;
|
||||
}
|
||||
265
src/fw/services/normal/accessory/smartstrap_link_control.c
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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 "drivers/accessory.h"
|
||||
#include "drivers/rtc.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_link_control.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <time.h>
|
||||
|
||||
#define LINK_CONTROL_VERSION 1
|
||||
#define TIMEOUT_MS 100
|
||||
#define MAX_DATA_LENGTH 4
|
||||
#define MAX_FRAME_LENGTH (sizeof(FrameHeader) + MAX_DATA_LENGTH)
|
||||
//! The number of consecutive invalid link control responses before we try to reconnect
|
||||
#define MAX_STRIKES 3
|
||||
//! How often we'll go without some valid data from the smartstrap before sending a status message
|
||||
//! and disconnecting if the smartstrap doesn't reply. This is in seconds.
|
||||
#define STATUS_CHECK_INTERVAL 5
|
||||
//! The minimum number of seconds between connnection requests to avoid spamming the smartstrap.
|
||||
#define MIN_CONNECTION_REQUEST_INTERVAL 1
|
||||
//! The minimum number of seconds between status requests to avoid spamming the smartstrap
|
||||
#define MIN_STATUS_REQUEST_INTERVAL 5
|
||||
|
||||
typedef enum {
|
||||
LinkControlTypeInvalid = 0,
|
||||
LinkControlTypeStatus = 1,
|
||||
LinkControlTypeProfiles = 2,
|
||||
LinkControlTypeBaud = 3,
|
||||
NumLinkControlTypes
|
||||
} LinkControlType;
|
||||
|
||||
typedef enum {
|
||||
LinkControlStatusOk = 0,
|
||||
LinkControlStatusBaudRate = 1,
|
||||
LinkControlStatusDisconnect = 2
|
||||
} LinkControlStatus;
|
||||
|
||||
static const AccessoryBaud BAUDS[] = {
|
||||
AccessoryBaud9600,
|
||||
AccessoryBaud14400,
|
||||
AccessoryBaud19200,
|
||||
AccessoryBaud28800,
|
||||
AccessoryBaud38400,
|
||||
AccessoryBaud57600,
|
||||
AccessoryBaud62500,
|
||||
AccessoryBaud115200,
|
||||
AccessoryBaud125000,
|
||||
AccessoryBaud230400,
|
||||
AccessoryBaud250000,
|
||||
AccessoryBaud460800
|
||||
};
|
||||
_Static_assert(ARRAY_LENGTH(BAUDS) - 1 == AccessoryBaud460800, "BAUDS doesn't match AccessoryBaud");
|
||||
|
||||
typedef struct {
|
||||
uint8_t version;
|
||||
LinkControlType type:8;
|
||||
uint8_t data[];
|
||||
} FrameHeader;
|
||||
|
||||
//! store supported profiles as a series of bits
|
||||
static uint32_t s_profiles;
|
||||
_Static_assert(sizeof(s_profiles) * 8 >= NumSmartstrapProfiles, "s_profiles is too small");
|
||||
//! MBuf used for receiving
|
||||
static MBuf *s_read_mbuf;
|
||||
static uint8_t s_read_data[MAX_FRAME_LENGTH];
|
||||
//! The type of the most recent link control message which was sent
|
||||
static LinkControlType s_type;
|
||||
//! Number of consecutive bad status message responses received from the smartstrap
|
||||
static int s_strikes;
|
||||
|
||||
|
||||
static void prv_do_send(LinkControlType type) {
|
||||
FrameHeader header = (FrameHeader) {
|
||||
.version = LINK_CONTROL_VERSION,
|
||||
.type = type
|
||||
};
|
||||
s_type = type;
|
||||
MBuf send_mbuf = MBUF_EMPTY;
|
||||
mbuf_set_data(&send_mbuf, &header, sizeof(header));
|
||||
PBL_ASSERTN(!s_read_mbuf);
|
||||
s_read_mbuf = mbuf_get(s_read_data, MAX_FRAME_LENGTH, MBufPoolSmartstrap);
|
||||
SmartstrapResult result = smartstrap_send(SmartstrapProfileLinkControl, &send_mbuf, s_read_mbuf,
|
||||
TIMEOUT_MS);
|
||||
if (result != SmartstrapResultOk) {
|
||||
mbuf_free(s_read_mbuf);
|
||||
s_read_mbuf = NULL;
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Sending of link control message failed: result=%d, type=%d", result,
|
||||
type);
|
||||
smartstrap_link_control_disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_fatal_error_strike(void) {
|
||||
s_strikes++;
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Fatal error strike %d", s_strikes);
|
||||
if (s_strikes >= MAX_STRIKES) {
|
||||
// out of strikes
|
||||
smartstrap_link_control_disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static bool prv_read_complete(bool success, uint32_t length) {
|
||||
const FrameHeader *header = mbuf_get_data(s_read_mbuf);
|
||||
mbuf_free(s_read_mbuf);
|
||||
s_read_mbuf = NULL;
|
||||
const uint32_t data_length = length - sizeof(FrameHeader);
|
||||
if (!success ||
|
||||
(length < sizeof(FrameHeader)) ||
|
||||
(data_length > MAX_DATA_LENGTH) ||
|
||||
(header->type != s_type) ||
|
||||
(header->version != LINK_CONTROL_VERSION)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid link control response (type=%d).", s_type);
|
||||
if (s_type == LinkControlTypeStatus) {
|
||||
prv_fatal_error_strike();
|
||||
} else if (!s_profiles) {
|
||||
smartstrap_link_control_disconnect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
s_strikes = 0;
|
||||
|
||||
if (header->type == LinkControlTypeStatus) {
|
||||
// status message
|
||||
const LinkControlStatus status = header->data[0];
|
||||
if (status == LinkControlStatusOk) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got link control status: Ok");
|
||||
smartstrap_connection_state_set(true);
|
||||
} else if (status == LinkControlStatusBaudRate) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got link control status: Baud rate");
|
||||
prv_do_send(LinkControlTypeBaud);
|
||||
} else if (status == LinkControlStatusDisconnect) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got link control status: Disconnect");
|
||||
smartstrap_link_control_disconnect();
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Got link control status: INVALID (%d)", status);
|
||||
smartstrap_link_control_disconnect();
|
||||
success = false;
|
||||
}
|
||||
} else if (header->type == LinkControlTypeProfiles) {
|
||||
// profiles message
|
||||
if ((data_length % sizeof(uint16_t)) == 0) {
|
||||
s_profiles = 0;
|
||||
uint16_t *profiles = (uint16_t *)header->data;
|
||||
const uint32_t num_profiles = data_length / sizeof(uint16_t);
|
||||
for (uint32_t i = 0; i < num_profiles; i++) {
|
||||
if ((profiles[i] > SmartstrapProfileInvalid) &&
|
||||
(profiles[i] < NumSmartstrapProfiles) &&
|
||||
(profiles[i] != SmartstrapProfileLinkControl)) {
|
||||
// handle the valid profile
|
||||
s_profiles |= (1 << profiles[i]);
|
||||
}
|
||||
}
|
||||
if (s_profiles == 0) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "No profiles specified");
|
||||
smartstrap_link_control_disconnect();
|
||||
success = false;
|
||||
} else {
|
||||
prv_do_send(LinkControlTypeStatus);
|
||||
}
|
||||
} else {
|
||||
// length is invalid (should be an even multiple of the size of the profile value)
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Got invalid profiles length (%"PRIu32")", data_length);
|
||||
smartstrap_link_control_disconnect();
|
||||
success = false;
|
||||
}
|
||||
} else if (header->type == LinkControlTypeBaud) {
|
||||
// new baud rate
|
||||
const uint8_t requested_baud = header->data[0];
|
||||
if (requested_baud >= ARRAY_LENGTH(BAUDS)) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid baud rate (%"PRIu8")", requested_baud);
|
||||
smartstrap_link_control_disconnect();
|
||||
success = false;
|
||||
} else {
|
||||
accessory_set_baudrate(BAUDS[requested_baud]);
|
||||
prv_do_send(LinkControlTypeStatus);
|
||||
}
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid response type (%d)", header->type);
|
||||
smartstrap_link_control_disconnect();
|
||||
success = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void smartstrap_link_control_connect(void) {
|
||||
prv_do_send(LinkControlTypeProfiles);
|
||||
accessory_set_baudrate(AccessoryBaud9600);
|
||||
}
|
||||
|
||||
void smartstrap_link_control_disconnect(void) {
|
||||
s_strikes = 0;
|
||||
s_profiles = 0;
|
||||
accessory_set_baudrate(AccessoryBaud9600);
|
||||
smartstrap_connection_state_set(false);
|
||||
}
|
||||
|
||||
bool smartstrap_link_control_is_profile_supported(SmartstrapProfile profile) {
|
||||
PBL_ASSERTN((profile > SmartstrapProfileInvalid) && (profile < NumSmartstrapProfiles));
|
||||
return !!(s_profiles & (1 << profile));
|
||||
}
|
||||
|
||||
static bool prv_send_control(void) {
|
||||
static time_t s_last_connection_request_time = 0;
|
||||
static time_t s_last_status_check_time = 0;
|
||||
const time_t current_time = rtc_get_time();
|
||||
if (!smartstrap_is_connected() && accessory_is_present() &&
|
||||
(smartstrap_fsm_state_get() == SmartstrapStateReadReady)) {
|
||||
if (current_time > s_last_connection_request_time + MIN_CONNECTION_REQUEST_INTERVAL) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Smartstrap detected - attempting to connect.");
|
||||
s_last_connection_request_time = current_time;
|
||||
smartstrap_link_control_connect();
|
||||
}
|
||||
return true;
|
||||
} else if (smartstrap_connection_get_time_since_valid_data() > STATUS_CHECK_INTERVAL) {
|
||||
if (current_time > s_last_status_check_time + MIN_STATUS_REQUEST_INTERVAL) {
|
||||
// send a status message
|
||||
prv_do_send(LinkControlTypeStatus);
|
||||
if (accessory_bus_contention_detected()) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Bus contention while sending status message");
|
||||
// Count bus contention as a strike as it could be that the accessory is disconnected
|
||||
// or misbehaving.
|
||||
prv_fatal_error_strike();
|
||||
}
|
||||
s_last_status_check_time = current_time;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void prv_read_aborted(void) {
|
||||
mbuf_free(s_read_mbuf);
|
||||
s_read_mbuf = NULL;
|
||||
}
|
||||
|
||||
const SmartstrapProfileInfo *smartstrap_link_control_get_info(void) {
|
||||
static const SmartstrapProfileInfo s_link_control_info = {
|
||||
.profile = SmartstrapProfileLinkControl,
|
||||
.read_complete = prv_read_complete,
|
||||
.control = prv_send_control,
|
||||
.read_aborted = prv_read_aborted,
|
||||
};
|
||||
return &s_link_control_info;
|
||||
}
|
||||
32
src/fw/services/normal/accessory/smartstrap_link_control.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/app_smartstrap.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
//! Sends a connection request to the smartstrap (should only be called from smartstrap_connection)
|
||||
void smartstrap_link_control_connect(void);
|
||||
|
||||
//! Disconnects from the smartstrap
|
||||
void smartstrap_link_control_disconnect(void);
|
||||
|
||||
//! Checks whether the specified profile is supported by the smartstrap which is connected.
|
||||
bool smartstrap_link_control_is_profile_supported(SmartstrapProfile profile);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Registery for Smartstrap Profiles
|
||||
|
||||
// Syntax: REGISTER_SMARTSTRAP_PROFILE(func)
|
||||
// where func is a function of the following type:
|
||||
// const SmartstrapProfileInfo *func(void);
|
||||
//
|
||||
// An extern declaration will be automatically generated in smartstrap_profiles.h and the info
|
||||
// struct will be automatically inserted into the array in smartstrap_profiles.c.
|
||||
//
|
||||
// Note that the order is significant as it determines the order in which the profiles are given a
|
||||
// chance to send control messages.
|
||||
|
||||
REGISTER_SMARTSTRAP_PROFILE(smartstrap_link_control_get_info)
|
||||
REGISTER_SMARTSTRAP_PROFILE(smartstrap_raw_data_get_info)
|
||||
REGISTER_SMARTSTRAP_PROFILE(smartstrap_generic_service_get_info)
|
||||
|
||||
// vim:filetype=c
|
||||
184
src/fw/services/normal/accessory/smartstrap_profiles.c
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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 "process_management/process_manager.h"
|
||||
#include "services/normal/accessory/smartstrap_connection.h"
|
||||
#include "services/normal/accessory/smartstrap_link_control.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#define NUM_PROFILES() ARRAY_LENGTH(s_profile_info_functions)
|
||||
//! Iterates through the list of profiles and provides the info for each by calling the function
|
||||
#define FOREACH_PROFILE_INFO(info) \
|
||||
for (unsigned int i = 0; i < NUM_PROFILES(); i++) \
|
||||
if ((info = s_profile_info_functions[i]()) != NULL)
|
||||
|
||||
static const SmartstrapProfileGetInfoFunc s_profile_info_functions[] = {
|
||||
#define REGISTER_SMARTSTRAP_PROFILE(f) f,
|
||||
#include "services/normal/accessory/smartstrap_profile_registry.def"
|
||||
#undef REGISTER_SMARTSTRAP_PROFILE
|
||||
};
|
||||
// every profile except for SmartstrapProfileInvalid should be registered
|
||||
_Static_assert(NUM_PROFILES() == (NumSmartstrapProfiles - 1),
|
||||
"The number of registered profiles doesn't match the SmartstrapProfiles enum");
|
||||
|
||||
|
||||
static const SmartstrapProfileInfo *prv_get_info_by_profile(SmartstrapProfile profile) {
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
if (info->profile == profile) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void smartstrap_profiles_init(void) {
|
||||
// call the init handler for all the profiles
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
if (info->init) {
|
||||
info->init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void smartstrap_profiles_handle_connection_event(bool connected) {
|
||||
// send the event to the applicable profiles
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Dispatching smartstrap connection event (connected=%d)", connected);
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
if (info->connected) {
|
||||
info->connected(connected);
|
||||
}
|
||||
}
|
||||
if (connected) {
|
||||
smartstrap_connection_got_valid_data();
|
||||
}
|
||||
}
|
||||
|
||||
static const SmartstrapProfileInfo *prv_get_info_by_service_id(uint16_t service_id) {
|
||||
// find the profile with the lowest minimum service_id which is <= the specified one
|
||||
const SmartstrapProfileInfo *match_info = NULL;
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
if ((info->max_services > 0) && (info->min_service_id <= service_id) &&
|
||||
(!match_info || (info->min_service_id > match_info->min_service_id))) {
|
||||
// this is either the first match or a better match
|
||||
match_info = info;
|
||||
}
|
||||
}
|
||||
return match_info;
|
||||
}
|
||||
|
||||
SmartstrapResult smartstrap_profiles_handle_request(const SmartstrapRequest *request) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
// make sure this request is able to be fulfilled
|
||||
smartstrap_state_lock();
|
||||
const SmartstrapProfileInfo *info = prv_get_info_by_service_id(request->service_id);
|
||||
PBL_ASSERTN(info && info->send);
|
||||
if (!smartstrap_connection_has_subscriber() || !smartstrap_is_connected() ||
|
||||
!smartstrap_link_control_is_profile_supported(info->profile)) {
|
||||
smartstrap_state_unlock();
|
||||
return SmartstrapResultServiceUnavailable;
|
||||
}
|
||||
|
||||
SmartstrapResult result = info->send(request);
|
||||
smartstrap_state_unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void smartstrap_profiles_handle_read(bool success, SmartstrapProfile profile, uint32_t length) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
if (!success) {
|
||||
// this is a timeout
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Timed-out waiting for a response from the smartstrap");
|
||||
}
|
||||
|
||||
// dispatch the read based on the profile
|
||||
const SmartstrapProfileInfo *info = prv_get_info_by_profile(profile);
|
||||
PBL_ASSERTN(info && info->read_complete);
|
||||
smartstrap_state_lock();
|
||||
if (info->read_complete(success, length)) {
|
||||
smartstrap_connection_got_valid_data();
|
||||
}
|
||||
smartstrap_state_unlock();
|
||||
// If we are connected, kick the connection monitor right away. Otherwise, just let it wake up
|
||||
// itself based on its own timer. This prevents us spamming the smartstrap with connection
|
||||
// requests.
|
||||
if (smartstrap_is_connected()) {
|
||||
// send the next message
|
||||
smartstrap_connection_kick_monitor();
|
||||
}
|
||||
}
|
||||
|
||||
void smartstrap_profiles_handle_read_aborted(SmartstrapProfile profile) {
|
||||
PBL_ASSERTN(portIN_CRITICAL());
|
||||
// dispatch the aborted read based on the profile
|
||||
const SmartstrapProfileInfo *info = prv_get_info_by_profile(profile);
|
||||
if (info && info->read_aborted) {
|
||||
info->read_aborted();
|
||||
}
|
||||
}
|
||||
|
||||
void smartstrap_profiles_handle_notification(bool success, SmartstrapProfile profile) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
if (!success) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Dropped notification due to a timeout on the context frame.");
|
||||
return;
|
||||
} else if (!smartstrap_is_connected()) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Dropped notification due to not being connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// dispatch the notification based on the profile
|
||||
const SmartstrapProfileInfo *info = prv_get_info_by_profile(profile);
|
||||
if (info && info->notify) {
|
||||
smartstrap_state_lock();
|
||||
info->notify();
|
||||
smartstrap_state_unlock();
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Dropped notification for unsupported profile: %d", profile);
|
||||
}
|
||||
}
|
||||
|
||||
bool smartstrap_profiles_send_control(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
bool did_send = false;
|
||||
smartstrap_state_lock();
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
if (info->control && info->control()) {
|
||||
did_send = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
smartstrap_state_unlock();
|
||||
return did_send;
|
||||
}
|
||||
|
||||
|
||||
unsigned int smartstrap_profiles_get_max_services(void) {
|
||||
unsigned int max = 0;
|
||||
const SmartstrapProfileInfo *info;
|
||||
FOREACH_PROFILE_INFO(info) {
|
||||
max += info->max_services;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
101
src/fw/services/normal/accessory/smartstrap_profiles.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "util/mbuf.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
//! The currently-supported Smartstrap profiles
|
||||
typedef enum {
|
||||
SmartstrapProfileInvalid = 0,
|
||||
SmartstrapProfileLinkControl = 1,
|
||||
SmartstrapProfileRawData = 2,
|
||||
SmartstrapProfileGenericService = 3,
|
||||
NumSmartstrapProfiles,
|
||||
} SmartstrapProfile;
|
||||
|
||||
typedef struct {
|
||||
uint16_t service_id;
|
||||
uint16_t attribute_id;
|
||||
MBuf *write_mbuf;
|
||||
MBuf *read_mbuf;
|
||||
uint16_t timeout_ms;
|
||||
} SmartstrapRequest;
|
||||
|
||||
typedef void (*SmartstrapProfileInitHandler)(void);
|
||||
typedef void (*SmartstrapProfileConnectedHandler)(bool connected);
|
||||
typedef SmartstrapResult (*SmartstrapProfileSendHandler)(const SmartstrapRequest *request);
|
||||
typedef bool (*SmartstrapProfileReadCompleteHandler)(bool success, uint32_t length);
|
||||
typedef void (*SmartstrapProfileReadAbortedHandler)(void);
|
||||
typedef void (*SmartstrapProfileNotifyHandler)(void);
|
||||
typedef bool (*SmartstrapProfileSendControlHandler)(void);
|
||||
|
||||
typedef struct {
|
||||
//! The profile this info applies to
|
||||
SmartstrapProfile profile;
|
||||
//! The maximum number of services which a smartstrap may support for this profile
|
||||
uint8_t max_services;
|
||||
//! The loweest service id which this profile supports
|
||||
uint16_t min_service_id;
|
||||
//! Optional handler for initialization
|
||||
SmartstrapProfileInitHandler init;
|
||||
//! Optional handler for connection changes
|
||||
SmartstrapProfileConnectedHandler connected;
|
||||
//! Required handler for sending requests
|
||||
SmartstrapProfileSendHandler send;
|
||||
//! Required handler for completed read requests
|
||||
SmartstrapProfileReadCompleteHandler read_complete;
|
||||
//! Optional handler for aborted requests (NOTE: called from a critical region)
|
||||
SmartstrapProfileReadAbortedHandler read_aborted;
|
||||
//! Optional handler for notifications
|
||||
SmartstrapProfileNotifyHandler notify;
|
||||
//! Optional handler to send any pending control messages
|
||||
SmartstrapProfileSendControlHandler control;
|
||||
} SmartstrapProfileInfo;
|
||||
|
||||
typedef const SmartstrapProfileInfo *(*SmartstrapProfileGetInfoFunc)(void);
|
||||
|
||||
// generate funciton prototypes for profile info functions
|
||||
#define REGISTER_SMARTSTRAP_PROFILE(f) const SmartstrapProfileInfo *f(void);
|
||||
#include "services/normal/accessory/smartstrap_profile_registry.def"
|
||||
#undef REGISTER_SMARTSTRAP_PROFILE
|
||||
|
||||
void smartstrap_profiles_init(void);
|
||||
|
||||
//! Make a smartstrap request
|
||||
SmartstrapResult smartstrap_profiles_handle_request(const SmartstrapRequest *request);
|
||||
|
||||
//! Handle a smartstrap read (either complete frame or timeout)
|
||||
void smartstrap_profiles_handle_read(bool success, SmartstrapProfile profile, uint32_t length);
|
||||
|
||||
//! Handle an aborted (canceled) read request
|
||||
void smartstrap_profiles_handle_read_aborted(SmartstrapProfile profile);
|
||||
|
||||
//! Handle a smartstrap notification (either valid context frame or timeout)
|
||||
void smartstrap_profiles_handle_notification(bool success, SmartstrapProfile profile);
|
||||
|
||||
//! Handle a connection event
|
||||
void smartstrap_profiles_handle_connection_event(bool connected);
|
||||
|
||||
//! Goes through the profiles in order and allows them to send control messages. Returns true once
|
||||
//! one of them sends something (or false if none of them send anything).
|
||||
bool smartstrap_profiles_send_control(void);
|
||||
|
||||
//! Returns the maximum number of services supported across all the profiles.
|
||||
unsigned int smartstrap_profiles_get_max_services(void);
|
||||
74
src/fw/services/normal/accessory/smartstrap_raw_data.c
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 "kernel/events.h"
|
||||
#include "services/normal/accessory/smartstrap_attribute.h"
|
||||
#include "services/normal/accessory/smartstrap_comms.h"
|
||||
#include "services/normal/accessory/smartstrap_link_control.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "system/logging.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "util/mbuf.h"
|
||||
|
||||
#define RAW_DATA_MAX_SERVICES 1
|
||||
|
||||
|
||||
static bool prv_read_complete(bool success, uint32_t length) {
|
||||
// we don't allow reads of more than UINT16_MAX
|
||||
if (length > UINT16_MAX) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Got read of length %"PRIu32" which is longer than UINT16_MAX", length);
|
||||
success = false;
|
||||
}
|
||||
// send the read complete event directly to the app
|
||||
SmartstrapResult result = success ? SmartstrapResultOk : SmartstrapResultTimeOut;
|
||||
smartstrap_attribute_send_event(SmartstrapDataReceivedEvent, SmartstrapProfileRawData, result,
|
||||
SMARTSTRAP_RAW_DATA_SERVICE_ID, SMARTSTRAP_RAW_DATA_ATTRIBUTE_ID,
|
||||
length);
|
||||
return success;
|
||||
}
|
||||
|
||||
static void prv_handle_notification(void) {
|
||||
smartstrap_attribute_send_event(SmartstrapNotifyEvent, SmartstrapProfileRawData,
|
||||
SmartstrapResultOk, SMARTSTRAP_RAW_DATA_SERVICE_ID,
|
||||
SMARTSTRAP_RAW_DATA_ATTRIBUTE_ID, 0);
|
||||
}
|
||||
|
||||
static void prv_set_connected(bool connected) {
|
||||
if (connected && smartstrap_link_control_is_profile_supported(SmartstrapProfileRawData)) {
|
||||
smartstrap_connection_state_set_by_service(SMARTSTRAP_RAW_DATA_SERVICE_ID, true);
|
||||
}
|
||||
}
|
||||
|
||||
static SmartstrapResult prv_send(const SmartstrapRequest *request) {
|
||||
return smartstrap_send(SmartstrapProfileRawData, request->write_mbuf, request->read_mbuf,
|
||||
request->timeout_ms);
|
||||
}
|
||||
|
||||
|
||||
const SmartstrapProfileInfo *smartstrap_raw_data_get_info(void) {
|
||||
static const SmartstrapProfileInfo s_profile_info = {
|
||||
.profile = SmartstrapProfileRawData,
|
||||
.max_services = RAW_DATA_MAX_SERVICES,
|
||||
.min_service_id = SMARTSTRAP_RAW_DATA_SERVICE_ID,
|
||||
.connected = prv_set_connected,
|
||||
.send = prv_send,
|
||||
.read_complete = prv_read_complete,
|
||||
.notify = prv_handle_notification,
|
||||
};
|
||||
return &s_profile_info;
|
||||
}
|
||||
212
src/fw/services/normal/accessory/smartstrap_state.c
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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 "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "mcu/interrupts.h"
|
||||
#include "services/normal/accessory/smartstrap_profiles.h"
|
||||
#include "services/normal/accessory/smartstrap_state.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "os/mutex.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
//! The current FSM state
|
||||
static volatile SmartstrapState s_fsm_state = SmartstrapStateUnsubscribed;
|
||||
//! Whether or not we're connected to a smartstrap
|
||||
static bool s_is_connected = false;
|
||||
//! The smartstrap state lock
|
||||
static PebbleMutex *s_state_lock;
|
||||
//! The maximum number of services we could have connected
|
||||
static uint32_t s_max_services;
|
||||
//! The services we are currently connected to
|
||||
static uint16_t *s_connected_services;
|
||||
//! The number of connected services
|
||||
static uint32_t s_num_connected_services = 0;
|
||||
static PebbleMutex *s_services_lock;
|
||||
|
||||
|
||||
void smartstrap_state_init(void) {
|
||||
s_state_lock = mutex_create();
|
||||
s_services_lock = mutex_create();
|
||||
s_max_services = smartstrap_profiles_get_max_services();
|
||||
s_connected_services = kernel_zalloc_check(s_max_services * sizeof(uint16_t));
|
||||
}
|
||||
|
||||
static void prv_assert_valid_fsm_transition(SmartstrapState prev_state, SmartstrapState new_state) {
|
||||
if (new_state == SmartstrapStateUnsubscribed) {
|
||||
// we can go to SmartstrapStateUnsubscribed from any state
|
||||
PBL_ASSERTN(!mcu_state_is_isr());
|
||||
} else if ((prev_state == SmartstrapStateUnsubscribed) &&
|
||||
(new_state == SmartstrapStateReadReady)) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
} else if ((prev_state == SmartstrapStateReadReady) &&
|
||||
(new_state == SmartstrapStateNotifyInProgress)) {
|
||||
PBL_ASSERTN(mcu_state_is_isr());
|
||||
} else if ((prev_state == SmartstrapStateReadReady) &&
|
||||
(new_state == SmartstrapStateReadDisabled)) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
} else if ((prev_state == SmartstrapStateNotifyInProgress) &&
|
||||
(new_state == SmartstrapStateReadComplete)) {
|
||||
PBL_ASSERTN(mcu_state_is_isr() || (pebble_task_get_current() == PebbleTask_NewTimers));
|
||||
} else if ((prev_state == SmartstrapStateReadDisabled) &&
|
||||
(new_state == SmartstrapStateReadInProgress)) {
|
||||
PBL_ASSERTN(mcu_state_is_isr() || (pebble_task_get_current() == PebbleTask_KernelBackground));
|
||||
} else if ((prev_state == SmartstrapStateReadDisabled) &&
|
||||
(new_state == SmartstrapStateReadReady)) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
} else if ((prev_state == SmartstrapStateReadInProgress) &&
|
||||
(new_state == SmartstrapStateReadComplete)) {
|
||||
PBL_ASSERTN(mcu_state_is_isr() || (pebble_task_get_current() == PebbleTask_NewTimers));
|
||||
} else if ((prev_state == SmartstrapStateReadComplete) &&
|
||||
(new_state == SmartstrapStateReadReady)) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
} else {
|
||||
// all other transitions are invalid
|
||||
WTF;
|
||||
}
|
||||
}
|
||||
|
||||
bool smartstrap_fsm_state_test_and_set(SmartstrapState expected_state, SmartstrapState next_state) {
|
||||
const bool did_set = __atomic_compare_exchange_n(&s_fsm_state, &expected_state, next_state, false,
|
||||
__ATOMIC_RELAXED, __ATOMIC_RELAXED);
|
||||
if (did_set) {
|
||||
prv_assert_valid_fsm_transition(expected_state, next_state);
|
||||
}
|
||||
return did_set;
|
||||
}
|
||||
|
||||
void smartstrap_fsm_state_set(SmartstrapState next_state) {
|
||||
prv_assert_valid_fsm_transition(s_fsm_state, next_state);
|
||||
s_fsm_state = next_state;
|
||||
}
|
||||
|
||||
void smartstrap_fsm_state_reset(void) {
|
||||
// we should only force an update to the FSM state in a critical region
|
||||
PBL_ASSERTN(portIN_CRITICAL());
|
||||
s_fsm_state = SmartstrapStateReadReady;
|
||||
}
|
||||
|
||||
SmartstrapState smartstrap_fsm_state_get(void) {
|
||||
return s_fsm_state;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_services_lock
|
||||
static int prv_find_connected_service(uint16_t service_id) {
|
||||
mutex_assert_held_by_curr_task(s_services_lock, true);
|
||||
for (uint32_t i = 0; i < s_num_connected_services; i++) {
|
||||
if (s_connected_services[i] == service_id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_services_lock
|
||||
static bool prv_remove_connected_service(uint16_t service_id) {
|
||||
mutex_assert_held_by_curr_task(s_services_lock, true);
|
||||
int index = prv_find_connected_service(service_id);
|
||||
if (index == -1) {
|
||||
return false;
|
||||
}
|
||||
PBL_ASSERTN(s_num_connected_services > 0);
|
||||
// move the last entry into this slot to remove this entry from the array
|
||||
s_num_connected_services--;
|
||||
s_connected_services[index] = s_connected_services[s_num_connected_services];
|
||||
return true;
|
||||
}
|
||||
|
||||
//! NOTE: the caller must hold s_services_lock
|
||||
static void prv_set_service_connected(uint16_t service_id, bool connected) {
|
||||
mutex_assert_held_by_curr_task(s_services_lock, true);
|
||||
if (connected) {
|
||||
if (prv_find_connected_service(service_id) != -1) {
|
||||
// already connected
|
||||
return;
|
||||
}
|
||||
// insert the service_id
|
||||
PBL_ASSERTN(s_num_connected_services < s_max_services);
|
||||
s_connected_services[s_num_connected_services++] = service_id;
|
||||
} else if (!prv_remove_connected_service(service_id)) {
|
||||
// we weren't previously connected
|
||||
return;
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Connection state for service (0x%x) changed to %d", service_id,
|
||||
connected);
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_SMARTSTRAP_EVENT,
|
||||
.smartstrap = {
|
||||
.type = SmartstrapConnectionEvent,
|
||||
.result = connected ? SmartstrapResultOk : SmartstrapResultServiceUnavailable,
|
||||
.service_id = service_id
|
||||
},
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
void smartstrap_connection_state_set_by_service(uint16_t service_id, bool connected) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
mutex_lock(s_services_lock);
|
||||
prv_set_service_connected(service_id, connected);
|
||||
mutex_unlock(s_services_lock);
|
||||
}
|
||||
|
||||
void smartstrap_connection_state_set(bool connected) {
|
||||
if (connected == s_is_connected) {
|
||||
return;
|
||||
}
|
||||
// if we're disconnecting, disconnect the services first
|
||||
if (s_is_connected) {
|
||||
mutex_lock(s_services_lock);
|
||||
while (s_num_connected_services) {
|
||||
const uint16_t service_id = s_connected_services[s_num_connected_services - 1];
|
||||
prv_set_service_connected(service_id, false);
|
||||
}
|
||||
s_num_connected_services = 0;
|
||||
mutex_unlock(s_services_lock);
|
||||
}
|
||||
s_is_connected = connected;
|
||||
smartstrap_profiles_handle_connection_event(connected);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_smartstrap_is_service_connected, uint16_t service_id) {
|
||||
if (!smartstrap_is_connected()) {
|
||||
return false;
|
||||
}
|
||||
mutex_lock(s_services_lock);
|
||||
bool result = prv_find_connected_service(service_id) != -1;
|
||||
mutex_unlock(s_services_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool smartstrap_is_connected(void) {
|
||||
return (s_fsm_state != SmartstrapStateUnsubscribed) && s_is_connected;
|
||||
}
|
||||
|
||||
void smartstrap_state_lock(void) {
|
||||
mutex_lock(s_state_lock);
|
||||
}
|
||||
|
||||
void smartstrap_state_unlock(void) {
|
||||
mutex_unlock(s_state_lock);
|
||||
}
|
||||
|
||||
void smartstrap_state_assert_locked_by_current_task(void) {
|
||||
mutex_assert_held_by_curr_task(s_state_lock, true);
|
||||
}
|
||||
98
src/fw/services/normal/accessory/smartstrap_state.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* FSM state transitions:
|
||||
* +------------------+------------------+--------------+---------------------------+
|
||||
* | From State | To State | Task | Event |
|
||||
* +------------------+------------------+--------------+---------------------------+
|
||||
* | Unsubscribed | ReadReady | KernelBG | First subscriber |
|
||||
* | ReadReady | NotifyInProgress | ISR | Break character received |
|
||||
* | ReadReady | ReadDisabled | KernelBG | Send started |
|
||||
* | NotifyInProgress | ReadComplete | ISR | Complete frame received |
|
||||
* | NotifyInProgress | ReadComplete | NewTimer | Read timeout |
|
||||
* | ReadDisabled | ReadInProgress | KernelBG,ISR | Send completed (is_read) |
|
||||
* | ReadDisabled | ReadReady | KernelBG | Send completed (!is_read) |
|
||||
* | ReadDisabled | ReadReady | KernelBG | Send failed |
|
||||
* | ReadInProgress | ReadComplete | ISR | Complete frame received |
|
||||
* | ReadInProgress | ReadComplete | NewTimer | Read timeout |
|
||||
* | ReadComplete | ReadReady | KernelBG | Frame processed |
|
||||
* | *ANY STATE* | Unsubscribed | KernelBG | No more subscribers |
|
||||
* +------------------+------------------+--------------+---------------------------+
|
||||
* Notes:
|
||||
* - Only KernelBG can send frames when s_is_connected == false
|
||||
* - Transitions which can take place from "Any" task are not allowed from ISRs
|
||||
* - Received data is ignored in any state except ReadInProgress or NotifyInProgress
|
||||
* - Break characters are ignored in any state except ReadReady
|
||||
* - We can only start sending data after a successful transition from ReadReady to ReadDisabled
|
||||
*/
|
||||
typedef enum {
|
||||
SmartstrapStateUnsubscribed,
|
||||
SmartstrapStateReadReady,
|
||||
SmartstrapStateNotifyInProgress,
|
||||
SmartstrapStateReadDisabled,
|
||||
SmartstrapStateReadInProgress,
|
||||
SmartstrapStateReadComplete
|
||||
} SmartstrapState;
|
||||
|
||||
//! Initialize the smartstrap state
|
||||
void smartstrap_state_init(void);
|
||||
|
||||
//! Attempt to transition from expected_state to next_state and returns whether or not we
|
||||
//! transitioned successfully. This transition is done atomically.
|
||||
bool smartstrap_fsm_state_test_and_set(SmartstrapState expected_state, SmartstrapState next_state);
|
||||
|
||||
//! Sets the FSM state, regardless of what the current state is.
|
||||
//! @note The caller must ensure that there can be no other task or an ISR trying to access or
|
||||
//! change the state at the same time. If there is a posiblity for contention, the caller should use
|
||||
//! prv_fsm_state_test_and_set instead or enter a critical region.
|
||||
void smartstrap_fsm_state_set(SmartstrapState next_state);
|
||||
|
||||
//! Change the FSM state to ReadReady without doing any assertions. Should only be used with great
|
||||
//! care and from a critical region (such as within smartstrap_send_cancel).
|
||||
void smartstrap_fsm_state_reset(void);
|
||||
|
||||
//! Returns the current FSM state
|
||||
SmartstrapState smartstrap_fsm_state_get(void);
|
||||
|
||||
//! Returns whether or not we're connected to a smartstrap
|
||||
bool smartstrap_is_connected(void);
|
||||
|
||||
//! Acquires the smartstrap lock
|
||||
void smartstrap_state_lock(void);
|
||||
|
||||
//! Releases the smartstrap lock
|
||||
void smartstrap_state_unlock(void);
|
||||
|
||||
//! Asserts that the current task has acquired the state lock
|
||||
void smartstrap_state_assert_locked_by_current_task(void);
|
||||
|
||||
//! Set whether or not the specified service is currently connected
|
||||
void smartstrap_connection_state_set_by_service(uint16_t service_id, bool connected);
|
||||
|
||||
//! Set whether or not we are connected to a smartstrap
|
||||
void smartstrap_connection_state_set(bool connected);
|
||||
|
||||
|
||||
// syscalls
|
||||
|
||||
//! Returns whether or not the specified service is available on a connected smartstrap
|
||||
bool sys_smartstrap_is_service_connected(uint16_t service_id);
|
||||
1516
src/fw/services/normal/activity/activity.c
Normal file
547
src/fw/services/normal/activity/activity.h
Normal file
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "applib/accel_service_private.h"
|
||||
#include "applib/health_service.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
// Max # of days of history we store
|
||||
#define ACTIVITY_HISTORY_DAYS 30
|
||||
|
||||
// The max number of activity sessions we collect and cache at a time. Usually, there will only be
|
||||
// about 4 or 5 sleep sessions (1 container and a handful of restful periods) in a night and
|
||||
// a handful of walk and/or run sessions. Allocating space for 32 to should be more than enough.
|
||||
#define ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT 32
|
||||
|
||||
// Number of calories in a kcalorie
|
||||
#define ACTIVITY_CALORIES_PER_KCAL 1000
|
||||
|
||||
// Values for ActivitySettingGender
|
||||
typedef enum {
|
||||
ActivityGenderFemale = 0,
|
||||
ActivityGenderMale = 1,
|
||||
ActivityGenderOther = 2
|
||||
} ActivityGender;
|
||||
|
||||
// Activity Settings Struct, for storing to prefs
|
||||
typedef struct PACKED ActivitySettings {
|
||||
int16_t height_mm;
|
||||
int16_t weight_dag;
|
||||
bool tracking_enabled;
|
||||
bool activity_insights_enabled;
|
||||
bool sleep_insights_enabled;
|
||||
int8_t age_years;
|
||||
int8_t gender;
|
||||
} ActivitySettings;
|
||||
|
||||
// Heart Rate Preferences Struct, for storing to prefs
|
||||
typedef struct PACKED HeartRatePreferences {
|
||||
uint8_t resting_hr;
|
||||
uint8_t elevated_hr;
|
||||
uint8_t max_hr;
|
||||
uint8_t zone1_threshold;
|
||||
uint8_t zone2_threshold;
|
||||
uint8_t zone3_threshold;
|
||||
} HeartRatePreferences;
|
||||
|
||||
// Activity HRM Settings Struct, for storing to prefs
|
||||
typedef struct PACKED ActivityHRMSettings {
|
||||
bool enabled;
|
||||
} ActivityHRMSettings;
|
||||
|
||||
// Default values, taken from http://www.cdc.gov/nchs/fastats/body-measurements.htm
|
||||
#define ACTIVITY_DEFAULT_HEIGHT_MM 1620 // 5'3.8"
|
||||
// dag - decagram (10 g)
|
||||
#define ACTIVITY_DEFAULT_WEIGHT_DAG 7539 // 166.2 lbs
|
||||
#define ACTIVITY_DEFAULT_GENDER ActivityGenderFemale
|
||||
#define ACTIVITY_DEFAULT_AGE_YEARS 30
|
||||
|
||||
#define ACTIVITY_DEFAULT_PREFERENCES { \
|
||||
.tracking_enabled = false, \
|
||||
.activity_insights_enabled = false, \
|
||||
.sleep_insights_enabled = false, \
|
||||
.age_years = ACTIVITY_DEFAULT_AGE_YEARS, \
|
||||
.gender = ACTIVITY_DEFAULT_GENDER, \
|
||||
.height_mm = ACTIVITY_DEFAULT_HEIGHT_MM, \
|
||||
.weight_dag = ACTIVITY_DEFAULT_WEIGHT_DAG, \
|
||||
}
|
||||
|
||||
#define ACTIVITY_HEART_RATE_DEFAULT_PREFERENCES { \
|
||||
.resting_hr = 70, \
|
||||
.elevated_hr = 100, \
|
||||
.max_hr = 220 - ACTIVITY_DEFAULT_AGE_YEARS, \
|
||||
.zone1_threshold = 130 /* 50% of HRR */, \
|
||||
.zone2_threshold = 154 /* 70% of HRR */, \
|
||||
.zone3_threshold = 172 /* 85% of HRR */, \
|
||||
}
|
||||
|
||||
#define ACTIVITY_HRM_DEFAULT_PREFERENCES { \
|
||||
.enabled = true, \
|
||||
}
|
||||
|
||||
// We consider values outside of this range to be invalid
|
||||
// In the future we could pick these values based on user history
|
||||
#define ACTIVITY_DEFAULT_MIN_HR 40
|
||||
#define ACTIVITY_DEFAULT_MAX_HR 200
|
||||
|
||||
// Activity metric enums, accepted by activity_get_metric()
|
||||
typedef enum {
|
||||
ActivityMetricFirst = 0,
|
||||
ActivityMetricStepCount = ActivityMetricFirst,
|
||||
ActivityMetricActiveSeconds,
|
||||
ActivityMetricRestingKCalories,
|
||||
ActivityMetricActiveKCalories,
|
||||
ActivityMetricDistanceMeters,
|
||||
ActivityMetricSleepTotalSeconds,
|
||||
ActivityMetricSleepRestfulSeconds,
|
||||
ActivityMetricSleepEnterAtSeconds, // What time the user fell asleep. Measured in
|
||||
// seconds after midnight.
|
||||
ActivityMetricSleepExitAtSeconds, // What time the user woke up. Measured in
|
||||
// seconds after midnight
|
||||
ActivityMetricSleepState, // returns an ActivitySleepState enum value
|
||||
ActivityMetricSleepStateSeconds, // how many seconds we've been in the
|
||||
// ActivityMetricSleepState state
|
||||
ActivityMetricLastVMC,
|
||||
|
||||
ActivityMetricHeartRateRawBPM, // Most recent heart rate reading
|
||||
ActivityMetricHeartRateRawQuality, // Heart rate signal quality
|
||||
ActivityMetricHeartRateRawUpdatedTimeUTC, // UTC of last heart rate update
|
||||
ActivityMetricHeartRateFilteredBPM, // Most recent "Stable (median)" HR reading
|
||||
ActivityMetricHeartRateFilteredUpdatedTimeUTC, // UTC of last stable HR reading
|
||||
|
||||
ActivityMetricHeartRateZone1Minutes,
|
||||
ActivityMetricHeartRateZone2Minutes,
|
||||
ActivityMetricHeartRateZone3Minutes,
|
||||
|
||||
// KEEP THIS AT THE END
|
||||
ActivityMetricNumMetrics,
|
||||
ActivityMetricInvalid = ActivityMetricNumMetrics,
|
||||
} ActivityMetric;
|
||||
|
||||
|
||||
// Activity session types, used in ActivitySession struct
|
||||
typedef enum {
|
||||
ActivitySessionType_None = 0,
|
||||
|
||||
// ActivityType_Sleep encapsulates an entire sleep session from sleep entry to wake, and
|
||||
// contains both light and deep sleep periods. An ActivityType_DeepSleep session identifies
|
||||
// a restful period and its start and end times will always be inside of a ActivityType_Sleep
|
||||
// session.
|
||||
ActivitySessionType_Sleep = 1,
|
||||
|
||||
// A restful period, these will always be inside of a ActivityType_Sleep session
|
||||
ActivitySessionType_RestfulSleep = 2,
|
||||
|
||||
// Like ActivityType_Sleep, but labeled as a nap because of its duration and time (as
|
||||
// compared to the assumed nightly sleep).
|
||||
ActivitySessionType_Nap = 3,
|
||||
|
||||
// A restful period that was part of a nap, these will always be inside of a
|
||||
// ActivityType_Nap session
|
||||
ActivitySessionType_RestfulNap = 4,
|
||||
|
||||
// A "significant" length walk
|
||||
ActivitySessionType_Walk = 5,
|
||||
|
||||
// A run
|
||||
ActivitySessionType_Run = 6,
|
||||
|
||||
// Open workout. Basically a catch all / generic activity type
|
||||
ActivitySessionType_Open = 7,
|
||||
|
||||
// Leave at end
|
||||
ActivitySessionTypeCount,
|
||||
ActivitySessionType_Invalid = ActivitySessionTypeCount,
|
||||
} ActivitySessionType;
|
||||
|
||||
// Sleep state, used in AlgorithmStateMinuteData and to express possible values of
|
||||
// ActivityMetricSleepState when calling activity_get_metric().
|
||||
typedef enum {
|
||||
ActivitySleepStateAwake = 0,
|
||||
ActivitySleepStateRestfulSleep,
|
||||
ActivitySleepStateLightSleep,
|
||||
ActivitySleepStateUnknown,
|
||||
} ActivitySleepState;
|
||||
|
||||
|
||||
// Data included for stepping related activities.
|
||||
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
|
||||
// an update to documentation on this wiki page:
|
||||
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
|
||||
typedef struct PACKED {
|
||||
uint16_t steps; // number of steps
|
||||
uint16_t active_kcalories; // number of active kcalories
|
||||
uint16_t resting_kcalories; // number of resting kcalories
|
||||
uint16_t distance_meters; // distance covered
|
||||
} ActivitySessionDataStepping;
|
||||
|
||||
// Data included for sleep related activities
|
||||
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
|
||||
// an update to documentation on this wiki page:
|
||||
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
|
||||
typedef struct {
|
||||
} ActivitySessionDataSleeping;
|
||||
|
||||
#define ACTIVITY_SESSION_MAX_LENGTH_MIN MINUTES_PER_DAY
|
||||
|
||||
typedef struct PACKED {
|
||||
time_t start_utc; // session start time
|
||||
uint16_t length_min; // length of session in minutes
|
||||
ActivitySessionType type:8; // type of activity
|
||||
union {
|
||||
struct {
|
||||
uint8_t ongoing:1; // activity still ongoing
|
||||
uint8_t manual:1; // activity is a manual one
|
||||
uint8_t reserved:6;
|
||||
};
|
||||
uint8_t flags;
|
||||
};
|
||||
union {
|
||||
ActivitySessionDataStepping step_data;
|
||||
ActivitySessionDataSleeping sleep_data;
|
||||
};
|
||||
} ActivitySession;
|
||||
|
||||
// Structure of data logging records generated by raw sample collection
|
||||
// Each of the 32bit samples in the record is encoded as follows:
|
||||
// Each axis is encoded into 10 bits, by shifting the 16-bit raw value right by 3 bits and
|
||||
// masking with 0x3FF. This is done because the max dynamic range of an axis is +/- 4000 and
|
||||
// the least significant 3 bits are more or less noise.
|
||||
// 0bxx 10bits_x 10bits_y 10bits_z The accel sensor generated a run of 0bxx samples with
|
||||
// the given x, y, and z values
|
||||
#define ACTIVITY_RAW_SAMPLES_VERSION 2
|
||||
#define ACTIVITY_RAW_SAMPLES_MAX_ENTRIES 25
|
||||
|
||||
// Utilities for the encoded samples collected by raw sample collection.
|
||||
#define ACTIVITY_RAW_SAMPLE_VALUE_BITS (10)
|
||||
#define ACTIVITY_RAW_SAMPLE_VALUE_MASK (0x03FF) // 10 bits per axis
|
||||
|
||||
// We throw away the least significant 3 bits and keep only 10 bits per axix. The + 4 is used
|
||||
// so that we round to nearest instead of rounding down as a result of the shift right
|
||||
#define ACTIVITY_RAW_SAMPLE_SHIFT 3
|
||||
#define ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(x) ((((x) + 4) >> ACTIVITY_RAW_SAMPLE_SHIFT) \
|
||||
& ACTIVITY_RAW_SAMPLE_VALUE_MASK)
|
||||
|
||||
#define ACTIVITY_RAW_SAMPLE_MAX_RUN_SIZE 3
|
||||
#define ACTIVITY_RAW_SAMPLE_GET_RUN_SIZE(s) ((s) >> (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS))
|
||||
#define ACTIVITY_RAW_SAMPLE_SET_RUN_SIZE(s, r) (s |= (r) << (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS))
|
||||
#define ACTIVITY_RAW_SAMPLE_SIGN_EXTEND(x) ((x) & 0x1000 ? -1 * (0x2000 - (x)) : (x))
|
||||
|
||||
#define ACTIVITY_RAW_SAMPLE_GET_X(s) \
|
||||
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND((((uint32_t)s >> (2 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
|
||||
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
|
||||
#define ACTIVITY_RAW_SAMPLE_GET_Y(s) \
|
||||
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND(((s >> ACTIVITY_RAW_SAMPLE_VALUE_BITS) \
|
||||
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
|
||||
#define ACTIVITY_RAW_SAMPLE_GET_Z(s) \
|
||||
ACTIVITY_RAW_SAMPLE_SIGN_EXTEND((s \
|
||||
& ACTIVITY_RAW_SAMPLE_VALUE_MASK) << ACTIVITY_RAW_SAMPLE_SHIFT)
|
||||
|
||||
#define ACTIVITY_RAW_SAMPLE_ENCODE(run_size, x, y, z) \
|
||||
((run_size) << (3 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
|
||||
| (ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(x) << (2 * ACTIVITY_RAW_SAMPLE_VALUE_BITS)) \
|
||||
| (ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(y) << ACTIVITY_RAW_SAMPLE_VALUE_BITS) \
|
||||
| ACTIVITY_RAW_SAMPLE_VALUE_ENCODE(z)
|
||||
|
||||
#define ACTIVITY_RAW_SAMPLE_FLAG_FIRST_RECORD 0x01 // Set for first record of session
|
||||
#define ACTIVITY_RAW_SAMPLE_FLAG_LAST_RECORD 0x02 // set for last record of session
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
uint16_t version; // Set to ACTIVITY_RAW_SAMPLE_VERSION
|
||||
uint16_t session_id; // raw sample session id
|
||||
uint32_t time_local; // local time
|
||||
uint8_t flags; // one or more of ACTIVITY_RAW_SAMPLE_FLAG_.*
|
||||
uint8_t len; // length of this blob, including this entire header
|
||||
uint8_t num_samples; // number of uncompressed samples that this blob represents
|
||||
uint8_t num_entries; // number of elements in the entries array below
|
||||
uint32_t entries[ACTIVITY_RAW_SAMPLES_MAX_ENTRIES];
|
||||
// array of entries, each entry can represent multiple samples
|
||||
// if we detect run lengths
|
||||
} ActivityRawSamplesRecord;
|
||||
|
||||
|
||||
//! Init the activity tracking service. This does not start it up - to start it up call
|
||||
//! activity_start_tracking();
|
||||
//! @return true if successfully initialized
|
||||
bool activity_init(void);
|
||||
|
||||
//! Start the activity tracking service. This starts sampling of the accelerometer
|
||||
//! @param test_mode if true, samples must be fed in using activity_feed_samples()
|
||||
//! @return true if successfully started
|
||||
bool activity_start_tracking(bool test_mode);
|
||||
|
||||
//! Stop the activity tracking service.
|
||||
//! @return true if successfully stopped
|
||||
bool activity_stop_tracking(void);
|
||||
|
||||
//! Return true if activity tracking is currently running
|
||||
//! @return true if activity tracking is currently running
|
||||
bool activity_tracking_on(void);
|
||||
|
||||
//! Enable/disable the activity service. This callback is ONLY for use by the service manager's
|
||||
//! services_set_runlevel() method. If false gets passed to this method, then tracking is
|
||||
//! turned off regardless of the state as set by activity_start_tracking/activity_stop_tracking.
|
||||
void activity_set_enabled(bool enable);
|
||||
|
||||
// Functions for getting and setting the activity preferences (defined in shell/normal/prefs.c)
|
||||
|
||||
//! Enable/disable activity tracking and store new setting in prefs for the next reboot
|
||||
//! @param enable if true, enable activity tracking
|
||||
void activity_prefs_tracking_set_enabled(bool enable);
|
||||
|
||||
//! Returns true if activity tracking is enabled
|
||||
bool activity_prefs_tracking_is_enabled(void);
|
||||
|
||||
//! Records the current time when called. Used to determine when activity was first used
|
||||
// so that we can send insights X days after activation
|
||||
void activity_prefs_set_activated(void);
|
||||
|
||||
//! @return The utc timestamp of the first call to activity_prefs_set_activated()
|
||||
//! returns 0 if activity_prefs_set_activated() has never been called
|
||||
time_t activity_prefs_get_activation_time(void);
|
||||
|
||||
typedef enum ActivationDelayInsightType ActivationDelayInsightType;
|
||||
|
||||
//! @return True if the activation delay insight has fired
|
||||
bool activity_prefs_has_activation_delay_insight_fired(ActivationDelayInsightType type);
|
||||
|
||||
//! @return Mark an activation delay insight as having fired
|
||||
void activity_prefs_set_activation_delay_insight_fired(ActivationDelayInsightType type);
|
||||
|
||||
//! @return Which version of the health app was last opened
|
||||
//! @note 0 is "never opened"
|
||||
uint8_t activity_prefs_get_health_app_opened_version(void);
|
||||
|
||||
//! @return Record that the health app has been opened at a given version
|
||||
void activity_prefs_set_health_app_opened_version(uint8_t version);
|
||||
|
||||
//! @return Which version of the workout app was last opened
|
||||
//! @note 0 is "never opened"
|
||||
uint8_t activity_prefs_get_workout_app_opened_version(void);
|
||||
|
||||
//! @return Record that the workout app has been opened at a given version
|
||||
void activity_prefs_set_workout_app_opened_version(uint8_t version);
|
||||
|
||||
//! Enable/disable activity insights
|
||||
//! @param enable if true, enable activity insights
|
||||
void activity_prefs_activity_insights_set_enabled(bool enable);
|
||||
|
||||
//! Returns true if activity insights are enabled
|
||||
bool activity_prefs_activity_insights_are_enabled(void);
|
||||
|
||||
//! Enable/disable sleep insights
|
||||
//! @param enable if true, enable sleep insights
|
||||
void activity_prefs_sleep_insights_set_enabled(bool enable);
|
||||
|
||||
//! Returns true if sleep insights are enabled
|
||||
bool activity_prefs_sleep_insights_are_enabled(void);
|
||||
|
||||
//! Set the user height
|
||||
//! @param height_mm the height in mm
|
||||
void activity_prefs_set_height_mm(uint16_t height_mm);
|
||||
|
||||
//! Get the user height
|
||||
//! @return the user's height in mm
|
||||
uint16_t activity_prefs_get_height_mm(void);
|
||||
|
||||
//! Set the user weight
|
||||
//! @param weight_dag the weight in dag (decagrams)
|
||||
void activity_prefs_set_weight_dag(uint16_t weight_dag);
|
||||
|
||||
//! Get the user weight
|
||||
//! @return the user's weight in dag
|
||||
uint16_t activity_prefs_get_weight_dag(void);
|
||||
|
||||
//! Set the user's gender
|
||||
//! @param gender the new gender
|
||||
void activity_prefs_set_gender(ActivityGender gender);
|
||||
|
||||
//! Get the user's gender
|
||||
//! @return the user's set gender
|
||||
ActivityGender activity_prefs_get_gender(void);
|
||||
|
||||
//! Set the user's age
|
||||
//! @param age_years the user's age in years
|
||||
void activity_prefs_set_age_years(uint8_t age_years);
|
||||
|
||||
//! Get the user's age in years
|
||||
//! @return the user's age in years
|
||||
uint8_t activity_prefs_get_age_years(void);
|
||||
|
||||
//! Get the user's resting heart rate
|
||||
uint8_t activity_prefs_heart_get_resting_hr(void);
|
||||
|
||||
//! Get the user's elevated heart rate
|
||||
uint8_t activity_prefs_heart_get_elevated_hr(void);
|
||||
|
||||
//! Get the user's max heart rate
|
||||
uint8_t activity_prefs_heart_get_max_hr(void);
|
||||
|
||||
//! Get the user's hr zone1 threshold (lowest HR in zone 1)
|
||||
uint8_t activity_prefs_heart_get_zone1_threshold(void);
|
||||
|
||||
//! Get the user's hr zone2 threshold (lowest HR in zone 2)
|
||||
uint8_t activity_prefs_heart_get_zone2_threshold(void);
|
||||
|
||||
//! Get the user's hr zone3 threshold (lowest HR in zone 3)
|
||||
uint8_t activity_prefs_heart_get_zone3_threshold(void);
|
||||
|
||||
//! Return true if the HRM is enabled, false if not
|
||||
bool activity_prefs_heart_rate_is_enabled(void);
|
||||
|
||||
//! Get the current and (optionally) historical values for a given metric. The caller passes
|
||||
//! in a pointer to an array that will be filled in with the results (current value for today at
|
||||
//! index 0, yesterday's at index 1, etc.)
|
||||
//! @param[in] metric which metric to fetch
|
||||
//! @param[in] history_len This must contain the length of the history array being passed in (as
|
||||
//! number of entries). To determine a max size for this array, call
|
||||
//! health_service_max_days_history().
|
||||
//! @param[out] history pointer to int32_t array that will contain the returned metric. The current
|
||||
//! value will be at index 0, yesterday's at index 1, etc. For days where no history is
|
||||
//! available, -1 will be written. For some metrics, like HealthMetricActiveDayID and
|
||||
//! HealthMetricSleepDayID, history is not applicable, so all entries past entry 0 will
|
||||
//! always be filled in with -1.
|
||||
//! @return true on success, false on failure
|
||||
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history);
|
||||
|
||||
//! Get the typical value for a metric on a given day of the week
|
||||
bool activity_get_metric_typical(ActivityMetric metric, DayInWeek day, int32_t *value_out);
|
||||
|
||||
//! Get the value for a metric over the last 4 weeks
|
||||
bool activity_get_metric_monthly_avg(ActivityMetric metric, int32_t *value_out);
|
||||
|
||||
|
||||
//! Get detailed info about activity sessions. This fills in an array with info on all of the
|
||||
//! activity sessions that ended after 12am (midnight) of the current day. The caller must allocate
|
||||
//! space for the array and tell this method how many entries the array can hold
|
||||
//! ("session_entries"). This call returns the actual number of entries required, which may be
|
||||
//! greater or less than the passed in size. If it is greater, only the first session_entries are
|
||||
//! filled in.
|
||||
//! @param[in,out] *session_entries size of sessions array (as number of elements) on entry.
|
||||
//! On exit, this is set to the number of entries required to hold all sessions.
|
||||
//! @param[out] sessions this array is filled in with the list of sessions.
|
||||
//! @return true on success, false on failure
|
||||
bool activity_get_sessions(uint32_t *session_entries, ActivitySession *sessions);
|
||||
|
||||
//! Return historical minute data.
|
||||
//! IMPORTANT: This call will block on KernelBG, so it can only be called from the app or
|
||||
//! worker task.
|
||||
//! @param[in] minute_data pointer to an array of HealthMinuteData records that will be filled
|
||||
//! in with the historical minute data
|
||||
//! @param[in,out] num_records On entry, the number of records the minute_data array can hold.
|
||||
//! On exit, the number of records in the minute data array that were written, including
|
||||
//! any missing minutes between 0 and *num_records. To check to see if a specific minute is
|
||||
//! missing, compare the vmc value of that record to HEALTH_MINUTE_DATA_MISSING_VMC_VALUE.
|
||||
//! @param[in,out] utc_start on entry, the UTC time of the first requested record. On exit,
|
||||
//! the UTC time of the first record returned.
|
||||
//! @return true on success, false on failure
|
||||
bool activity_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
|
||||
time_t *utc_start);
|
||||
|
||||
// Metric averages, returned by activity_get_step_averages()
|
||||
#define ACTIVITY_NUM_METRIC_AVERAGES (4 * 24) //!< one average for each 15 minute interval of a day
|
||||
#define ACTIVITY_METRIC_AVERAGES_UNKNOWN 0xFFFF //!< indicates the average is unknown
|
||||
typedef struct {
|
||||
uint16_t average[ACTIVITY_NUM_METRIC_AVERAGES];
|
||||
} ActivityMetricAverages;
|
||||
|
||||
//! Return step averages.
|
||||
//! @param[in] day_of_week day of the week to get averages for. Sunday: 0, Monday: 1, etc.
|
||||
//! @param[out] averages pointer to ActivityStepAverages structure that will be filled
|
||||
//! in with the step averages.
|
||||
//! @return true on success, false on failure
|
||||
bool activity_get_step_averages(DayInWeek day_of_week, ActivityMetricAverages *averages);
|
||||
|
||||
//! Control raw accel sample collection. This method can be used to start and stop raw
|
||||
//! accel sample collection. The samples are sent to data logging with tag
|
||||
//! ACTIVITY_DLS_TAG_RAW_SAMPLES and also PBL_LOG messages are generated by base64 encoding the
|
||||
//! data (so that it can be sent in a support request). Every time raw sample collection is
|
||||
//! enabled, a new raw sample session id is created. This session id is saved along with the
|
||||
//! samples and can be displayed to the user in the watch UI to help later identify specific
|
||||
//! sessions.
|
||||
//! @param[in] enable if true, enable sample collection
|
||||
//! @param[in] disable if true, disable sample collection
|
||||
//! @param[out] *enabled true if sample collection is currently enabled
|
||||
//! @param[out] *session_id the current raw sample session id. If sampling is currently disabled,
|
||||
//! this is the session id of the most recently ended session.
|
||||
//! @param[out] *num_samples the number of samples collected for the current session. If sampling is
|
||||
//! currently disabled, this is the number of samples collected in the most recently
|
||||
//! ended session.
|
||||
//! @param[out] *seconds the number of seconds of data collected for the current session. If
|
||||
//! sampling is currently disabled, this is the number of seconds of data in the most recently
|
||||
//! ended session.
|
||||
//! @return true on success, false on error
|
||||
bool activity_raw_sample_collection(bool enable, bool disable, bool *enabled,
|
||||
uint32_t *session_id, uint32_t *num_samples, uint32_t *seconds);
|
||||
|
||||
//! Dump the current sleep data using PBL_LOG. We write out base64 encoded data using PBL_LOG
|
||||
//! so that it can be extracted using a support request.
|
||||
//! @return true on success, false on error
|
||||
//! IMPORTANT: This call will block on KernelBG, so it can only be called from the app or
|
||||
//! worker task.
|
||||
bool activity_dump_sleep_log(void);
|
||||
|
||||
//! Used by test apps (running on firmware): feed in samples, bypassing the accelerometer.
|
||||
//! In order to use this, you must have called activity_start_tracking(test_mode = true);
|
||||
//! @param[in] data array of samples to feed in
|
||||
//! @param[in] num_samples number of samples in the data array
|
||||
//! @return true on success, false on error
|
||||
bool activity_test_feed_samples(AccelRawData *data, uint32_t num_samples);
|
||||
|
||||
//! Used by test apps (running on firmware): call the periodic minute callback. This can be used to
|
||||
//! accelerate tests, to run in non-real time.
|
||||
//! @return true on success, false on error
|
||||
bool activity_test_run_minute_callback(void);
|
||||
|
||||
//! Used by test apps (running on firmware): Get info on the minute data file
|
||||
//! @param[in] compact_first if true, compact the file first before getting info
|
||||
//! @param[out] *num_records how many records it contains
|
||||
//! @param[out] *data_bytes how many bytes of data it contains
|
||||
//! @param[out] *minutes how many minutes of data it contains
|
||||
//! @return true on success, false on error
|
||||
bool activity_test_minute_file_info(bool compact_first, uint32_t *num_records, uint32_t *data_bytes,
|
||||
uint32_t *minutes);
|
||||
|
||||
//! Used by test apps (running on firmware): Fill up the minute data file with as much data as
|
||||
//! possible. Used for testing performance of compaction and checking for watchdog timeouts when
|
||||
//! the file gets very large.
|
||||
//! @return true on success, false on error
|
||||
bool activity_test_fill_minute_file(void);
|
||||
|
||||
//! Used by test apps (running on firmware): Send fake records to data logging. This sends the
|
||||
//! following records: AlgMinuteDLSRecord, ActivityLegacySleepSessionDataLoggingRecord,
|
||||
//! ActivitySessionDataLoggingRecord (one for each activity type).
|
||||
//! Useful for mobile app testing
|
||||
//! @return true if success
|
||||
bool activity_test_send_fake_dls_records(void);
|
||||
|
||||
//! Used by test apps (running on firmware): Set the current step count
|
||||
//! Useful for testing the health app
|
||||
//! @param[in] new_steps the number of steps to set the current steps to
|
||||
void activity_test_set_steps_and_avg(int32_t new_steps, int32_t current_avg, int32_t daily_avg);
|
||||
|
||||
//! Used by test apps (running on firmware): Set the past seven days of history
|
||||
//! Useful for testing the health app
|
||||
void activity_test_set_steps_history();
|
||||
|
||||
//! Used by test apps (running on firmware): Set the past seven days of history
|
||||
//! Useful for testing the health app
|
||||
void activity_test_set_sleep_history();
|
||||
257
src/fw/services/normal/activity/activity_algorithm.h
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "applib/accel_service.h"
|
||||
#include "services/normal/activity/activity.h"
|
||||
|
||||
#define ACTIVITY_ALGORITHM_MAX_SAMPLES 25
|
||||
|
||||
// Version of our minute file minute records
|
||||
// Version history:
|
||||
// 4: Initial version
|
||||
// 5: Added the flags field and the plugged_in bit
|
||||
// 5 (3/1/16): Added the active bit to flags
|
||||
// 6: Added heart rate bpm
|
||||
#define ALG_MINUTE_FILE_RECORD_VERSION 6
|
||||
|
||||
// Format of each minute in our minute file. In the minute file, which is stored as a settings file
|
||||
// on the watch, we store a subset of what we send to data logging since we only need the
|
||||
// information required by the sleep algorithm and the information that could be returned by
|
||||
// the health_service_get_minute_history() API call.
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
// Base fields, present in versions 4 and 5
|
||||
uint8_t steps; // # of steps in this minute
|
||||
uint8_t orientation; // average orientation of the watch
|
||||
uint16_t vmc; // VMC (Vector Magnitude Counts) for this minute
|
||||
uint8_t light; // light sensor reading divided by
|
||||
// ALG_RAW_LIGHT_SENSOR_DIVIDE_BY
|
||||
// New fields added in version 5
|
||||
union {
|
||||
struct {
|
||||
uint8_t plugged_in:1;
|
||||
uint8_t active:1; // This is an "active" minute
|
||||
uint8_t reserved:6;
|
||||
};
|
||||
uint8_t flags;
|
||||
};
|
||||
} AlgMinuteFileSampleV5;
|
||||
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
// Base fields, present in versions <= 5
|
||||
AlgMinuteFileSampleV5 v5_fields;
|
||||
// New fields added in version 6
|
||||
uint8_t heart_rate_bpm;
|
||||
} AlgMinuteFileSample;
|
||||
|
||||
|
||||
// Version of our minute data logging records.
|
||||
// NOTE: AlgDlsMinuteData and the mobile app will continue to assume it can parse the blob,
|
||||
// only appending more properties is allowed.
|
||||
|
||||
// Android 3.10-4.0 requires bit 2 to be set, while iOS requires the value to be <= 255.
|
||||
// Available versions are: 4, 5, 6, 7, 12, 13, 14, 15, 20, ...
|
||||
|
||||
// Version history:
|
||||
// 4: Initial version
|
||||
// 5: Added the bases.flags field
|
||||
// 6: Added based.flags.active, resting_calories, active_calories, and distance_cm
|
||||
// 7: Added heart rate bpm
|
||||
// 12: Added total heart rate weight
|
||||
// 13: Added heart rate zone
|
||||
// 14: ... (NYI, you decide!)
|
||||
#define ALG_DLS_MINUTES_RECORD_VERSION 13
|
||||
|
||||
_Static_assert((ALG_DLS_MINUTES_RECORD_VERSION & (1 << 2)) > 0,
|
||||
"Android 3.10-4.0 requires bit 2 to be set");
|
||||
_Static_assert(ALG_DLS_MINUTES_RECORD_VERSION <= 225,
|
||||
"iOS requires version less that 255");
|
||||
|
||||
// Format of each minute in our data logging minute records.
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
// Base fields, which are also stored in the minute file on the watch. These are
|
||||
// present in versions 4 and 5.
|
||||
AlgMinuteFileSampleV5 base;
|
||||
|
||||
// New fields added in version 6
|
||||
uint16_t resting_calories; // number of resting calories burned in this minute
|
||||
uint16_t active_calories; // number of active calories burned in this minute
|
||||
uint16_t distance_cm; // distance in centimeters traveled in this minute
|
||||
|
||||
// New fields added in version 7
|
||||
uint8_t heart_rate_bpm; // weighted median hr value in this minute
|
||||
|
||||
// New fields added in version 12
|
||||
uint16_t heart_rate_total_weight_x100; // total weight of all HR values multiplied by 100
|
||||
|
||||
// New fields added in version 13
|
||||
uint8_t heart_rate_zone; // the hr zone for this minute
|
||||
} AlgMinuteDLSSample;
|
||||
|
||||
|
||||
// We store minute data in this struct into a circular buffer and then transfer from there to
|
||||
// data logging and to the minute file in PFS as we get a batch big enough.
|
||||
typedef struct {
|
||||
time_t utc_sec;
|
||||
AlgMinuteDLSSample data;
|
||||
} AlgMinuteRecord;
|
||||
|
||||
|
||||
// Record header. The same header is used for minute file records and minute data logging records
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
uint16_t version; // Set to ALG_DLS_MINUTES_RECORD_VERSION or
|
||||
// ALG_MINUTE_FILE_RECORD_VERSION
|
||||
uint32_t time_utc; // UTC time
|
||||
int8_t time_local_offset_15_min; // add this many 15 minute intervals to UTC to get local time.
|
||||
uint8_t sample_size; // size in bytes of each sample
|
||||
uint8_t num_samples; // # of samples included (ALG_MINUTES_PER_RECORD)
|
||||
} AlgMinuteRecordHdr;
|
||||
|
||||
|
||||
// Format of each data logging minute data record
|
||||
#define ALG_MINUTES_PER_DLS_RECORD 15
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
AlgMinuteRecordHdr hdr;
|
||||
AlgMinuteDLSSample samples[ALG_MINUTES_PER_DLS_RECORD];
|
||||
} AlgMinuteDLSRecord;
|
||||
|
||||
// Format of each minute file record
|
||||
#define ALG_MINUTES_PER_FILE_RECORD 15
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
AlgMinuteRecordHdr hdr;
|
||||
AlgMinuteFileSample samples[ALG_MINUTES_PER_FILE_RECORD];
|
||||
} AlgMinuteFileRecord;
|
||||
|
||||
|
||||
// Size quota for the minute file
|
||||
#define ALG_MINUTE_DATA_FILE_LEN 0x20000
|
||||
|
||||
// Max possible number of entries we can fit in our settings file if there was no overhead to
|
||||
// the settings file at all. The actual number we can fit is less than this.
|
||||
#define ALG_MINUTE_FILE_MAX_ENTRIES (ALG_MINUTE_DATA_FILE_LEN / sizeof(AlgMinuteFileRecord))
|
||||
|
||||
//! Init the algorithm
|
||||
//! @param[out] sampling_rate the required sampling rate is returned in this variable
|
||||
//! @return true if success
|
||||
bool activity_algorithm_init(AccelSamplingRate *sampling_rate);
|
||||
|
||||
//! Called at the start of the activity teardown process
|
||||
void activity_algorithm_early_deinit(void);
|
||||
|
||||
//! Deinit the algorithm
|
||||
//! @return true if success
|
||||
bool activity_algorithm_deinit(void);
|
||||
|
||||
//! Set the user metrics. These are used for the calorie calculation today, and possibly other
|
||||
//! calculations in the future.
|
||||
//! @return true if success
|
||||
bool activity_algorithm_set_user(uint32_t height_mm, uint32_t weight_g, ActivityGender gender,
|
||||
uint32_t age_years);
|
||||
|
||||
//! Process accel samples
|
||||
//! @param[in] data pointer to the accel samples
|
||||
//! @param[in] num_samples number of samples to process
|
||||
//! @param[in] timestamp timestamp of the first sample in ms
|
||||
void activity_algorithm_handle_accel(AccelRawData *data, uint32_t num_samples,
|
||||
uint64_t timestamp_ms);
|
||||
|
||||
//! Called once per minute so the algorithm can collect minute stats and log them. This is
|
||||
//! usually the data that gets used to compute sleep.
|
||||
//! @param[in] utc_sec the UTC timestamp when the minute handler was first triggered
|
||||
//! @param[out] record_out an AlgMinuteRecord that will be filled in
|
||||
void activity_algorithm_minute_handler(time_t utc_sec, AlgMinuteRecord *record_out);
|
||||
|
||||
//! Return the current number of steps computed
|
||||
//! @param[out] steps the number of steps is returned in this variable
|
||||
//! @return true if success
|
||||
bool activity_algorithm_get_steps(uint16_t *steps);
|
||||
|
||||
//! Tells the activity algorithm whether or not it should automatically track activities
|
||||
//! @param enable true to start tracking, false to stop tracking
|
||||
void activity_algorithm_enable_activity_tracking(bool enable);
|
||||
|
||||
//! Return the most recent stepping rate computed. This rate is returned as a number of steps
|
||||
//! and an elapsed time.
|
||||
//! @param[out] steps the number of steps taken during the last 'elapsed_sec' is returned in this
|
||||
//! variable.
|
||||
//! @param[out] elapsed_ms the number of elapsed milliseconds is returned in this variable
|
||||
//! @param[out] end_sec the UTC timestamp of the last time rate was computed is returned in this
|
||||
//! variable.
|
||||
//! @return true if success
|
||||
bool activity_algorithm_get_step_rate(uint16_t *steps, uint32_t *elapsed_ms, time_t *end_sec);
|
||||
|
||||
//! Reset all metrics that the algorithm tracks. Used at midnight to reset all metrics for a new
|
||||
//! day and whenever new values are written into healthDB
|
||||
//! @return true if success
|
||||
bool activity_algorithm_metrics_changed_notification(void);
|
||||
|
||||
//! Set the algorithm steps to the given value. Used when first starting up the algorithm after
|
||||
//! a watch reboot.
|
||||
//! @param[in] steps set the number of steps to this
|
||||
//! @return true if success
|
||||
bool activity_algorithm_set_steps(uint16_t steps);
|
||||
|
||||
//! Return the timestamp of the last minute that was processed by the sleep detector.
|
||||
time_t activity_algorithm_get_last_sleep_utc(void);
|
||||
|
||||
//! Send current minute data right away
|
||||
void activity_algorithm_send_minutes(void);
|
||||
|
||||
//! Scan the list of activity sessions for sleep sessions and relabel the ones that should be
|
||||
//! labeled as naps.
|
||||
//! @param[in] num_sessions number of activity sessions
|
||||
//! @param[in] sessions pointer to array of activity sessions
|
||||
void activity_algorithm_post_process_sleep_sessions(uint16_t num_sessions,
|
||||
ActivitySession *sessions);
|
||||
|
||||
//! Retrieve minute history
|
||||
//! @param[in] minute_data pointer to an array of HealthMinuteData records that will be filled
|
||||
//! in with the historical minute data
|
||||
//! @param[in,out] num_records On entry, the number of records the minute_data array can hold.
|
||||
//! On exit, the number of records in the minute data array that were written, including
|
||||
//! any missing minutes between 0 and *num_records. To check to see if a specific minute is
|
||||
//! missing, compare the vmc value of that record to HEALTH_MINUTE_DATA_MISSING_VMC_VALUE.
|
||||
//! @param[in,out] utc_start on entry, the UTC time of the first requested record. On exit,
|
||||
//! the UTC time of the first record returned.
|
||||
//! @return true if success
|
||||
bool activity_algorithm_get_minute_history(HealthMinuteData *minute_data, uint32_t *num_records,
|
||||
time_t *utc_start);
|
||||
|
||||
//! Dump the current sleep file to PBL_LOG. We write out base64 encoded data using PBL_LOG
|
||||
//! so that it can be extracted using a support request.
|
||||
//! @return true if success
|
||||
bool activity_algorithm_dump_minute_data_to_log(void);
|
||||
|
||||
//! Get info on the sleep file
|
||||
//! @param[in] compact_first if true, compact the file first
|
||||
//! @param[out] *num_records number of records in file
|
||||
//! @param[out] *data_bytes bytes of data it contains
|
||||
//! @param[out] *minutes how many minutes of data it contains
|
||||
//! @return true if success
|
||||
bool activity_algorithm_minute_file_info(bool compact_first, uint32_t *num_records,
|
||||
uint32_t *data_bytes, uint32_t *minutes);
|
||||
|
||||
//! Fill the sleep file
|
||||
//! @return true if success
|
||||
bool activity_algorithm_test_fill_minute_file(void);
|
||||
|
||||
//! Send a fake minute logging record to data logging. Useful for mobile app testing
|
||||
//! @return true if success
|
||||
bool activity_algorithm_test_send_fake_minute_data_dls_record(void);
|
||||
|
||||
183
src/fw/services/normal/activity/activity_calculators.c
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 "activity_calculators.h"
|
||||
|
||||
#include "services/normal/activity/activity.h"
|
||||
#include "services/normal/activity/activity_private.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#include <util/math.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Compute distance (in millimeters) covered by the taking the given number of steps in the given
|
||||
// amount of time.
|
||||
//
|
||||
// This function first computes a stride length based on the user's height, gender, and
|
||||
// rate of stepping. It then multiplies the stride length by the number of steps taken to get the
|
||||
// distance covered.
|
||||
//
|
||||
// Generally, the faster you go, the longer your stride length, and stride length is roughly
|
||||
// linearly proportional to cadence. The proportionality factor though depends on height, and
|
||||
// shorter users will have a steeper slope than taller users.
|
||||
// The general equation for stride length is:
|
||||
// stride_len = (a * steps/minute + b) * height
|
||||
// where a and b depend on height and gender
|
||||
//
|
||||
// @param[in] steps How many steps were taken
|
||||
// @param[in] ms How many milliseconds elapsed while the steps were taken
|
||||
// @param[out] distance covered (in millimeters)
|
||||
uint32_t activity_private_compute_distance_mm(uint32_t steps, uint32_t ms) {
|
||||
if ((steps == 0) || (ms == 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// For a rough ballpack figure, according to
|
||||
// http://livehealthy.chron.com/determine-stride-pedometer-height-weight-4518.html
|
||||
// The average stride length in mm is:
|
||||
// men: 0.415 * height(mm)
|
||||
// women: 0.413 * height(mm)
|
||||
// An average cadence would be about 100 steps/min, so plugging in that cadence into the
|
||||
// computations below should generate a stride length roughly around 0.414 * height.
|
||||
//
|
||||
const uint64_t steps_64 = steps;
|
||||
const uint64_t ms_64 = ms;
|
||||
const uint64_t height_mm_64 = activity_prefs_get_height_mm();
|
||||
|
||||
// Generate the 'a' factor. Eventually, this will be based on height and/or gender. For now,
|
||||
// set it to .003129
|
||||
const uint64_t k_a_x10000 = 31;
|
||||
|
||||
// Generate the 'b' factor. Eventually, this may be based on height and/or gender. For now,
|
||||
// set it to 0.14485
|
||||
const uint64_t k_b_x10000 = 1449;
|
||||
|
||||
// The factor we use to avoid fractional arithmetic
|
||||
const uint64_t k_x10000 = 10000;
|
||||
|
||||
// We want: stride_len = (a * steps/minute + b) * height
|
||||
// Since we have cadence in steps and milliseconds, this becomes:
|
||||
// stride_len = (a * steps * 1000 * 60 / milliseconds + b) * height
|
||||
// Compute the "(a * steps * 1000 * 60 / milliseconds + b)" component:
|
||||
uint64_t stride_len_component = ROUND(k_a_x10000 * steps_64 * MS_PER_SECOND * SECONDS_PER_MINUTE,
|
||||
ms_64) + k_b_x10000;
|
||||
|
||||
// Multiply by height to get stride_len, then by steps to get distance, then factor out our
|
||||
// constant multiplier at the very end to minimize rounding errors.
|
||||
uint32_t distance_mm = ROUND(stride_len_component * height_mm_64 * steps, k_x10000);
|
||||
|
||||
// Return distance in mm
|
||||
ACTIVITY_LOG_DEBUG("Got delta distance of %"PRIu32" mm", distance_mm);
|
||||
return distance_mm;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Compute active calories (in calories, not kcalories) covered by going the given distance in
|
||||
// the given amount of time.
|
||||
//
|
||||
// This method uses a formula for active calories as presented in this paper:
|
||||
// https://www.researchgate.net/profile/Glen_Duncan2/publication/
|
||||
// 221568418_Validated_caloric_expenditure_estimation_using_a_single_body-worn_sensor/
|
||||
// links/0912f4fb562b675d63000000.pdf
|
||||
//
|
||||
// In the paper, the formulas for walking and running compute energy in ml:
|
||||
// walking:
|
||||
// active_ml = 0.1 * speed_m_per_min * minutes * weight_kg
|
||||
// running:
|
||||
// active_ml = 0.2 * speed_m_per_min * minutes * weight_kg
|
||||
//
|
||||
// Converting to calories (5.01 calories per ml) and plugging in distance for speed * time, we get
|
||||
// the following. We will define walking as less then 4.5MPH (120 meters/minute)
|
||||
// for walking:
|
||||
// active_cal = 0.1 * distance_m * weight_kg * 5.01
|
||||
// = 0.501 * distance_m * weight_kg
|
||||
// for running:
|
||||
// active_cal = 0.2 * distance_m * weight_kg * 5.01
|
||||
// = 1.002 * distance_m * weight_kg
|
||||
//
|
||||
// For a rough ballpack figure, a 73kg person walking 80 meters in a minute burns about
|
||||
// 2925 active calories (2.9 kcalories)
|
||||
// That same 73kg person running 140 meters in a minute burns about 10,240 active calories
|
||||
// (10.2 kcalories)
|
||||
//
|
||||
// @param[in] distance_mm distance covered in millimeters
|
||||
// @param[in] ms How many milliseconds elapsed while the distance was covered
|
||||
// @param[out] active calories
|
||||
uint32_t activity_private_compute_active_calories(uint32_t distance_mm, uint32_t ms) {
|
||||
if ((distance_mm == 0) || (ms == 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t distance_mm_64 = distance_mm;
|
||||
uint64_t ms_64 = ms;
|
||||
|
||||
// Figure out the rate and see if it's walking or running. We set the walking threshold at
|
||||
// 120 m/min. This is 2m/s or 2 mm/ms
|
||||
const unsigned int k_max_walking_rate_mm_per_min = 120 * MM_PER_METER;
|
||||
uint64_t rate_mm_per_min = distance_mm_64 * MS_PER_SECOND * SECONDS_PER_MINUTE / ms_64;
|
||||
bool walking = (rate_mm_per_min <= k_max_walking_rate_mm_per_min);
|
||||
uint64_t k_constant_x1000;
|
||||
if (walking) {
|
||||
k_constant_x1000 = 501;
|
||||
} else {
|
||||
k_constant_x1000 = 1002;
|
||||
}
|
||||
|
||||
uint64_t weight_dag = activity_prefs_get_weight_dag(); // 10 grams = 1 dag
|
||||
|
||||
uint32_t calories = ROUND(k_constant_x1000 * (uint64_t)distance_mm * weight_dag,
|
||||
1000 * MM_PER_METER * ACTIVITY_DAG_PER_KG);
|
||||
|
||||
// Return calories
|
||||
ACTIVITY_LOG_DEBUG("Got delta active calories of %"PRIu32" ", calories);
|
||||
return calories;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
uint32_t activity_private_compute_resting_calories(uint32_t elapsed_minutes) {
|
||||
// This computes resting metabolic rate in calories based on the MD Mifflin and ST St jeor
|
||||
// formula. This formula gives the number of kcalories expended per day
|
||||
uint32_t calories_per_day;
|
||||
ActivityGender gender = activity_prefs_get_gender();
|
||||
uint64_t weight_dag = activity_prefs_get_weight_dag();
|
||||
uint64_t height_mm = activity_prefs_get_height_mm();
|
||||
uint64_t age_years = activity_prefs_get_age_years();
|
||||
|
||||
// For men: kcalories = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) + 5
|
||||
// For women: kcalories = 10 * weight(kg) + 6.25 * height(cm) - 5 * age(y) - 161
|
||||
calories_per_day = (100 * weight_dag)
|
||||
+ (625 * height_mm)
|
||||
- (5000 * age_years);
|
||||
if (gender == ActivityGenderMale) {
|
||||
calories_per_day += 5000;
|
||||
} else if (gender == ActivityGenderFemale) {
|
||||
calories_per_day -= 161000;
|
||||
} else {
|
||||
// midpoint of 5000 and -161000
|
||||
calories_per_day -= 78000;
|
||||
}
|
||||
|
||||
// Scale by the requested number of minutes
|
||||
uint32_t resting_calories = ROUND(calories_per_day * elapsed_minutes, MINUTES_PER_DAY);
|
||||
ACTIVITY_LOG_DEBUG("resting_calories: %"PRIu32"", resting_calories);
|
||||
return resting_calories;
|
||||
}
|
||||
34
src/fw/services/normal/activity/activity_calculators.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Compute distance (in millimeters) covered by the taking the given number of steps in the given
|
||||
// amount of time.
|
||||
uint32_t activity_private_compute_distance_mm(uint32_t steps, uint32_t ms);
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Compute active calories (in calories, not kcalories) covered by going the given distance in
|
||||
// the given amount of time.
|
||||
uint32_t activity_private_compute_active_calories(uint32_t distance_mm, uint32_t ms);
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Compute resting calories (in calories, not kcalories) within the elapsed time given
|
||||
uint32_t activity_private_compute_resting_calories(uint32_t elapsed_minutes);
|
||||
2471
src/fw/services/normal/activity/activity_insights.c
Normal file
117
src/fw/services/normal/activity/activity_insights.h
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "activity_private.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum PercentTier {
|
||||
PercentTier_AboveAverage = 0,
|
||||
PercentTier_OnAverage,
|
||||
PercentTier_BelowAverage,
|
||||
PercentTier_Fail,
|
||||
PercentTierCount
|
||||
} PercentTier;
|
||||
|
||||
// Insight types (for analytics)
|
||||
typedef enum ActivityInsightType {
|
||||
ActivityInsightType_Unknown = 0,
|
||||
ActivityInsightType_SleepReward,
|
||||
ActivityInsightType_ActivityReward,
|
||||
ActivityInsightType_SleepSummary,
|
||||
ActivityInsightType_ActivitySummary,
|
||||
ActivityInsightType_Day1,
|
||||
ActivityInsightType_Day4,
|
||||
ActivityInsightType_Day10,
|
||||
ActivityInsightType_ActivitySessionSleep,
|
||||
ActivityInsightType_ActivitySessionNap,
|
||||
ActivityInsightType_ActivitySessionWalk,
|
||||
ActivityInsightType_ActivitySessionRun,
|
||||
ActivityInsightType_ActivitySessionOpen,
|
||||
} ActivityInsightType;
|
||||
|
||||
// Insight response types (for analytics)
|
||||
typedef enum ActivityInsightResponseType {
|
||||
ActivityInsightResponseTypePositive = 0,
|
||||
ActivityInsightResponseTypeNeutral,
|
||||
ActivityInsightResponseTypeNegative,
|
||||
ActivityInsightResponseTypeClassified,
|
||||
ActivityInsightResponseTypeMisclassified,
|
||||
} ActivityInsightResponseType;
|
||||
|
||||
typedef enum ActivationDelayInsightType {
|
||||
// New vals must be added on the end. These are used in a prefs bitfield
|
||||
ActivationDelayInsightType_Day1,
|
||||
ActivationDelayInsightType_Day4,
|
||||
ActivationDelayInsightType_Day10,
|
||||
ActivationDelayInsightTypeCount,
|
||||
} ActivationDelayInsightType;
|
||||
|
||||
// Various stats for metrics that are used to determine when it's ok to trigger an insight
|
||||
typedef struct ActivityInsightMetricHistoryStats {
|
||||
uint8_t total_days;
|
||||
uint8_t consecutive_days;
|
||||
ActivityScalarStore median;
|
||||
ActivityScalarStore mean;
|
||||
ActivityMetric metric;
|
||||
} ActivityInsightMetricHistoryStats;
|
||||
|
||||
//! Called at midnight rollover to recalculate medians/totals for metric history
|
||||
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
|
||||
//! s_activity_state.mutex
|
||||
void activity_insights_recalculate_stats(void);
|
||||
|
||||
//! Init activity insights
|
||||
//! IMPORTANT: This call is not thread safe and should only be called from activity_init (since it
|
||||
//! is called during boot when no other task might use an activity service call)
|
||||
//! @param[in] now_utc Current time
|
||||
void activity_insights_init(time_t now_utc);
|
||||
|
||||
//! Called by prv_minute_system_task_cb whenever it updates sleep metrics
|
||||
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
|
||||
//! s_activity_state.mutex
|
||||
//! @param[in] now_utc Current time
|
||||
void activity_insights_process_sleep_data(time_t now_utc);
|
||||
|
||||
//! Called once per minute by prv_minute_system_task_cb to check step insights
|
||||
//! IMPORTANT: This call is not thread safe and must only be called when activity.c is holding
|
||||
//! s_activity_state.mutex
|
||||
//! @param[in] now_utc Current time
|
||||
void activity_insights_process_minute_data(time_t now_utc);
|
||||
|
||||
void activity_insights_push_activity_session_notification(time_t notif_time,
|
||||
ActivitySession *session,
|
||||
int32_t avg_hr,
|
||||
int32_t *hr_zone_time_s);
|
||||
|
||||
//! Used by test apps: Pushes the 3 variants of each summary pin to the timeline and a notification
|
||||
//! for the last variant of each
|
||||
void activity_insights_test_push_summary_pins(void);
|
||||
|
||||
//! Used by test apps: Pushes the 2 rewards to the watch
|
||||
void activity_insights_test_push_rewards(void);
|
||||
|
||||
//! Used by test apps: Pushes the day 1, 4 and 10 insights
|
||||
void activity_insights_test_push_day_insights(void);
|
||||
|
||||
//! Used by test apps: Pushes a run and a walk notification
|
||||
void activity_insights_test_push_walk_run_sessions(void);
|
||||
|
||||
//! Used by test apps: Pushes a nap pin and notification
|
||||
void activity_insights_test_push_nap_session(void);
|
||||
783
src/fw/services/normal/activity/activity_metrics.c
Normal file
@@ -0,0 +1,783 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "applib/data_logging.h"
|
||||
#include "applib/health_service.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "os/tick.h"
|
||||
#include "popups/health_tracking_ui.h"
|
||||
#include "services/common/analytics/analytics_event.h"
|
||||
#include "services/normal/protobuf_log/protobuf_log.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/base64.h"
|
||||
#include "util/math.h"
|
||||
#include "util/size.h"
|
||||
#include "util/stats.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#include "activity.h"
|
||||
#include "activity_algorithm.h"
|
||||
#include "activity_calculators.h"
|
||||
#include "activity_insights.h"
|
||||
#include "activity_private.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Storage converters. These convert metrics from their storage type (ActivityScalarStore,
|
||||
// which is only 16-bits) into the uint32_t value returned by activity_get_metric. For example,
|
||||
// we might convert minutes to seconds.
|
||||
static uint32_t prv_convert_none(ActivityScalarStore in) {
|
||||
return in;
|
||||
}
|
||||
|
||||
static uint32_t prv_convert_minutes_to_seconds(ActivityScalarStore in) {
|
||||
return (uint32_t)in * SECONDS_PER_MINUTE;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Returns info about each metric we capture
|
||||
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info) {
|
||||
ActivityState *state = activity_private_state();
|
||||
*info = (ActivityMetricInfo) {
|
||||
.converter = prv_convert_none,
|
||||
};
|
||||
switch (metric) {
|
||||
case ActivityMetricStepCount:
|
||||
info->value_p = &state->step_data.steps;
|
||||
info->settings_key = ActivitySettingsKeyStepCountHistory;
|
||||
info->has_history = true;
|
||||
break;
|
||||
case ActivityMetricActiveSeconds:
|
||||
info->value_p = &state->step_data.step_minutes;
|
||||
info->settings_key = ActivitySettingsKeyStepMinutesHistory;
|
||||
info->has_history = true;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricDistanceMeters:
|
||||
info->value_p = &state->step_data.distance_meters;
|
||||
info->settings_key = ActivitySettingsKeyDistanceMetersHistory;
|
||||
info->has_history = true;
|
||||
break;
|
||||
case ActivityMetricRestingKCalories:
|
||||
info->value_p = &state->step_data.resting_kcalories;
|
||||
info->settings_key = ActivitySettingsKeyRestingKCaloriesHistory;
|
||||
info->has_history = true;
|
||||
break;
|
||||
case ActivityMetricActiveKCalories:
|
||||
info->value_p = &state->step_data.active_kcalories;
|
||||
info->settings_key = ActivitySettingsKeyActiveKCaloriesHistory;
|
||||
info->has_history = true;
|
||||
break;
|
||||
case ActivityMetricSleepTotalSeconds:
|
||||
info->value_p = &state->sleep_data.total_minutes;
|
||||
info->settings_key = ActivitySettingsKeySleepTotalMinutesHistory;
|
||||
info->has_history = true;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricSleepRestfulSeconds:
|
||||
info->value_p = &state->sleep_data.restful_minutes;
|
||||
info->settings_key = ActivitySettingsKeySleepDeepMinutesHistory;
|
||||
info->has_history = true;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricSleepEnterAtSeconds:
|
||||
info->value_p = &state->sleep_data.enter_at_minute;
|
||||
info->settings_key = ActivitySettingsKeySleepEnterAtHistory;
|
||||
info->has_history = true;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricSleepExitAtSeconds:
|
||||
info->value_p = &state->sleep_data.exit_at_minute;
|
||||
info->settings_key = ActivitySettingsKeySleepExitAtHistory;
|
||||
info->has_history = true;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricSleepState:
|
||||
info->value_p = &state->sleep_data.cur_state;
|
||||
info->settings_key = ActivitySettingsKeySleepState;
|
||||
break;
|
||||
case ActivityMetricSleepStateSeconds:
|
||||
info->value_p = &state->sleep_data.cur_state_elapsed_minutes;
|
||||
info->settings_key = ActivitySettingsKeySleepStateMinutes;
|
||||
info->converter = prv_convert_minutes_to_seconds;
|
||||
break;
|
||||
case ActivityMetricLastVMC:
|
||||
info->value_p = &state->last_vmc;
|
||||
info->settings_key = ActivitySettingsKeyLastVMC;
|
||||
break;
|
||||
case ActivityMetricHeartRateRawBPM:
|
||||
info->value_p = &state->hr.metrics.current_bpm;
|
||||
break;
|
||||
case ActivityMetricHeartRateRawQuality:
|
||||
info->value_p = &state->hr.metrics.current_quality;
|
||||
break;
|
||||
case ActivityMetricHeartRateRawUpdatedTimeUTC:
|
||||
info->value_u32p = &state->hr.metrics.current_update_time_utc;
|
||||
break;
|
||||
case ActivityMetricHeartRateFilteredBPM:
|
||||
info->value_p = &state->hr.metrics.last_stable_bpm;
|
||||
break;
|
||||
case ActivityMetricHeartRateFilteredUpdatedTimeUTC:
|
||||
info->value_u32p = &state->hr.metrics.last_stable_bpm_update_time_utc;
|
||||
break;
|
||||
case ActivityMetricHeartRateZone1Minutes:
|
||||
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone1];
|
||||
info->settings_key = ActivitySettingsKeyHeartRateZone1Minutes;
|
||||
info->has_history = false;
|
||||
break;
|
||||
case ActivityMetricHeartRateZone2Minutes:
|
||||
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone2];
|
||||
info->settings_key = ActivitySettingsKeyHeartRateZone2Minutes;
|
||||
info->has_history = false;
|
||||
break;
|
||||
case ActivityMetricHeartRateZone3Minutes:
|
||||
info->value_p = &state->hr.metrics.minutes_in_zone[HRZone_Zone3];
|
||||
info->settings_key = ActivitySettingsKeyHeartRateZone3Minutes;
|
||||
info->has_history = false;
|
||||
break;
|
||||
case ActivityMetricNumMetrics:
|
||||
WTF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Set the value of a given metric
|
||||
// The current value will only be overridden if the new value is higher
|
||||
// Historical values can be overridden with any value
|
||||
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek wday, int32_t value) {
|
||||
if (!activity_tracking_on()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
|
||||
switch (metric) {
|
||||
case ActivityMetricActiveSeconds:
|
||||
case ActivityMetricSleepTotalSeconds:
|
||||
case ActivityMetricSleepRestfulSeconds:
|
||||
case ActivityMetricSleepEnterAtSeconds:
|
||||
case ActivityMetricSleepExitAtSeconds:
|
||||
// We only store minutes for these metrics. Convert before saving
|
||||
value /= SECONDS_PER_MINUTE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ActivityMetricInfo m_info = {};
|
||||
activity_metrics_prv_get_metric_info(metric, &m_info);
|
||||
const DayInWeek cur_wday = time_util_get_day_in_week(rtc_get_time());
|
||||
|
||||
bool current_value_updated = false;
|
||||
|
||||
if (cur_wday == wday) {
|
||||
// Update our cached copy of the value if it is larger than what we currently have
|
||||
if (m_info.value_p && value > *m_info.value_p) {
|
||||
*m_info.value_p = value;
|
||||
current_value_updated = true;
|
||||
} else if (m_info.value_u32p && (uint32_t)value > *m_info.value_u32p) {
|
||||
*m_info.value_u32p = value;
|
||||
current_value_updated = true;
|
||||
}
|
||||
} else if (m_info.has_history) {
|
||||
// This update is for a day in the past. Modify the copy stored in the settings file
|
||||
SettingsFile *file = activity_private_settings_open();
|
||||
if (!file) {
|
||||
goto unlock;
|
||||
}
|
||||
ActivitySettingsValueHistory history;
|
||||
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key),
|
||||
&history, sizeof(history));
|
||||
|
||||
int day = positive_modulo(cur_wday - wday, DAYS_PER_WEEK);
|
||||
if (history.values[day] != value) {
|
||||
history.values[day] = value;
|
||||
|
||||
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
|
||||
&history, sizeof(history));
|
||||
}
|
||||
activity_private_settings_close(file);
|
||||
}
|
||||
|
||||
if (current_value_updated) {
|
||||
if (metric == ActivityMetricStepCount) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_HEALTH_SERVICE_EVENT,
|
||||
.health_event = {
|
||||
.type = HealthEventMovementUpdate,
|
||||
.data.movement_update = {
|
||||
.steps = value,
|
||||
},
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
} else if (metric == ActivityMetricDistanceMeters) {
|
||||
state->distance_mm = state->step_data.distance_meters * MM_PER_METER;
|
||||
} else if (metric == ActivityMetricActiveKCalories) {
|
||||
state->active_calories = state->step_data.active_kcalories * ACTIVITY_CALORIES_PER_KCAL;
|
||||
} else if (metric == ActivityMetricRestingKCalories) {
|
||||
state->resting_calories = state->step_data.resting_kcalories * ACTIVITY_CALORIES_PER_KCAL;
|
||||
}
|
||||
activity_algorithm_metrics_changed_notification();
|
||||
}
|
||||
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
// Shift the history back one day and reset the current day's stats.
|
||||
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
|
||||
static void NOINLINE prv_shift_history(time_t utc_now) {
|
||||
ActivityState *state = activity_private_state();
|
||||
PBL_LOG(LOG_LEVEL_INFO, "resetting metrics for new day");
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
SettingsFile *file = activity_private_settings_open();
|
||||
if (!file) {
|
||||
goto unlock;
|
||||
}
|
||||
ActivitySettingsValueHistory history;
|
||||
ActivityMetricInfo m_info;
|
||||
|
||||
for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
|
||||
metric++) {
|
||||
activity_metrics_prv_get_metric_info(metric, &m_info);
|
||||
|
||||
// Shift the history
|
||||
if (m_info.has_history) {
|
||||
PBL_ASSERTN(m_info.value_p);
|
||||
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
|
||||
sizeof(history));
|
||||
|
||||
for (int i = ACTIVITY_HISTORY_DAYS - 1; i >= 1; i--) {
|
||||
history.values[i] = history.values[i - 1];
|
||||
}
|
||||
// We just wrapped up yesterday
|
||||
history.values[1] = *m_info.value_p;
|
||||
|
||||
// Reset stats for today
|
||||
history.values[0] = 0;
|
||||
history.utc_sec = utc_now;
|
||||
|
||||
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &history,
|
||||
sizeof(history));
|
||||
}
|
||||
}
|
||||
activity_private_settings_close(file);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Called from activity_get_metric() every time a client asks for a metric. Also called
|
||||
// periodically from the minute handler before we save current metrics to setting.
|
||||
static void prv_update_real_time_derived_metrics(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
state->step_data.distance_meters = ROUND(state->distance_mm,
|
||||
MM_PER_METER);
|
||||
ACTIVITY_LOG_DEBUG("new distance: %"PRIu16"", state->step_data.distance_meters);
|
||||
|
||||
state->step_data.active_kcalories = ROUND(state->active_calories,
|
||||
ACTIVITY_CALORIES_PER_KCAL);
|
||||
ACTIVITY_LOG_DEBUG("new active kcal: %"PRIu16"", state->step_data.active_kcalories);
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Called periodically from the minute handler to update step derived metrics that do not have to
|
||||
// be updated in real time.
|
||||
// We use NOINLINE to reduce the stack requirements during the minute handler (see PBL-38130)
|
||||
static void NOINLINE prv_update_step_derived_metrics(time_t utc_sec) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
int minute_of_day = time_util_get_minute_of_day(utc_sec);
|
||||
// The "no-steps-during-sleep" logic can introduce negative steps, so make sure we clip
|
||||
// negative steps to 0 when computing the metrics below
|
||||
uint16_t steps_in_minute = 0;
|
||||
if (state->step_data.steps >= state->steps_per_minute_last_steps) {
|
||||
steps_in_minute = state->step_data.steps
|
||||
- state->steps_per_minute_last_steps;
|
||||
}
|
||||
|
||||
// Update the walking rate
|
||||
state->steps_per_minute = steps_in_minute;
|
||||
state->steps_per_minute_last_steps = state->step_data.steps;
|
||||
ACTIVITY_LOG_DEBUG("new steps/minute: %"PRIu16"", state->steps_per_minute);
|
||||
|
||||
// Update the number of stepping minutes and the last active minute
|
||||
if (state->steps_per_minute >= ACTIVITY_ACTIVE_MINUTE_MIN_STEPS) {
|
||||
state->step_data.step_minutes++;
|
||||
ACTIVITY_LOG_DEBUG("new step minutes: %"PRIu16"", state->step_data.step_minutes);
|
||||
|
||||
// The prior minute was the most recent active one
|
||||
state->last_active_minute = time_util_minute_of_day_adjust(minute_of_day, -1);
|
||||
ACTIVITY_LOG_DEBUG("last active minute: %"PRIu16"", state->last_active_minute);
|
||||
}
|
||||
|
||||
// Update the resting calories
|
||||
state->resting_calories = activity_private_compute_resting_calories(minute_of_day);
|
||||
state->step_data.resting_kcalories = ROUND(state->resting_calories,
|
||||
ACTIVITY_CALORIES_PER_KCAL);
|
||||
ACTIVITY_LOG_DEBUG("resting kcalories: %"PRIu16"",
|
||||
state->step_data.resting_kcalories);
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Pushes an HR Median/Filtered/LastStable event.
|
||||
static void prv_push_median_hr_event(uint8_t median_hr) {
|
||||
if (median_hr > 0) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_HEALTH_SERVICE_EVENT,
|
||||
.health_event = {
|
||||
.type = HealthEventHeartRateUpdate,
|
||||
.data.heart_rate_update = {
|
||||
.current_bpm = median_hr,
|
||||
.is_filtered = true,
|
||||
}
|
||||
}
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Calculates and stores the most recent minutes median heart rate value.
|
||||
// Used for the health_service and the minute level data.
|
||||
static void prv_update_median_hr_bpm(ActivityState *state) {
|
||||
const ActivityHRSupport *hr = &state->hr;
|
||||
|
||||
const uint16_t num_hr_samples = hr->num_samples;
|
||||
if (num_hr_samples > 0) {
|
||||
int32_t median, total_weight;
|
||||
|
||||
// Stats requires an int32_t array and we need one for both the samples and the weights
|
||||
int32_t *sample_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
|
||||
int32_t *weight_buf = task_zalloc_check(num_hr_samples * sizeof(int32_t));
|
||||
for (size_t i = 0; i < num_hr_samples; i++) {
|
||||
sample_buf[i] = hr->samples[i];
|
||||
weight_buf[i] = hr->weights[i];
|
||||
}
|
||||
|
||||
// Calculate the total weight
|
||||
stats_calculate_basic(StatsBasicOp_Sum, weight_buf, hr->num_samples, NULL, NULL,
|
||||
&total_weight);
|
||||
|
||||
// Calculate the weighted median
|
||||
median = stats_calculate_weighted_median(sample_buf, weight_buf, num_hr_samples);
|
||||
task_free(sample_buf);
|
||||
task_free(weight_buf);
|
||||
|
||||
state->hr.metrics.last_stable_bpm = (uint8_t)median;
|
||||
state->hr.metrics.last_stable_bpm_update_time_utc = rtc_get_time();
|
||||
state->hr.metrics.previous_median_bpm = (uint8_t)median;
|
||||
state->hr.metrics.previous_median_total_weight_x100 = total_weight;
|
||||
|
||||
prv_push_median_hr_event(state->hr.metrics.previous_median_bpm);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
static void prv_write_hr_zone_info_to_flash(HRZone zone) {
|
||||
ActivityMetric metric;
|
||||
if (zone == HRZone_Zone1) {
|
||||
metric = ActivityMetricHeartRateZone1Minutes;
|
||||
} else if (zone == HRZone_Zone2) {
|
||||
metric = ActivityMetricHeartRateZone2Minutes;
|
||||
} else if (zone == HRZone_Zone3) {
|
||||
metric = ActivityMetricHeartRateZone3Minutes;
|
||||
} else {
|
||||
// Don't store data for Zone 0
|
||||
return;
|
||||
}
|
||||
|
||||
SettingsFile *file = activity_private_settings_open();
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityMetricInfo m_info;
|
||||
activity_metrics_prv_get_metric_info(metric, &m_info);
|
||||
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key),
|
||||
m_info.value_p, sizeof(*m_info.value_p));
|
||||
activity_private_settings_close(file);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// The median HR should get updated before calling this
|
||||
static void prv_update_current_hr_zone(ActivityState *state) {
|
||||
int32_t hr_median;
|
||||
activity_metrics_prv_get_median_hr_bpm(&hr_median, NULL);
|
||||
HRZone new_hr_zone = hr_util_get_hr_zone(hr_median);
|
||||
|
||||
if (new_hr_zone != HRZone_Zone0 && state->hr.num_samples < ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE) {
|
||||
// There wasn't enough data in the past minute to give us confidence that
|
||||
// the new HR zone will represents that minute, default to Zone0
|
||||
new_hr_zone = HRZone_Zone0;
|
||||
}
|
||||
|
||||
bool new_hr_elevated = hr_util_is_elevated(hr_median);
|
||||
// Before changing the zone make sure the user has an elevated heart rate.
|
||||
// This prevents erroneous HRM readings accumulating minutes in zone 1.
|
||||
// Then only go up/down 1 zone per minute.
|
||||
// This prevents erroneous HRM readings accumulating minutes in higher zones.
|
||||
if (!state->hr.metrics.is_hr_elevated && new_hr_elevated) {
|
||||
state->hr.metrics.is_hr_elevated = new_hr_elevated;
|
||||
} else if (new_hr_zone > state->hr.metrics.current_hr_zone) {
|
||||
state->hr.metrics.current_hr_zone++;
|
||||
} else if (new_hr_zone < state->hr.metrics.current_hr_zone) {
|
||||
state->hr.metrics.current_hr_zone--;
|
||||
} else if (!new_hr_elevated) {
|
||||
state->hr.metrics.is_hr_elevated = new_hr_elevated;
|
||||
}
|
||||
|
||||
state->hr.metrics.minutes_in_zone[state->hr.metrics.current_hr_zone]++;
|
||||
|
||||
prv_write_hr_zone_info_to_flash(state->hr.metrics.current_hr_zone);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Called periodically from the minute handler to update the median HR and time spent in HR zones
|
||||
static void prv_update_hr_derived_metrics(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
// Update the median HR / HR weight for the minute
|
||||
prv_update_median_hr_bpm(state);
|
||||
|
||||
// Update our current HR zone (based on the median which is calculated above)
|
||||
prv_update_current_hr_zone(state);
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// The metrics minute handler
|
||||
void activity_metrics_prv_minute_handler(time_t utc_sec) {
|
||||
ActivityState *state = activity_private_state();
|
||||
|
||||
uint16_t cur_day_index = time_util_get_day(utc_sec);
|
||||
if (cur_day_index != state->cur_day_index) {
|
||||
// If we've just encountered a midnight rollover, shift history to the new day
|
||||
// before we compute metrics for the new day
|
||||
prv_shift_history(utc_sec);
|
||||
}
|
||||
|
||||
// Update the derived metrics
|
||||
prv_update_real_time_derived_metrics();
|
||||
prv_update_step_derived_metrics(utc_sec);
|
||||
prv_update_hr_derived_metrics();
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
ActivityScalarStore activity_metrics_prv_steps_per_minute(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
return state->steps_per_minute;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
uint32_t activity_metrics_prv_get_distance_mm(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
return state->distance_mm;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
uint32_t activity_metrics_prv_get_resting_calories(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
return state->resting_calories;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
uint32_t activity_metrics_prv_get_active_calories(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
return state->active_calories;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
uint32_t activity_metrics_prv_get_steps(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
return state->step_data.steps;
|
||||
}
|
||||
|
||||
static uint8_t prv_get_hr_quality_weight(HRMQuality quality) {
|
||||
static const struct {
|
||||
HRMQuality quality;
|
||||
uint8_t weight_x100;
|
||||
} s_hr_quality_weights_x100[] = {
|
||||
{HRMQuality_NoAccel, 0 },
|
||||
{HRMQuality_OffWrist, 0 },
|
||||
{HRMQuality_NoSignal, 0 },
|
||||
{HRMQuality_Worst, 1 },
|
||||
{HRMQuality_Poor, 1 },
|
||||
{HRMQuality_Acceptable, 60 },
|
||||
{HRMQuality_Good, 65 },
|
||||
{HRMQuality_Excellent, 85 },
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < ARRAY_LENGTH(s_hr_quality_weights_x100); i++) {
|
||||
if (quality == s_hr_quality_weights_x100[i].quality) {
|
||||
return s_hr_quality_weights_x100[i].weight_x100;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
HRZone activity_metrics_prv_get_hr_zone(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
|
||||
return state->hr.metrics.current_hr_zone;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
|
||||
int32_t *heart_rate_total_weight_x100_out) {
|
||||
ActivityState *state = activity_private_state();
|
||||
|
||||
if (median_out) {
|
||||
*median_out = state->hr.metrics.previous_median_bpm;
|
||||
}
|
||||
if (heart_rate_total_weight_x100_out) {
|
||||
*heart_rate_total_weight_x100_out = state->hr.metrics.previous_median_total_weight_x100;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void activity_metrics_prv_reset_hr_stats(void) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
state->hr.num_samples = 0;
|
||||
state->hr.num_quality_samples = 0;
|
||||
memset(state->hr.samples, 0, sizeof(state->hr.samples));
|
||||
memset(state->hr.weights, 0, sizeof(state->hr.weights));
|
||||
|
||||
state->hr.metrics.previous_median_bpm = 0;
|
||||
state->hr.metrics.previous_median_total_weight_x100 = 0;
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
|
||||
time_t now_uptime) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
// Update stats used for computing the average
|
||||
if (hrm_event->bpm.bpm > 0) {
|
||||
// This should get reset about once a minute, so X minutes worth of samples means something
|
||||
// is terribly wrong.
|
||||
PBL_ASSERT(state->hr.num_samples <= ACTIVITY_MAX_HR_SAMPLES, "Too many samples");
|
||||
state->hr.samples[state->hr.num_samples] = hrm_event->bpm.bpm;
|
||||
state->hr.weights[state->hr.num_samples] =
|
||||
prv_get_hr_quality_weight(hrm_event->bpm.quality);
|
||||
if (hrm_event->bpm.quality >= ACTIVITY_MIN_HR_QUALITY_THRESH) {
|
||||
state->hr.num_quality_samples++;
|
||||
}
|
||||
|
||||
state->hr.num_samples++;
|
||||
}
|
||||
// Update the timestamp used for figuring out when we should change the sampling period.
|
||||
// This is based on uptime so that it doesn't get messed up if the mobile changes the
|
||||
// UTC time on us.
|
||||
state->hr.last_sample_ts = now_uptime;
|
||||
|
||||
// Save the BPM, quality, and update time (UTC) of the last reading for activity_get_metric()
|
||||
state->hr.metrics.current_bpm = hrm_event->bpm.bpm;
|
||||
state->hr.metrics.current_quality = hrm_event->bpm.quality;
|
||||
state->hr.metrics.current_update_time_utc = now_utc;
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now) {
|
||||
ActivityState *state = activity_private_state();
|
||||
// Roll back the history if needed and init each of the metrics for today
|
||||
for (ActivityMetric metric = ActivityMetricFirst; metric < ActivityMetricNumMetrics;
|
||||
metric++) {
|
||||
ActivityMetricInfo m_info;
|
||||
activity_metrics_prv_get_metric_info(metric, &m_info);
|
||||
if (m_info.has_history) {
|
||||
PBL_ASSERTN(m_info.value_p);
|
||||
ActivitySettingsValueHistory old_history = { 0 };
|
||||
ActivitySettingsValueHistory new_history = { 0 };
|
||||
|
||||
// In case we change the length of the history, fetch the old size
|
||||
int fetch_size = sizeof(old_history);
|
||||
fetch_size = MIN(fetch_size, settings_file_get_len(file, &m_info.settings_key,
|
||||
sizeof(m_info.settings_key)));
|
||||
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &old_history,
|
||||
fetch_size);
|
||||
|
||||
uint16_t day = time_util_get_day(old_history.utc_sec);
|
||||
int old_age = state->cur_day_index - day;
|
||||
|
||||
// If this is resting kcalories, the default for each day is not 0
|
||||
if (metric == ActivityMetricRestingKCalories) {
|
||||
uint32_t full_day_resting_calories =
|
||||
activity_private_compute_resting_calories(MINUTES_PER_DAY);
|
||||
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
|
||||
if (i == 0) {
|
||||
uint32_t elapsed_minutes = time_util_get_minute_of_day(utc_now);
|
||||
uint32_t cur_day_resting_calories =
|
||||
activity_private_compute_resting_calories(elapsed_minutes);
|
||||
new_history.values[i] = ROUND(cur_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
|
||||
} else {
|
||||
new_history.values[i] = ROUND(full_day_resting_calories, ACTIVITY_CALORIES_PER_KCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy values from old history into correct slot in new history
|
||||
for (int i = 0; i < ACTIVITY_HISTORY_DAYS; i++) {
|
||||
int new_index = i + old_age;
|
||||
if (new_index >= 0 && new_index < ACTIVITY_HISTORY_DAYS) {
|
||||
new_history.values[new_index] = old_history.values[i];
|
||||
}
|
||||
}
|
||||
// init the time stamp if not initialized yet
|
||||
if (new_history.utc_sec == 0) {
|
||||
new_history.utc_sec = utc_now;
|
||||
}
|
||||
|
||||
// Init current value
|
||||
*m_info.value_p = new_history.values[0];
|
||||
|
||||
// Only write to flash if the values change or this is a new day (to update the timestamp)
|
||||
if (memcmp(old_history.values, new_history.values, sizeof(old_history.values)) != 0
|
||||
|| old_age != 0) {
|
||||
// Write out the updated history
|
||||
settings_file_set(file, &m_info.settings_key, sizeof(m_info.settings_key), &new_history,
|
||||
sizeof(new_history));
|
||||
}
|
||||
|
||||
} else if (m_info.settings_key != ActivitySettingsKeyInvalid) {
|
||||
// Metric with no history, just init current value
|
||||
PBL_ASSERTN(m_info.value_p);
|
||||
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), m_info.value_p,
|
||||
sizeof(*m_info.value_p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
bool activity_get_metric(ActivityMetric metric, uint32_t history_len, int32_t *history) {
|
||||
ActivityState *state = activity_private_state();
|
||||
bool success = true;
|
||||
|
||||
// Default results
|
||||
for (uint32_t i = 0; i < history_len; i++) {
|
||||
history[i] = -1;
|
||||
}
|
||||
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
if (!activity_prefs_tracking_is_enabled() && pebble_task_get_current() == PebbleTask_App) {
|
||||
health_tracking_ui_app_show_disabled();
|
||||
}
|
||||
|
||||
// Update derived metrics
|
||||
prv_update_real_time_derived_metrics();
|
||||
|
||||
ActivityMetricInfo m_info;
|
||||
activity_metrics_prv_get_metric_info(metric, &m_info);
|
||||
|
||||
if (history_len == 0) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Clip history length
|
||||
history_len = MIN(history_len, ACTIVITY_HISTORY_DAYS);
|
||||
if (!m_info.has_history) {
|
||||
history_len = 1;
|
||||
}
|
||||
|
||||
// Fill in current value
|
||||
if (m_info.value_p) {
|
||||
history[0] = m_info.converter(*m_info.value_p);
|
||||
} else {
|
||||
PBL_ASSERTN(m_info.value_u32p && (m_info.converter == prv_convert_none));
|
||||
history[0] = *m_info.value_u32p;
|
||||
}
|
||||
ACTIVITY_LOG_DEBUG("get current metric %"PRIi32" : %"PRIi32"", (int32_t)metric, history[0]);
|
||||
|
||||
// Look up historical values
|
||||
if (history_len > 1) {
|
||||
// Read from the history stored in settings
|
||||
ActivitySettingsValueHistory setting_history = {};
|
||||
SettingsFile *file = activity_private_settings_open();
|
||||
if (!file) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Settings file DNE. No need to continue getting metric");
|
||||
success = false;
|
||||
goto unlock;
|
||||
}
|
||||
settings_file_get(file, &m_info.settings_key, sizeof(m_info.settings_key), &setting_history,
|
||||
sizeof(setting_history));
|
||||
for (uint32_t i = 1; i < history_len; i++) {
|
||||
history[i] = m_info.converter(setting_history.values[i]);
|
||||
ACTIVITY_LOG_DEBUG("get metric %"PRIi32" %"PRIu32" days ago: %"PRIi32"", (int32_t)metric,
|
||||
i, history[i]);
|
||||
}
|
||||
activity_private_settings_close(file);
|
||||
}
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
DEFINE_SYSCALL(bool, sys_activity_get_metric, ActivityMetric metric,
|
||||
uint32_t history_len, int32_t *history) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
if (history) {
|
||||
syscall_assert_userspace_buffer(history, history_len * sizeof(*history));
|
||||
}
|
||||
}
|
||||
|
||||
return activity_get_metric(metric, history_len, history);
|
||||
}
|
||||
534
src/fw/services/normal/activity/activity_private.h
Normal file
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "activity.h"
|
||||
#include "hr_util.h"
|
||||
|
||||
#include "applib/event_service_client.h"
|
||||
#include "kernel/events.h"
|
||||
#include "os/mutex.h"
|
||||
#include "services/normal/data_logging/data_logging_service.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define ACTIVITY_LOG_DEBUG(fmt, args...) \
|
||||
PBL_LOG_D(LOG_DOMAIN_ACTIVITY, LOG_LEVEL_DEBUG, fmt, ## args)
|
||||
|
||||
#define ACTIVITY_HEXDUMP(data, length) \
|
||||
PBL_HEXDUMP_D(LOG_DOMAIN_DATA_ACTIVITY, LOG_LEVEL_DEBUG, data, length)
|
||||
|
||||
// How often we update settings with the current step/sleep stats for today.
|
||||
#define ACTIVITY_SETTINGS_UPDATE_MIN 15
|
||||
|
||||
// How often we recompute the activity sessions (like sleep, walks, runs). This has significant
|
||||
// enough CPU requirements to warrant only recomputing occasionally
|
||||
#define ACTIVITY_SESSION_UPDATE_MIN 15
|
||||
|
||||
// Every scalar metric and setting is stored in globals and in the settings file using this
|
||||
// typedef
|
||||
typedef uint16_t ActivityScalarStore;
|
||||
#define ACTIVITY_SCALAR_MAX UINT16_MAX
|
||||
|
||||
// Each step average interval covers this many minutes
|
||||
#define ACTIVITY_STEP_AVERAGES_MINUTES (MINUTES_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES)
|
||||
|
||||
// flash vs. the most amount of data we could lose if we reset.
|
||||
#define ACTIVITY_STEP_AVERAGES_PER_KEY 4
|
||||
#define ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY \
|
||||
(ACTIVITY_NUM_METRIC_AVERAGES / ACTIVITY_STEP_AVERAGES_PER_KEY)
|
||||
|
||||
// If we see at least this many steps in a minute, it was an "active minute"
|
||||
#define ACTIVITY_ACTIVE_MINUTE_MIN_STEPS 40
|
||||
|
||||
// We consider any sleep session that ends after this minute of the day (representing 9pm) as
|
||||
// part of the next day's sleep
|
||||
#define ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (21 * MINUTES_PER_HOUR)
|
||||
|
||||
// Default HeartRate sampling period (Must take a sample every X seconds by default)
|
||||
#define ACTIVITY_DEFAULT_HR_PERIOD_SEC (10 * SECONDS_PER_MINUTE)
|
||||
|
||||
// Default HeartRate sampling ON time (Stays on for X seconds every
|
||||
// ACTIVITY_DEFAULT_HR_PERIOD_SEC seconds)
|
||||
#define ACTIVITY_DEFAULT_HR_ON_TIME_SEC (SECONDS_PER_MINUTE)
|
||||
|
||||
// Turn off the HR device after we've received X number of thresholded samples
|
||||
#define ACTIVITY_MIN_NUM_SAMPLES_SHORT_CIRCUIT (15)
|
||||
|
||||
// The minimum number of samples needed before we can approximate the user's HR zone
|
||||
#define ACTIVITY_MIN_NUM_SAMPLES_FOR_HR_ZONE (10)
|
||||
|
||||
#define ACTIVITY_MIN_HR_QUALITY_THRESH (HRMQuality_Good)
|
||||
|
||||
// HRM Subscription values during ON and OFF periods
|
||||
#define ACTIVITY_HRM_SUBSCRIPTION_ON_PERIOD_SEC (1)
|
||||
#define ACTIVITY_HRM_SUBSCRIPTION_OFF_PERIOD_SEC (SECONDS_PER_DAY)
|
||||
|
||||
// Max number of stored HR samples to compute the median
|
||||
#define ACTIVITY_MAX_HR_SAMPLES (3 * SECONDS_PER_MINUTE)
|
||||
|
||||
// Conversion factors
|
||||
#define ACTIVITY_DAG_PER_KG 100
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Settings file info and keys
|
||||
#define ACTIVITY_SETTINGS_FILE_NAME "activity"
|
||||
#define ACTIVITY_SETTINGS_FILE_LEN 0x4000
|
||||
|
||||
// The version of our settings file
|
||||
// Version 1 - ActivitySettingsKeyVersion didn't exist
|
||||
// Version 2 - Changed file size from 2k to 16k
|
||||
#define ACTIVITY_SETTINGS_CURRENT_VERSION 2
|
||||
|
||||
typedef struct {
|
||||
uint32_t utc_sec; // timestamp of first entry in list
|
||||
// One entry per day. The most recent day (today) is stored at index 0
|
||||
ActivityScalarStore values[ACTIVITY_HISTORY_DAYS];
|
||||
} ActivitySettingsValueHistory;
|
||||
|
||||
|
||||
// Keys of the settings we save in our settings file.
|
||||
typedef enum {
|
||||
ActivitySettingsKeyInvalid = 0, // Used for error discovery
|
||||
ActivitySettingsKeyVersion, // uint16_t: ACTIVITY_SETTINGS_CURRENT_VERSION
|
||||
ActivitySettingsKeyUnused0, // Unused
|
||||
ActivitySettingsKeyUnused1, // Unused
|
||||
ActivitySettingsKeyUnused2, // Unused
|
||||
ActivitySettingsKeyUnused3, // Unused
|
||||
|
||||
ActivitySettingsKeyStepCountHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeyStepMinutesHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeyUnused4, // Unused
|
||||
ActivitySettingsKeyDistanceMetersHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeySleepTotalMinutesHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeySleepDeepMinutesHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeySleepEntryMinutesHistory, // ActivitySettingsValueHistory
|
||||
// How long it took to fall asleep
|
||||
ActivitySettingsKeySleepEnterAtHistory, // ActivitySettingsValueHistory
|
||||
// What time the user fell asleep. Measured in
|
||||
// minutes after midnight.
|
||||
ActivitySettingsKeySleepExitAtHistory, // ActivitySettingsValueHistory
|
||||
// What time the user woke up. Measured in
|
||||
// minutes after midnight
|
||||
ActivitySettingsKeySleepState, // uint16_t
|
||||
ActivitySettingsKeySleepStateMinutes, // uint16_t
|
||||
ActivitySettingsKeyStepAveragesWeekdayFirst, // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
|
||||
ActivitySettingsKeyStepAveragesWeekdayLast =
|
||||
ActivitySettingsKeyStepAveragesWeekdayFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,
|
||||
|
||||
ActivitySettingsKeyStepAveragesWeekendFirst, // ACTIVITY_STEP_AVERAGES_PER_CHUNK * uint16_t
|
||||
ActivitySettingsKeyStepAveragesWeekendLast =
|
||||
ActivitySettingsKeyStepAveragesWeekendFirst + ACTIVITY_STEP_AVERAGES_KEYS_PER_DAY - 1,
|
||||
ActivitySettingsKeyAgeYears, // uint16_t: age in years
|
||||
|
||||
ActivitySettingsKeyUnused5, // Unused
|
||||
|
||||
ActivitySettingsKeyInsightSleepRewardTime, // time_t: time we last showed the sleep reward
|
||||
// This will be 0 if we haven't triggered one yet
|
||||
ActivitySettingsKeyInsightActivityRewardTime, // time_t: time we last showed the activity reward
|
||||
// This will be 0 if we haven't triggered one yet
|
||||
ActivitySettingsKeyInsightActivitySummaryState, // SummaryPinLastState: the UUID and last time the
|
||||
// pin was added
|
||||
ActivitySettingsKeyInsightSleepSummaryState, // SummaryPinLastState: the UUID and last time the
|
||||
// pin was added
|
||||
ActivitySettingsKeyRestingKCaloriesHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeyActiveKCaloriesHistory, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeyLastSleepActivityUTC, // time_t: UTC timestamp of the last sleep related
|
||||
// activity we logged to analytics
|
||||
ActivitySettingsKeyLastRestfulSleepActivityUTC, // time_t: UTC timestamp of the last restful sleep
|
||||
// related activity we logged to analytics
|
||||
ActivitySettingsKeyLastStepActivityUTC, // time_t: UTC timestamp of the last step related
|
||||
// activity we logged to analytics
|
||||
ActivitySettingsKeyStoredActivities, // ActivitySession[
|
||||
// ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT]
|
||||
ActivitySettingsKeyInsightNapSessionTime, // time_t: time we last showed the nap pin
|
||||
ActivitySettingsKeyInsightActivitySessionTime, // time_t: time we last showed the activity pin
|
||||
ActivitySettingsKeyLastVMC, // uint16_t: the VMC at the last processed minute
|
||||
ActivitySettingsKeyRestingHeartRate, // ActivitySettingsValueHistory
|
||||
ActivitySettingsKeyHeartRateZone1Minutes,
|
||||
ActivitySettingsKeyHeartRateZone2Minutes,
|
||||
ActivitySettingsKeyHeartRateZone3Minutes,
|
||||
} ActivitySettingsKey;
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Internal structs
|
||||
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivityStepData
|
||||
// is an ActivityScalarStore
|
||||
typedef struct {
|
||||
ActivityScalarStore steps;
|
||||
ActivityScalarStore step_minutes;
|
||||
ActivityScalarStore distance_meters;
|
||||
ActivityScalarStore resting_kcalories;
|
||||
ActivityScalarStore active_kcalories;
|
||||
} ActivityStepData;
|
||||
|
||||
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that every element of ActivitySleepData
|
||||
// is an ActivityScalarStore
|
||||
typedef struct {
|
||||
ActivityScalarStore total_minutes;
|
||||
ActivityScalarStore restful_minutes;
|
||||
ActivityScalarStore enter_at_minute; // minutes after midnight
|
||||
ActivityScalarStore exit_at_minute; // minutes after midnight
|
||||
ActivityScalarStore cur_state; // HealthActivity
|
||||
ActivityScalarStore cur_state_elapsed_minutes;
|
||||
} ActivitySleepData;
|
||||
|
||||
// IMPORTANT: activity_metrics_prv_get_metric_info() assumes that elements of
|
||||
// ActivityHeartRateData are ActivityScalarStore by default. The update_time_utc is
|
||||
// specially coded as a 32-bit metric and is allowed to be because we don't persist it in
|
||||
// the settings file and it has no history
|
||||
typedef struct {
|
||||
ActivityScalarStore current_bpm; // Most current reading
|
||||
uint32_t current_update_time_utc; // Timestamp of the current HR reading
|
||||
ActivityScalarStore current_hr_zone;
|
||||
ActivityScalarStore resting_bpm;
|
||||
ActivityScalarStore current_quality; // HRMQuality
|
||||
ActivityScalarStore last_stable_bpm;
|
||||
uint32_t last_stable_bpm_update_time_utc; // Timestamp of the last stable BPM
|
||||
ActivityScalarStore previous_median_bpm; // Most recently calculated median HR in a minute
|
||||
int32_t previous_median_total_weight_x100;
|
||||
ActivityScalarStore minutes_in_zone[HRZoneCount];
|
||||
bool is_hr_elevated;
|
||||
} ActivityHeartRateData;
|
||||
|
||||
|
||||
// This callback used to convert a metric from the storage format (as a ActivityScalarStore) into
|
||||
// the return format (uint32_t) returned by activity_get_metric. It might convert minutes to
|
||||
// seconds, etc.
|
||||
typedef uint32_t (*ActivityMetricConverter)(ActivityScalarStore storage_value);
|
||||
|
||||
// Filled in by activity_metrics_prv_get_metric_info()
|
||||
typedef struct {
|
||||
ActivityScalarStore *value_p; // pointer to storage in globals
|
||||
uint32_t *value_u32p; // alternate value pointer for 32-bit metrics. These
|
||||
// can NOT have history and settings_key MUST be
|
||||
// ActivitySettingsKeyInvalid.
|
||||
bool has_history; // True if this metric has history. This determines the
|
||||
// size of the value as stored in settings
|
||||
ActivitySettingsKey settings_key; // Settings key for this value
|
||||
ActivityMetricConverter converter; // convert from storage value to return value.
|
||||
} ActivityMetricInfo;
|
||||
|
||||
// Used by activity_feed_samples
|
||||
typedef struct {
|
||||
uint16_t num_samples;
|
||||
AccelRawData data[];
|
||||
} ActivityFeedSamples;
|
||||
|
||||
// Version of our legacy sleep session logging records (prior to FW 3.11). NOTE: The version
|
||||
// field is treated as a bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set,
|
||||
// we are free to add more fields to the end of ActivityLegacySleepSessionDataLoggingRecord and the
|
||||
// mobile app will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app
|
||||
// will know that it has no chance of parsing the blob (until the mobile app is updated of course).
|
||||
#define ACTIVITY_SLEEP_SESSION_LOGGING_VERSION 1
|
||||
|
||||
// Data logging record used to send sleep sessions to the phone
|
||||
typedef struct PACKED {
|
||||
uint16_t version; // set to ACTIVITY_SLEEP_SESSION_LOGGING_VERSION
|
||||
int32_t utc_to_local; // Add this to UTC to get local time
|
||||
uint32_t start_utc; // The start time in UTC
|
||||
uint32_t end_utc; // The end time in UTC
|
||||
uint32_t restful_secs;
|
||||
} ActivityLegacySleepSessionDataLoggingRecord;
|
||||
|
||||
// Version of our activity session logging records. NOTE: The version field is treated as a
|
||||
// bitfield. For version 1, only bit 0 is set. As long as we keep bit 0 set, we are free to
|
||||
// add more fields to the end of ActivitySessionDataLoggingRecord and the mobile app
|
||||
// will continue to assume it can parse the blob. If bit 0 is cleared, the mobile app will know that
|
||||
// it has no chance of parsing the blob (until the mobile app is updated of course).
|
||||
#define ACTIVITY_SESSION_LOGGING_VERSION 3
|
||||
|
||||
// Data logging record used to send activity sessions to the phone
|
||||
// NOTE: modifying this struct requires a bump to the ACTIVITY_SESSION_LOGGING_VERSION and
|
||||
// an update to documentation on this wiki page:
|
||||
// https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=46301269
|
||||
typedef struct PACKED {
|
||||
uint16_t version; // set to ACTIVITY_SESSION_LOGGING_VERSION
|
||||
uint16_t size; // size of this structure
|
||||
uint16_t activity; // ActivitySessionType: the type of activity
|
||||
int32_t utc_to_local; // Add this to UTC to get local time
|
||||
uint32_t start_utc; // The start time in UTC
|
||||
uint32_t elapsed_sec; // Elapsed time in seconds
|
||||
|
||||
// New fields add in version 3
|
||||
union {
|
||||
ActivitySessionDataStepping step_data;
|
||||
ActivitySessionDataSleeping sleep_data;
|
||||
};
|
||||
} ActivitySessionDataLoggingRecord;
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Globals
|
||||
|
||||
// Support for raw accel sample collection
|
||||
typedef struct {
|
||||
// The data logging session for the current sample collection session
|
||||
DataLoggingSession *dls_session;
|
||||
|
||||
// Most recently encoded accel sample value. Used for detecting and encoding runs of the same
|
||||
// value
|
||||
uint32_t prev_sample; // See comments in ActivityRawSamplesRecord for encoding
|
||||
uint8_t run_size; // run size of prev_sample
|
||||
|
||||
// The currently forming record
|
||||
ActivityRawSamplesRecord record;
|
||||
|
||||
// large enough to base64 encode half of the record at once.
|
||||
char base64_buf[sizeof(ActivityRawSamplesRecord)];
|
||||
|
||||
// True if we are forming the first record
|
||||
bool first_record;
|
||||
} ActivitySampleCollectionData;
|
||||
|
||||
// This type is defined in measurements_log.h but we can't include measurements_log.h in this header
|
||||
// because of build issues with the auto-generated SDK files.
|
||||
typedef void *ProtobufLogRef;
|
||||
|
||||
// Support for heart rate
|
||||
typedef struct {
|
||||
ActivityHeartRateData metrics; // ActivityMetrics for heart rate
|
||||
|
||||
HRMSessionRef hrm_session; // The HRM session we use
|
||||
ProtobufLogRef log_session; // The measurements log we send data to
|
||||
|
||||
bool currently_sampling; // Are we activity sampling the HR
|
||||
uint32_t toggled_sampling_at_ts; // When we last toggled our sampling rate
|
||||
// (from time_get_uptime_seconds)
|
||||
|
||||
uint32_t last_sample_ts; // When we last received a HR sample
|
||||
// (from time_get_uptime_seconds)
|
||||
|
||||
uint16_t num_samples; // number of samples in the past minute
|
||||
uint16_t num_quality_samples; // number of samples in the past minute that have met our
|
||||
// quality threshold ACTIVITY_MIN_HR_QUALITY_THRESH
|
||||
// NOTE: Used to short circuit
|
||||
// our HR polling when enough samples have been taken
|
||||
uint8_t samples[ACTIVITY_MAX_HR_SAMPLES]; // HR Samples stored
|
||||
uint8_t weights[ACTIVITY_MAX_HR_SAMPLES]; // HR Sample Weights
|
||||
} ActivityHRSupport;
|
||||
|
||||
typedef struct {
|
||||
// Mutex for serializing access to these globals
|
||||
PebbleRecursiveMutex *mutex;
|
||||
|
||||
// Semaphore used for waiting for KernelBG to finish a callback
|
||||
SemaphoreHandle_t bg_wait_semaphore;
|
||||
|
||||
// Accel session ref
|
||||
AccelServiceState *accel_session;
|
||||
|
||||
// Event Service to keep track of whether the charger is connected
|
||||
EventServiceInfo charger_subscription;
|
||||
|
||||
// Cumulative stats for today
|
||||
ActivityStepData step_data;
|
||||
ActivitySleepData sleep_data;
|
||||
|
||||
// We accumulate distance in mm to and active/resting calories in calories (not kcalories) to
|
||||
// minimize rounding errors since we increment them every time we get a new rate reading from the
|
||||
// algorithm (every 5 seconds).
|
||||
uint32_t distance_mm;
|
||||
uint32_t active_calories;
|
||||
uint32_t resting_calories;
|
||||
ActivityScalarStore last_vmc;
|
||||
uint8_t last_orientation;
|
||||
time_t rate_last_update_time;
|
||||
|
||||
// Most recently calculated minute average walking rate
|
||||
ActivityScalarStore steps_per_minute;
|
||||
ActivityScalarStore steps_per_minute_last_steps;
|
||||
|
||||
// The most recent minute that had any significant step activity. Used for computing
|
||||
// amount of time it takes to fall asleep
|
||||
uint16_t last_active_minute;
|
||||
|
||||
// Heart rate support
|
||||
ActivityHRSupport hr;
|
||||
|
||||
// Most recent values from prv_get_day()
|
||||
uint16_t cur_day_index;
|
||||
|
||||
// Modulo counter used to periodically update settings file
|
||||
int8_t update_settings_counter;
|
||||
|
||||
// Captured activity sessions
|
||||
uint16_t activity_sessions_count; // how many sessions we have captured
|
||||
ActivitySession activity_sessions[ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT];
|
||||
bool need_activities_saved; // true if activities need to be persisted
|
||||
|
||||
// Set to true when a new sleep session is registered
|
||||
bool sleep_sessions_modified;
|
||||
|
||||
// Exit time for the last sleep/step activities we logged. Used to prevent logging the same event
|
||||
// more than once.
|
||||
time_t logged_sleep_activity_exit_at_utc;
|
||||
time_t logged_restful_sleep_activity_exit_at_utc;
|
||||
time_t logged_step_activity_exit_at_utc;
|
||||
|
||||
// Data logging session used for sending activity sessions (introduced in v3.11)
|
||||
DataLoggingSession *activity_dls_session;
|
||||
|
||||
// Variables used for detecting "significant activity" events
|
||||
time_t activity_event_start_utc; // UTC of first active minute, 0 if none detected
|
||||
|
||||
// True if service has been enabled via services_set_runlevel.
|
||||
bool enabled_run_level;
|
||||
// True if the current state of charging allows the service to run.
|
||||
bool enabled_charging_state;
|
||||
|
||||
// True if activity tracking should be started. If enabled is false, this can still be true
|
||||
// and will tell us that we should re-start tracking once enabled gets set again.
|
||||
bool should_be_started;
|
||||
|
||||
// True if tracking has actually been started. This will only ever be set if enabled is also
|
||||
// true.
|
||||
bool started;
|
||||
|
||||
// Support for raw accel sample collection
|
||||
bool sample_collection_enabled;
|
||||
uint16_t sample_collection_session_id; // raw sample collection session id
|
||||
time_t sample_collection_seconds; // if enabled is true, the UTC when sample
|
||||
// collection started, else the # of seconds of
|
||||
// of data in recently ended session
|
||||
uint16_t sample_collection_num_samples; // number of samples collected so far
|
||||
ActivitySampleCollectionData *sample_collection_data;
|
||||
|
||||
// True if activity_start_tracking was called with test_mode = true
|
||||
bool test_mode;
|
||||
bool pending_test_cb;
|
||||
} ActivityState;
|
||||
|
||||
//! Get pointer to the activity state
|
||||
ActivityState *activity_private_state(void);
|
||||
|
||||
//! Get whether HRM is present
|
||||
bool activity_is_hrm_present(void);
|
||||
|
||||
//! Shared with activity_insights.c - opens the activity settings file
|
||||
//! IMPORTANT: This function must only be called during activity init routines or while holding
|
||||
//! the activity mutex
|
||||
SettingsFile *activity_private_settings_open(void);
|
||||
|
||||
//! Shared with activity_insights.c - closes the activity settings file
|
||||
//! IMPORTANT: This function must only be called during activity init routines or while holding
|
||||
//! the activity mutex
|
||||
void activity_private_settings_close(SettingsFile *file);
|
||||
|
||||
//! Used by test apps (running on firmware): Re-initialize activity service. If reset_settings is
|
||||
//! true, all persistent data is cleared
|
||||
//! @param[in] reset_settings if true, reset all stored settings
|
||||
//! @param[in] tracking_on if true, turn on tracking if not already on. Otherwise, preserve
|
||||
//! the current tracking status
|
||||
//! @param[in] sleep_history if not NULL, rewrite sleep history to these values
|
||||
//! @param[in] step_history if not NULL, rewrite step history to these values
|
||||
bool activity_test_reset(bool reset_settings, bool tracking_on,
|
||||
const ActivitySettingsValueHistory *sleep_history,
|
||||
const ActivitySettingsValueHistory *step_history);
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// Activity Sessions
|
||||
// Load in the stored activities from our settings file
|
||||
void activity_sessions_prv_init(SettingsFile *file, time_t utc_now);
|
||||
|
||||
// Get the UTC time bounds for the current day
|
||||
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc,
|
||||
time_t *exit_utc);
|
||||
|
||||
// Remove all activity sessions that are older than "today", those that are invalid because they
|
||||
// are in the future, and optionally those that are still ongoing.
|
||||
void activity_sessions_prv_remove_out_of_range_activity_sessions(time_t utc_sec,
|
||||
bool remove_ongoing);
|
||||
|
||||
//! Return true if the given activity type is sleep related
|
||||
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type);
|
||||
|
||||
//! Return true if the given activity type has session that is currently ongoing.
|
||||
bool activity_sessions_is_session_type_ongoing(ActivitySessionType activity_type);
|
||||
|
||||
//! Register a new activity session. This is called by the algorithm logic when it detects a new
|
||||
//! activity.
|
||||
void activity_sessions_prv_add_activity_session(ActivitySession *session);
|
||||
|
||||
//! Delete an activity session. This is called by the algorithm logic when it decides to not
|
||||
//! register a sleep session after all. Only sessions that are still 'ongoing' are allowed to be
|
||||
//! deleted.
|
||||
void activity_sessions_prv_delete_activity_session(ActivitySession *session);
|
||||
|
||||
//! Perform our once a minute activity session maintenance logic
|
||||
void activity_sessions_prv_minute_handler(time_t utc_sec);
|
||||
|
||||
//! Send an activity session to data logging
|
||||
void activity_sessions_prv_send_activity_session_to_data_logging(ActivitySession *session);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Activity Metrics
|
||||
|
||||
//! Init all metrics
|
||||
void activity_metrics_prv_init(SettingsFile *file, time_t utc_now);
|
||||
|
||||
//! Returns info about each metric we capture
|
||||
void activity_metrics_prv_get_metric_info(ActivityMetric metric, ActivityMetricInfo *info);
|
||||
|
||||
//! Perform our once a minute metrics maintenance logic
|
||||
void activity_metrics_prv_minute_handler(time_t utc_sec);
|
||||
|
||||
//! Returns the number of millimeters the user has walked so far today (since midnight)
|
||||
uint32_t activity_metrics_prv_get_distance_mm(void);
|
||||
|
||||
//! Returns the number of resting calories the user has consumed so far today (since midnight)
|
||||
uint32_t activity_metrics_prv_get_resting_calories(void);
|
||||
|
||||
//! Returns the number of active calories the user has consumed so far today (since midnight)
|
||||
uint32_t activity_metrics_prv_get_active_calories(void);
|
||||
|
||||
//! Retrieve the median heart rate and the total weight x100 since it was last reset.
|
||||
//! If no readings were recorded since it was reset, it will return 0.
|
||||
//! This median can be reset using activity_metrics_prv_reset_hr_stats().
|
||||
//! It is by default reset once a minute.
|
||||
void activity_metrics_prv_get_median_hr_bpm(int32_t *median_out,
|
||||
int32_t *heart_rate_total_weight_x100_out);
|
||||
|
||||
//! Retrieve the current HR zone since it was last reset.
|
||||
//! If no readings were recorded since it was reset, it will return 0.
|
||||
//! This HR zone can be reset using activity_metrics_prv_reset_hr_stats().
|
||||
//! It is by default reset once a minute.
|
||||
HRZone activity_metrics_prv_get_hr_zone(void);
|
||||
|
||||
//! Reset the average / median heart rate and hr zone
|
||||
void activity_metrics_prv_reset_hr_stats(void);
|
||||
|
||||
//! Feed in a new heart rate sample that will be used to update the median. This updates
|
||||
//! the value returned by activity_metrics_prv_get_median_hr_bpm().
|
||||
void activity_metrics_prv_add_median_hr_sample(PebbleHRMEvent *hrm_event, time_t now_utc,
|
||||
time_t now_uptime);
|
||||
|
||||
//! Returns the number of steps the user has taken so far today (since midnight)
|
||||
uint32_t activity_metrics_prv_get_steps(void);
|
||||
|
||||
//! Returns the number of steps the user has walked in the past minute
|
||||
ActivityScalarStore activity_metrics_prv_steps_per_minute(void);
|
||||
|
||||
//! Set a metric's value. Used from BlobDB to honor requests from the phone
|
||||
void activity_metrics_prv_set_metric(ActivityMetric metric, DayInWeek day, int32_t value);
|
||||
733
src/fw/services/normal/activity/activity_sessions.c
Normal file
@@ -0,0 +1,733 @@
|
||||
/*
|
||||
* 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 "services/common/analytics/analytics_event.h"
|
||||
#include "services/normal/alarms/alarm.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#include <pebbleos/cron.h>
|
||||
|
||||
#include "activity.h"
|
||||
#include "activity_algorithm.h"
|
||||
#include "activity_insights.h"
|
||||
#include "activity_private.h"
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Figure out the cutoff times for sleep and step activities for today given the current time
|
||||
static void prv_get_earliest_end_times_utc(time_t utc_sec, time_t *sleep_earliest_end_utc,
|
||||
time_t *step_earliest_end_utc) {
|
||||
time_t start_of_today_utc = time_util_get_midnight_of(utc_sec);
|
||||
int last_sleep_second_of_day = ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
|
||||
*sleep_earliest_end_utc = start_of_today_utc - (SECONDS_PER_DAY - last_sleep_second_of_day);
|
||||
*step_earliest_end_utc = start_of_today_utc;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Remove all activity sessions that are older than "today", those that are invalid because they
|
||||
// are in the future, and optionally those that are still ongoing.
|
||||
void activity_sessions_prv_remove_out_of_range_activity_sessions(time_t utc_sec,
|
||||
bool remove_ongoing) {
|
||||
ActivityState *state = activity_private_state();
|
||||
uint16_t num_sessions_to_clear = 0;
|
||||
uint16_t *session_entries = &state->activity_sessions_count;
|
||||
ActivitySession *sessions = state->activity_sessions;
|
||||
|
||||
// Figure out the cutoff times for sleep and step activities
|
||||
time_t sleep_earliest_end_utc;
|
||||
time_t step_earliest_end_utc;
|
||||
prv_get_earliest_end_times_utc(utc_sec, &sleep_earliest_end_utc, &step_earliest_end_utc);
|
||||
|
||||
for (uint32_t i = 0; i < *session_entries; i++) {
|
||||
time_t end_utc;
|
||||
if (activity_sessions_prv_is_sleep_activity(sessions[i].type)) {
|
||||
end_utc = sleep_earliest_end_utc;
|
||||
} else {
|
||||
end_utc = step_earliest_end_utc;
|
||||
}
|
||||
|
||||
// See if we should keep this activity
|
||||
time_t end_time = sessions[i].start_utc + (sessions[i].length_min * SECONDS_PER_MINUTE);
|
||||
if ((end_time >= end_utc) && (end_time <= utc_sec)
|
||||
&& (!remove_ongoing || !sessions[i].ongoing)) {
|
||||
// Keep it
|
||||
continue;
|
||||
}
|
||||
|
||||
// This one needs to be removed
|
||||
uint32_t remaining = *session_entries - i - 1;
|
||||
memcpy(&sessions[i], &sessions[i + 1], remaining * sizeof(*sessions));
|
||||
(*session_entries)--;
|
||||
num_sessions_to_clear++;
|
||||
i--;
|
||||
}
|
||||
|
||||
// Zero out unused sessions at end. This is important because when we re-init from stored
|
||||
// settings, we detect the number of sessions we have by checking for non-zero ones
|
||||
memset(&sessions[*session_entries], 0, num_sessions_to_clear * sizeof(ActivitySession));
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Return true if the given activity type is a sleep activity
|
||||
bool activity_sessions_prv_is_sleep_activity(ActivitySessionType activity_type) {
|
||||
switch (activity_type) {
|
||||
case ActivitySessionType_Sleep:
|
||||
case ActivitySessionType_RestfulSleep:
|
||||
case ActivitySessionType_Nap:
|
||||
case ActivitySessionType_RestfulNap:
|
||||
return true;
|
||||
case ActivitySessionType_Walk:
|
||||
case ActivitySessionType_Run:
|
||||
case ActivitySessionType_Open:
|
||||
return false;
|
||||
case ActivitySessionType_None:
|
||||
case ActivitySessionTypeCount:
|
||||
break;
|
||||
}
|
||||
WTF;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Return true if this is a valid activity session
|
||||
static bool prv_is_valid_activity_session(ActivitySession *session) {
|
||||
// Make sure the type is valid
|
||||
switch (session->type) {
|
||||
case ActivitySessionType_Sleep:
|
||||
case ActivitySessionType_RestfulSleep:
|
||||
case ActivitySessionType_Nap:
|
||||
case ActivitySessionType_RestfulNap:
|
||||
case ActivitySessionType_Walk:
|
||||
case ActivitySessionType_Run:
|
||||
case ActivitySessionType_Open:
|
||||
break;
|
||||
case ActivitySessionType_None:
|
||||
case ActivitySessionTypeCount:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid activity type: %d", (int)session->type);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The length must be reasonable
|
||||
if (session->length_min > ACTIVITY_SESSION_MAX_LENGTH_MIN) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid duration: %"PRIu16" ", session->length_min);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The flags must be valid
|
||||
if (session->reserved != 0) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Invalid flags: %d", (int)session->reserved);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Return true if two activity sessions are equal in their type and start time
|
||||
// @param[in] session_a ptr to first session
|
||||
// @param[in] session_b ptr to second session
|
||||
// @param[in] any_sleep if true, a match occurs if session_a and session_b are both sleep
|
||||
// activities, even if they are different types of sleep
|
||||
static bool prv_activity_sessions_equal(ActivitySession *session_a, ActivitySession *session_b,
|
||||
bool any_sleep) {
|
||||
bool type_matches;
|
||||
|
||||
const bool a_is_sleep = activity_sessions_prv_is_sleep_activity(session_a->type);
|
||||
const bool b_is_sleep = activity_sessions_prv_is_sleep_activity(session_b->type);
|
||||
|
||||
if (any_sleep && a_is_sleep && b_is_sleep) {
|
||||
type_matches = true;
|
||||
} else {
|
||||
type_matches = (session_a->type == session_b->type);
|
||||
}
|
||||
|
||||
return type_matches && (session_a->start_utc == session_b->start_utc);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Register a new activity. Called by the algorithm code when it detects a new activity.
|
||||
// If we already have this activity registered, it is updated.
|
||||
void activity_sessions_prv_add_activity_session(ActivitySession *session) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
if (!session->ongoing) {
|
||||
state->need_activities_saved = true;
|
||||
}
|
||||
|
||||
// Modifying a sleep session?
|
||||
if (activity_sessions_prv_is_sleep_activity(session->type)) {
|
||||
state->sleep_sessions_modified = true;
|
||||
}
|
||||
|
||||
// If this is an existing activity, update it
|
||||
ActivitySession *stored_session = state->activity_sessions;
|
||||
for (uint16_t i = 0; i < state->activity_sessions_count; i++, stored_session++) {
|
||||
if (prv_activity_sessions_equal(session, stored_session, true /*any_sleep*/)) {
|
||||
state->activity_sessions[i] = *session;
|
||||
goto unlock;
|
||||
}
|
||||
}
|
||||
|
||||
// If no more room, fail
|
||||
if (state->activity_sessions_count >= ACTIVITY_MAX_ACTIVITY_SESSIONS_COUNT) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "No more room for additional activities");
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Add this activity in
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Adding activity session %d, start_time: %"PRIu32,
|
||||
(int)session->type, (uint32_t)session->start_utc);
|
||||
state->activity_sessions[state->activity_sessions_count++] = *session;
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Delete an ongoing activity. Called by the algorithm code when it decides that an activity
|
||||
// that was previously ongoing should not be registered after all.
|
||||
void activity_sessions_prv_delete_activity_session(ActivitySession *session) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
// Look for this activity
|
||||
int found_session_idx = -1;
|
||||
ActivitySession *stored_session = state->activity_sessions;
|
||||
for (uint16_t i = 0; i < state->activity_sessions_count; i++, stored_session++) {
|
||||
if (prv_activity_sessions_equal(session, stored_session, false /*any_sleep*/)) {
|
||||
found_session_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If session not found, do nothing
|
||||
if (found_session_idx < 0) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Session to delete not found");
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// The session we are deleting must be ongoing
|
||||
PBL_ASSERT(stored_session->ongoing, "Only ongoing sessions can be deleted");
|
||||
|
||||
// Remove this session
|
||||
int num_to_move = state->activity_sessions_count - found_session_idx - 1;
|
||||
PBL_ASSERTN(num_to_move >= 0);
|
||||
if (num_to_move == 0) {
|
||||
memset(&state->activity_sessions[found_session_idx], 0, sizeof(ActivitySession));
|
||||
} else {
|
||||
memmove(&state->activity_sessions[found_session_idx],
|
||||
&state->activity_sessions[found_session_idx + 1],
|
||||
num_to_move * sizeof(ActivitySession));
|
||||
}
|
||||
state->activity_sessions_count--;
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Compute the total number of restful sleep seconds within a range of time
|
||||
static uint32_t prv_sleep_restful_seconds(uint32_t num_sessions, ActivitySession *sessions,
|
||||
time_t start_utc, time_t end_utc) {
|
||||
// Iterate through the sleep sessions, accumulating the total restful seconds seen between
|
||||
// start_utc and end_utc
|
||||
ActivitySession *session = sessions;
|
||||
uint32_t restful_sec = 0;
|
||||
for (uint32_t i = 0; i < num_sessions; i++, session++) {
|
||||
if ((session->type != ActivitySessionType_RestfulSleep)
|
||||
&& (session->type != ActivitySessionType_RestfulNap)) {
|
||||
continue;
|
||||
}
|
||||
if ((session->start_utc >= start_utc)
|
||||
&& ((time_t)(session->start_utc + (session->length_min * SECONDS_PER_MINUTE)) <= end_utc)) {
|
||||
restful_sec += session->length_min * SECONDS_PER_MINUTE;
|
||||
}
|
||||
}
|
||||
return restful_sec;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Send an activity session (including sleep sessions) to data logging
|
||||
void activity_sessions_prv_send_activity_session_to_data_logging(ActivitySession *session) {
|
||||
ActivityState *state = activity_private_state();
|
||||
time_t start_local = time_utc_to_local(session->start_utc);
|
||||
ActivitySessionDataLoggingRecord dls_record = {
|
||||
.version = ACTIVITY_SESSION_LOGGING_VERSION,
|
||||
.size = sizeof(ActivitySessionDataLoggingRecord),
|
||||
.activity = session->type,
|
||||
.utc_to_local = start_local - session->start_utc,
|
||||
.start_utc = (uint32_t)session->start_utc,
|
||||
.elapsed_sec = session->length_min * SECONDS_PER_MINUTE,
|
||||
};
|
||||
if (activity_sessions_prv_is_sleep_activity(session->type)) {
|
||||
dls_record.sleep_data = session->sleep_data;
|
||||
} else {
|
||||
dls_record.step_data = session->step_data;
|
||||
}
|
||||
|
||||
if (state->activity_dls_session == NULL) {
|
||||
// We don't need to be buffered since we are logging from the KernelBG task and this
|
||||
// saves having to allocate another buffer from the kernel heap.
|
||||
const bool buffered = false;
|
||||
const bool resume = false;
|
||||
Uuid system_uuid = UUID_SYSTEM;
|
||||
state->activity_dls_session = dls_create(
|
||||
DlsSystemTagActivitySession, DATA_LOGGING_BYTE_ARRAY, sizeof(dls_record),
|
||||
buffered, resume, &system_uuid);
|
||||
if (!state->activity_dls_session) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Error creating activity DLS session");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Log the record
|
||||
DataLoggingResult result = dls_log(state->activity_dls_session, &dls_record, 1);
|
||||
if (result != DATA_LOGGING_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Error %"PRIi32" while logging activity to DLS", (int32_t)result);
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Logging activity event %d, start_time: %"PRIu32", "
|
||||
"elapsed_min: %"PRIu16", end_time: %"PRIu32" ",
|
||||
(int)session->type, (uint32_t)session->start_utc, session->length_min,
|
||||
(uint32_t)session->start_utc + (session->length_min * SECONDS_PER_MINUTE));
|
||||
}
|
||||
|
||||
|
||||
// This structre holds stats we collected from going through a list of sleep sessions. It is
|
||||
// filled in by prv_compute_sleep_stats
|
||||
typedef struct {
|
||||
ActivityScalarStore total_minutes;
|
||||
ActivityScalarStore restful_minutes;
|
||||
time_t enter_utc; // When we entered sleep
|
||||
time_t today_exit_utc; // last exit time for today, for regular sleep only
|
||||
time_t last_exit_utc; // last exit time (sleep or nap, ignoring "today" boundary)
|
||||
time_t last_deep_exit_utc; // last deep sleep exit time (sleep or nap, ignoring "today" boundary)
|
||||
uint32_t last_session_len_sec;
|
||||
} ActivitySleepStats;
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Goes through a list of activity sessions and collect sleep stats
|
||||
// @param[in] now_utc the UTC time when the activity sessions were computed
|
||||
// @param[in] min_end_utc Only include sleep sessions that end AFTER this time
|
||||
// @param[in] max_end_utc Only include sleep sessions that end BEFORE this time
|
||||
// @param[in] last_processed_utc When activity sessions were computed, this is the UTC of the
|
||||
// most recent minute we had access to when activities were computed.
|
||||
// @param[out] stats this structure is filled in with the sleep stats
|
||||
// @return True if there were sleep session, False if not
|
||||
static bool prv_compute_sleep_stats(time_t now_utc, time_t min_end_utc, time_t max_end_utc,
|
||||
ActivitySleepStats *stats) {
|
||||
ActivityState *state = activity_private_state();
|
||||
*stats = (ActivitySleepStats) { };
|
||||
|
||||
bool rv = false;
|
||||
|
||||
// Iterate through the sleep sessions, accumulating the total sleep minutes, total
|
||||
// restful minutes, sleep enter time, and sleep exit time.
|
||||
ActivitySession *session = state->activity_sessions;
|
||||
for (uint32_t i = 0; i < state->activity_sessions_count; i++, session++) {
|
||||
// Get info on this session
|
||||
stats->last_session_len_sec = session->length_min * SECONDS_PER_MINUTE;
|
||||
time_t session_exit_utc = session->start_utc + stats->last_session_len_sec;
|
||||
|
||||
// Skip if it ended too early
|
||||
if (session_exit_utc < min_end_utc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((session->type == ActivitySessionType_Sleep)
|
||||
|| (session->type == ActivitySessionType_Nap)) {
|
||||
rv = true;
|
||||
// Accumulate sleep container stats
|
||||
if (session_exit_utc <= max_end_utc) {
|
||||
stats->total_minutes += session->length_min;
|
||||
}
|
||||
// Only regular sleep (not naps) should affect the enter and exit times
|
||||
if (session->type == ActivitySessionType_Sleep) {
|
||||
stats->enter_utc = (stats->enter_utc != 0) ? MIN(session->start_utc, stats->enter_utc)
|
||||
: session->start_utc;
|
||||
if ((session_exit_utc > stats->today_exit_utc) && (session_exit_utc <= max_end_utc)) {
|
||||
stats->today_exit_utc = session_exit_utc;
|
||||
}
|
||||
}
|
||||
stats->last_exit_utc = MAX(session_exit_utc, stats->last_exit_utc);
|
||||
} else if ((session->type == ActivitySessionType_RestfulSleep)
|
||||
|| (session->type == ActivitySessionType_RestfulNap)) {
|
||||
if (session_exit_utc <= max_end_utc) {
|
||||
// Accumulate restful sleep stats
|
||||
stats->restful_minutes += session->length_min;
|
||||
}
|
||||
stats->last_deep_exit_utc = MAX(stats->last_deep_exit_utc, session_exit_utc);
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Goes through a list of activity sessions and updates our sleep totals in the metrics
|
||||
// accordingly. We also take this opportunity to post a sleep metric changed event for the SDK
|
||||
// if the sleep totals have changed.
|
||||
// @param num_sessions the number of sessions in the sessions array
|
||||
// @param sessions array of activity sessions
|
||||
// @param now_utc the UTC time when the activity sessions were computed
|
||||
// @param max_end_utc Only include sleep sessions that end BEFORE this time
|
||||
// @param last_processed_utc When activity sessions were computed, this is the UTC of the
|
||||
// most recent minute we had access to when activities were computed.
|
||||
static void prv_update_sleep_metrics(time_t now_utc, time_t max_end_utc,
|
||||
time_t last_processed_utc) {
|
||||
ActivityState *state = activity_private_state();
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
// We will be filling in this structure based on the sleep sessions
|
||||
ActivitySleepData *sleep_data = &state->sleep_data;
|
||||
|
||||
// If we detect a change in the sleep metrics, we want to post a health event
|
||||
ActivitySleepData prev_sleep_data = *sleep_data;
|
||||
|
||||
// Collect stats on sleep
|
||||
ActivitySleepStats stats;
|
||||
if (!prv_compute_sleep_stats(now_utc, 0 /*min_end_utc*/, max_end_utc, &stats)) {
|
||||
// We didn't have any sleep data exit early
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Update our sleep metrics
|
||||
sleep_data->total_minutes = stats.total_minutes;
|
||||
sleep_data->restful_minutes = stats.restful_minutes;
|
||||
|
||||
// Fill in the enter and exit minute
|
||||
uint16_t enter_minute = time_util_get_minute_of_day(stats.enter_utc);
|
||||
uint16_t exit_minute = time_util_get_minute_of_day(stats.today_exit_utc);
|
||||
sleep_data->enter_at_minute = enter_minute;
|
||||
sleep_data->exit_at_minute = exit_minute;
|
||||
|
||||
// Fill in the rest of the sleep data metrics: the current state, and how long we have been
|
||||
// in the current state
|
||||
uint32_t delta_min = abs((int32_t)(last_processed_utc - stats.last_exit_utc))
|
||||
/ SECONDS_PER_MINUTE;
|
||||
|
||||
// Figure out our current state
|
||||
if (delta_min > 1) {
|
||||
// We are awake
|
||||
sleep_data->cur_state = ActivitySleepStateAwake;
|
||||
if (stats.last_exit_utc != 0) {
|
||||
sleep_data->cur_state_elapsed_minutes = (now_utc - stats.last_exit_utc)
|
||||
/ SECONDS_PER_MINUTE;
|
||||
} else {
|
||||
sleep_data->cur_state_elapsed_minutes = MINUTES_PER_DAY;
|
||||
}
|
||||
} else {
|
||||
// We are still sleeping
|
||||
if (stats.last_deep_exit_utc == stats.last_exit_utc) {
|
||||
sleep_data->cur_state = ActivitySleepStateRestfulSleep;
|
||||
} else {
|
||||
sleep_data->cur_state = ActivitySleepStateLightSleep;
|
||||
}
|
||||
sleep_data->cur_state_elapsed_minutes = (stats.last_session_len_sec + now_utc
|
||||
- stats.last_exit_utc) / SECONDS_PER_MINUTE;
|
||||
}
|
||||
|
||||
// If the info that is part of a health sleep event has changed, send out a notification event
|
||||
if ((sleep_data->total_minutes != prev_sleep_data.total_minutes)
|
||||
|| (sleep_data->restful_minutes != prev_sleep_data.restful_minutes)) {
|
||||
// Post a sleep changed event
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_HEALTH_SERVICE_EVENT,
|
||||
.health_event = {
|
||||
.type = HealthEventSleepUpdate,
|
||||
.data.sleep_update = {
|
||||
.total_seconds = sleep_data->total_minutes * SECONDS_PER_MINUTE,
|
||||
.total_restful_seconds = sleep_data->restful_minutes * SECONDS_PER_MINUTE,
|
||||
},
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
if (sleep_data->cur_state != prev_sleep_data.cur_state) {
|
||||
// Debug logging
|
||||
ACTIVITY_LOG_DEBUG("total_min: %"PRIu16", deep_min: %"PRIu16", state: %"PRIu16", "
|
||||
"state_min: %"PRIu16"",
|
||||
sleep_data->total_minutes,
|
||||
sleep_data->restful_minutes,
|
||||
sleep_data->cur_state,
|
||||
sleep_data->cur_state_elapsed_minutes);
|
||||
}
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
void activity_sessions_prv_get_sleep_bounds_utc(time_t now_utc, time_t *enter_utc,
|
||||
time_t *exit_utc) {
|
||||
// Get useful UTC times
|
||||
time_t start_of_today_utc = time_util_get_midnight_of(now_utc);
|
||||
int minute_of_day = time_util_get_minute_of_day(now_utc);
|
||||
int last_sleep_second_of_day = ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
|
||||
|
||||
int first_sleep_utc;
|
||||
if (minute_of_day < ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY) {
|
||||
// It is before the ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY (currently 9pm) cutoff, so use
|
||||
// the previou day's cutoff
|
||||
first_sleep_utc = start_of_today_utc - (SECONDS_PER_DAY - last_sleep_second_of_day);
|
||||
} else {
|
||||
// It is after 9pm, so use the 9pm cutoff
|
||||
first_sleep_utc = start_of_today_utc + last_sleep_second_of_day;
|
||||
}
|
||||
|
||||
// Compute stats for today
|
||||
ActivitySleepStats stats;
|
||||
prv_compute_sleep_stats(now_utc, first_sleep_utc /*min_utc*/, now_utc /*max_utc*/, &stats);
|
||||
*enter_utc = stats.enter_utc;
|
||||
*exit_utc = stats.today_exit_utc;
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------------
|
||||
// Goes through a list of activity sessions and logs new ones to data logging
|
||||
static void prv_log_activities(time_t now_utc) {
|
||||
ActivityState *state = activity_private_state();
|
||||
// Activity classes. All of the activities in a class share the same "_exit_at_utc" state in
|
||||
// the globals and the same settings key to persist it.
|
||||
enum {
|
||||
// for ActivitySessionType_Sleep, ActivitySessionType_Nap
|
||||
ActivityClass_Sleep = 0,
|
||||
// for ActivitySessionType_RestfulSleep, ActivitySessionType_RestfulNap
|
||||
ActivityClass_RestfulSleep = 1,
|
||||
// for ActivitySessionType_Walk, ActivitySessionType_Run, ActivitySessionType_Open
|
||||
ActivityClass_Step = 2,
|
||||
|
||||
// Leave at end
|
||||
ActivityClassCount,
|
||||
};
|
||||
|
||||
// List of event classes and info on each
|
||||
typedef struct {
|
||||
ActivitySettingsKey key; // settings key used to store last UTC time for this activity class
|
||||
time_t *exit_utc; // pointer to last UTC time in our globals
|
||||
bool modified; // true if we need to update it.
|
||||
} ActivityClassParams;
|
||||
|
||||
ActivityClassParams class_settings[ActivityClassCount] = {
|
||||
{ActivitySettingsKeyLastSleepActivityUTC,
|
||||
&state->logged_sleep_activity_exit_at_utc, false},
|
||||
|
||||
{ActivitySettingsKeyLastRestfulSleepActivityUTC,
|
||||
&state->logged_restful_sleep_activity_exit_at_utc, false},
|
||||
|
||||
{ActivitySettingsKeyLastStepActivityUTC,
|
||||
&state->logged_step_activity_exit_at_utc, false},
|
||||
};
|
||||
|
||||
bool logged_event = false;
|
||||
ActivitySession *session = state->activity_sessions;
|
||||
for (uint32_t i = 0; i < state->activity_sessions_count; i++, session++) {
|
||||
// Get info on this activity
|
||||
uint32_t session_len_sec = session->length_min * SECONDS_PER_MINUTE;
|
||||
time_t session_exit_utc = session->start_utc + session_len_sec;
|
||||
|
||||
ActivityClassParams *params = NULL;
|
||||
switch (session->type) {
|
||||
case ActivitySessionType_Sleep:
|
||||
case ActivitySessionType_Nap:
|
||||
params = &class_settings[ActivityClass_Sleep];
|
||||
break;
|
||||
|
||||
case ActivitySessionType_RestfulSleep:
|
||||
case ActivitySessionType_RestfulNap:
|
||||
params = &class_settings[ActivityClass_RestfulSleep];
|
||||
break;
|
||||
|
||||
case ActivitySessionType_Walk:
|
||||
case ActivitySessionType_Run:
|
||||
case ActivitySessionType_Open:
|
||||
params = &class_settings[ActivityClass_Step];
|
||||
break;
|
||||
case ActivitySessionType_None:
|
||||
case ActivitySessionTypeCount:
|
||||
WTF;
|
||||
break;
|
||||
}
|
||||
PBL_ASSERTN(params);
|
||||
|
||||
// If this is an event we already logged, or it's still onging, don't log it
|
||||
if (session->ongoing || (session_exit_utc <= *params->exit_utc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't log *any* sleep events until we know for sure we are awake. For restful sessions
|
||||
// in particular, even if the session ended, it might later be converted to a restful nap
|
||||
// session (after the container sleep session it is in finally ends).
|
||||
if (activity_sessions_prv_is_sleep_activity(session->type)) {
|
||||
if (state->sleep_data.cur_state != ActivitySleepStateAwake) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Log this event
|
||||
activity_sessions_prv_send_activity_session_to_data_logging(session);
|
||||
*params->exit_utc = session_exit_utc;
|
||||
params->modified = true;
|
||||
logged_event = true;
|
||||
}
|
||||
|
||||
// Update settings file if any events were logged
|
||||
if (logged_event) {
|
||||
mutex_lock_recursive(state->mutex);
|
||||
SettingsFile *file = activity_private_settings_open();
|
||||
if (file) {
|
||||
for (int i = 0; i < ActivityClassCount; i++) {
|
||||
ActivityClassParams *params = &class_settings[i];
|
||||
status_t result = settings_file_set(file, ¶ms->key, sizeof(params->key),
|
||||
params->exit_utc, sizeof(*params->exit_utc));
|
||||
if (result != S_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Error saving last event time");
|
||||
}
|
||||
}
|
||||
activity_private_settings_close(file);
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Load in the stored activities from our settings file
|
||||
void activity_sessions_prv_init(SettingsFile *file, time_t utc_now) {
|
||||
ActivityState *state = activity_private_state();
|
||||
ActivitySettingsKey key = ActivitySettingsKeyStoredActivities;
|
||||
|
||||
// Check the length first. The settings_file_get() call will not return an error if we ask
|
||||
// for less than the value size
|
||||
int stored_len = settings_file_get_len(file, &key, sizeof(key));
|
||||
if (stored_len != sizeof(state->activity_sessions)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Stored activities not found or incompatible");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read in the stored activities
|
||||
status_t result = settings_file_get(file, &key, sizeof(key), state->activity_sessions,
|
||||
sizeof(state->activity_sessions));
|
||||
if (result != S_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan to see how many valid activities we have.
|
||||
ActivitySession *session = state->activity_sessions;
|
||||
ActivitySession null_session = {};
|
||||
for (unsigned i = 0; i < ARRAY_LENGTH(state->activity_sessions); i++, session++) {
|
||||
if (!memcmp(session, &null_session, sizeof(null_session))) {
|
||||
// Empty session detected, we are done
|
||||
break;
|
||||
}
|
||||
if (!prv_is_valid_activity_session(session)) {
|
||||
// NOTE: We check for full validity as well as we can (rather than just checking for a
|
||||
// non-null activity start time for example) because there have been cases where
|
||||
// flash got corrupted, as in PBL-37848
|
||||
PBL_HEXDUMP(LOG_LEVEL_INFO, (void *)state->activity_sessions,
|
||||
sizeof(state->activity_sessions));
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid activity session detected - could be flash corrruption");
|
||||
|
||||
// Zero out flash so that we don't get into a reboot loop
|
||||
memset(state->activity_sessions, 0, sizeof(state->activity_sessions));
|
||||
settings_file_set(file, &key, sizeof(key), state->activity_sessions,
|
||||
sizeof(state->activity_sessions));
|
||||
WTF;
|
||||
}
|
||||
state->activity_sessions_count++;
|
||||
}
|
||||
|
||||
// Remove any activities that don't belong to "today" or that are ongoing
|
||||
activity_sessions_prv_remove_out_of_range_activity_sessions(utc_now, true /*remove_ongoing*/);
|
||||
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Restored %"PRIu16" activities from storage",
|
||||
state->activity_sessions_count);
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
void NOINLINE activity_sessions_prv_minute_handler(time_t utc_sec) {
|
||||
ActivityState *state = activity_private_state();
|
||||
time_t last_sleep_processed_utc = activity_algorithm_get_last_sleep_utc();
|
||||
|
||||
// Post process sleep sessions if we got any new sleep sessions that showed up
|
||||
if (state->sleep_sessions_modified) {
|
||||
// Post-process the sleep activities. This is where we relabel sleep sessions as nap
|
||||
// sessions, depending on time and length heuristics.
|
||||
activity_algorithm_post_process_sleep_sessions(state->activity_sessions_count,
|
||||
state->activity_sessions);
|
||||
state->sleep_sessions_modified = false;
|
||||
}
|
||||
|
||||
// Update sleep metrics
|
||||
// For today's metrics, we include sleep sessions that end between
|
||||
// ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY the previous day and ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY
|
||||
// today. activity_algorithm_get_activity_sessions() insures that we only get sessions
|
||||
// that end after ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY the previous day, so we just need to insure
|
||||
// that the end BEFORE ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY today.
|
||||
int last_sleep_utc_of_day = time_util_get_midnight_of(utc_sec)
|
||||
+ ACTIVITY_LAST_SLEEP_MINUTE_OF_DAY * SECONDS_PER_MINUTE;
|
||||
prv_update_sleep_metrics(utc_sec, last_sleep_utc_of_day,
|
||||
last_sleep_processed_utc);
|
||||
|
||||
// Log any new activites we detected to the phone
|
||||
prv_log_activities(utc_sec);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
bool activity_sessions_is_session_type_ongoing(ActivitySessionType type) {
|
||||
ActivityState *state = activity_private_state();
|
||||
bool rv = false;
|
||||
|
||||
mutex_lock_recursive(state->mutex);
|
||||
{
|
||||
for (int i = 0; i < state->activity_sessions_count; i++) {
|
||||
const ActivitySession *session = &state->activity_sessions[i];
|
||||
if (session->type == type && session->ongoing) {
|
||||
rv = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex_unlock_recursive(state->mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
DEFINE_SYSCALL(bool, sys_activity_sessions_is_session_type_ongoing, ActivitySessionType type) {
|
||||
return activity_sessions_is_session_type_ongoing(type);
|
||||
}
|
||||
BIN
src/fw/services/normal/activity/docs/fft_arm_swing.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/fw/services/normal/activity/docs/fft_driving.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/fw/services/normal/activity/docs/fft_non_walk.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/fw/services/normal/activity/docs/fft_walking.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
260
src/fw/services/normal/activity/docs/index.md
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
# Health Algorithms
|
||||
## Step Counting
|
||||
The step counting algorithm uses input from the accelerometer sensor to detect when the user is walking and how many steps have been taken. The accelerometer measures acceleration in each of 3 axes: x, y, and z. A perfectly still watch resting flat on a table will have 1G (1 “Gravity”) of acceleration in the z direction (due to gravity) and 0G’s in both the x and y axes. If you tilt the watch on its side for example, the z reading will go to 0 and then either x or y will show +/-1G (depending on which of the 4 sides you tilt it to). During watch movement, the x, y, and z readings will vary over time due to the watch’s changing orientation to gravity as well as the acceleration of the watch when it changes direction or speed. The pattern of these variations in the accelerometer readings over time can be used to detect if, and how fast, the user is stepping.
|
||||
|
||||
There are generally two dominant signals that show up in the accelerometer readings when a person is walking or running. The first is the signal due to your feet hitting the ground. This signal shows up as a spike in the accelerometer readings each time a foot hits the ground and will be more or less pronounced depending on the cushioning of your shoes, the type of flooring, etc. Another signal that can show up is from the arm swinging motion, and the strength of this will vary depending on the user’s walking style, whether their hand is in their pocket or not, whether they are carrying something, etc.
|
||||
|
||||
Of these two signals, the foot fall one is the most reliable since a user will not always be swinging their arms when walking. The goal of the step tracking algorithm is to isolate and detect this foot fall signal, while not getting confused by other signals (arm swings, random arm movements, etc.).
|
||||
|
||||
An overall outline of the approach taken by the stepping algorithm (glossing over the details for now) is as follows:
|
||||
|
||||
1. Separate the accelerometer sensor readings into 5 second epochs.
|
||||
2. For each 5 second epoch, compute an FFT (Fast Fourier Transform) to get the energy of the signal at different frequencies (called the _spectral density_)
|
||||
3. Examine the FFT output using a set of heuristics to identify the foot fall signal (if present) and its frequency.
|
||||
4. The frequency of the foot fall signal (if present) is outputted as the number of steps taken in that epoch.
|
||||
|
||||
As an example, if the FFT of a 5 second epoch shows a significant amount of foot fall signal at a frequency of 2Hz, we can assume the person has walked 10 steps (2Hz x 5 seconds) in that epoch.
|
||||
|
||||
### Example Data
|
||||
The following figure shows an example of the raw accelerometer data of a five-second epoch when a user is walking 10 steps. The x, y, and z axis signals are each shown in a different color. In this plot, there is a fairly evident five-cycle rhythm in the red and green axes, which happens to be the arm swing signal (for every 2 steps taken, only 1 full arm swing cycle occurs). The ten-cycle foot fall signal however is difficult to see in this particular sample because the arm swing is so strong.
|
||||
|
||||

|
||||
|
||||
The spectral density of that same walk sample, showing the amount of energy present at different frequencies, is shown in the following figure (this particular plot was generated from a sample longer than 5 seconds, so will be less noisy than any individual 5 second epoch). Here, the spectral density of the x, y, and z axes as well as the combined signal are each plotted in a different color. The _combined_ signal is computed as the magnitude of the x, y, and z spectral density at each frequency:
|
||||
combined[f] = sqrt(x[f]^2 + y[f]^2 + z[f]^2)
|
||||
|
||||
_Note that the y axis on this plot is not simply Power, but rather “Power / Average Power”, where “Average Power” is the average power of that particular signal._
|
||||
|
||||

|
||||
|
||||
You can see in the above spectral density plot that the dominant frequency in this example is 1Hz, corresponding to the 5 arm swings that occurred in these 5 seconds.
|
||||
|
||||
There are also several smaller peaks at the following frequencies:
|
||||
|
||||
- ~.25Hz: non-stepping signal, most likely random arm movements
|
||||
- 2Hz: stepping frequency + 2nd harmonic of arm swing
|
||||
- 3Hz: 3rd harmonic of arm swing
|
||||
- 4Hz: 2nd harmonic of steps + 4th harmonic of arm swing
|
||||
- 5Hz: 5th harmonic of arm swing
|
||||
- 8Hz: 4th harmonic of steps
|
||||
|
||||
The logic used to pull out and identify the stepping signal from the spectral density output will be described later, but first we have to introduce the concept of VMC, or Vector Magnitude Counts.
|
||||
|
||||
### VMC
|
||||
|
||||
VMC, or Vector Magnitude Counts, is a measure of the overall amount of movement in the watch over time. When the watch is perfectly still, the VMC will be 0 and greater amounts of movement result in higher VMC numbers. Running, for example results in a higher VMC than walking.
|
||||
|
||||
The VMC computation in Pebble Health was developed in conjunction with the Stanford Wearables lab and has been calibrated to match the VMC numbers produced by the [Actigraph](http://www.actigraphcorp.com/product-category/activity-monitors/) wrist-worn device. The Actigraph is commonly used today for medical research studies. The Stanford Wearables lab will be publishing the VMC computation used in the Pebble Health algorithm and this transparency of the algorithm will enable the Pebble to be used for medical research studies as well.
|
||||
|
||||
VMC is computed using the formula below. Before the accelerometer readings are incorporated into this computation however, each axis’ signal is run through a bandpass filter with a design of 0.25Hz to 1.75Hz.
|
||||
|
||||

|
||||
|
||||
The following pseudo code summarizes the VMC calculation for N samples worth of data in each axis. The accelerometer is sampled at 25 samples per second in the Health algorithm, so the VMC calculation for 1 second’s worth of data would process 25 samples from each axis. The _bandpass\_filter_ method referenced in this pseudo code is a convolution filter with a frequency response of 0.25 to 1.75Hz:
|
||||
|
||||
for each axis in x, y, z:
|
||||
axis_sum[axis] = 0
|
||||
for each sample in axis from 0 to N:
|
||||
filtered_sample = bandpass_filter(sample,
|
||||
filter_state)
|
||||
axis_sum[axis] += abs(filtered_sample)
|
||||
|
||||
VMC = scaling_factor * sqrt(axis_sum[x]^2
|
||||
+ axis_sum[y]^2
|
||||
+ axis_sum[z]^2)
|
||||
|
||||
|
||||
The step algorithm makes use of a VMC that is computed over each 5-second epoch. In addition to this 5-second VMC, the algorithm also computes a VMC over each minute of data. It saves this 1-minute VMC to persistent storage and sends it to data logging as well so that it will eventually get pushed to the phone and saved to a server. The 1-minute VMC values stored in persistent storage can be accessed by 3rd party apps through the Health API. It is these 1-minute VMC values that are designed to match the Actigraph computations and are most useful to medical researcher studies.
|
||||
|
||||
### Step Identification
|
||||
|
||||
As mentioned above, accelerometer data is processed in chunks of 5 seconds (one epoch) at a time. For each epoch, we use the combined spectral density (FFT output) and the 5-second VMC as inputs to the step identification logic.
|
||||
|
||||
#### Generating the FFT output
|
||||
The accelerometer is sampled at 25Hz, so each 5 second epoch comprises 125 samples in each axis. An FFT must have an input width which is a power of 2, so for each axis, we subtract the mean and then 0-extend to get 128 samples before computing the FFT for that axis.
|
||||
|
||||
An FFT of a real signal with 128 samples produces 128 outputs that represent 64 different frequencies. For each frequency, the FFT produces a real and an imaginary component (thus the 128 outputs for 64 different frequencies). The absolute value of the real and imaginary part denote the amplitude at a particular frequency, while the angle represents the phase of that frequency. It is the amplitude of each frequency that we are interested in, so we compute sqrt(real^2 + imag^2) of each frequency to end up with just 64 outputs.
|
||||
|
||||
Once the 64 values for each of the 3 axes have been computed, we combine them to get the overall energy at each frequency as follows:
|
||||
|
||||
for i = 0 to 63:
|
||||
energy[i] = sqrt(amp_x[i]^2 + amp_y[i]^2 + amp_z[i]^2)
|
||||
|
||||
In this final array of 64 elements, element 0 represents the DC component (0 Hz), element 1 represents a frequency of 1 cycle per epoch (1 / 5s = 0.2Hz), element 2 represents a frequency of 2 cycles per epoch (2 / 5s = 0.4Hz), etc. If the user is walking at a rate of 9 steps every 5 seconds, then a spike will appear at index 9 in this array (9 / 5s = 1.8Hz).
|
||||
|
||||
As an example, the following shows the FFT output of a user walking approximately 9 steps in an epoch (with very little arm swing):
|
||||
|
||||

|
||||
|
||||
#### Determining the stepping frequency
|
||||
|
||||
Once the FFT output and VMC have been obtained, we search for the most likely stepping frequency. The naive approach is to simply locate the frequency with the highest amplitude among all possible stepping frequencies. That would work fine for the example just shown above where there is a clear peak at index 9 of the FFT, which happens to be the stepping frequency.
|
||||
|
||||
However, for some users the arm swinging signal can be as large or larger than the stepping signal, and happens to be at half the stepping frequency. If a user is walking at a quick pace, the arm swinging signal could easily be misinterpreted as the stepping signal of a slow walk. The following is the FFT of such an example. The stepping signal shows up at indices 9 and 10, but there is a larger peak at the arm-swing frequency at index 5.
|
||||
|
||||

|
||||
|
||||
To deal with these possible confusions between arm-swing and stepping signals, the VMC is used to narrow down which range of frequencies the stepping is likely to fall in. Based on the VMC level, we search one of three different ranges of frequencies to find which frequency has the most energy and is the likely stepping frequency. When the VMC is very low, we search through a range of frequencies that represent a slow walk, and for higher VMCs we search through ranges of frequencies that represent faster walks or runs.
|
||||
|
||||
Once we find a stepping frequency candidate within the expected range, we further refine the choice by factoring in the harmonics of the stepping/arm swinging. Occasionally, a max signal in the stepping range does not represent the actual stepping rate - it might be off by one or two indices due to noise in the signal, or it might be very close in value to the neighboring frequency, making it hard to determine which is the optimal one to use. This is evident in the arm-swinging output shown above where the energy at index 9 is very close to the energy at index 10.
|
||||
|
||||
As mentioned earlier, we often see significant energy at the harmonics of both the arm-swinging and the stepping frequency. A harmonic is an integer multiple of the fundamental frequency (i.e. a stepping frequency of 2 Hz will result in harmonics at 4Hz, 6Hz, 8Hz, etc.). To further refine the stepping frequency choice, we evaluate all possible stepping frequencies near the first candidate (+/- 2 indices on each side) and add in the energy of the harmonics for each. For each evaluation, we add up the energy of that stepping frequency, the arm energy that would correspond to that stepping frequency (the energy at half the stepping frequency), and the 2nd thru 5th harmonics of both the stepping and arm-swinging frequencies. Among these 5 different candidate stepping frequencies, we then choose the one that ended up with the most energy overall.
|
||||
|
||||
At the end of this process, we have the most likely stepping frequency, **if** the user is indeed walking. The next step is to determine whether or not the user is in fact walking or not.
|
||||
|
||||
#### Classifying step vs non-step epochs
|
||||
|
||||
In order to classify an epoch as walking or non-walking, we compute and check a number of metrics from the FFT output.
|
||||
|
||||
The first such metric is the _walking score_ which is the sum of the energy in the stepping related frequencies (signal energy) divided by the sum of energy of all frequencies (total energy). The signal energy includes the stepping frequency, arm-swing frequency, and each of their harmonics. If a person is indeed walking, the majority of the signal will appear at these signal frequencies, yielding a high walking score.
|
||||
|
||||
The second constraint that the epoch must pass is that the VMC must be above a _minimum stepping VMC_ threshold. A higher threshold is used if the detected stepping rate is higher.
|
||||
|
||||
The third constraint that the epoch must pass is that the amount of energy in the very low frequency components must be relatively low. To evaluate this constraint, the amount of energy in the low frequency components (indices 0 through 4) is summed and then divided by the signal energy (computed above). If this ratio is below a set _low frequency ratio_ threshold, the constraint is satisfied. The example below is typical of many epochs that are non-stepping epochs - a large amount of the energy appears in the very low frequency area.
|
||||
|
||||

|
||||
|
||||
The fourth and final constraint that the epoch must pass is that the energy in the high frequencies must be relatively low. To evaluate this constraint, the amount of energy in the high frequency components (index 50 and above) is summed and then divided by the signal energy. If this ratio is below a set _high frequency ratio_ threshold, the constraint is satisfied. This helps to avoid counting driving epochs as stepping epochs. In many instances, the vibration of the engine in a car will show up as energy at these high frequencies as shown in the following diagram.
|
||||
|
||||

|
||||
|
||||
#### Partial Epochs
|
||||
|
||||
If the user starts or ends a walk in the middle of an epoch, the epoch will likely not pass the checks for a full fledged stepping epoch and these steps will therefore not get counted. To adjust for this undercounting, the algorithm introduces the concept of _partial epochs_.
|
||||
|
||||
The required _walking score_ and _minimum VMC_ are lower for a partial epoch vs. a normal epoch and there are no constraints on the low or high frequency signal ratios. To detect if an epoch is a _partial epoch_ we only check that the _walking score_ is above the _partial epoch walking score_ threshold and that the VMC is above the _partial epoch minimum VMC_ threshold.
|
||||
|
||||
If we detect a partial epoch, and either the prior or next epoch were classified as a stepping epoch, we add in half the number of steps that were detected in the adjacent stepping epoch. This helps to average out the undercounting that would normally occur at the start and end of a walk. For a very short walk that is less than 2 epochs long though, there is still a chance that no steps at all would be counted.
|
||||
|
||||
----
|
||||
|
||||
## Sleep Tracking
|
||||
|
||||
The sleep tracking algorithm uses the minute-level VMC values and minute-level average orientation of the watch to determine if/when the user is sleeping and whether or not the user is in “restful” sleep.
|
||||
|
||||
The minute-level VMC was described above. It gives a measure of the overall amount of movement seen by the watch in each minute.
|
||||
|
||||
The average orientation is a quantized (currently 8 bits) indication of the 3-D angle of the watch. It is computed once per minute based on the average accelerometer reading seen in each of the 3 axes. The angle of the watch in the X-Y plane is computed and quantized into the lower 4 bits and the angle of that vector with the Z-axis is then quantized and stored in the upper 4 bits.
|
||||
|
||||
### Sleep detection
|
||||
|
||||
The following discussion uses the term _sleep minute_. To determine if a minute is a _sleep minute_, we perform a convolution of the VMC values around that minute (using the 4 minutes immediately before and after the given minute) to generate a _filtered VMC_ and compare the _filtered VMC_ value to a threshold. If the result is below a determined sleep threshold, we count it as a _sleep minute_.
|
||||
|
||||
A rough outline of the sleep algorithm is as follows.
|
||||
|
||||
1. Sleep is entered if there are at least 5 _sleep minutes_ in a row.
|
||||
2. Sleep continues until there are at least 11 non-_sleep minutes_ in a row.
|
||||
3. If there were at least 60 minutes between the above sleep enter and sleep exit times, it is counted as a valid sleep session.
|
||||
|
||||
There are some exceptions to the above rules however:
|
||||
|
||||
- After sleep has been entered, if we see any minute with an exceptionally high _filtered VMC_, we end the sleep session immediately.
|
||||
- If it is early in the sleep session (the first 60 minutes), we require 14 non-_sleep minutes_ in a row to consider the user as awake instead of 11.
|
||||
- If at least 80% of the minutes have slight movement in them (even if each one is not high enough to make it a non-_sleep minute_), we consider the user awake.
|
||||
- If we detect that the watch was not being worn during the above time (see below), we invalidate the sleep session.
|
||||
|
||||
#### Restful sleep
|
||||
|
||||
Once we detect a sleep session using the above logic, we make another pass through that same data to see if there are any periods within that session that might be considered as _restful sleep_.
|
||||
|
||||
A _restful sleep minute_ is a minute where the _filtered VMC_ is below the _restful sleep minute_ threshold (this is lower than the normal _sleep minute_ threshold).
|
||||
|
||||
1. Restful sleep is entered if there are at least 20 _restful sleep minutes_ in a row.
|
||||
2. Restful sleep continues until there is at least 1 minute that is not a _restful sleep minute_.
|
||||
|
||||
### Detecting not-worn
|
||||
|
||||
Without some additional logic in place, the above rules would think a user is in a sleep session if the watch is not being worn. This is because there would be no movement and the VMC values would all be 0, or at least very low.
|
||||
|
||||
Once we detect a possible sleep session, we run that same data through the “not-worn” detection logic to determine if the watch was not being worn during that time. This is a set of heuristics that are designed to distinguish not-worn from sleep.
|
||||
|
||||
The following description uses the term _not worn minute_. A _not worn minute_ is a minute where **either** of the following is true:
|
||||
|
||||
- The VMC (the raw VMC, not _filtered VMC_) is below the _not worn_ threshold and the average orientation is same as it was the prior minute
|
||||
- The watch is charging
|
||||
|
||||
If we see **both** of the following, we assume the watch is not being worn:
|
||||
|
||||
1. There are at least 100 _not worn_ minutes in a row in the sleep session
|
||||
2. The _not worn_ section from #1 starts within 20 minutes of the start of the candidate sleep session and ends within 10 minutes of the end of the candidate sleep session.
|
||||
|
||||
The 100 minute required run length for _not worn_ might seem long, but it is not uncommon to see valid restful sleep sessions for a user that approach 100 minutes in length.
|
||||
|
||||
The orientation check is useful for situations where a watch is resting on a table, but encounters an occasional vibration due to floor or table shaking. This vibration shows up as a non-zero VMC and can look like the occasional movements that are normal during sleep. During actual sleep however, it is more likely that the user will change positions and end up at a different orientation on the next minute.
|
||||
|
||||
----
|
||||
|
||||
## System Integration
|
||||
|
||||
The following sections discuss how the step and sleep tracking algorithms are integrated into the firmware.
|
||||
|
||||
### Code organization
|
||||
|
||||
The core of the Health support logic is implemented in the activity service, which is in the `src/fw/services/normal/activity` directory. The 3rd party API, which calls into the activity service, is implemented in `src/fw/applib/health_service.c.`
|
||||
|
||||
The activity service implements the step and sleep algorithms and all of the supporting logic required to integrate the algorithms into the system. It has the following directory structure:
|
||||
|
||||
src/fw/services/normal/activity
|
||||
activity.c
|
||||
activity_insights.c
|
||||
kraepelin/
|
||||
kraepelin_algorithm.c
|
||||
activity_algorithm_kraepelin.c
|
||||
|
||||
- **activity.c** This is the main module for the activity service. It implements the API for the activity service and the high level glue layer around the underlying step and sleep algorithms. This module contains only algorithm agnostic code and should require minimal changes if an alternative implementation for step or sleep tracking is incorporated in the future.
|
||||
- **activity\_insights.c** This module implements the logic for generating Health timeline pins and notifications.
|
||||
- **kraepelin** This subdirectory contains the code for the Kraepelin step and sleep algorithm, which is the name given to the current set of algorithms described in this document. This logic is broken out from the generic interface code in activity.c to make it easier to substitute in alternative algorithm implementations in the future if need be.
|
||||
- **kraepelin\_algorithm.c** The core step and sleep algorithm code. This module is intended to be operating system agnostic and contains minimal calls to external functions. This module originated from open source code provided by the Stanford Wearables Lab.
|
||||
- **kraepelin/activity\_algorightm\_kraepelin.c** This module wraps the core algorithm code found in `kraepelin_algorithm.c` to make it conform to the internal activity service algorithm API expected by activity.c. An alternative algorithm implementation would just need to implement this same API in order for it to be accessible from `activity.c`. This modules handles all memory allocations, persistent storage management, and other system integration functions for the raw algorithm code found in kraepelin\_algorithm.c.
|
||||
|
||||
The 3rd party Health API is implemented in `src/fw/applib/health_service.c`. The `health_service.c` module implements the “user land” logic for the Health API and makes calls into the activity service (which runs in privileged mode) to access the raw step and sleep data.
|
||||
|
||||
### Step Counting
|
||||
|
||||
The `activity.c` module asks the algorithm implementation `activity_algorithm_kraepelin.c` what accel sampling rate it requires and handles all of the logic required to subscribe to the accel service with that sampling rate. All algorithmic processing (both step and sleep) in the activity service is always done from the KernelBG task, so `activity.c` subscribes to the accel service from a KernelBG callback and provides the accel service a callback method which is implemented in `activity.c`.
|
||||
|
||||
When `activity.c’s` accel service callback is called, it simply passes the raw accel data onto the underlying algorithm’s accel data handler implemented `activity_algorithm_kraepelin.c`. This handler in turn calls into the core algorithm code in `kraepelin_algorithm.c` to execute the raw step algorithm code and increments total steps by the number of steps returned by that method. Since the step algorithm in `kraepelin_algorithm.c` is based on five-second epochs and the accel service callback gets called once a second (25 samples per second), the call into `kraepelin_algorithm.c` will only return a non-zero step count value once every 5 times it is called.
|
||||
|
||||
Whenever a call is made to `activity.c` to get the total number of steps accumulated so far, `activity.c` will ask the `activity_algorithm_kraepelin.c` module for that count. The `activity_algorithm_kraepelin.c` module maintains that running count directly and returns it without needing to call into the raw algorithm code.
|
||||
|
||||
At midnight of each day, `activity.c` will make a call into `activity_algorithm_kraeplin.c` to reset the running count of steps back to 0.
|
||||
|
||||
### Sleep processing
|
||||
|
||||
For sleep processing, the `activity_algorithm_kraepelin.c` module has a much bigger role than it does for step processing. The core sleep algorithm in `kraepelin_algorithm.c` simply expects an array of VMC and average orientation values (one each per minute) and from that it identifies where the sleep sessions are. It is the role of `activity_algorithm_kraepelin.c` to build up this array of VMC values for the core algorithm and it does this by fetching the stored VMC and orientation values from persistent storage. The `activity_algorithm_kraepelin.c` module includes logic that periodically captures the VMC and orientation for each minute from the core algorithm module and saves those values to persistent storage for this purpose as well as for retrieval by the 3rd party API call that can be used by an app or worker to fetch historical minute-level values.
|
||||
|
||||
Currently, `activity.c` asks `activity_algorithm_kraepelin.c` to recompute sleep every 15 minutes. When asked to recompute sleep, `activity_algorithm_kraepelin.c` fetches the last 36 hours of VMC and orientation data from persistent storage and passes that array of values to the core sleep algorithm. When we compute sleep for the current day, we include all sleep sessions that *end* after midnight of the current day, so they may have started sometime before midnight. Including 36 hours of minute data means that, if asked to compute sleep at 11:59pm for example, we can go as far back as a sleep session that started at 6pm the prior day.
|
||||
|
||||
To keep memory requirements to a minimum, we encode each minute VMC value into a single byte for purposes of recomputing sleep. The raw VMC values that we store in persistent storage are 16-bit values, so we take the square root of each 16-bit value to compress it into a single byte. The average orientation is also encoded as a single byte. The 36 hours of minute data therefore requires that an array of 36 \* 60 \* 2 (4320) bytes be temporarily allocated and passed to the core sleep algorithm logic.
|
||||
|
||||
The core sleep logic in `kraepelin_algorithm.c` does not have any concept of what timestamp corresponds to each VMC value in the array, it only needs to describe the sleep sessions in terms of indices into the array. It is the role of `activity_algorithm_kraepelin.c` to translate these indices into actual UTC time stamps for use by the activity service.
|
||||
|
||||
|
||||
## Algorithm Development and Testing
|
||||
|
||||
There is a full set of unit tests in `tests/fw/services/activity` for testing the step and sleep algorithms. These tests run captured sample data through the `kraepelin_algorithm.c` algorithm code to verify the expected number of steps or sleep sessions.
|
||||
|
||||
### Step Algorithm Testing
|
||||
|
||||
For testing the step algorithm, raw accel data is fed into the step algorithm. This raw accel data is stored in files as raw tuples of x, y z, accelerometer readings and can be found in the `tests/fixtures/activity/step_samples` directory.
|
||||
|
||||
Although these files have the C syntax, they are not compiled but are read in and parsed by the unit tests at run-time. Each sample in each file contains meta-data that tells the unit test the expected number of steps for that sample, which is used to determine if the test passes or not.
|
||||
|
||||
To capture these samples, the activity service has a special mode that can be turned on for raw sample capture and the `Activity Demo` app has an item in its debug menu for turning on this mode. When this mode is turned on, the activity service saves raw samples to data logging, and at the same time, also captures the raw sample data to the Pebble logs as base64 encoded binary data. Capturing the accel data to the logs makes it super convenient to pull that data out of the watch simply by issuing a support request from the mobile app.
|
||||
|
||||
The `tools/activity/parse_activity_data_logging_records.py` script can be used to parse the raw accel samples out of a log file that was captured as part of a support request or from a binary file containing the data logging records captured via data logging. This tool outputs a text file, in C syntax, that can be used directly by the step tracking unit tests.
|
||||
|
||||
The unit test that processes all of the step samples in `tests/fixtures/activity/step_samples` insures that the number of steps computed by the algorithm for each sample is within the allowed minimum and maximum for that sample (as defined by the meta data included in each sample file). It also computes an overall error amount across all sample files and generates a nice summary report for reference purposes. When tuning the algorithm, these summary reports can be used to easily compare results for various potential changes.
|
||||
|
||||
### Sleep Algorithm Testing
|
||||
|
||||
For testing the sleep algorithm, minute-by-minute VMC values are fed into the algorithm code. The set of sample sleep files used by the unit tests are found in the `tests/fixtures/activity/sleep_samples` directory. As is the case for the step samples, these files are parsed by the unit tests at run-time even though they are in C syntax.
|
||||
|
||||
To capture these samples, the activity service has a special call that will result in a dump of the contents of the last 36 hours of minute data to the Pebble logs. The `Activity Demo` app has an item in its debug menu for triggering this call. When this call is made, the activity service will fetch the last 36 hours of minute data from persistent storage, base64 encode it, and put it into the Pebble logs so that it can be easily retrieved using a support request from the mobile app.
|
||||
|
||||
As is the case for step data, the `tools/activity/parse_activity_data_logging_records.py` script can also be used to extract the minute data out of a support request log file and will in turn generate a text file that can be directly parsed by the sleep algorithm unit tests.
|
||||
|
||||
Each sleep sample file contains meta data in it that provides upper and lower bounds for each of the sleep metrics that can be computed by the algorithm (total amount of sleep, total amount of restful sleep, sleep start time, sleep end time, etc.). These metrics are checked by the unit tests to determine if each sample passes.
|
||||
|
||||
Note that the minute-by-minute VMC values can always be captured up to 36 hours **after** a sleep issue has been discovered on the watch since the watch is always storing these minute statistics in persistent storage. In contrast, turning on capture of raw accel data for a step algorithm issue must be done before the user starts the activity since capturing raw accel data is too expensive (memory and power-wise) to leave on all the time.
|
||||
BIN
src/fw/services/normal/activity/docs/raw_accel_5s.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/fw/services/normal/activity/docs/spectial_density.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/fw/services/normal/activity/docs/vmc_formula.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
195
src/fw/services/normal/activity/health_util.c
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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 "health_util.h"
|
||||
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/activity/activity.h"
|
||||
#include "shell/prefs.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/units.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static void prv_convert_duration_to_hours_and_minutes(int duration_s, int *hours, int *minutes) {
|
||||
*hours = (duration_s / SECONDS_PER_HOUR) ?: INT_MIN;
|
||||
*minutes = ((duration_s % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) ?: INT_MIN;
|
||||
if (*minutes == INT_MIN && *hours == INT_MIN) {
|
||||
*hours = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int health_util_format_hours_and_minutes(char *buffer, size_t buffer_size, int duration_s,
|
||||
void *i18n_owner) {
|
||||
int hours;
|
||||
int minutes;
|
||||
prv_convert_duration_to_hours_and_minutes(duration_s, &hours, &minutes);
|
||||
int pos = 0;
|
||||
if (hours != INT_MIN) {
|
||||
pos += snprintf(buffer + pos, buffer_size - pos, i18n_get("%dH", i18n_owner), hours);
|
||||
if (minutes != INT_MIN && pos < (int)buffer_size - 1) {
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
}
|
||||
if (minutes != INT_MIN) {
|
||||
pos += snprintf(buffer + pos, buffer_size - pos, i18n_get("%dM", i18n_owner), minutes);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
int health_util_format_hours_minutes_seconds(char *buffer, size_t buffer_size, int duration_s,
|
||||
bool leading_zero, void *i18n_owner) {
|
||||
const int hours = duration_s / SECONDS_PER_HOUR;
|
||||
const int minutes = (duration_s % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
|
||||
const int seconds = (duration_s % SECONDS_PER_HOUR) % SECONDS_PER_MINUTE;
|
||||
if (hours > 0) {
|
||||
const char *fmt = leading_zero ? "%02d:%02d:%02d" : "%d:%02d:%02d";
|
||||
return snprintf(buffer, buffer_size, i18n_get(fmt, i18n_owner), hours, minutes, seconds);
|
||||
} else {
|
||||
const char *fmt = leading_zero ? "%02d:%02d" : "%d:%02d";
|
||||
return snprintf(buffer, buffer_size, i18n_get(fmt, i18n_owner), minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
int health_util_format_minutes_and_seconds(char *buffer, size_t buffer_size, int duration_s,
|
||||
void *i18n_owner) {
|
||||
int minutes = duration_s / SECONDS_PER_MINUTE;
|
||||
int seconds = duration_s % SECONDS_PER_MINUTE;
|
||||
return snprintf(buffer, buffer_size, i18n_get("%d:%d", i18n_owner), minutes, seconds);
|
||||
}
|
||||
|
||||
GTextNodeText *health_util_create_text_node(int buffer_size, GFont font, GColor color,
|
||||
GTextNodeContainer *container) {
|
||||
GTextNodeText *text_node = graphics_text_node_create_text(buffer_size);
|
||||
if (container) {
|
||||
graphics_text_node_container_add_child(container, &text_node->node);
|
||||
}
|
||||
text_node->font = font;
|
||||
text_node->color = color;
|
||||
return text_node;
|
||||
}
|
||||
|
||||
GTextNodeText *health_util_create_text_node_with_text(const char *text, GFont font, GColor color,
|
||||
GTextNodeContainer *container) {
|
||||
GTextNodeText *text_node = health_util_create_text_node(0, font, color, container);
|
||||
text_node->text = text;
|
||||
return text_node;
|
||||
}
|
||||
|
||||
void health_util_duration_to_hours_and_minutes_text_node(int duration_s, void *i18n_owner,
|
||||
GFont number_font, GFont units_font,
|
||||
GColor color,
|
||||
GTextNodeContainer *container) {
|
||||
int hours;
|
||||
int minutes;
|
||||
prv_convert_duration_to_hours_and_minutes(duration_s, &hours, &minutes);
|
||||
const int units_offset_y = fonts_get_font_height(number_font) - fonts_get_font_height(units_font);
|
||||
const int hours_and_minutes_buffer_size = sizeof("00");
|
||||
if (hours != INT_MIN) {
|
||||
GTextNodeText *hours_text_node = health_util_create_text_node(hours_and_minutes_buffer_size,
|
||||
number_font, color, container);
|
||||
snprintf((char *) hours_text_node->text, hours_and_minutes_buffer_size,
|
||||
i18n_get("%d", i18n_owner), hours);
|
||||
|
||||
GTextNodeText *hours_units_text_node = health_util_create_text_node_with_text(
|
||||
i18n_get("H", i18n_owner), units_font, color, container);
|
||||
hours_units_text_node->node.offset.y = units_offset_y;
|
||||
}
|
||||
|
||||
if (hours != INT_MIN && minutes != INT_MIN) {
|
||||
// add a space between the H and the number of minutes
|
||||
health_util_create_text_node_with_text(i18n_get(" ", i18n_owner), units_font, color, container);
|
||||
}
|
||||
|
||||
if (minutes != INT_MIN) {
|
||||
GTextNodeText *minutes_text_node = health_util_create_text_node(hours_and_minutes_buffer_size,
|
||||
number_font, color, container);
|
||||
snprintf((char *) minutes_text_node->text, hours_and_minutes_buffer_size,
|
||||
i18n_get("%d", i18n_owner), minutes);
|
||||
|
||||
GTextNodeText *minutes_units_text_node = health_util_create_text_node_with_text(
|
||||
i18n_get("M", i18n_owner), units_font, color, container);
|
||||
minutes_units_text_node->node.offset.y = units_offset_y;
|
||||
}
|
||||
}
|
||||
|
||||
void health_util_convert_fraction_to_whole_and_decimal_part(int numerator, int denominator,
|
||||
int* whole_part, int *decimal_part) {
|
||||
const int figure = ROUND(numerator * 100, denominator * 10);
|
||||
*whole_part = figure / 10;
|
||||
*decimal_part = figure % 10;
|
||||
}
|
||||
|
||||
int health_util_format_whole_and_decimal(char *buffer, size_t buffer_size, int numerator,
|
||||
int denominator) {
|
||||
int converted_distance_whole_part = 0;
|
||||
int converted_distance_decimal_part = 0;
|
||||
health_util_convert_fraction_to_whole_and_decimal_part(numerator, denominator,
|
||||
&converted_distance_whole_part,
|
||||
&converted_distance_decimal_part);
|
||||
const char *fmt_i18n = i18n_noop("%d.%d");
|
||||
const int rv = snprintf(buffer, buffer_size, i18n_get(fmt_i18n, buffer),
|
||||
converted_distance_whole_part, converted_distance_decimal_part);
|
||||
i18n_free(fmt_i18n, buffer);
|
||||
return rv;
|
||||
}
|
||||
|
||||
int health_util_get_distance_factor(void) {
|
||||
switch (shell_prefs_get_units_distance()) {
|
||||
case UnitsDistance_Miles:
|
||||
return METERS_PER_MILE;
|
||||
case UnitsDistance_KM:
|
||||
return METERS_PER_KM;
|
||||
case UnitsDistanceCount:
|
||||
break;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *health_util_get_distance_string(const char *miles_string, const char *km_string) {
|
||||
switch (shell_prefs_get_units_distance()) {
|
||||
case UnitsDistance_Miles:
|
||||
return miles_string;
|
||||
case UnitsDistance_KM:
|
||||
return km_string;
|
||||
case UnitsDistanceCount:
|
||||
break;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
int health_util_format_distance(char *buffer, size_t buffer_size, uint32_t distance_m) {
|
||||
return health_util_format_whole_and_decimal(buffer, buffer_size, distance_m,
|
||||
health_util_get_distance_factor());
|
||||
}
|
||||
|
||||
void health_util_convert_distance_to_whole_and_decimal_part(int distance_m, int *whole_part,
|
||||
int *decimal_part) {
|
||||
const int conversion_factor = health_util_get_distance_factor();
|
||||
health_util_convert_fraction_to_whole_and_decimal_part(distance_m, conversion_factor,
|
||||
whole_part, decimal_part);
|
||||
}
|
||||
|
||||
time_t health_util_get_pace(int time_s, int distance_meter) {
|
||||
if (!distance_meter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ROUND(time_s * health_util_get_distance_factor(), distance_meter);
|
||||
}
|
||||
136
src/fw/services/normal/activity/health_util.h
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/layer.h"
|
||||
#include "apps/system_apps/timeline/text_node.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//! The maximum number of text nodes needed in a text node container
|
||||
#define MAX_TEXT_NODES 5
|
||||
|
||||
//! Extra 4 bytes is for i18n purposes
|
||||
#define HEALTH_WHOLE_AND_DECIMAL_LENGTH (sizeof("00.0") + 4)
|
||||
|
||||
//! Format a duration in seconds to hours and minutes, e.g. "12H 59M"
|
||||
//! If duration is less than an hour, the format of "59M" is used.
|
||||
//! If duration is a multiple of an hour, the format of "12H" is used.
|
||||
//! If duration is 0, the string "0H" is used.
|
||||
//! @param[in,out] buffer the string buffer to write to
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param duration_s the duration is seconds
|
||||
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
|
||||
//! @return snprintf-style number of bytes needed to be written not including the null terminator
|
||||
int health_util_format_hours_and_minutes(char *buffer, size_t buffer_size, int duration_s,
|
||||
void *i18n_owner);
|
||||
|
||||
//! Create a text node and add it to the container and set the font and color
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param font GFont to be used for the text node
|
||||
//! @param color GColor to be used fot the text node
|
||||
//! @param container GTextNodeContainer that the text node will be added to
|
||||
GTextNodeText *health_util_create_text_node(int buffer_size, GFont font, GColor color,
|
||||
GTextNodeContainer *container);
|
||||
|
||||
//! Create a text node with text and add it to the container and set the font and color
|
||||
//! @param text the text string to be used for the text node
|
||||
//! @param font GFont to be used for the text node
|
||||
//! @param color GColor to be used fot the text node
|
||||
//! @param container GTextNodeContainer that the text node will be added to
|
||||
GTextNodeText *health_util_create_text_node_with_text(const char *text, GFont font, GColor color,
|
||||
GTextNodeContainer *container);
|
||||
|
||||
//! Format a duration in seconds to hours, minutes and seconds, e.g. "1:15:32"
|
||||
//! @param[in,out] buffer the string buffer to write to
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param duration_s the duration is seconds
|
||||
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
|
||||
//! @return snprintf-style number of bytes needed to be written not including the null terminator
|
||||
int health_util_format_hours_minutes_seconds(char *buffer, size_t buffer_size, int duration_s,
|
||||
bool leading_zero, void *i18n_owner);
|
||||
|
||||
//! Format a duration in seconds to minutes and seconds, e.g. "5:32"
|
||||
//! @param[in,out] buffer the string buffer to write to
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param duration_s the duration is seconds
|
||||
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
|
||||
//! @return snprintf-style number of bytes needed to be written not including the null terminator
|
||||
int health_util_format_minutes_and_seconds(char *buffer, size_t buffer_size, int duration_s,
|
||||
void *i18n_owner);
|
||||
|
||||
//! Format a duration in seconds to hours and minutes, e.g. "12H 59M", using text node
|
||||
//! number_font will be used for the nodes with hours and minutes,
|
||||
//! units_font will be used for the "H" and "M"
|
||||
//! If duration is less than an hour, the format of "59M" is used.
|
||||
//! If duration is a multiple of an hour, the format of "12H" is used.
|
||||
//! If duration is 0, the string "0H" is used.
|
||||
//! @param duration_s the duration is seconds
|
||||
//! @param i18n_owner i18n owner that must be called with i18n_free_all some time after usage
|
||||
//! @param number_font GFont to be used for the number text node
|
||||
//! @param units_font GFont to be used for the units text node
|
||||
//! @param color GColor to be used for the number and units text nodes
|
||||
//! @param container GTextNodeContainer that will have the new number and units text nodes added to
|
||||
void health_util_duration_to_hours_and_minutes_text_node(int duration_s, void *i18n_owner,
|
||||
GFont number_font, GFont units_font,
|
||||
GColor color,
|
||||
GTextNodeContainer *container);
|
||||
|
||||
//! Convert a fraction into its whole and decimal parts
|
||||
//! ex. 5/2 has a whole part of 2 and a decimal part of .5
|
||||
//! @param numerator the numerator of the fraction
|
||||
//! @param denominator the denominator of the fraction
|
||||
//! @param[out] whole_part the whole part of the decimal representation
|
||||
//! @param[out] decimal_part the decimal part of the decimal representation
|
||||
void health_util_convert_fraction_to_whole_and_decimal_part(int numerator, int denominator,
|
||||
int* whole_part, int *decimal_part);
|
||||
|
||||
//! Formats a fraction into its whole and decimal parts, e.g. "42.3"
|
||||
//! @param[in,out] buffer the string buffer to write to
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param numerator the numerator of the fraction
|
||||
//! @param denominator the denominator of the fraction
|
||||
//! @return number of bytes written to buffer not including the null terminator
|
||||
int health_util_format_whole_and_decimal(char *buffer, size_t buffer_size, int numerator,
|
||||
int denominator);
|
||||
|
||||
//! @return meters conversion factor for the user's distance pref
|
||||
int health_util_get_distance_factor(void);
|
||||
|
||||
//! @return the pace from a distance in meters and a time in seconds
|
||||
time_t health_util_get_pace(int time_s, int distance_meter);
|
||||
|
||||
//! Get the meters units string for the user's distance pref
|
||||
//! @param miles_string the units string to use if the user's preference is miles
|
||||
//! @param km_string the units string to use if the user's preference is kilometers
|
||||
//! @return meters units string matching the user's distance pref
|
||||
const char *health_util_get_distance_string(const char *miles_string, const char *km_string);
|
||||
|
||||
//! Formats distance in meters based on the user's units preference, e.g. "42.3"
|
||||
//! @param[in,out] buffer the string buffer to write to
|
||||
//! @param buffer_size the size of the string buffer
|
||||
//! @param distance_m the distance in meters
|
||||
//! @return number of bytes written to buffer not including the null terminator
|
||||
int health_util_format_distance(char *buffer, size_t buffer_size, uint32_t distance_m);
|
||||
|
||||
//! Convert distance in meters its whole and decimal parts in the user's distance pref
|
||||
//! @param distance_m the distance in meters
|
||||
//! @param[out] whole_part the whole part of the converted decimal representation
|
||||
//! @param[out] decimal_part the decimal part of the converted decimal representation
|
||||
void health_util_convert_distance_to_whole_and_decimal_part(int distance_m, int *whole_part,
|
||||
int *decimal_part);
|
||||
40
src/fw/services/normal/activity/hr_util.c
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 "hr_util.h"
|
||||
|
||||
#include "activity.h"
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
HRZone hr_util_get_hr_zone(int bpm) {
|
||||
const int zone_thresholds[HRZone_Max] = {
|
||||
activity_prefs_heart_get_zone1_threshold(),
|
||||
activity_prefs_heart_get_zone2_threshold(),
|
||||
activity_prefs_heart_get_zone3_threshold(),
|
||||
};
|
||||
|
||||
HRZone zone;
|
||||
for (zone = HRZone_Zone0; zone < HRZone_Max; zone++) {
|
||||
if (bpm < zone_thresholds[zone]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
|
||||
bool hr_util_is_elevated(int bpm) {
|
||||
return bpm >= activity_prefs_heart_get_elevated_hr();
|
||||
}
|
||||
35
src/fw/services/normal/activity/hr_util.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum HRZone {
|
||||
HRZone_Zone0,
|
||||
HRZone_Zone1,
|
||||
HRZone_Zone2,
|
||||
HRZone_Zone3,
|
||||
|
||||
HRZoneCount,
|
||||
HRZone_Max = HRZone_Zone3,
|
||||
} HRZone;
|
||||
|
||||
//! Returns the HR Zone for a given BPM
|
||||
HRZone hr_util_get_hr_zone(int bpm);
|
||||
|
||||
//! Returns whether the BPM should be considered elevated
|
||||
bool hr_util_is_elevated(int bpm);
|
||||
263
src/fw/services/normal/activity/insights_settings.c
Normal file
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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 <inttypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "activity.h"
|
||||
#include "insights_settings.h"
|
||||
#include "os/mutex.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/size.h"
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_FILENAME "insights"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE 4096
|
||||
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY "version"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION 0
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION 4
|
||||
|
||||
static PebbleMutex *s_insight_settings_mutex;
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD_DEFAULT { \
|
||||
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
|
||||
.enabled = false, \
|
||||
.reward = { \
|
||||
.min_days_data = 6, \
|
||||
.continuous_min_days_data = 2, \
|
||||
.target_qualifying_days = 2, \
|
||||
.target_percent_of_median = 120, \
|
||||
.notif_min_interval_seconds = 7 * SECONDS_PER_DAY, \
|
||||
.sleep.trigger_after_wakeup_seconds = 2 * SECONDS_PER_HOUR \
|
||||
} \
|
||||
}
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY_DEFAULT { \
|
||||
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
|
||||
.enabled = true, \
|
||||
.summary = { \
|
||||
.above_avg_threshold = 10, \
|
||||
.below_avg_threshold = -10, \
|
||||
.fail_threshold = -50, \
|
||||
.sleep = { \
|
||||
.max_fail_minutes = 7 * MINUTES_PER_HOUR, \
|
||||
.trigger_notif_seconds = 30 * SECONDS_PER_MINUTE, \
|
||||
.trigger_notif_activity = 20, \
|
||||
.trigger_notif_active_minutes = 5 \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD_DEFAULT { \
|
||||
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
|
||||
.enabled = false, \
|
||||
.reward = {\
|
||||
.min_days_data = 6, \
|
||||
.continuous_min_days_data = 0, \
|
||||
.target_qualifying_days = 0, \
|
||||
.target_percent_of_median = 150, \
|
||||
.notif_min_interval_seconds = 1 * SECONDS_PER_DAY, \
|
||||
.activity = { \
|
||||
.trigger_active_minutes = 2, \
|
||||
.trigger_steps_per_minute = 50 \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY_DEFAULT { \
|
||||
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
|
||||
.enabled = true, \
|
||||
.summary = { \
|
||||
.above_avg_threshold = 10, \
|
||||
.below_avg_threshold = -10, \
|
||||
.fail_threshold = -50, \
|
||||
.activity = { \
|
||||
.trigger_minute = (20 * MINUTES_PER_HOUR) + 30, \
|
||||
.update_threshold_steps = 1000, \
|
||||
.update_max_interval_seconds = 30 * SECONDS_PER_MINUTE, \
|
||||
.show_notification = true, \
|
||||
.max_fail_steps = 10000, \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION_DEFAULT { \
|
||||
.version = ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION, \
|
||||
.enabled = true, \
|
||||
.session = { \
|
||||
.show_notification = true, \
|
||||
.activity = { \
|
||||
.trigger_elapsed_minutes = 20, \
|
||||
.trigger_cooldown_minutes = 10, \
|
||||
}, \
|
||||
} \
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *key;
|
||||
ActivityInsightSettings default_val;
|
||||
} AISDefault;
|
||||
|
||||
static const AISDefault AIS_DEFAULTS[] = {
|
||||
{
|
||||
.key = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD,
|
||||
.default_val = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD_DEFAULT
|
||||
},
|
||||
{
|
||||
.key = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY,
|
||||
.default_val = ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY_DEFAULT
|
||||
},
|
||||
{
|
||||
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD,
|
||||
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD_DEFAULT
|
||||
},
|
||||
{
|
||||
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY,
|
||||
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY_DEFAULT
|
||||
},
|
||||
{
|
||||
.key = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION,
|
||||
.default_val = ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION_DEFAULT
|
||||
},
|
||||
};
|
||||
|
||||
// Return true if we successfully opened the file
|
||||
static bool prv_open_settings_and_lock(SettingsFile *file) {
|
||||
mutex_lock(s_insight_settings_mutex);
|
||||
if (settings_file_open(file, ACTIVITY_INSIGHTS_SETTINGS_FILENAME,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE) == S_SUCCESS) {
|
||||
return true;
|
||||
} else {
|
||||
mutex_unlock(s_insight_settings_mutex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the settings file and release the lock
|
||||
static void prv_close_settings_and_unlock(SettingsFile *file) {
|
||||
settings_file_close(file);
|
||||
mutex_unlock(s_insight_settings_mutex);
|
||||
}
|
||||
|
||||
void activity_insights_settings_init(void) {
|
||||
// Create our mutex
|
||||
s_insight_settings_mutex = mutex_create();
|
||||
|
||||
SettingsFile file;
|
||||
if (settings_file_open(&file,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_FILENAME,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_FILE_SIZE) == S_SUCCESS) {
|
||||
if (!settings_file_exists(&file,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
|
||||
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY))) {
|
||||
// init version to 0
|
||||
const uint16_t default_version = ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION;
|
||||
settings_file_set(&file,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
|
||||
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY),
|
||||
&default_version,
|
||||
sizeof(uint16_t));
|
||||
}
|
||||
|
||||
settings_file_close(&file);
|
||||
return;
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to create activity insights settings file");
|
||||
}
|
||||
|
||||
uint16_t activity_insights_settings_get_version(void) {
|
||||
uint16_t version = ACTIVITY_INSIGHTS_SETTINGS_DEFAULT_VERSION;
|
||||
SettingsFile file;
|
||||
if (prv_open_settings_and_lock(&file)) {
|
||||
settings_file_get(&file,
|
||||
ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY,
|
||||
strlen(ACTIVITY_INSIGHTS_SETTINGS_VERSION_KEY),
|
||||
&version,
|
||||
sizeof(uint16_t));
|
||||
prv_close_settings_and_unlock(&file);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
bool activity_insights_settings_read(const char *insight_name,
|
||||
ActivityInsightSettings *settings_out) {
|
||||
bool rv = false;
|
||||
*settings_out = (ActivityInsightSettings) {};
|
||||
|
||||
SettingsFile file;
|
||||
if (prv_open_settings_and_lock(&file)) {
|
||||
if (settings_file_get(&file,
|
||||
insight_name, strlen(insight_name),
|
||||
settings_out, sizeof(*settings_out)) != S_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Didn't find insight with key %s", insight_name);
|
||||
goto close;
|
||||
}
|
||||
|
||||
if (settings_out->version != ACTIVITY_INSIGHTS_SETTINGS_CURRENT_STRUCT_VERSION) {
|
||||
// versions don't match, bail out!
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "activity insights struct version mismatch");
|
||||
goto close;
|
||||
}
|
||||
|
||||
rv = true;
|
||||
close:
|
||||
prv_close_settings_and_unlock(&file);
|
||||
}
|
||||
|
||||
if (!rv) {
|
||||
// Use default value if we didn't find anything else
|
||||
for (unsigned i = 0; i < ARRAY_LENGTH(AIS_DEFAULTS); ++i) {
|
||||
if (strcmp(insight_name, AIS_DEFAULTS[i].key) == 0) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Using default for insight %s", insight_name);
|
||||
*settings_out = AIS_DEFAULTS[i].default_val;
|
||||
rv = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool activity_insights_settings_write(const char *insight_name,
|
||||
ActivityInsightSettings *settings) {
|
||||
bool rv = false;
|
||||
|
||||
SettingsFile file;
|
||||
if (prv_open_settings_and_lock(&file)) {
|
||||
if (settings_file_set(&file,
|
||||
insight_name, strlen(insight_name),
|
||||
settings, sizeof(*settings)) != S_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Unable to save insight setting with key %s", insight_name);
|
||||
} else {
|
||||
rv = true;
|
||||
}
|
||||
prv_close_settings_and_unlock(&file);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
PFSCallbackHandle activity_insights_settings_watch(PFSFileChangedCallback callback) {
|
||||
return pfs_watch_file(ACTIVITY_INSIGHTS_SETTINGS_FILENAME, callback, FILE_CHANGED_EVENT_CLOSED,
|
||||
NULL);
|
||||
}
|
||||
|
||||
void activity_insights_settings_unwatch(PFSCallbackHandle cb_handle) {
|
||||
pfs_unwatch_file(cb_handle);
|
||||
}
|
||||
136
src/fw/services/normal/activity/insights_settings.h
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "activity.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_REWARD "sleep_reward"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_SLEEP_SUMMARY "sleep_summary"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_REWARD "activity_reward"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SUMMARY "activity_summary"
|
||||
#define ACTIVITY_INSIGHTS_SETTINGS_ACTIVITY_SESSION "activity_session"
|
||||
|
||||
typedef struct PACKED ActivityRewardSettings {
|
||||
// Note: these parameters are the number of days in addition to 'today' that we want to look at
|
||||
uint8_t min_days_data; //!< How many days of the metric's history we require
|
||||
uint8_t continuous_min_days_data; //!< How many consecutive days of history we require
|
||||
uint8_t target_qualifying_days; //!< Days that must be above target (on top of 'today')
|
||||
|
||||
uint16_t target_percent_of_median; //!< Percentage of median qualifying days must hit
|
||||
uint32_t notif_min_interval_seconds; //!< How often we allow this insight to be shown
|
||||
|
||||
// Insight-specific values
|
||||
union {
|
||||
struct PACKED {
|
||||
uint16_t trigger_after_wakeup_seconds; //!< Time we wait before showing sleep reward
|
||||
} sleep;
|
||||
|
||||
struct PACKED {
|
||||
uint8_t trigger_active_minutes; //!< Time we must be currently active before showing reward
|
||||
uint8_t trigger_steps_per_minute; //!< Steps per minute required for an 'active' minute
|
||||
} activity;
|
||||
};
|
||||
} ActivityRewardSettings;
|
||||
|
||||
typedef struct PACKED ActivitySummarySettings {
|
||||
int8_t above_avg_threshold; //!< Values greater than this are counted as above avg
|
||||
//!< In relation to 100% (eg 105% would be 5)
|
||||
int8_t below_avg_threshold; //!< Values less than this are counted as above avg
|
||||
//!< In relation to 100% (eg 93% would be -7)
|
||||
int8_t fail_threshold; //!< Values less than this are counted as fail
|
||||
//!< In releastion to 100% (e.g. 55% would be -45)
|
||||
|
||||
union {
|
||||
struct PACKED {
|
||||
uint16_t trigger_minute; //!< Minute of the day that we trigger the pin
|
||||
uint16_t update_threshold_steps; //!< Step delta that will cause the pin to update
|
||||
uint32_t update_max_interval_seconds; //!< Max time we'll go without updating the pin
|
||||
bool show_notification; //!< Whether to show a notification
|
||||
uint16_t max_fail_steps; //!< Don't show negative if walked more than X steps
|
||||
} activity;
|
||||
|
||||
struct PACKED {
|
||||
uint16_t max_fail_minutes; //!< Don't show negative if slept more than X minutes
|
||||
uint16_t trigger_notif_seconds; //!< Time in seconds after wakeup to notify about sleep
|
||||
uint16_t trigger_notif_activity; //!< Minimum amount of steps per minute to trigger the
|
||||
//!< Sleep summary notification
|
||||
uint8_t trigger_notif_active_minutes; //!< Minimum amount of active minutes to trigger the
|
||||
//!< Sleep summary notification
|
||||
} sleep;
|
||||
};
|
||||
} ActivitySummarySettings;
|
||||
|
||||
typedef struct PACKED ActivitySessionSettings {
|
||||
bool show_notification; //!< Whether to show a notification
|
||||
|
||||
union {
|
||||
struct PACKED {
|
||||
uint16_t trigger_elapsed_minutes; //!< Minimum length of a walk to be given an insight
|
||||
uint16_t trigger_cooldown_minutes; //!< Minutes wait after end of session before notifying
|
||||
} activity;
|
||||
};
|
||||
} ActivitySessionSettings;
|
||||
|
||||
typedef struct PACKED ActivityInsightSettings {
|
||||
// Common parameters
|
||||
uint8_t version; //!< Current version of the struct - must be first
|
||||
|
||||
bool enabled; //!< Insight enabled
|
||||
uint8_t unused; //!< Unused
|
||||
|
||||
union {
|
||||
ActivityRewardSettings reward;
|
||||
ActivitySummarySettings summary;
|
||||
ActivitySessionSettings session;
|
||||
};
|
||||
} ActivityInsightSettings;
|
||||
|
||||
|
||||
//! Read a setting from the insights settings
|
||||
//! @param insights_name the name of the insight for which to get a setting
|
||||
//! @param[out] settings out an ActivityInsightSettings struct to which the data will be written
|
||||
//! @returns true if the setting was found and the data is valid, false otherwise
|
||||
//! @note if this function returns false, settings_out will be zeroed out.
|
||||
bool activity_insights_settings_read(const char *insight_name,
|
||||
ActivityInsightSettings *settings_out);
|
||||
|
||||
//! Write a setting to the insights settings (used for testing)
|
||||
//! @param insights_name the name of the insight for which to get a setting
|
||||
//! @param settings an ActivityInsightSettings struct which contains the data to be written
|
||||
//! @returns true if the setting was successfully saved
|
||||
bool activity_insights_settings_write(const char *insight_name,
|
||||
ActivityInsightSettings *settings);
|
||||
|
||||
//! Get the current version of the insights settings
|
||||
//! @return the version number for the current insights settings
|
||||
//! @note this is separate from the struct version
|
||||
uint16_t activity_insights_settings_get_version(void);
|
||||
|
||||
//! Initialize insights settings
|
||||
void activity_insights_settings_init(void);
|
||||
|
||||
//! Watch the insights settings file. The callback is called whenever the file is closed with
|
||||
//! modifications or deleted
|
||||
//! @param callback Function to call when the file has been modified
|
||||
//! @return Callback handle for passing into \ref activity_insights_settings_unwatch
|
||||
PFSCallbackHandle activity_insights_settings_watch(PFSFileChangedCallback callback);
|
||||
|
||||
//! Stop watching the settings file
|
||||
//! @param cb_handle Callback handle which was returned by \ref activity_insights_settings_watch
|
||||
void activity_insights_settings_unwatch(PFSCallbackHandle cb_handle);
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/time/time.h"
|
||||
#include "kraepelin_algorithm.h"
|
||||
|
||||
// We divide the raw light sensor reading by this factor before storing it into AlgDlsMinuteData
|
||||
#define ALG_RAW_LIGHT_SENSOR_DIVIDE_BY 16
|
||||
|
||||
// Nap constraints, also used by unit tests
|
||||
// A sleep session in this range is always considered "primary" (not nap) sleep
|
||||
// ... if it ends after this minute in the evening
|
||||
#define ALG_PRIMARY_EVENING_MINUTE (21 * MINUTES_PER_HOUR) // 9pm
|
||||
// ... or starts before this minute in the morning
|
||||
#define ALG_PRIMARY_MORNING_MINUTE (12 * MINUTES_PER_HOUR) // 12pm
|
||||
|
||||
// A sleep session outside of the primary range is considered a nap if it is less than
|
||||
// this duration, otherwise it is considered a primary sleep session
|
||||
#define ALG_MAX_NAP_MINUTES (3 * MINUTES_PER_HOUR)
|
||||
|
||||
// Max number of hours of past data we process to figure out sleep for "today". If a sleep
|
||||
// cycle *ends* after midnight today, then we still count it as today's sleep. That means the
|
||||
// start of the sleep cycle could have started more than 24 hours ago.
|
||||
#define ALG_SLEEP_HISTORY_HOURS_FOR_TODAY 36
|
||||
640
src/fw/services/normal/activity/workout_service.c
Normal file
@@ -0,0 +1,640 @@
|
||||
/*
|
||||
* 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
|
||||
81
src/fw/services/normal/activity/workout_service.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "activity.h"
|
||||
#include "hr_util.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
//! Workouts are very similar to ActivitySessions, the only difference is that they are manually
|
||||
//! started / stopped, and update more frequently than automatically detected activities.
|
||||
|
||||
//! Note: If a workout is in progress, then we disable automatic activity detection.
|
||||
//! Note: Only 1 workout at a time is supported
|
||||
|
||||
void workout_service_init(void);
|
||||
|
||||
//! Called by the frontend application to signal that the app has been opened.
|
||||
//! @note Must be called from PebbleTask_App
|
||||
void workout_service_frontend_opened(void);
|
||||
|
||||
//! Called by the frontend application to signal that the app has been closed.
|
||||
//! @note Must be called from PebbleTask_App
|
||||
void workout_service_frontend_closed(void);
|
||||
|
||||
//! Event handler for Health events
|
||||
void workout_service_health_event_handler(PebbleHealthEvent *event);
|
||||
|
||||
//! Event handler for Activity events
|
||||
void workout_service_activity_event_handler(PebbleActivityEvent *event);
|
||||
|
||||
//! Event handler for Workout events
|
||||
void workout_service_workout_event_handler(PebbleWorkoutEvent *event);
|
||||
|
||||
//! Returns true if there is an ongoing workout
|
||||
bool workout_service_is_workout_ongoing(void);
|
||||
|
||||
//! Returns true if the activity type is a supported workout
|
||||
bool workout_service_is_workout_type_supported(ActivitySessionType type);
|
||||
|
||||
//! Start a new workout
|
||||
//! This stops / saves all onoing automatically detected activity sessions
|
||||
//! All workouts must eventually get stopped
|
||||
bool workout_service_start_workout(ActivitySessionType type);
|
||||
|
||||
//! Pause / unpause the currect workout
|
||||
bool workout_service_pause_workout(bool should_be_paused);
|
||||
|
||||
//! Stops the current workout. Resumes automatic activity session detection
|
||||
bool workout_service_stop_workout(void);
|
||||
|
||||
//! Starts a workout using the data from the given activity session
|
||||
bool workout_service_takeover_activity_session(ActivitySession *session);
|
||||
|
||||
//! Returns true if there is a paused workout
|
||||
bool workout_service_is_paused(void);
|
||||
|
||||
//! Get the current workout type
|
||||
//! Returns true if a workout is going on
|
||||
bool workout_service_get_current_workout_type(ActivitySessionType *type_out);
|
||||
|
||||
//! Dumps the current state of the workout
|
||||
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);
|
||||
1249
src/fw/services/normal/alarms/alarm.c
Normal file
178
src/fw/services/normal/alarms/alarm.h
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "board/board.h"
|
||||
|
||||
#include "util/time/time.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//! @file alarm.h
|
||||
//! Allows a user to set an alarm for a given time in the future. When this time arrives, a
|
||||
//! PEBBLE_ALARM_CLOCK_EVENT event will be put.
|
||||
//!
|
||||
//! These alarm settings will be persisted across watch resets.
|
||||
|
||||
#define SMART_ALARM_RANGE_S (30 * SECONDS_PER_MINUTE)
|
||||
#define SMART_ALARM_SNOOZE_DELAY_S (1 * SECONDS_PER_MINUTE)
|
||||
#define SMART_ALARM_MAX_LIGHT_SLEEP_S (30 * SECONDS_PER_MINUTE)
|
||||
#define SMART_ALARM_MAX_SMART_SNOOZE (SMART_ALARM_RANGE_S / SMART_ALARM_SNOOZE_DELAY_S)
|
||||
|
||||
#define ALARMS_APP_HIGHLIGHT_COLOR PBL_IF_COLOR_ELSE(GColorJaegerGreen, GColorBlack)
|
||||
|
||||
typedef int AlarmId; //! A unique ID that can be used to refer to each configured alarm.
|
||||
|
||||
#define ALARM_INVALID_ID (-1)
|
||||
|
||||
typedef enum AlarmKind {
|
||||
ALARM_KIND_EVERYDAY = 0, // Alarms of this type will happen each day
|
||||
ALARM_KIND_WEEKENDS, // Alarms of this type will happen Monday - Friday
|
||||
ALARM_KIND_WEEKDAYS, // Alarms of this type happen Saturaday and Sunday
|
||||
ALARM_KIND_JUST_ONCE, // Alarms of this type will happen next time the specified time occurs
|
||||
ALARM_KIND_CUSTOM, // Alarms of this type happen on specified days
|
||||
} AlarmKind;
|
||||
|
||||
typedef enum AlarmType {
|
||||
AlarmType_Basic,
|
||||
AlarmType_Smart,
|
||||
AlarmTypeCount,
|
||||
} AlarmType;
|
||||
|
||||
typedef struct AlarmInfo {
|
||||
int hour; //<! Range 0-23, where 0 is 12am
|
||||
int minute; //<! Range is 0-59
|
||||
AlarmKind kind; //<! The kind of recurrence the alarm will have
|
||||
//! A bool for each weekday (Sunday = index 0) enabled
|
||||
bool (*scheduled_days)[DAYS_PER_WEEK];
|
||||
bool enabled; //<! Whether the alarm to go off at the specified time
|
||||
bool is_smart; //<! Whether the alarm is a Smart Alarm
|
||||
} AlarmInfo;
|
||||
|
||||
typedef void (*AlarmForEach)(AlarmId id, const AlarmInfo *info, void *context);
|
||||
|
||||
//! Creates an alarm
|
||||
//! @param info The alarm configuration to be created with
|
||||
AlarmId alarm_create(const AlarmInfo *info);
|
||||
|
||||
//! @param id The alarm that should be updated
|
||||
//! @param hour Range 0-23, where 0 is 12am
|
||||
//! @param minute Range is 0-59
|
||||
void alarm_set_time(AlarmId id, int hour, int minute);
|
||||
|
||||
//! @param id The alarm that should be updated
|
||||
//! @param kind The new kind of the alarm
|
||||
//! @note The CUSTOM kind will be ignored
|
||||
void alarm_set_kind(AlarmId id, AlarmKind kind);
|
||||
|
||||
//! @param id The alarm that should be updated
|
||||
//! @param scheduled_days[DAYS_PER_WEEK] A bool for each weekday (Sunday = index 0) enabled
|
||||
//! each weekday that is marked as true
|
||||
void alarm_set_custom(AlarmId id, const bool scheduled_days[DAYS_PER_WEEK]);
|
||||
|
||||
//! @param id The alarm that should be updated
|
||||
//! @param smart Whether the alarm is a smart alarm
|
||||
void alarm_set_smart(AlarmId id, bool smart);
|
||||
|
||||
//! @param id The alarm for which the scheduled_days array should be updated for
|
||||
//! @param scheduled_days[DAYS_PER_WEEK] An empty bool array for each weekday (Sunday = index 0)
|
||||
//! that is to be updated. Alarms will run on each weekday that is marked as true
|
||||
//! @return True if the alarm exists, False otherwise
|
||||
bool alarm_get_custom_days(AlarmId id, bool scheduled_days[DAYS_PER_WEEK]);
|
||||
|
||||
//! @param id The alarm that should be modified
|
||||
//! @param enable Whether to enable the alarm
|
||||
void alarm_set_enabled(AlarmId id, bool enable);
|
||||
|
||||
//! @param id The alarm that should be deleted
|
||||
void alarm_delete(AlarmId id);
|
||||
|
||||
//! @param id The alarm that is being queried
|
||||
//! @return True if the alarm exists and is not disabled, Flase otherwise
|
||||
bool alarm_get_enabled(AlarmId id);
|
||||
|
||||
//! @param id The alarm that should be deleted
|
||||
//! @param hour_out Hour which the alarm is scheduled for
|
||||
//! @param minute_out Minute which the alarm is scheduled for
|
||||
//! @return True if the alarm is scheduled, False if not (disabled / doesn't exist)
|
||||
bool alarm_get_hours_minutes(AlarmId id, int *hour_out, int *minute_out);
|
||||
|
||||
//! @param id The alarm that is being queried
|
||||
//! @param kind_out The type of alarm
|
||||
//! @return True if the alarm exists, False otherwise
|
||||
bool alarm_get_kind(AlarmId id, AlarmKind *kind_out);
|
||||
|
||||
//! @param next_alarm_time_out The time of the next enabled alarm
|
||||
//! @return True if at least one alarm is enabled, False if no alarms are enabled.
|
||||
bool alarm_get_next_enabled_alarm(time_t *next_alarm_time_out);
|
||||
|
||||
//! @return True if the next enabled alarm is a smart alarm, False if no alarms are enabled or the
|
||||
//! next alarm is not smart.
|
||||
bool alarm_is_next_enabled_alarm_smart(void);
|
||||
|
||||
//! @param id The alarm that is being queried
|
||||
//! @param time_out The number of seconds until the alarm is scheduled to go off
|
||||
//! @return True if the alarm is scheduled, False if not (disabled / doesn't exist)
|
||||
bool alarm_get_time_until(AlarmId id, time_t *time_out);
|
||||
|
||||
//! Starts a snooze timer for the current snooze delay.
|
||||
void alarm_set_snooze_alarm(void);
|
||||
|
||||
//! @return Snooze delay in minutes.
|
||||
uint16_t alarm_get_snooze_delay(void);
|
||||
|
||||
//! Set the snooze delay for all alarms
|
||||
//! @param delay_m snooze delay in minutes.
|
||||
void alarm_set_snooze_delay(uint16_t delay_m);
|
||||
|
||||
//! Dismisses the most recently triggered alarm.
|
||||
void alarm_dismiss_alarm(void);
|
||||
|
||||
//! Runs the callback for each alarm pairing
|
||||
void alarm_for_each(AlarmForEach cb, void *context);
|
||||
|
||||
//! @return True if the max number of alarms hasn't been saved, False otherwise
|
||||
bool alarm_can_schedule(void);
|
||||
|
||||
//! Call this when the clock time has changed. This will reschedule all the alarms so they'll go
|
||||
//! off at the right time. This is required because the alarm subsystem registers timers that go
|
||||
//! off at a number of seconds as opposed to an absolute time, and the number of seconds before
|
||||
//! the timer goes off changes when the clock time changes.
|
||||
void alarm_handle_clock_change(void);
|
||||
|
||||
void alarm_init(void);
|
||||
|
||||
//! Enable or disable alarms globally.
|
||||
void alarm_service_enable_alarms(bool enable);
|
||||
|
||||
//! Get the string (e.g. "Weekends") for a given AlarmKind and all-caps specification
|
||||
const char *alarm_get_string_for_kind(AlarmKind kind, bool all_caps);
|
||||
|
||||
//! For an alarm of type custom, retrieve a string representing the days that the alarm is set for
|
||||
//! Example: 1 day: ("Mondays", "Tuesdays"), multiple days: ("Mon,Sat,Sun", "Tue,Thu")
|
||||
//! @param [in] scheduled_days[DAYS_PER_WEEK] A bool for each weekday (Sunday = index 0) enabled
|
||||
//! @param [out] alarm_day_text A character array that is to be updated with the days. It should
|
||||
//! have a minimum of 28 bytes allocated
|
||||
void alarm_get_string_for_custom(bool scheduled_days[DAYS_PER_WEEK], char *alarm_day_text);
|
||||
|
||||
#if CAPABILITY_HAS_HEALTH_TRACKING
|
||||
//! Set the alarms app version opened
|
||||
void alarm_prefs_set_alarms_app_opened(uint8_t version);
|
||||
|
||||
//! Get the alarms app version opened
|
||||
uint8_t alarm_prefs_get_alarms_app_opened(void);
|
||||
#endif
|
||||
89
src/fw/services/normal/alarms/alarm_pin.c
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 "alarm_pin.h"
|
||||
|
||||
#include "apps/system_app_ids.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/blob_db/pin_db.h"
|
||||
#include "services/normal/timeline/attribute.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "services/normal/timeline/timeline_resources.h"
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
//! Sets attributes for an alarm pin
|
||||
static void prv_set_pin_attributes(AttributeList *list, AlarmType type, AlarmKind kind) {
|
||||
const bool is_smart = (type == AlarmType_Smart);
|
||||
attribute_list_add_cstring(list, AttributeIdTitle,
|
||||
is_smart ? i18n_get("Smart Alarm", list) : i18n_get("Alarm", list));
|
||||
attribute_list_add_resource_id(list, AttributeIdIconPin,
|
||||
is_smart ? TIMELINE_RESOURCE_SMART_ALARM :
|
||||
TIMELINE_RESOURCE_ALARM_CLOCK);
|
||||
attribute_list_add_resource_id(list, AttributeIdIconTiny, TIMELINE_RESOURCE_ALARM_CLOCK);
|
||||
const bool all_caps = false;
|
||||
const char *alarm_string = i18n_get(alarm_get_string_for_kind(kind, all_caps), list);
|
||||
attribute_list_add_cstring(list, AttributeIdSubtitle, alarm_string);
|
||||
attribute_list_add_uint8(list, AttributeIdAlarmKind, kind);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
static void prv_set_edit_action_attributes(AttributeList *list) {
|
||||
attribute_list_add_cstring(list, AttributeIdTitle, i18n_get("Edit", list));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
void alarm_pin_add(time_t alarm_time, AlarmId id, AlarmType type, AlarmKind kind, Uuid *uuid_out) {
|
||||
const unsigned num_actions = 1; // We are just supporting "edit" for now
|
||||
TimelineItemActionGroup action_group = {
|
||||
.num_actions = num_actions,
|
||||
.actions = task_zalloc_check(sizeof(TimelineItemAction) * num_actions),
|
||||
};
|
||||
AttributeList edit_attr_list = {0};
|
||||
prv_set_edit_action_attributes(&edit_attr_list);
|
||||
action_group.actions[0] = (TimelineItemAction) {
|
||||
.id = (uint8_t) id, // id is guarenteed to be valid here, and we only support 10 alarms
|
||||
.type = TimelineItemActionTypeOpenWatchApp,
|
||||
.attr_list = edit_attr_list,
|
||||
};
|
||||
|
||||
AttributeList pin_attr_list = {0};
|
||||
prv_set_pin_attributes(&pin_attr_list, type, kind);
|
||||
TimelineItem *item = timeline_item_create_with_attributes(alarm_time, 0, TimelineItemTypePin,
|
||||
LayoutIdAlarm, &pin_attr_list, &action_group);
|
||||
item->header.from_watch = true;
|
||||
item->header.parent_id = (Uuid)UUID_ALARMS_DATA_SOURCE;
|
||||
|
||||
pin_db_insert_item_without_event(item);
|
||||
|
||||
i18n_free_all(&pin_attr_list);
|
||||
i18n_free_all(&edit_attr_list);
|
||||
attribute_list_destroy_list(&pin_attr_list);
|
||||
attribute_list_destroy_list(&edit_attr_list);
|
||||
task_free(action_group.actions);
|
||||
|
||||
if (uuid_out) {
|
||||
*uuid_out = item->header.id;
|
||||
}
|
||||
|
||||
timeline_item_destroy(item);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
void alarm_pin_remove(Uuid *alarm_id) {
|
||||
pin_db_delete((uint8_t *)alarm_id, sizeof(Uuid));
|
||||
}
|
||||
24
src/fw/services/normal/alarms/alarm_pin.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/uuid.h"
|
||||
#include "services/normal/alarms/alarm.h"
|
||||
|
||||
void alarm_pin_add(time_t alarm_time, AlarmId id, AlarmType type, AlarmKind kind, Uuid *uuid_out);
|
||||
|
||||
void alarm_pin_remove(Uuid *alarm_id);
|
||||
212
src/fw/services/normal/analytics/analytics.c
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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 <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "apps/system_apps/launcher/launcher_app.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/tick.h"
|
||||
#include "process_management/worker_manager.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/list.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/analytics/analytics_storage.h"
|
||||
#include "services/common/analytics/analytics_metric.h"
|
||||
#include "services/common/analytics/analytics_heartbeat.h"
|
||||
#include "services/common/analytics/analytics_logging.h"
|
||||
#include "services/common/analytics/analytics_external.h"
|
||||
|
||||
// Stopwatches
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
AnalyticsMetric metric;
|
||||
RtcTicks starting_ticks;
|
||||
uint32_t count_per_sec;
|
||||
AnalyticsClient client;
|
||||
} AnalyticsStopwatchNode;
|
||||
|
||||
static ListNode *s_stopwatch_list = NULL;
|
||||
static bool prv_is_stopwatch_for_metric(ListNode *found_node, void *data);
|
||||
|
||||
void analytics_init(void) {
|
||||
analytics_metric_init();
|
||||
analytics_storage_init();
|
||||
analytics_logging_init();
|
||||
}
|
||||
|
||||
void analytics_set(AnalyticsMetric metric, int64_t value, AnalyticsClient client) {
|
||||
analytics_set_for_uuid(metric, value, analytics_uuid_for_client(client));
|
||||
}
|
||||
|
||||
void analytics_max(AnalyticsMetric metric, int64_t val, AnalyticsClient client) {
|
||||
const Uuid *uuid = analytics_uuid_for_client(client);
|
||||
|
||||
analytics_storage_take_lock();
|
||||
|
||||
AnalyticsHeartbeat *heartbeat = analytics_storage_find(metric, uuid, AnalyticsClient_Ignore);
|
||||
if (heartbeat) {
|
||||
const int64_t prev_value = analytics_heartbeat_get(heartbeat, metric);
|
||||
if (prev_value < val) {
|
||||
analytics_heartbeat_set(heartbeat, metric, val);
|
||||
}
|
||||
}
|
||||
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
void analytics_set_for_uuid(AnalyticsMetric metric, int64_t value, const Uuid *uuid) {
|
||||
analytics_storage_take_lock();
|
||||
|
||||
AnalyticsHeartbeat *heartbeat = analytics_storage_find(metric, uuid, AnalyticsClient_Ignore);
|
||||
if (heartbeat) {
|
||||
// We allow only a limited number of app heartbeats to accumulate. A NULL means we reached the
|
||||
// limit
|
||||
analytics_heartbeat_set(heartbeat, metric, value);
|
||||
}
|
||||
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
void analytics_set_entire_array(AnalyticsMetric metric, const void *value, AnalyticsClient client) {
|
||||
analytics_storage_take_lock();
|
||||
|
||||
AnalyticsHeartbeat *heartbeat = analytics_storage_find(metric, NULL, client);
|
||||
if (heartbeat) {
|
||||
// We allow only a limited number of app heartbeats to accumulate. A NULL means we reached the
|
||||
// limite
|
||||
analytics_heartbeat_set_entire_array(heartbeat, metric, value);
|
||||
}
|
||||
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
|
||||
void analytics_inc(AnalyticsMetric metric, AnalyticsClient client) {
|
||||
analytics_add(metric, 1, client);
|
||||
}
|
||||
|
||||
void analytics_inc_for_uuid(AnalyticsMetric metric, const Uuid *uuid) {
|
||||
analytics_add_for_uuid(metric, 1, uuid);
|
||||
}
|
||||
|
||||
void analytics_add_for_uuid(AnalyticsMetric metric, int64_t amount, const Uuid *uuid) {
|
||||
analytics_storage_take_lock();
|
||||
|
||||
// We don't currently allow incrementing signed integers, because the
|
||||
// only intended use of this API call is to increment counters, and counters
|
||||
// should always be unsigned. This restriction could be changed in the future,
|
||||
// however.
|
||||
PBL_ASSERTN(analytics_metric_is_unsigned(metric));
|
||||
|
||||
AnalyticsHeartbeat *heartbeat = analytics_storage_find(metric, uuid, AnalyticsClient_Ignore);
|
||||
if (heartbeat) {
|
||||
// We allow only a limited number of app heartbeats to accumulate. A NULL means we reached the
|
||||
// limit
|
||||
uint64_t val = analytics_heartbeat_get(heartbeat, metric);
|
||||
analytics_heartbeat_set(heartbeat, metric, val + amount);
|
||||
}
|
||||
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
void analytics_add(AnalyticsMetric metric, int64_t amount, AnalyticsClient client) {
|
||||
analytics_add_for_uuid(metric, amount, analytics_uuid_for_client(client));
|
||||
}
|
||||
|
||||
///////////////////
|
||||
// Stopwatches
|
||||
static bool prv_is_stopwatch_for_metric(ListNode *found_node, void *data) {
|
||||
AnalyticsStopwatchNode* stopwatch_node = (AnalyticsStopwatchNode*)found_node;
|
||||
return stopwatch_node->metric == (AnalyticsMetric)data;
|
||||
}
|
||||
AnalyticsStopwatchNode *prv_find_stopwatch(AnalyticsMetric metric) {
|
||||
ListNode *node = list_find(s_stopwatch_list, prv_is_stopwatch_for_metric, (void*)metric);
|
||||
return (AnalyticsStopwatchNode*)node;
|
||||
}
|
||||
|
||||
static uint32_t prv_stopwatch_elapsed_ms(AnalyticsStopwatchNode *stopwatch, uint64_t current_ticks) {
|
||||
const uint64_t dt_ms = ticks_to_milliseconds(current_ticks - stopwatch->starting_ticks);
|
||||
return (((uint64_t) stopwatch->count_per_sec) * dt_ms) / MS_PER_SECOND;
|
||||
}
|
||||
|
||||
void analytics_stopwatch_start(AnalyticsMetric metric, AnalyticsClient client) {
|
||||
analytics_stopwatch_start_at_rate(metric, MS_PER_SECOND, client);
|
||||
}
|
||||
|
||||
void analytics_stopwatch_start_at_rate(AnalyticsMetric metric, uint32_t count_per_sec, AnalyticsClient client) {
|
||||
analytics_storage_take_lock();
|
||||
|
||||
// Stopwatch metrics must be UINT32!
|
||||
PBL_ASSERTN(analytics_metric_element_type(metric) == ANALYTICS_METRIC_ELEMENT_TYPE_UINT32);
|
||||
|
||||
if (prv_find_stopwatch(metric)) {
|
||||
// TODO: Increment this back up to LOG_LEVEL_WARNING when it doesn't happen
|
||||
// on every bootup (PBL-5393)
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Analytics stopwatch for metric %d already started!", metric);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
AnalyticsStopwatchNode *stopwatch = kernel_malloc_check(sizeof(*stopwatch));
|
||||
*stopwatch = (AnalyticsStopwatchNode) {
|
||||
.metric = metric,
|
||||
.starting_ticks = rtc_get_ticks(),
|
||||
.count_per_sec = count_per_sec,
|
||||
.client = client,
|
||||
};
|
||||
|
||||
list_init(&stopwatch->node);
|
||||
s_stopwatch_list = list_prepend(s_stopwatch_list, &stopwatch->node);
|
||||
|
||||
unlock:
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
void analytics_stopwatch_stop(AnalyticsMetric metric) {
|
||||
analytics_storage_take_lock();
|
||||
|
||||
AnalyticsStopwatchNode *stopwatch = prv_find_stopwatch(metric);
|
||||
if (!stopwatch) {
|
||||
// TODO: Incerement this back up to LOG_LEVEL_WARNING when it doesn't happen
|
||||
// on every bootup (PBL-5393)
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Analytics stopwatch for metric %d already stopped!", metric);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
analytics_add(metric, prv_stopwatch_elapsed_ms(stopwatch, rtc_get_ticks()), stopwatch->client);
|
||||
|
||||
list_remove(&stopwatch->node, &s_stopwatch_list, NULL);
|
||||
kernel_free(stopwatch);
|
||||
|
||||
unlock:
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
void analytics_stopwatches_update(uint64_t current_ticks) {
|
||||
PBL_ASSERTN(analytics_storage_has_lock());
|
||||
|
||||
ListNode *cur = s_stopwatch_list;
|
||||
while (cur != NULL) {
|
||||
AnalyticsStopwatchNode *node = (AnalyticsStopwatchNode*)cur;
|
||||
analytics_add(node->metric, prv_stopwatch_elapsed_ms(node, current_ticks), node->client);
|
||||
node->starting_ticks = current_ticks;
|
||||
cur = cur->next;
|
||||
}
|
||||
}
|
||||
75
src/fw/services/normal/analytics/analytics_data_syscalls.c
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 "services/common/analytics/analytics.h"
|
||||
#include "services/common/analytics/analytics_event.h"
|
||||
#include "services/common/analytics/analytics_logging.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_set, AnalyticsMetric metric, uint64_t value,
|
||||
AnalyticsClient client) {
|
||||
analytics_set(metric, value, client);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_set_entire_array, AnalyticsMetric metric, const void *value,
|
||||
AnalyticsClient client) {
|
||||
analytics_set_entire_array(metric, value, client);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_add, AnalyticsMetric metric, uint64_t increment,
|
||||
AnalyticsClient client) {
|
||||
analytics_add(metric, increment, client);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_inc, AnalyticsMetric metric, AnalyticsClient client) {
|
||||
analytics_inc(metric, client);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_stopwatch_start, AnalyticsMetric metric,
|
||||
AnalyticsClient client) {
|
||||
analytics_stopwatch_start(metric, client);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_stopwatch_stop, AnalyticsMetric metric) {
|
||||
analytics_stopwatch_stop(metric);
|
||||
}
|
||||
|
||||
static bool prv_is_event_allowed(const AnalyticsEventBlob *const event_blob) {
|
||||
switch (event_blob->event) {
|
||||
case AnalyticsEvent_AppOOMNative:
|
||||
case AnalyticsEvent_AppOOMRocky:
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Don't allow any other event types:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_logging_log_event, AnalyticsEventBlob *event_blob) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(event_blob, sizeof(*event_blob));
|
||||
}
|
||||
if (!prv_is_event_allowed(event_blob)) {
|
||||
syscall_failed();
|
||||
}
|
||||
analytics_logging_log_event(event_blob);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(void, sys_analytics_max, AnalyticsMetric metric, int64_t val,
|
||||
AnalyticsClient client) {
|
||||
analytics_max(metric, val, client);
|
||||
}
|
||||
701
src/fw/services/normal/analytics/analytics_event.c
Normal file
@@ -0,0 +1,701 @@
|
||||
/*
|
||||
* 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 <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/analytics/analytics_event.h"
|
||||
#include "services/common/analytics/analytics_logging.h"
|
||||
#include "services/common/analytics/analytics_storage.h"
|
||||
|
||||
#include "apps/system_apps/launcher/launcher_app.h"
|
||||
#include "comm/ble/gap_le_connection.h"
|
||||
#include "comm/bt_lock.h"
|
||||
#include "comm/ble/gap_le_connection.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/comm_session/session_internal.h"
|
||||
#include "services/normal/alarms/alarm.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
_Static_assert(sizeof(AnalyticsEventBlob) == 36,
|
||||
"When the blob format or size changes, be sure to bump up ANALYTICS_EVENT_BLOB_VERSION");
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log an app launch event
|
||||
static bool prv_send_uuid(AnalyticsEvent event_enum, const Uuid *uuid) {
|
||||
if (uuid_is_invalid(uuid) || uuid_is_system(uuid)) {
|
||||
// No need to log apps with invalid uuids. This is typically built-in test apps like "Light
|
||||
// config" that we don't bother to declare a UUID for
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: The sdkshell doesn't have a launcher menu so this causes a linker error. Maybe the
|
||||
// mapping of events to analytics should also be shell-specific?
|
||||
#ifndef SHELL_SDK
|
||||
// No need to log the launcher menu app
|
||||
if (uuid_equal(uuid, &launcher_menu_app_get_app_info()->uuid)) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log an out-of-memory situation for an app.
|
||||
|
||||
void analytics_event_app_oom(AnalyticsEvent type,
|
||||
uint32_t requested_size, uint32_t total_size, uint32_t total_free,
|
||||
uint32_t largest_free_block) {
|
||||
PBL_ASSERTN(type == AnalyticsEvent_AppOOMNative || type == AnalyticsEvent_AppOOMRocky);
|
||||
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = type,
|
||||
.app_oom = {
|
||||
.requested_size = requested_size,
|
||||
.total_size = total_size,
|
||||
.total_free = MIN(total_free, UINT16_MAX),
|
||||
.largest_free_block = MIN(largest_free_block, UINT16_MAX),
|
||||
},
|
||||
};
|
||||
if (!sys_process_manager_get_current_process_uuid(&event_blob.app_oom.app_uuid)) {
|
||||
// Process has no UUID
|
||||
return;
|
||||
}
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
ANALYTICS_LOG_DEBUG("app oom: is_rocky=%u, req_sz=%"PRIu32" tot_sz=%"PRIu32" free=%"PRIu32
|
||||
" max_free=%"PRIu32,
|
||||
(type == AnalyticsEvent_AppOOMRocky),
|
||||
requested_size, total_size, total_free, largest_free_block);
|
||||
#endif
|
||||
|
||||
sys_analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a generic app launch event
|
||||
void analytics_event_app_launch(const Uuid *uuid) {
|
||||
if (!prv_send_uuid(AnalyticsEvent_AppLaunch, uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_AppLaunch,
|
||||
.app_launch.uuid = *uuid,
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(uuid, uuid_string);
|
||||
ANALYTICS_LOG_DEBUG("app launch event: uuid %s", uuid_string);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin open/create/update event.
|
||||
static void prv_simple_pin_event(time_t timestamp, const Uuid *parent_id,
|
||||
AnalyticsEvent event_enum, const char *verb) {
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = event_enum,
|
||||
.pin_open_create_update.time_utc = timestamp,
|
||||
.pin_open_create_update.parent_id = *parent_id,
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(&event_blob.pin_open_create_update.parent_id, uuid_string);
|
||||
ANALYTICS_LOG_DEBUG("pin %s event: timestamp: %"PRIu32", uuid:%s", verb,
|
||||
event_blob.pin_open_create_update.time_utc, uuid_string);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin open event.
|
||||
void analytics_event_pin_open(time_t timestamp, const Uuid *parent_id) {
|
||||
prv_simple_pin_event(timestamp, parent_id, AnalyticsEvent_PinOpen, "open");
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin created event.
|
||||
void analytics_event_pin_created(time_t timestamp, const Uuid *parent_id) {
|
||||
prv_simple_pin_event(timestamp, parent_id, AnalyticsEvent_PinCreated, "created");
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin updated event.
|
||||
void analytics_event_pin_updated(time_t timestamp, const Uuid *parent_id) {
|
||||
prv_simple_pin_event(timestamp, parent_id, AnalyticsEvent_PinUpdated, "updated");
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin action event.
|
||||
void analytics_event_pin_action(time_t timestamp, const Uuid *parent_id,
|
||||
TimelineItemActionType action_type) {
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_PinAction,
|
||||
.pin_action.time_utc = timestamp,
|
||||
.pin_action.parent_id = *parent_id,
|
||||
.pin_action.type = action_type,
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(&event_blob.pin_action.parent_id, uuid_string);
|
||||
ANALYTICS_LOG_DEBUG("pin action event: timestamp: %"PRIu32", uuid:%s, action:%"PRIu8,
|
||||
event_blob.pin_action.time_utc, uuid_string, action_type);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a pin app launch event.
|
||||
void analytics_event_pin_app_launch(time_t timestamp, const Uuid *parent_id) {
|
||||
if (!prv_send_uuid(AnalyticsEvent_PinAppLaunch, parent_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_PinAppLaunch,
|
||||
.pin_app_launch.time_utc = timestamp,
|
||||
.pin_app_launch.parent_id = *parent_id,
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(&event_blob.pin_app_launch.parent_id, uuid_string);
|
||||
ANALYTICS_LOG_DEBUG("pin app launch event: timestamp: %"PRIu32", uuid:%s",
|
||||
event_blob.pin_app_launch.time_utc, uuid_string);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a canned response event
|
||||
void analytics_event_canned_response(const char *response, bool successfully_sent) {
|
||||
// Format the event specific info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = successfully_sent ? AnalyticsEvent_CannedReponseSent
|
||||
: AnalyticsEvent_CannedReponseFailed,
|
||||
};
|
||||
|
||||
if (!response) {
|
||||
event_blob.canned_response.response_size_bytes = 0;
|
||||
} else {
|
||||
event_blob.canned_response.response_size_bytes = strlen(response);
|
||||
}
|
||||
|
||||
if (successfully_sent) {
|
||||
ANALYTICS_LOG_DEBUG("canned response sent event: response_size_bytes:%d",
|
||||
event_blob.canned_response.response_size_bytes);
|
||||
} else {
|
||||
ANALYTICS_LOG_DEBUG("canned response failed event: response_size_bytes:%d",
|
||||
event_blob.canned_response.response_size_bytes);
|
||||
}
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a voice response event
|
||||
void analytics_event_voice_response(AnalyticsEvent event_type, uint16_t response_size_bytes,
|
||||
uint16_t response_len_chars, uint32_t response_len_ms,
|
||||
uint8_t error_count, uint8_t num_sessions, Uuid *app_uuid) {
|
||||
|
||||
PBL_ASSERTN((event_type >= AnalyticsEvent_VoiceTranscriptionAccepted) &&
|
||||
(event_type <= AnalyticsEvent_VoiceTranscriptionAutomaticallyAccepted));
|
||||
|
||||
// Format the event specific info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = event_type,
|
||||
};
|
||||
|
||||
event_blob.voice_response = (AnalyticsEventVoiceResponse) {
|
||||
.response_size_bytes = response_size_bytes,
|
||||
.response_len_chars = response_len_chars,
|
||||
.response_len_ms = response_len_ms,
|
||||
.num_sessions = num_sessions,
|
||||
.error_count = error_count,
|
||||
.app_uuid = *app_uuid,
|
||||
};
|
||||
|
||||
const char *msg = "Other";
|
||||
switch (event_type) {
|
||||
case AnalyticsEvent_VoiceTranscriptionAccepted:
|
||||
msg = "Accepted";
|
||||
break;
|
||||
case AnalyticsEvent_VoiceTranscriptionRejected:
|
||||
msg = "Rejected";
|
||||
break;
|
||||
case AnalyticsEvent_VoiceTranscriptionAutomaticallyAccepted:
|
||||
msg = "Automatically accepted";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ANALYTICS_LOG_DEBUG("voice response %s event: size: %"PRIu16"; length (chars): %"PRIu16
|
||||
"; length (ms): %"PRIu32"; Errors: %"PRIu8"; Sessions: %"PRIu8, msg,
|
||||
event_blob.voice_response.response_size_bytes, event_blob.voice_response.response_len_chars,
|
||||
event_blob.voice_response.response_len_ms, event_blob.voice_response.error_count,
|
||||
event_blob.voice_response.num_sessions);
|
||||
// Use syscall because this is called by voice_window
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a BLE HRM event
|
||||
void analytics_event_ble_hrm(BleHrmEventSubtype subtype) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_BleHrmEvent,
|
||||
.ble_hrm = {
|
||||
.subtype = subtype,
|
||||
},
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("BLE HRM Event %u", subtype);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a bluetooth disconnection event
|
||||
void analytics_event_bt_connection_or_disconnection(AnalyticsEvent type, uint8_t reason) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = type,
|
||||
};
|
||||
|
||||
event_blob.bt_connection_disconnection.reason = reason;
|
||||
|
||||
ANALYTICS_LOG_DEBUG("Event %d - BT (dis)connection: Reason: %"PRIu8,
|
||||
event_blob.event,
|
||||
event_blob.bt_connection_disconnection.reason);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
void analytics_event_bt_le_disconnection(uint8_t reason, uint8_t remote_bt_version,
|
||||
uint16_t remote_bt_company_id,
|
||||
uint16_t remote_bt_subversion) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_BtLeDisconnect,
|
||||
.ble_disconnection = {
|
||||
.reason = reason,
|
||||
.remote_bt_version = remote_bt_version,
|
||||
.remote_bt_company_id = remote_bt_company_id,
|
||||
.remote_bt_subversion_number = remote_bt_subversion,
|
||||
}
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("Event %d - BT disconnection: Reason: %"PRIu8, event_blob.event,
|
||||
event_blob.bt_connection_disconnection.reason);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a bluetooth error
|
||||
void analytics_event_bt_error(AnalyticsEvent type, uint32_t error) {
|
||||
AnalyticsEventBlob event_blob = {};
|
||||
event_blob.event = type,
|
||||
event_blob.bt_error.error_code = error;
|
||||
|
||||
ANALYTICS_LOG_DEBUG("bluetooth event %d - error: %"PRIu32,
|
||||
event_blob.event,
|
||||
event_blob.bt_error.error_code);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
//! Log when app_launch trigger failed.
|
||||
void analytics_event_bt_app_launch_error(uint8_t gatt_error) {
|
||||
analytics_event_bt_error(AnalyticsEvent_BtAppLaunchError, gatt_error);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
//! Log when a Pebble Protocol session is closed.
|
||||
void analytics_event_session_close(bool is_system_session, const Uuid *optional_app_uuid,
|
||||
CommSessionCloseReason reason, uint16_t session_duration_mins) {
|
||||
AnalyticsEventBlob event_blob = {};
|
||||
event_blob.event = (is_system_session ? AnalyticsEvent_PebbleProtocolSystemSessionEnd :
|
||||
AnalyticsEvent_PebbleProtocolAppSessionEnd);
|
||||
event_blob.pp_common_session_close.close_reason = reason;
|
||||
event_blob.pp_common_session_close.duration_minutes = session_duration_mins;
|
||||
|
||||
if (!is_system_session && optional_app_uuid) {
|
||||
memcpy(&event_blob.pp_app_session_close.app_uuid, optional_app_uuid, sizeof(Uuid));
|
||||
}
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_str[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(optional_app_uuid, uuid_str);
|
||||
ANALYTICS_LOG_DEBUG("Session close event. is_system_session=%u, uuid=%s, "
|
||||
"reason=%u, duration_mins=%"PRIu16,
|
||||
is_system_session, uuid_str, reason, session_duration_mins);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
//! Log when the CC2564x BT chip becomes unresponsive
|
||||
void analytics_event_bt_cc2564x_lockup_error(void) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_BtLockupError,
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("CC2564x lockup event");
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a crash event
|
||||
void analytics_event_crash(uint8_t crash_code, uint32_t link_register) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_Crash,
|
||||
.crash_report.crash_code = crash_code,
|
||||
.crash_report.link_register = link_register
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("Crash occured: Code %"PRIu8" / LR: %"PRIu32,
|
||||
event_blob.crash_report.crash_code, event_blob.crash_report.link_register);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log local bluetooth disconnection reason
|
||||
void analytics_event_local_bt_disconnect(uint16_t conn_handle, uint32_t lr) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_LocalBtDisconnect,
|
||||
};
|
||||
|
||||
event_blob.local_bt_disconnect.lr = lr;
|
||||
event_blob.local_bt_disconnect.conn_handle = conn_handle;
|
||||
|
||||
ANALYTICS_LOG_DEBUG("Event %d - BT Disconnect: Handle:%"PRIu16" LR: %"PRIu32,
|
||||
event_blob.event,
|
||||
conn_handle,
|
||||
lr);
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log an Apple Media Service event.
|
||||
void analytics_event_ams(uint8_t type, int32_t aux_info) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_BtLeAMS,
|
||||
.ams = {
|
||||
.type = type,
|
||||
.aux_info = aux_info,
|
||||
},
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("Event %d - AMS: type:%d aux_info: %"PRId32, event_blob.event, type, aux_info);
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log stationary mode events
|
||||
void analytics_event_stationary_state_change(time_t timestamp, uint8_t state_change) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_StationaryModeSwitch,
|
||||
.sd = {
|
||||
.timestamp = timestamp,
|
||||
.state_change = state_change,
|
||||
}
|
||||
};
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a health insight created event.
|
||||
void analytics_event_health_insight_created(time_t timestamp,
|
||||
ActivityInsightType insight_type,
|
||||
PercentTier pct_tier) {
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_HealthInsightCreated,
|
||||
.health_insight_created = {
|
||||
.time_utc = timestamp,
|
||||
.insight_type = insight_type,
|
||||
.percent_tier = pct_tier,
|
||||
}
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
ANALYTICS_LOG_DEBUG("health insight created event: timestamp: %"PRIu32", type:%"PRIu8,
|
||||
timestamp, insight_type);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log a health insight response event.
|
||||
void analytics_event_health_insight_response(time_t timestamp, ActivityInsightType insight_type,
|
||||
ActivitySessionType activity_type,
|
||||
ActivityInsightResponseType response_id) {
|
||||
// Format the event specifc info in the blob. The analytics_logging_log_event() method will fill
|
||||
// in the common fields
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_HealthInsightResponse,
|
||||
.health_insight_response = {
|
||||
.time_utc = timestamp,
|
||||
.insight_type = insight_type,
|
||||
.activity_type = activity_type,
|
||||
.response_id = response_id,
|
||||
}
|
||||
};
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
ANALYTICS_LOG_DEBUG("health insight response event: timestamp: %"PRIu32", type:%"PRIu8 \
|
||||
", response:%"PRIu8, timestamp, insight_type, response_id);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Log an App Crash event
|
||||
void analytics_event_app_crash(const Uuid *uuid, uint32_t pc, uint32_t lr,
|
||||
const uint8_t *build_id, bool is_rocky_app) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = (is_rocky_app ? AnalyticsEvent_RockyAppCrash : AnalyticsEvent_AppCrash),
|
||||
.app_crash_report = {
|
||||
.uuid = *uuid,
|
||||
.pc = pc,
|
||||
.lr = lr,
|
||||
},
|
||||
};
|
||||
|
||||
if (build_id) {
|
||||
memcpy(event_blob.app_crash_report.build_id_slice, build_id,
|
||||
sizeof(event_blob.app_crash_report.build_id_slice));
|
||||
}
|
||||
|
||||
#if LOG_DOMAIN_ANALYTICS
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(uuid, uuid_string);
|
||||
ANALYTICS_LOG_DEBUG("App Crash event: uuid:%s, pc: %p, lr: %p",
|
||||
uuid_string, (void *)pc, (void *)lr);
|
||||
#endif
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
extern bool comm_session_is_valid(const CommSession *session);
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
static bool prv_get_connection_details(CommSession *session, bool *is_ppogatt,
|
||||
uint16_t *conn_interval) {
|
||||
bt_lock();
|
||||
|
||||
if (!session || !comm_session_is_valid(session)) {
|
||||
bt_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool tmp_is_ppogatt =
|
||||
(comm_session_analytics_get_transport_type(session) == CommSessionTransportType_PPoGATT);
|
||||
|
||||
uint16_t tmp_conn_interval = 0;
|
||||
if (tmp_is_ppogatt) {
|
||||
GAPLEConnection *conn = gap_le_connection_get_gateway();
|
||||
if (conn) {
|
||||
tmp_conn_interval = conn->conn_params.conn_interval_1_25ms;
|
||||
}
|
||||
}
|
||||
|
||||
bt_unlock();
|
||||
|
||||
if (is_ppogatt) {
|
||||
*is_ppogatt = tmp_is_ppogatt;
|
||||
}
|
||||
if (conn_interval) {
|
||||
*conn_interval = tmp_conn_interval;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void analytics_event_put_byte_stats(
|
||||
CommSession *session, bool crc_good, uint8_t type,
|
||||
uint32_t bytes_transferred, uint32_t elapsed_time_ms,
|
||||
uint32_t conn_events, uint32_t sync_errors, uint32_t skip_errors, uint32_t other_errors) {
|
||||
|
||||
bool is_ppogatt = false;
|
||||
uint16_t conn_interval = 0;
|
||||
if (!prv_get_connection_details(session, &is_ppogatt, &conn_interval)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_PutByteTime,
|
||||
.pb_time = {
|
||||
.ppogatt = is_ppogatt,
|
||||
.conn_intvl_1_25ms = MIN(conn_interval, UINT8_MAX),
|
||||
.crc_good = crc_good,
|
||||
.type = type,
|
||||
.bytes_transferred = bytes_transferred,
|
||||
.elapsed_time_ms = elapsed_time_ms,
|
||||
.conn_events = MIN(conn_events, UINT32_MAX),
|
||||
.sync_errors = MIN(sync_errors, UINT16_MAX),
|
||||
.skip_errors = MIN(skip_errors, UINT16_MAX),
|
||||
.other_errors = MIN(other_errors, UINT16_MAX),
|
||||
},
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("PutBytes event: is_ppogatt: %d, bytes: %d, time ms: %d",
|
||||
(int)event_blob.pb_time.ppogatt,
|
||||
(int)event_blob.pb_time.bytes_transferred,
|
||||
(int)event_blob.pb_time.elapsed_time_ms);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
#if !PLATFORM_TINTIN
|
||||
void analytics_event_vibe_access(VibePatternFeature vibe_feature, VibeScoreId pattern_id) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_VibeAccess,
|
||||
. vibe_access_data = {
|
||||
.feature = (uint8_t) vibe_feature,
|
||||
.vibe_pattern_id = (uint8_t) pattern_id
|
||||
}
|
||||
};
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
void analytics_event_alarm(AnalyticsEvent event_type, const AlarmInfo *info) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = event_type,
|
||||
.alarm = {
|
||||
.hour = info->hour,
|
||||
.minute = info->minute,
|
||||
.is_smart = info->is_smart,
|
||||
.kind = info->kind,
|
||||
},
|
||||
};
|
||||
|
||||
memcpy(event_blob.alarm.scheduled_days, info->scheduled_days,
|
||||
sizeof(event_blob.alarm.scheduled_days));
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
void analytics_event_bt_chip_boot(uint8_t build_id[BUILD_ID_EXPECTED_LEN],
|
||||
uint32_t crash_lr, uint32_t reboot_reason_code) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_BtChipBoot,
|
||||
.bt_chip_boot = {
|
||||
.crash_lr = crash_lr,
|
||||
.reboot_reason = reboot_reason_code,
|
||||
},
|
||||
};
|
||||
|
||||
memcpy(event_blob.bt_chip_boot.build_id, build_id, sizeof(BUILD_ID_EXPECTED_LEN));
|
||||
|
||||
ANALYTICS_LOG_DEBUG("BtChipBoot event: crash_lr: 0x%x, reboot_reason: %"PRIu32,
|
||||
(int)event_blob.bt_chip_boot.crash_lr,
|
||||
event_blob.bt_chip_boot.reboot_reason);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
void analytics_event_PPoGATT_disconnect(time_t timestamp, bool successful_reconnect) {
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_PPoGATTDisconnect,
|
||||
.ppogatt_disconnect = {
|
||||
.successful_reconnect = successful_reconnect,
|
||||
.time_utc = timestamp,
|
||||
},
|
||||
};
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
|
||||
|
||||
void analytics_event_get_bytes_stats(CommSession *session, uint8_t type,
|
||||
uint32_t bytes_transferred, uint32_t elapsed_time_ms,
|
||||
uint32_t conn_events, uint32_t sync_errors,
|
||||
uint32_t skip_errors, uint32_t other_errors) {
|
||||
bool is_ppogatt = false;
|
||||
uint16_t conn_interval = 0;
|
||||
if (!prv_get_connection_details(session, &is_ppogatt, &conn_interval)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyticsEventBlob event_blob = {
|
||||
.event = AnalyticsEvent_GetBytesStats,
|
||||
.get_bytes_stats = {
|
||||
.ppogatt = is_ppogatt,
|
||||
.conn_intvl_1_25ms = MIN(conn_interval, UINT8_MAX),
|
||||
.type = type,
|
||||
.bytes_transferred = bytes_transferred,
|
||||
.elapsed_time_ms = elapsed_time_ms,
|
||||
.conn_events = conn_events,
|
||||
.sync_errors = MIN(sync_errors, UINT16_MAX),
|
||||
.skip_errors = MIN(skip_errors, UINT16_MAX),
|
||||
.other_errors = MIN(other_errors, UINT16_MAX),
|
||||
},
|
||||
};
|
||||
|
||||
ANALYTICS_LOG_DEBUG("GetBytesStats event: type: 0x%x, num_bytes: %"PRIu32", elapsed_ms: %"PRIu32,
|
||||
type, bytes_transferred, elapsed_time_ms);
|
||||
|
||||
analytics_logging_log_event(&event_blob);
|
||||
}
|
||||
48
src/fw/services/normal/analytics/analytics_external.c
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 "bluetooth/analytics.h"
|
||||
#include "services/common/analytics/analytics_external.h"
|
||||
|
||||
void analytics_external_update(void) {
|
||||
analytics_external_collect_battery();
|
||||
analytics_external_collect_accel_xyz_delta();
|
||||
analytics_external_collect_app_cpu_stats();
|
||||
analytics_external_collect_app_flash_read_stats();
|
||||
analytics_external_collect_cpu_stats();
|
||||
analytics_external_collect_stop_inhibitor_stats(rtc_get_ticks());
|
||||
analytics_external_collect_chip_specific_parameters();
|
||||
analytics_external_collect_bt_pairing_info();
|
||||
analytics_external_collect_ble_parameters();
|
||||
analytics_external_collect_ble_pairing_info();
|
||||
analytics_external_collect_system_flash_statistics();
|
||||
analytics_external_collect_backlight_settings();
|
||||
analytics_external_collect_notification_settings();
|
||||
analytics_external_collect_system_theme_settings();
|
||||
analytics_external_collect_ancs_info();
|
||||
analytics_external_collect_dls_stats();
|
||||
analytics_external_collect_i2c_stats();
|
||||
analytics_external_collect_stack_free();
|
||||
analytics_external_collect_alerts_preferences();
|
||||
analytics_external_collect_timeline_pin_stats();
|
||||
#if PLATFORM_SPALDING
|
||||
analytics_external_collect_display_offset();
|
||||
#endif
|
||||
analytics_external_collect_pfs_stats();
|
||||
analytics_external_collect_bt_chip_heartbeat();
|
||||
analytics_external_collect_kernel_heap_stats();
|
||||
analytics_external_collect_accel_samples_received();
|
||||
}
|
||||
282
src/fw/services/normal/analytics/analytics_heartbeat.c
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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 "services/common/analytics/analytics_heartbeat.h"
|
||||
#include "services/common/analytics/analytics_metric.h"
|
||||
#include "services/common/analytics/analytics_logging.h"
|
||||
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "util/math.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
uint32_t analytics_heartbeat_kind_data_size(AnalyticsHeartbeatKind kind) {
|
||||
AnalyticsMetric last = ANALYTICS_METRIC_INVALID;
|
||||
switch (kind) {
|
||||
case ANALYTICS_HEARTBEAT_KIND_DEVICE:
|
||||
last = ANALYTICS_DEVICE_METRIC_END - 1;
|
||||
break;
|
||||
case ANALYTICS_HEARTBEAT_KIND_APP:
|
||||
last = ANALYTICS_APP_METRIC_END - 1;
|
||||
break;
|
||||
}
|
||||
PBL_ASSERTN(last != ANALYTICS_METRIC_INVALID);
|
||||
return analytics_metric_offset(last) + analytics_metric_size(last);
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// Private
|
||||
static bool prv_verify_kinds_match(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
|
||||
AnalyticsMetricKind metric_kind = analytics_metric_kind(metric);
|
||||
if ((metric_kind == ANALYTICS_METRIC_KIND_DEVICE) &&
|
||||
(heartbeat->kind == ANALYTICS_HEARTBEAT_KIND_DEVICE)) {
|
||||
return true;
|
||||
} else if ((metric_kind == ANALYTICS_METRIC_KIND_APP) &&
|
||||
(heartbeat->kind == ANALYTICS_HEARTBEAT_KIND_APP)) {
|
||||
return true;
|
||||
} else {
|
||||
PBL_CROAK("Metric kind does not match heartbeat kind! %d %d", metric_kind, heartbeat->kind);
|
||||
}
|
||||
}
|
||||
static uint8_t *prv_heartbeat_get_location(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
|
||||
prv_verify_kinds_match(heartbeat, metric);
|
||||
if (analytics_metric_is_array(metric)) {
|
||||
PBL_CROAK("Attempt to use integer value for array metric.");
|
||||
}
|
||||
return heartbeat->data + analytics_metric_offset(metric);
|
||||
}
|
||||
static uint8_t *prv_heartbeat_get_array_location(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric,
|
||||
uint32_t index) {
|
||||
prv_verify_kinds_match(heartbeat, metric);
|
||||
if (!analytics_metric_is_array(metric)) {
|
||||
PBL_CROAK("Attempt to use array value for integer metric.");
|
||||
}
|
||||
uint32_t len = analytics_metric_num_elements(metric);
|
||||
uint32_t element_size = analytics_metric_element_size(metric);
|
||||
if (index > len) {
|
||||
PBL_CROAK("Attempt to use array value at invalid index %" PRId32 " (len %" PRId32 ")",
|
||||
index, len);
|
||||
}
|
||||
return heartbeat->data + analytics_metric_offset(metric) + index*element_size;
|
||||
}
|
||||
|
||||
static void prv_location_set_value(uint8_t *location, int64_t val, AnalyticsMetricElementType type) {
|
||||
switch (type) {
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_NIL:
|
||||
WTF;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT8:
|
||||
{
|
||||
*((uint8_t*)location) = (uint8_t)CLIP(val, 0, UINT8_MAX);
|
||||
return;
|
||||
}
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT16:
|
||||
{
|
||||
*((uint16_t*)location) = (uint16_t)CLIP(val, 0, UINT16_MAX);
|
||||
return;
|
||||
}
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT32:
|
||||
{
|
||||
*((uint32_t*)location) = (uint32_t)CLIP(val, 0, UINT32_MAX);
|
||||
return;
|
||||
}
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT8:
|
||||
{
|
||||
*((int8_t*)location) = (int8_t)CLIP(val, INT8_MIN, INT8_MAX);
|
||||
return;
|
||||
}
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT16:
|
||||
{
|
||||
*((int16_t*)location) = (int16_t)CLIP(val, INT16_MIN, INT16_MAX);
|
||||
return;
|
||||
}
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT32:
|
||||
{
|
||||
*((int32_t*)location) = (int32_t)CLIP(val, INT32_MIN, INT32_MAX);
|
||||
return;
|
||||
}
|
||||
}
|
||||
WTF; // Should not get here!
|
||||
}
|
||||
static int64_t prv_location_get_value(uint8_t *location, AnalyticsMetricElementType type) {
|
||||
switch (type) {
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_NIL:
|
||||
WTF;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT8:
|
||||
return *(uint8_t*)location;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT16:
|
||||
return *(uint16_t*)location;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT32:
|
||||
return *(uint32_t*)location;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT8:
|
||||
return *(int8_t*)location;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT16:
|
||||
return *(int16_t*)location;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT32:
|
||||
return *(int32_t*)location;
|
||||
}
|
||||
WTF; // Should not get here!
|
||||
}
|
||||
|
||||
//////////
|
||||
// Set
|
||||
void analytics_heartbeat_set(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, int64_t val) {
|
||||
uint8_t *location = prv_heartbeat_get_location(heartbeat, metric);
|
||||
prv_location_set_value(location, val, analytics_metric_element_type(metric));
|
||||
}
|
||||
|
||||
void analytics_heartbeat_set_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index, int64_t val) {
|
||||
uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, index);
|
||||
prv_location_set_value(location, val, analytics_metric_element_type(metric));
|
||||
}
|
||||
|
||||
void analytics_heartbeat_set_entire_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, const void* data) {
|
||||
uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, 0);
|
||||
uint32_t size = analytics_metric_size(metric);
|
||||
memcpy(location, data, size);
|
||||
}
|
||||
|
||||
/////////
|
||||
// Get
|
||||
int64_t analytics_heartbeat_get(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric) {
|
||||
uint8_t *location = prv_heartbeat_get_location(heartbeat, metric);
|
||||
return prv_location_get_value(location, analytics_metric_element_type(metric));
|
||||
}
|
||||
|
||||
int64_t analytics_heartbeat_get_array(AnalyticsHeartbeat *heartbeat, AnalyticsMetric metric, uint32_t index) {
|
||||
uint8_t *location = prv_heartbeat_get_array_location(heartbeat, metric, index);
|
||||
return prv_location_get_value(location, analytics_metric_element_type(metric));
|
||||
}
|
||||
|
||||
const Uuid *analytics_heartbeat_get_uuid(AnalyticsHeartbeat *heartbeat) {
|
||||
return (const Uuid*)prv_heartbeat_get_array_location(heartbeat, ANALYTICS_APP_METRIC_UUID, 0);
|
||||
}
|
||||
|
||||
///////////////////
|
||||
// Create / Clear
|
||||
AnalyticsHeartbeat *analytics_heartbeat_create(AnalyticsHeartbeatKind kind) {
|
||||
uint32_t size = sizeof(AnalyticsHeartbeat) + analytics_heartbeat_kind_data_size(kind);
|
||||
AnalyticsHeartbeat *heartbeat = kernel_malloc_check(size);
|
||||
heartbeat->kind = kind;
|
||||
analytics_heartbeat_clear(heartbeat);
|
||||
return heartbeat;
|
||||
}
|
||||
|
||||
AnalyticsHeartbeat *analytics_heartbeat_device_create() {
|
||||
AnalyticsHeartbeat *hb = analytics_heartbeat_create(ANALYTICS_HEARTBEAT_KIND_DEVICE);
|
||||
analytics_heartbeat_set(hb, ANALYTICS_DEVICE_METRIC_BLOB_KIND,
|
||||
ANALYTICS_BLOB_KIND_DEVICE_HEARTBEAT);
|
||||
analytics_heartbeat_set(hb, ANALYTICS_DEVICE_METRIC_BLOB_VERSION,
|
||||
ANALYTICS_DEVICE_HEARTBEAT_BLOB_VERSION);
|
||||
return hb;
|
||||
}
|
||||
|
||||
AnalyticsHeartbeat *analytics_heartbeat_app_create(const Uuid *uuid) {
|
||||
AnalyticsHeartbeat *hb = analytics_heartbeat_create(ANALYTICS_HEARTBEAT_KIND_APP);
|
||||
analytics_heartbeat_set_entire_array(hb, ANALYTICS_APP_METRIC_UUID, uuid);
|
||||
analytics_heartbeat_set(hb, ANALYTICS_APP_METRIC_BLOB_KIND,
|
||||
ANALYTICS_BLOB_KIND_APP_HEARTBEAT);
|
||||
analytics_heartbeat_set(hb, ANALYTICS_APP_METRIC_BLOB_VERSION,
|
||||
ANALYTICS_APP_HEARTBEAT_BLOB_VERSION);
|
||||
return hb;
|
||||
}
|
||||
|
||||
void analytics_heartbeat_clear(AnalyticsHeartbeat *heartbeat) {
|
||||
AnalyticsHeartbeatKind kind = heartbeat->kind;
|
||||
uint32_t size = sizeof(AnalyticsHeartbeat) + analytics_heartbeat_kind_data_size(kind);
|
||||
memset(heartbeat, 0, size);
|
||||
heartbeat->kind = kind;
|
||||
}
|
||||
|
||||
//////////////////
|
||||
// Debug
|
||||
#ifdef ANALYTICS_DEBUG
|
||||
// Function to get the name of a macro given it's runtime value. (i.e. mapping
|
||||
// (1: "ANALYTICS_DEVICE_METRIC_MSG_ID"),
|
||||
// (2: "ANALYTICS_DEVICE_METRIC_VERSION"),
|
||||
// ...
|
||||
// )
|
||||
#define CASE(name, ...) case name: return #name;
|
||||
static const char *prv_get_metric_name(AnalyticsMetric metric) {
|
||||
switch (metric) {
|
||||
ANALYTICS_METRIC_TABLE(CASE,CASE,CASE,,,,,,)
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
#undef CASE
|
||||
|
||||
static void prv_print_heartbeat(AnalyticsHeartbeat *heartbeat, AnalyticsMetric start, AnalyticsMetric end) {
|
||||
for (AnalyticsMetric metric = start + 1; metric < end; metric++) {
|
||||
const char *name = prv_get_metric_name(metric);
|
||||
if (!analytics_metric_is_array(metric)) {
|
||||
int64_t val = analytics_heartbeat_get(heartbeat, metric);
|
||||
if (val >= 0) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %" PRIu32 " (0x%" PRIx32")",
|
||||
analytics_metric_offset(metric), name, (uint32_t)val, (uint32_t)val);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %" PRId32 " (0x%" PRIx32")",
|
||||
analytics_metric_offset(metric), name, (int32_t)val, (int32_t)val);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const size_t BUF_LENGTH = 256;
|
||||
char buf[BUF_LENGTH];
|
||||
uint32_t written = 0;
|
||||
for (uint32_t i = 0; i < analytics_metric_num_elements(metric); i++) {
|
||||
if (written > BUF_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "print buffer overflow by %lu bytes",
|
||||
BUF_LENGTH - written);
|
||||
continue;
|
||||
}
|
||||
int64_t val = analytics_heartbeat_get_array(heartbeat, metric, i);
|
||||
const char *sep = (i == 0 ? "" : ", ");
|
||||
if (val >= 0) {
|
||||
written += snprintf(buf + written, BUF_LENGTH - written,
|
||||
"%s%" PRIu32 " (0x%" PRIx32 ")", sep, (uint32_t)val, (uint32_t)val);
|
||||
} else {
|
||||
written += snprintf(buf + written, BUF_LENGTH - written,
|
||||
"%s%" PRId32 " (0x%" PRIx32 ")", sep, (int32_t)val, (int32_t)val);
|
||||
}
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "%3" PRIu32 ": %s: %s", analytics_metric_offset(metric), name, buf);
|
||||
}
|
||||
}
|
||||
|
||||
void analytics_heartbeat_print(AnalyticsHeartbeat *heartbeat) {
|
||||
switch (heartbeat->kind) {
|
||||
case ANALYTICS_HEARTBEAT_KIND_DEVICE:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Device heartbeat:");
|
||||
prv_print_heartbeat(heartbeat, ANALYTICS_DEVICE_METRIC_START, ANALYTICS_DEVICE_METRIC_END);
|
||||
break;
|
||||
case ANALYTICS_HEARTBEAT_KIND_APP: {
|
||||
const Uuid *uuid = analytics_heartbeat_get_uuid(heartbeat);
|
||||
char uuid_buf[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(uuid, uuid_buf);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "App heartbeat for %s:", uuid_buf);
|
||||
prv_print_heartbeat(heartbeat, ANALYTICS_APP_METRIC_START, ANALYTICS_APP_METRIC_END);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Unable to print heartbeat: Unrecognized kind %d", heartbeat->kind);
|
||||
}
|
||||
}
|
||||
#else
|
||||
void analytics_heartbeat_print(AnalyticsHeartbeat *heartbeat) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Turn on ANALYTICS_DEBUG to get heartbeat printing support.");
|
||||
}
|
||||
#endif
|
||||
277
src/fw/services/normal/analytics/analytics_logging.c
Normal file
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* 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 <string.h>
|
||||
|
||||
#include "applib/data_logging.h"
|
||||
#include "comm/bt_lock.h"
|
||||
#include "drivers/rtc.h"
|
||||
|
||||
#include "os/tick.h"
|
||||
#include "kernel/event_loop.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
|
||||
#include "services/normal/data_logging/data_logging_service.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "services/common/system_task.h"
|
||||
|
||||
#include "services/common/analytics/analytics_event.h"
|
||||
#include "services/common/analytics/analytics_external.h"
|
||||
#include "services/common/analytics/analytics_heartbeat.h"
|
||||
#include "services/common/analytics/analytics_logging.h"
|
||||
#include "services/common/analytics/analytics_metric.h"
|
||||
#include "services/common/analytics/analytics_storage.h"
|
||||
#include "services/common/system_task.h"
|
||||
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
enum {
|
||||
#ifdef ANALYTICS_DEBUG
|
||||
HEARTBEAT_INTERVAL = 10 * 1000, // 10 seconds
|
||||
#else
|
||||
HEARTBEAT_INTERVAL = 60 * 60 * 1000, // 1 hour
|
||||
#endif
|
||||
};
|
||||
|
||||
static int s_heartbeat_timer;
|
||||
static uint32_t s_previous_send_ticks;
|
||||
|
||||
DataLoggingSessionRef s_device_heartbeat_session = NULL;
|
||||
DataLoggingSessionRef s_app_heartbeat_session = NULL;
|
||||
DataLoggingSessionRef s_event_session = NULL;
|
||||
|
||||
static void prv_schedule_retry();
|
||||
static void prv_create_event_session_cb(void *ignored);
|
||||
|
||||
static void prv_reset_local_session_ptrs(void) {
|
||||
s_device_heartbeat_session = NULL;
|
||||
s_app_heartbeat_session = NULL;
|
||||
s_event_session = NULL;
|
||||
}
|
||||
|
||||
static void prv_timer_callback(void *data) {
|
||||
if (!dls_initialized()) {
|
||||
// We need to wait until data logging is initialized before we can log heartbeats
|
||||
prv_schedule_retry();
|
||||
return;
|
||||
}
|
||||
if (!s_event_session) {
|
||||
// If the event session has not been created yet, create that. The only time we may have to do
|
||||
// this here is if the first call to prv_create_event_session_cb() during boot failed to create
|
||||
// the session.
|
||||
launcher_task_add_callback(prv_create_event_session_cb, NULL);
|
||||
}
|
||||
system_task_add_callback(analytics_logging_system_task_cb, NULL);
|
||||
new_timer_start(s_heartbeat_timer, HEARTBEAT_INTERVAL, prv_timer_callback, NULL, 0);
|
||||
}
|
||||
|
||||
static void prv_schedule_retry() {
|
||||
new_timer_start(s_heartbeat_timer, 5000, prv_timer_callback, NULL, 0);
|
||||
}
|
||||
|
||||
static DataLoggingSessionRef prv_create_dls(AnalyticsBlobKind kind, uint32_t item_length) {
|
||||
Uuid system_uuid = UUID_SYSTEM;
|
||||
bool buffered = false;
|
||||
const char *kind_str;
|
||||
uint32_t tag;
|
||||
|
||||
if (kind == ANALYTICS_BLOB_KIND_DEVICE_HEARTBEAT) {
|
||||
kind_str = "Device";
|
||||
tag = DlsSystemTagAnalyticsDeviceHeartbeat;
|
||||
} else if (kind == ANALYTICS_BLOB_KIND_APP_HEARTBEAT) {
|
||||
kind_str = "App";
|
||||
tag = DlsSystemTagAnalyticsAppHeartbeat;
|
||||
} else if (kind == ANALYTICS_BLOB_KIND_EVENT) {
|
||||
kind_str = "Event";
|
||||
buffered = true;
|
||||
tag = DlsSystemTagAnalyticsEvent;
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
|
||||
// TODO: Use different tag ids for device_hb and app_hb sessions.
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-5463
|
||||
const bool resume = false;
|
||||
DataLoggingSessionRef dls_session = dls_create(
|
||||
tag, DATA_LOGGING_BYTE_ARRAY, item_length, buffered, resume, &system_uuid);
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "%s HB Session: %p", kind_str, dls_session);
|
||||
if (!dls_session) {
|
||||
// Data logging full at boot. Reset it and try again 5s later
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Data logging full at boot. Clearing...");
|
||||
// We reset all data logging here, including data logging for applications,
|
||||
// because an inability to allocate a new session means all 200+ session
|
||||
// IDs are exhausted, likely caused by a misbehaving app_hb. See discussion at:
|
||||
// https://github.com/pebble/tintin/pull/1967#discussion-diff-11746345
|
||||
// And issue about removing/moving this at
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-5473
|
||||
prv_reset_local_session_ptrs();
|
||||
dls_clear();
|
||||
prv_schedule_retry();
|
||||
return NULL;
|
||||
}
|
||||
return dls_session;
|
||||
}
|
||||
|
||||
static void prv_dls_log(AnalyticsHeartbeat *device_hb, AnalyticsHeartbeatList *app_hbs) {
|
||||
dls_log(s_device_heartbeat_session, device_hb->data, 1);
|
||||
#ifdef ANALYTICS_DEBUG
|
||||
analytics_heartbeat_print(device_hb);
|
||||
#endif
|
||||
kernel_free(device_hb);
|
||||
|
||||
AnalyticsHeartbeatList *app_hb_node = app_hbs;
|
||||
while (app_hb_node) {
|
||||
AnalyticsHeartbeat *app_hb = app_hb_node->heartbeat;
|
||||
#ifdef ANALYTICS_DEBUG
|
||||
analytics_heartbeat_print(app_hb);
|
||||
#endif
|
||||
dls_log(s_app_heartbeat_session, app_hb->data, 1);
|
||||
|
||||
AnalyticsHeartbeatList *next = (AnalyticsHeartbeatList*)app_hb_node->node.next;
|
||||
kernel_free(app_hb);
|
||||
kernel_free(app_hb_node);
|
||||
app_hb_node = next;
|
||||
}
|
||||
}
|
||||
|
||||
// System task callback used to prepare and log the heartbeats using dls_log().
|
||||
void analytics_logging_system_task_cb(void *ignored) {
|
||||
if (!s_device_heartbeat_session) {
|
||||
uint32_t size = analytics_heartbeat_kind_data_size(ANALYTICS_HEARTBEAT_KIND_DEVICE);
|
||||
s_device_heartbeat_session = prv_create_dls(ANALYTICS_BLOB_KIND_DEVICE_HEARTBEAT, size);
|
||||
if (!s_device_heartbeat_session) return;
|
||||
}
|
||||
|
||||
// Tell the watchdog timer that we are still awake. dls_create() could take up to 4 seconds if
|
||||
// we can't get the ispp send buffer
|
||||
system_task_watchdog_feed();
|
||||
|
||||
if (!s_app_heartbeat_session) {
|
||||
uint32_t size = analytics_heartbeat_kind_data_size(ANALYTICS_HEARTBEAT_KIND_APP);
|
||||
s_app_heartbeat_session = prv_create_dls(ANALYTICS_BLOB_KIND_APP_HEARTBEAT, size);
|
||||
if (!s_app_heartbeat_session) return;
|
||||
}
|
||||
|
||||
// Tell the watchdog timer that we are still awake. dls_create() could take up to 4 seconds if
|
||||
// we can't get the ispp send buffer
|
||||
system_task_watchdog_feed();
|
||||
|
||||
analytics_external_update();
|
||||
|
||||
// Tell the watchdog timer that we are still awake. Occasionally, analytics_external_update()
|
||||
// could take a while to execute if it needs to wait for the bt_lock() for example
|
||||
system_task_watchdog_feed();
|
||||
|
||||
// The phone and proxy server expect us to send local time. The phone will imbed the time zone
|
||||
// offset into the blob and the proxy server will then use that to convert to UTC before it
|
||||
// gets placed into the database
|
||||
uint32_t timestamp = time_utc_to_local(rtc_get_time());
|
||||
uint64_t current_ticks = rtc_get_ticks();
|
||||
|
||||
AnalyticsHeartbeat *device_hb = NULL;
|
||||
AnalyticsHeartbeatList *app_hbs = NULL;
|
||||
|
||||
{
|
||||
analytics_storage_take_lock();
|
||||
|
||||
extern void analytics_stopwatches_update(uint64_t current_ticks);
|
||||
analytics_stopwatches_update(current_ticks);
|
||||
|
||||
// Hijack the device_hb and app_hb heartbeats from analytics_storage.
|
||||
// After this point, we own the memory, so analytics_storage will not
|
||||
// modify it anymore. Thus, we do not need to hold the lock while
|
||||
// logging.
|
||||
device_hb = analytics_storage_hijack_device_heartbeat();
|
||||
app_hbs = analytics_storage_hijack_app_heartbeats();
|
||||
|
||||
analytics_storage_give_lock();
|
||||
}
|
||||
|
||||
uint32_t dt_ticks = current_ticks - s_previous_send_ticks;
|
||||
uint32_t dt_ms = ticks_to_milliseconds(dt_ticks);
|
||||
s_previous_send_ticks = current_ticks;
|
||||
|
||||
analytics_heartbeat_set(device_hb, ANALYTICS_DEVICE_METRIC_TIMESTAMP, timestamp);
|
||||
analytics_heartbeat_set(device_hb, ANALYTICS_DEVICE_METRIC_DEVICE_UP_TIME, current_ticks);
|
||||
analytics_heartbeat_set(device_hb, ANALYTICS_DEVICE_METRIC_TIME_INTERVAL, dt_ms);
|
||||
|
||||
AnalyticsHeartbeatList *app_hb_node = app_hbs;
|
||||
while (app_hb_node) {
|
||||
AnalyticsHeartbeat *app_hb = app_hb_node->heartbeat;
|
||||
analytics_heartbeat_set(app_hb, ANALYTICS_APP_METRIC_TIMESTAMP, timestamp);
|
||||
analytics_heartbeat_set(app_hb, ANALYTICS_APP_METRIC_TIME_INTERVAL, dt_ms);
|
||||
app_hb_node = (AnalyticsHeartbeatList*)app_hb_node->node.next;
|
||||
}
|
||||
|
||||
prv_dls_log(device_hb, app_hbs);
|
||||
}
|
||||
|
||||
|
||||
// Launcher task callback used to create our event logging session.
|
||||
static void prv_create_event_session_cb(void *ignored) {
|
||||
if (!s_event_session) {
|
||||
s_event_session = prv_create_dls(ANALYTICS_BLOB_KIND_EVENT, sizeof(AnalyticsEventBlob));
|
||||
// If the above call fails, it will schedule our timer to try again in a few seconds
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_handle_log_event(AnalyticsEventBlob *event_blob) {
|
||||
if (!s_event_session) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Event dropped because session not created yet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log it
|
||||
dls_log(s_event_session, event_blob, 1);
|
||||
}
|
||||
|
||||
static void prv_handle_async_event_logging(void *data) {
|
||||
AnalyticsEventBlob *event_blob = (AnalyticsEventBlob *)data;
|
||||
prv_handle_log_event(event_blob);
|
||||
kernel_free(event_blob);
|
||||
}
|
||||
|
||||
void analytics_logging_log_event(AnalyticsEventBlob *event_blob) {
|
||||
// Fill in the meta info
|
||||
event_blob->kind = ANALYTICS_BLOB_KIND_EVENT;
|
||||
event_blob->version = ANALYTICS_EVENT_BLOB_VERSION;
|
||||
event_blob->timestamp = time_utc_to_local(rtc_get_time());
|
||||
|
||||
// TODO: We should be able to remove this once PBL-23925 is fixed
|
||||
if (bt_lock_is_held()) {
|
||||
// We run the risk of deadlocking if we hold the bt_lock at this point. If
|
||||
// it's the case then schedule a callback so the dls code runs while we no
|
||||
// longer hold the lock
|
||||
AnalyticsEventBlob *event_blob_copy =
|
||||
kernel_malloc_check(sizeof(AnalyticsEventBlob));
|
||||
memcpy(event_blob_copy, event_blob, sizeof(*event_blob_copy));
|
||||
system_task_add_callback(prv_handle_async_event_logging, event_blob_copy);
|
||||
} else {
|
||||
prv_handle_log_event(event_blob);
|
||||
}
|
||||
}
|
||||
|
||||
void analytics_logging_init(void) {
|
||||
s_heartbeat_timer = new_timer_create();
|
||||
s_previous_send_ticks = rtc_get_ticks();
|
||||
new_timer_start(s_heartbeat_timer, HEARTBEAT_INTERVAL, prv_timer_callback, NULL, 0);
|
||||
|
||||
// Create the event session on a launcher task callback because we have to wait for
|
||||
// services (like data logging service) to be initialized)
|
||||
launcher_task_add_callback(prv_create_event_session_cb, NULL);
|
||||
}
|
||||
151
src/fw/services/normal/analytics/analytics_metric.c
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 "services/common/analytics/analytics_metric.h"
|
||||
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
|
||||
typedef struct {
|
||||
AnalyticsMetricElementType element_type;
|
||||
uint8_t num_elements;
|
||||
} AnalyticsMetricDataType;
|
||||
|
||||
// http://stackoverflow.com/questions/11761703/overloading-macro-on-number-of-arguments
|
||||
#define GET_MACRO(_1, _2, _3, NAME, ...) NAME
|
||||
|
||||
#define ENTRY3(name, element_type, num_elements) {element_type, num_elements},
|
||||
#define ENTRY2(name, element_type) {element_type, 1},
|
||||
#define ENTRY1(name) {ANALYTICS_METRIC_ELEMENT_TYPE_NIL, 0},
|
||||
#define ENTRY(...) GET_MACRO(__VA_ARGS__, ENTRY3, ENTRY2, ENTRY1)(__VA_ARGS__)
|
||||
|
||||
// Mapping from type index to data type of metric. We waste some space here
|
||||
// by including the marker metrics, but it makes the code a fair bit simpler
|
||||
// since we don't need an index translation table.
|
||||
static const AnalyticsMetricDataType s_heartbeat_template[] = {
|
||||
ANALYTICS_METRIC_TABLE(ENTRY, ENTRY, ENTRY,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_UINT8,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_UINT16,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_UINT32,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_INT8,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_INT16,
|
||||
ANALYTICS_METRIC_ELEMENT_TYPE_INT32)
|
||||
};
|
||||
|
||||
#define NUM_METRICS ARRAY_LENGTH(s_heartbeat_template)
|
||||
|
||||
static const AnalyticsMetricDataType *prv_get_metric_data_type(AnalyticsMetric metric) {
|
||||
PBL_ASSERTN(analytics_metric_kind(metric) != ANALYTICS_METRIC_KIND_UNKNOWN);
|
||||
return &s_heartbeat_template[metric];
|
||||
}
|
||||
|
||||
AnalyticsMetricElementType analytics_metric_element_type(AnalyticsMetric metric) {
|
||||
return prv_get_metric_data_type(metric)->element_type;
|
||||
}
|
||||
|
||||
uint32_t analytics_metric_num_elements(AnalyticsMetric metric) {
|
||||
return prv_get_metric_data_type(metric)->num_elements;
|
||||
}
|
||||
|
||||
uint32_t analytics_metric_element_size(AnalyticsMetric metric) {
|
||||
const AnalyticsMetricDataType *type = prv_get_metric_data_type(metric);
|
||||
switch (type->element_type) {
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_NIL:
|
||||
return 0;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT8:
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT8:
|
||||
return 1;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT16:
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT16:
|
||||
return 2;
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_UINT32:
|
||||
case ANALYTICS_METRIC_ELEMENT_TYPE_INT32:
|
||||
return 4;
|
||||
}
|
||||
PBL_CROAK("no such element_type %d", type->element_type);
|
||||
}
|
||||
|
||||
uint32_t analytics_metric_size(AnalyticsMetric metric) {
|
||||
uint32_t num_elements = analytics_metric_num_elements(metric);
|
||||
uint32_t element_size = analytics_metric_element_size(metric);
|
||||
return num_elements * element_size;
|
||||
}
|
||||
|
||||
bool analytics_metric_is_array(AnalyticsMetric metric) {
|
||||
const AnalyticsMetricDataType *type = prv_get_metric_data_type(metric);
|
||||
return (type->num_elements > 1);
|
||||
}
|
||||
|
||||
bool analytics_metric_is_unsigned(AnalyticsMetric metric) {
|
||||
const AnalyticsMetricDataType *type = prv_get_metric_data_type(metric);
|
||||
return (type->element_type == ANALYTICS_METRIC_ELEMENT_TYPE_UINT32 ||
|
||||
type->element_type == ANALYTICS_METRIC_ELEMENT_TYPE_UINT16 ||
|
||||
type->element_type == ANALYTICS_METRIC_ELEMENT_TYPE_UINT8);
|
||||
}
|
||||
|
||||
static uint16_t s_metric_heartbeat_offset[NUM_METRICS];
|
||||
|
||||
void analytics_metric_init(void) {
|
||||
uint32_t device_offset = 0;
|
||||
uint32_t app_offset = 0;
|
||||
const uint16_t INVALID_OFFSET = ~0;
|
||||
for (AnalyticsMetric metric = ANALYTICS_METRIC_START;
|
||||
metric < ANALYTICS_METRIC_END; metric++) {
|
||||
switch (analytics_metric_kind(metric)) {
|
||||
case ANALYTICS_METRIC_KIND_DEVICE:
|
||||
s_metric_heartbeat_offset[metric] = device_offset;
|
||||
device_offset += analytics_metric_size(metric);
|
||||
PBL_ASSERTN(device_offset < INVALID_OFFSET);
|
||||
break;
|
||||
case ANALYTICS_METRIC_KIND_APP:
|
||||
s_metric_heartbeat_offset[metric] = app_offset;
|
||||
app_offset += analytics_metric_size(metric);
|
||||
PBL_ASSERTN(app_offset < INVALID_OFFSET);
|
||||
break;
|
||||
case ANALYTICS_METRIC_KIND_MARKER:
|
||||
// Marker metrics do not actually exist in either heartbeat, they are
|
||||
// markers only.
|
||||
s_metric_heartbeat_offset[metric] = INVALID_OFFSET;
|
||||
break;
|
||||
case ANALYTICS_METRIC_KIND_UNKNOWN:
|
||||
WTF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t analytics_metric_offset(AnalyticsMetric metric) {
|
||||
AnalyticsMetricKind kind = analytics_metric_kind(metric);
|
||||
PBL_ASSERTN((kind == ANALYTICS_METRIC_KIND_DEVICE) || (kind == ANALYTICS_METRIC_KIND_APP));
|
||||
return s_metric_heartbeat_offset[metric];
|
||||
}
|
||||
|
||||
AnalyticsMetricKind analytics_metric_kind(AnalyticsMetric metric) {
|
||||
if ((metric > ANALYTICS_DEVICE_METRIC_START)
|
||||
&& (metric < ANALYTICS_DEVICE_METRIC_END)) {
|
||||
return ANALYTICS_METRIC_KIND_DEVICE;
|
||||
} else if ((metric > ANALYTICS_APP_METRIC_START)
|
||||
&& (metric < ANALYTICS_APP_METRIC_END)) {
|
||||
return ANALYTICS_METRIC_KIND_APP;
|
||||
} else if ((metric >= ANALYTICS_METRIC_START)
|
||||
&& (metric <= ANALYTICS_METRIC_END)) {
|
||||
// "Marker" metrics are not actual real metrics, they are only used
|
||||
// to easily find the position of other metrics. (i.e. ANALYTICS_METRIC_START
|
||||
// is a "marker" metric).
|
||||
return ANALYTICS_METRIC_KIND_MARKER;
|
||||
} else {
|
||||
return ANALYTICS_METRIC_KIND_UNKNOWN;
|
||||
}
|
||||
}
|
||||
170
src/fw/services/normal/analytics/analytics_storage.c
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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 "os/mutex.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "process_management/worker_manager.h"
|
||||
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/analytics/analytics_metric.h"
|
||||
#include "services/common/analytics/analytics_storage.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
static PebbleRecursiveMutex *s_analytics_storage_mutex = NULL;
|
||||
|
||||
static AnalyticsHeartbeat *s_device_heartbeat = NULL;
|
||||
static AnalyticsHeartbeatList *s_app_heartbeat_list = NULL;
|
||||
|
||||
#define MAX_APP_HEARTBEATS 8
|
||||
|
||||
void analytics_storage_init(void) {
|
||||
s_analytics_storage_mutex = mutex_create_recursive();
|
||||
PBL_ASSERTN(s_analytics_storage_mutex);
|
||||
s_device_heartbeat = analytics_heartbeat_device_create();
|
||||
PBL_ASSERTN(s_device_heartbeat);
|
||||
}
|
||||
|
||||
//////////
|
||||
// Lock
|
||||
void analytics_storage_take_lock(void) {
|
||||
mutex_lock_recursive(s_analytics_storage_mutex);
|
||||
}
|
||||
|
||||
bool analytics_storage_has_lock(void) {
|
||||
bool has_lock = mutex_is_owned_recursive(s_analytics_storage_mutex);
|
||||
if (!has_lock) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Analytics lock is not held when it should be!");
|
||||
}
|
||||
return has_lock;
|
||||
}
|
||||
|
||||
void analytics_storage_give_lock(void) {
|
||||
mutex_unlock_recursive(s_analytics_storage_mutex);
|
||||
}
|
||||
|
||||
///////
|
||||
// Get
|
||||
AnalyticsHeartbeat *analytics_storage_hijack_device_heartbeat() {
|
||||
PBL_ASSERTN(analytics_storage_has_lock());
|
||||
|
||||
AnalyticsHeartbeat *device = s_device_heartbeat;
|
||||
|
||||
s_device_heartbeat = analytics_heartbeat_device_create();
|
||||
PBL_ASSERTN(s_device_heartbeat);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
AnalyticsHeartbeatList *analytics_storage_hijack_app_heartbeats() {
|
||||
PBL_ASSERTN(analytics_storage_has_lock());
|
||||
|
||||
AnalyticsHeartbeatList *apps = s_app_heartbeat_list;
|
||||
s_app_heartbeat_list = NULL;
|
||||
return apps;
|
||||
}
|
||||
|
||||
///////////
|
||||
// Search
|
||||
static bool prv_is_app_node_with_uuid(ListNode *found_node, void *data) {
|
||||
const Uuid *searching_for_uuid = (const Uuid*)data;
|
||||
AnalyticsHeartbeatList *app_node = (AnalyticsHeartbeatList*)found_node;
|
||||
const Uuid *found_uuid = analytics_heartbeat_get_uuid(app_node->heartbeat);
|
||||
return uuid_equal(searching_for_uuid, found_uuid);
|
||||
}
|
||||
|
||||
static AnalyticsHeartbeatList *prv_app_node_create(const Uuid *uuid) {
|
||||
AnalyticsHeartbeatList *app_heartbeat_node = kernel_malloc_check(sizeof(AnalyticsHeartbeatList));
|
||||
|
||||
list_init(&app_heartbeat_node->node);
|
||||
app_heartbeat_node->heartbeat = analytics_heartbeat_app_create(uuid);
|
||||
|
||||
return app_heartbeat_node;
|
||||
}
|
||||
|
||||
const Uuid *analytics_uuid_for_client(AnalyticsClient client) {
|
||||
const PebbleProcessMd *md;
|
||||
if (client == AnalyticsClient_CurrentTask) {
|
||||
PebbleTask task = pebble_task_get_current();
|
||||
if (task == PebbleTask_App) {
|
||||
client = AnalyticsClient_App;
|
||||
} else if (task == PebbleTask_Worker) {
|
||||
client = AnalyticsClient_Worker;
|
||||
} else {
|
||||
return NULL; // System UUID
|
||||
}
|
||||
}
|
||||
|
||||
if (client == AnalyticsClient_App) {
|
||||
md = app_manager_get_current_app_md();
|
||||
} else if (client == AnalyticsClient_Worker) {
|
||||
md = worker_manager_get_current_worker_md();
|
||||
} else if (client == AnalyticsClient_System) {
|
||||
return NULL;
|
||||
} else if (client == AnalyticsClient_Ignore) {
|
||||
return NULL;
|
||||
} else {
|
||||
WTF;
|
||||
}
|
||||
|
||||
if (md != NULL) {
|
||||
return &md->uuid;
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AnalyticsHeartbeat *analytics_storage_find(AnalyticsMetric metric, const Uuid *uuid,
|
||||
AnalyticsClient client) {
|
||||
PBL_ASSERTN(analytics_storage_has_lock());
|
||||
|
||||
switch (analytics_metric_kind(metric)) {
|
||||
case ANALYTICS_METRIC_KIND_DEVICE:
|
||||
PBL_ASSERTN(client == AnalyticsClient_Ignore || client == AnalyticsClient_System);
|
||||
return s_device_heartbeat;
|
||||
case ANALYTICS_METRIC_KIND_APP: {
|
||||
PBL_ASSERTN(client == AnalyticsClient_Ignore || client != AnalyticsClient_System);
|
||||
const Uuid uuid_system = UUID_SYSTEM;
|
||||
if (!uuid) {
|
||||
uuid = analytics_uuid_for_client(client);
|
||||
if (!uuid) {
|
||||
// There is a brief period of time where no app is running, which we
|
||||
// attribute to the system UUID. For now, this lets us track how
|
||||
// much time we are missing, although we probably want to try and
|
||||
// tighten this up as much as possible going forward.
|
||||
uuid = &uuid_system;
|
||||
}
|
||||
}
|
||||
ListNode *node = list_find((ListNode*)s_app_heartbeat_list,
|
||||
prv_is_app_node_with_uuid, (void*)uuid);
|
||||
AnalyticsHeartbeatList *app_node = (AnalyticsHeartbeatList*)node;
|
||||
if (!app_node) {
|
||||
if (list_count((ListNode *)s_app_heartbeat_list) >= MAX_APP_HEARTBEATS) {
|
||||
ANALYTICS_LOG_DEBUG("No more app heartbeat sessions available");
|
||||
return NULL;
|
||||
}
|
||||
app_node = prv_app_node_create(uuid);
|
||||
s_app_heartbeat_list = (AnalyticsHeartbeatList*)list_prepend(
|
||||
(ListNode*)s_app_heartbeat_list, &app_node->node);
|
||||
}
|
||||
return app_node->heartbeat;
|
||||
}
|
||||
default:
|
||||
WTF;
|
||||
}
|
||||
}
|
||||
604
src/fw/services/normal/app_cache.c
Normal file
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
* 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 "app_cache.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/app_storage.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/blob_db/pin_db.h"
|
||||
#include "services/normal/filesystem/app_file.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "shell/normal/quick_launch.h"
|
||||
#include "shell/normal/watchface.h"
|
||||
#include "shell/prefs.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/list.h"
|
||||
#include "util/math.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/units.h"
|
||||
|
||||
//! @file app_cache.c
|
||||
//! App Cache
|
||||
|
||||
//! The App Cache keeps track of the install date, last launch, launch count, and size of an
|
||||
//! application.
|
||||
//!
|
||||
//! A priority can also be calculated for each entry. It is calculated by a simple last used
|
||||
//! algorithm (TODO Improve: PBL-13209) which will help determine which application
|
||||
//! needs to be evicted in order to free up more space for other application binaries.
|
||||
//!
|
||||
//! When an entry is added into the app cache, it means the binaries now reside on the watch. On
|
||||
//! this function call, a callback is initiated to check if we need to free space for a possible
|
||||
//! future application. If so, the applications with the lowest priority that add up to or are
|
||||
//! greater than the space needed will be removed.
|
||||
//!
|
||||
//! It is assumed that there will ALWAYS be space for a single application of maximum size based
|
||||
//! on the platform. The only time when this isn't true is the time between "add_entry" and the
|
||||
//! callback to clean up the cache.
|
||||
|
||||
#define APP_CACHE_FILE_NAME "appcache"
|
||||
|
||||
//! each cache entry is ~16 bytes, 4000 / 16 = 250 apps
|
||||
#define APP_CACHE_MAX_SIZE 4000
|
||||
|
||||
//! Keep enough room for the maximum sized application based on platform, plus a little more room.
|
||||
//! Source: https://pebbletechnology.atlassian.net/wiki/display/DEV/PBW+3.0
|
||||
#if PLATFORM_TINTIN || PLATFORM_SILK || UNITTEST
|
||||
#define APP_SPACE_BUFFER KiBYTES(300)
|
||||
#else
|
||||
#define APP_SPACE_BUFFER MiBYTES(4)
|
||||
#endif
|
||||
|
||||
#define MAX_PRIORITY ((uint32_t)~0)
|
||||
|
||||
// 4 quick launch apps, 1 default watchface, 1 default worker
|
||||
#define DO_NOT_EVICT_LIST_SIZE (NUM_BUTTONS + 2)
|
||||
|
||||
static PebbleRecursiveMutex *s_app_cache_mutex = NULL;
|
||||
|
||||
//! Actual data structure stored in flash about an app cache entry
|
||||
typedef struct PACKED {
|
||||
time_t install_date;
|
||||
time_t last_launch;
|
||||
uint32_t total_size;
|
||||
uint16_t launch_count;
|
||||
} AppCacheEntry;
|
||||
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
AppInstallId id;
|
||||
uint32_t size;
|
||||
uint32_t priority;
|
||||
} EvictListNode;
|
||||
|
||||
typedef struct {
|
||||
EvictListNode *list;
|
||||
uint32_t bytes_needed;
|
||||
uint32_t bytes_in_list;
|
||||
const AppInstallId do_not_evict[DO_NOT_EVICT_LIST_SIZE];
|
||||
} EachEvictData;
|
||||
|
||||
//! Takes the information given in entry and calculates a new priority for the app.
|
||||
//!
|
||||
//! Policy rules:
|
||||
//! 1. App that has least recently launched or been installed app is evicted.
|
||||
static uint32_t prv_calculate_priority(AppCacheEntry *entry) {
|
||||
return (uint32_t) MAX(entry->last_launch, entry->install_date);
|
||||
}
|
||||
|
||||
//! Comparator for EvictListNode
|
||||
static int evict_node_comparator(void *a, void *b) {
|
||||
EvictListNode *a_node = (EvictListNode *)a;
|
||||
EvictListNode *b_node = (EvictListNode *)b;
|
||||
|
||||
if (b_node->priority > a_node->priority) {
|
||||
return 1;
|
||||
} else if (b_node->priority < a_node->priority) {
|
||||
return -1;
|
||||
} else {
|
||||
// bigger applications to have a lower priority
|
||||
if (b_node->size < a_node->size) {
|
||||
return 1;
|
||||
} else if (b_node->size > a_node->size) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! Trim the applications with highest priority while still keeping (bytes_in_list > bytes_needed)
|
||||
static void prv_trim_top_priorities(EvictListNode **list_node, uint32_t *bytes_in_list,
|
||||
uint32_t bytes_needed) {
|
||||
EvictListNode *node = *list_node;
|
||||
while (node) {
|
||||
EvictListNode *temp = node;
|
||||
if (node->size <= (*bytes_in_list - bytes_needed)) {
|
||||
*bytes_in_list -= node->size;
|
||||
node = (EvictListNode *)list_pop_head((ListNode *)node);
|
||||
kernel_free(temp);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
*list_node = node;
|
||||
}
|
||||
|
||||
//! Check if we need to free up some space in the cache. If so, do it.
|
||||
static void prv_cleanup_app_cache_if_needed(void *data) {
|
||||
uint32_t pfs_space = get_available_pfs_space();
|
||||
|
||||
if (pfs_space < APP_SPACE_BUFFER) {
|
||||
const uint32_t to_free = (APP_SPACE_BUFFER - pfs_space);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Cache OOS: Need to free %"PRIu32" bytes, PFS avail space: %"PRIu32"",
|
||||
to_free, pfs_space);
|
||||
app_cache_free_up_space(to_free);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_delete_cache_callback(void *data) {
|
||||
app_cache_flush();
|
||||
}
|
||||
|
||||
static void prv_delete_cached_files(void) {
|
||||
pfs_remove_files(is_app_file_name);
|
||||
}
|
||||
|
||||
static bool prv_is_in_list(AppInstallId id, const AppInstallId list[], uint8_t len) {
|
||||
for (unsigned int i = 0; i < len; i++) {
|
||||
if (list[i] == id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Settings Helpers
|
||||
//////////////////////
|
||||
|
||||
//! Settings iterator function that finds the entry with the lowest calculated priority
|
||||
static bool prv_each_free_up_space(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
// check entry is valid
|
||||
if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Invalid cache entry with key_len: %u and val_len: %u, flushing",
|
||||
info->key_len, info->val_len);
|
||||
system_task_add_callback(prv_delete_cache_callback, NULL);
|
||||
return false; // stop iterating, delete the file and binaries
|
||||
}
|
||||
|
||||
EachEvictData *data = (EachEvictData *)context;
|
||||
|
||||
AppInstallId id;
|
||||
AppCacheEntry entry;
|
||||
|
||||
info->get_key(file, (uint8_t *)&id, info->key_len);
|
||||
info->get_val(file, (uint8_t *)&entry, info->val_len);
|
||||
|
||||
// create node
|
||||
EvictListNode *node = kernel_malloc_check(sizeof(EvictListNode));
|
||||
list_init((ListNode *)node);
|
||||
|
||||
// give them an extremely high priority so that we only remove them if we really NEED to
|
||||
// This list contains defaults that we shouldn't be removing.
|
||||
uint32_t priority = 0;
|
||||
if (prv_is_in_list(id, data->do_not_evict, DO_NOT_EVICT_LIST_SIZE)) {
|
||||
priority = MAX_PRIORITY;
|
||||
}
|
||||
|
||||
*node = (EvictListNode) {
|
||||
.id = id,
|
||||
.size = entry.total_size,
|
||||
.priority = MAX(priority, prv_calculate_priority(&entry)),
|
||||
};
|
||||
|
||||
data->list = (EvictListNode *)list_sorted_add((ListNode *)data->list, (ListNode *)node,
|
||||
evict_node_comparator, false);
|
||||
data->bytes_in_list += node->size;
|
||||
|
||||
if (data->bytes_in_list > data->bytes_needed) {
|
||||
prv_trim_top_priorities(&data->list, &data->bytes_in_list, data->bytes_needed);
|
||||
}
|
||||
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// AppCache API's
|
||||
//////////////////////////
|
||||
|
||||
//! Updates metadata within the cache entry for the given AppInstallId. Will update such fields as
|
||||
//! launch count, last launch, and priority
|
||||
status_t app_cache_app_launched(AppInstallId app_id) {
|
||||
status_t rv;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
AppCacheEntry entry = { 0 };
|
||||
rv = settings_file_get(&file, (uint8_t *)&app_id, sizeof(AppInstallId),
|
||||
(uint8_t *)&entry, sizeof(AppCacheEntry));
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
entry.last_launch = rtc_get_time();
|
||||
entry.launch_count += 1;
|
||||
|
||||
rv = settings_file_set(&file, (uint8_t *)&app_id, sizeof(AppInstallId),
|
||||
(uint8_t *)&entry, sizeof(AppCacheEntry));
|
||||
} else {
|
||||
app_storage_delete_app(app_id);
|
||||
settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
}
|
||||
|
||||
settings_file_close(&file);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
//! Asks the app cache to remove 'bytes_needed' bytes of application binaries to free up space
|
||||
//! for other things.
|
||||
status_t app_cache_free_up_space(uint32_t bytes_needed) {
|
||||
if (bytes_needed == 0) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// we don't want to remove any default apps or quick launch apps, so keep them in a list.
|
||||
EachEvictData evict_data = (EachEvictData) {
|
||||
.bytes_needed = bytes_needed,
|
||||
.do_not_evict = {
|
||||
#if !SHELL_SDK
|
||||
quick_launch_get_app(BUTTON_ID_UP),
|
||||
quick_launch_get_app(BUTTON_ID_SELECT),
|
||||
quick_launch_get_app(BUTTON_ID_DOWN),
|
||||
quick_launch_get_app(BUTTON_ID_BACK),
|
||||
#endif
|
||||
watchface_get_default_install_id(),
|
||||
worker_preferences_get_default_worker(),
|
||||
},
|
||||
};
|
||||
|
||||
settings_file_each(&file, prv_each_free_up_space, &evict_data);
|
||||
settings_file_close(&file);
|
||||
|
||||
// remove all nodes found
|
||||
EvictListNode *node = evict_data.list;
|
||||
while (node) {
|
||||
EvictListNode *temp = node;
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Deleting application binaries for app id: %"PRIu32", size: %"PRIu32,
|
||||
node->id, node->size);
|
||||
app_cache_remove_entry(node->id);
|
||||
node = (EvictListNode *)list_pop_head((ListNode *)node);
|
||||
kernel_free(temp);
|
||||
}
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// AppCache Helpers
|
||||
//////////////////////
|
||||
|
||||
// Remove the filename entry in the PFSFileList (via context) that corresponds to the
|
||||
// app install id passed in via info
|
||||
static bool prv_remove_matching_resource_file_callback(SettingsFile *file,
|
||||
SettingsRecordInfo *info,
|
||||
void *context) {
|
||||
AppInstallId id;
|
||||
// examine the SettingsRecordInfo and extract the AppInstallId from it
|
||||
info->get_key(file, (uint8_t *)&id, info->key_len);
|
||||
// the context passed in is really a pointer to the resource_list
|
||||
PFSFileListEntry **resource_list = context;
|
||||
PFSFileListEntry *iter = *resource_list;
|
||||
while (iter) {
|
||||
// grab the next entry right now since we may delete the node we're looking at
|
||||
PFSFileListEntry *next = (PFSFileListEntry *)iter->list_node.next;
|
||||
if (app_file_parse_app_id(iter->name) == id) {
|
||||
// the AppInstallId of the file matches the one in the cache so we can remove this
|
||||
// entry from the resource_list (since we don't want to delete it)
|
||||
// note: resource_list may be updated if we happen to remove the first entry in the list
|
||||
list_remove(&(iter->list_node), (ListNode**)resource_list, NULL);
|
||||
kernel_free(iter); // free up the memory for the node we just removed
|
||||
break; // we can quit now that we've found a match for this id
|
||||
}
|
||||
iter = next;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delete files from resource_list that don't correspond to entries in the app cache
|
||||
static void prv_app_cache_find_and_delete_orphans(PFSFileListEntry **resource_list) {
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return;
|
||||
}
|
||||
// resource_list contains all of the resource files we found. We only
|
||||
// want to delete orphans so we can remove any entries from the list that correspond
|
||||
// to items in the app cache...
|
||||
// prv_remove_matching_resource_file_callback scans resource_list and removes the entry
|
||||
// corresponding to the passed-in application's id
|
||||
settings_file_each(&file, prv_remove_matching_resource_file_callback, resource_list);
|
||||
settings_file_close(&file);
|
||||
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
|
||||
// resource_list now only contains filenames of resource files that don't have corresponding
|
||||
// entries in the app cache. We can safely delete these files.
|
||||
PFSFileListEntry *iter = *resource_list;
|
||||
while (iter) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Orphaned resource file removed: %s", iter->name);
|
||||
pfs_remove(iter->name);
|
||||
iter = (PFSFileListEntry *)iter->list_node.next;
|
||||
}
|
||||
}
|
||||
|
||||
// The bug addressed in PBL-34010 caused resource files to remain in the filesystem even
|
||||
// after the associated application had been deleted. This function attempts to find such
|
||||
// orphaned files and remove them. Note: further to the bug in PBL-34010, this function will
|
||||
// remove any resource files that are not related to apps currently in the cache.
|
||||
static void prv_purge_orphaned_resource_files(void) {
|
||||
// create a list of all app resource files in the filesystem
|
||||
PFSFileListEntry *resource_files = pfs_create_file_list(is_app_resource_file_name);
|
||||
// delete app resource files that don't correspond to entries in the app cache
|
||||
prv_app_cache_find_and_delete_orphans(&resource_files);
|
||||
pfs_delete_file_list(resource_files);
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// AppCache Settings API's
|
||||
//////////////////////////
|
||||
|
||||
//! Set up the app cache
|
||||
void app_cache_init(void) {
|
||||
s_app_cache_mutex = mutex_create_recursive();
|
||||
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
// if no cache file exists, then we should go ahead and clean up any files that are left over
|
||||
int fd = pfs_open(APP_CACHE_FILE_NAME, OP_FLAG_READ, FILE_TYPE_STATIC, 0);
|
||||
if (fd < 0) {
|
||||
prv_delete_cached_files();
|
||||
goto unlock;
|
||||
}
|
||||
pfs_close(fd);
|
||||
}
|
||||
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
|
||||
prv_purge_orphaned_resource_files();
|
||||
}
|
||||
|
||||
//! Adds an entry with the given AppInstallId to the cache
|
||||
status_t app_cache_add_entry(AppInstallId app_id, uint32_t total_size) {
|
||||
status_t rv;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
AppCacheEntry entry = {
|
||||
.install_date = rtc_get_time(),
|
||||
.last_launch = 0,
|
||||
.launch_count = 0,
|
||||
.total_size = total_size,
|
||||
};
|
||||
|
||||
rv = settings_file_set(&file, (uint8_t *)&app_id, sizeof(AppInstallId),
|
||||
(uint8_t *)&entry, sizeof(AppCacheEntry));
|
||||
|
||||
settings_file_close(&file);
|
||||
|
||||
// cleanup the cache if we need to
|
||||
system_task_add_callback(prv_cleanup_app_cache_if_needed, NULL);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
//! Tests if an entry with the given AppInstallId is in the cache
|
||||
bool app_cache_entry_exists(AppInstallId app_id) {
|
||||
bool exists = false;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
exists = settings_file_exists(&file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
|
||||
if (exists && !app_storage_app_exists(app_id)) {
|
||||
settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
exists = false;
|
||||
}
|
||||
|
||||
settings_file_close(&file);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return exists;
|
||||
}
|
||||
|
||||
//! Removes an entry with the given AppInstallId from the cache
|
||||
status_t app_cache_remove_entry(AppInstallId app_id) {
|
||||
status_t rv;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
rv = settings_file_delete(&file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
if (rv == S_SUCCESS) {
|
||||
// Will delete an app from the filesystem.
|
||||
app_storage_delete_app(app_id);
|
||||
}
|
||||
|
||||
settings_file_close(&file);
|
||||
}
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_APP_CACHE_EVENT,
|
||||
.app_cache_event = {
|
||||
.cache_event_type = PebbleAppCacheEvent_Removed,
|
||||
.install_id = app_id,
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
void app_cache_flush(void) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
pfs_remove(APP_CACHE_FILE_NAME);
|
||||
prv_delete_cached_files();
|
||||
}
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Testing only
|
||||
////////////////////////////////
|
||||
|
||||
static bool prv_each_get_size(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) {
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
uint32_t *cache_size = (uint32_t *)context;
|
||||
AppCacheEntry entry;
|
||||
info->get_val(file, (uint8_t *)&entry, info->val_len);
|
||||
*cache_size += entry.total_size;
|
||||
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
uint32_t app_cache_get_size(void) {
|
||||
uint32_t cache_size = 0;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
settings_file_each(&file, prv_each_get_size, &cache_size);
|
||||
settings_file_close(&file);
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return cache_size;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
AppInstallId id;
|
||||
uint32_t priority;
|
||||
} AppCacheEachData;
|
||||
|
||||
//! Settings iterator function that finds the entry with the lowest calculated priority
|
||||
static bool prv_each_min_priority(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
// check entry is valid
|
||||
if ((info->key_len != sizeof(AppInstallId)) || (info->val_len != sizeof(AppCacheEntry))) {
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
AppCacheEachData *to_evict = (AppCacheEachData *)context;
|
||||
|
||||
AppInstallId id;
|
||||
AppCacheEntry entry;
|
||||
|
||||
info->get_key(file, (uint8_t *)&id, info->key_len);
|
||||
info->get_val(file, (uint8_t *)&entry, info->val_len);
|
||||
|
||||
uint32_t entry_priority = prv_calculate_priority(&entry);
|
||||
if (entry_priority < to_evict->priority) {
|
||||
to_evict->id = id;
|
||||
to_evict->priority = entry_priority;
|
||||
}
|
||||
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
//! Find the entry in the app cache with the lowest calculated priority
|
||||
AppInstallId app_cache_get_next_eviction(void) {
|
||||
AppInstallId ret_value = INSTALL_ID_INVALID;
|
||||
mutex_lock_recursive(s_app_cache_mutex);
|
||||
{
|
||||
SettingsFile file;
|
||||
status_t rv = settings_file_open(&file, APP_CACHE_FILE_NAME, APP_CACHE_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// set max so that any application will have a lower priority.
|
||||
AppCacheEachData to_evict = {
|
||||
.id = INSTALL_ID_INVALID,
|
||||
.priority = MAX_PRIORITY,
|
||||
};
|
||||
settings_file_each(&file, prv_each_min_priority, (void *)&to_evict);
|
||||
|
||||
settings_file_close(&file);
|
||||
ret_value = to_evict.id;
|
||||
}
|
||||
unlock:
|
||||
mutex_unlock_recursive(s_app_cache_mutex);
|
||||
return ret_value;
|
||||
}
|
||||
56
src/fw/services/normal/app_cache.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/app_install_types.h"
|
||||
#include "system/status_codes.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//! @file app_cache.c
|
||||
//! AppCache
|
||||
//!
|
||||
//! The AppCache keeps track of a cache of the applications that have binaries that reside on the
|
||||
//! watch. When an app's binaries are removed from the watch, the entry with the same AppInstallId
|
||||
//! is removed from the AppCache.
|
||||
//!
|
||||
//! When the app storage space has run out, a call to the app cache will retrieve the entry that
|
||||
//! needs to be removed.
|
||||
|
||||
//! Initializes the AppCache
|
||||
void app_cache_init(void);
|
||||
|
||||
//! Adds a blank entry with the given AppInstallId and total size to the AppCache
|
||||
status_t app_cache_add_entry(AppInstallId app_id, uint32_t total_size);
|
||||
|
||||
//! Removes an entry with the given AppInstallId from the AppCache
|
||||
status_t app_cache_remove_entry(AppInstallId app_id);
|
||||
|
||||
//! Checks whether an entry with the given AppInstallId is in the AppCache.
|
||||
bool app_cache_entry_exists(AppInstallId app_id);
|
||||
|
||||
//! Increments data stored about an entry with the given AppInstallId in the AppCache
|
||||
status_t app_cache_app_launched(AppInstallId app_id);
|
||||
|
||||
//! Ask the app cache to free up n bytes in case other parts of the system need room in the
|
||||
//! filesystem
|
||||
status_t app_cache_free_up_space(uint32_t bytes_needed);
|
||||
|
||||
//! Clears the entire AppCache
|
||||
//! NOTE: Must be called from PebbleTask_KernelBackground
|
||||
void app_cache_flush(void);
|
||||
442
src/fw/services/normal/app_fetch_endpoint.c
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* 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 "app_fetch_endpoint.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "applib/rockyjs/rocky_res.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/pebble_process_info.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/put_bytes/put_bytes.h"
|
||||
#include "services/common/system_task.h"
|
||||
#include "services/normal/app_cache.h"
|
||||
#include "services/normal/blob_db/app_db.h"
|
||||
#include "services/normal/process_management/app_storage.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/math.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
//! Used for keeping track of binaries that are loaded through put_bytes
|
||||
typedef struct {
|
||||
AppInstallId app_id;
|
||||
uint32_t total_size;
|
||||
AppFetchResult prev_error;
|
||||
bool cancelling;
|
||||
bool in_progress;
|
||||
bool app;
|
||||
bool worker;
|
||||
bool resources;
|
||||
} AppFetchState;
|
||||
|
||||
//! Command type
|
||||
enum {
|
||||
APP_FETCH_INSTALL_COMMAND = 0x01,
|
||||
} AppFetchCommand;
|
||||
|
||||
//! Possible results that come back from the INSTALL_COMMAND
|
||||
enum {
|
||||
APP_FETCH_INSTALL_RESPONSE = 0x01,
|
||||
} AppFetchResponse;
|
||||
|
||||
//! Possible results that come back from the INSTALL_COMMAND
|
||||
enum {
|
||||
APP_FETCH_RESPONSE_STARTING = 0x01,
|
||||
APP_FETCH_RESPONSE_BUSY = 0x02,
|
||||
APP_FETCH_RESPONSE_UUID_INVALID = 0x03,
|
||||
APP_FETCH_RESPONSE_NO_DATA = 0x04,
|
||||
} AppFetchInstallResult;
|
||||
|
||||
//! Data sent to mobile phone for an INSTALL_COMMAND
|
||||
typedef struct PACKED {
|
||||
uint8_t command;
|
||||
Uuid uuid;
|
||||
AppInstallId app_id;
|
||||
} AppFetchInstallRequest;
|
||||
|
||||
//! Timeout used to determine how long we should wait before the phone starts sending the app
|
||||
//! we requested (by issuing a put_bytes request).
|
||||
#define FETCH_TIMEOUT_MS 15000
|
||||
|
||||
//! State for the app fetch flow
|
||||
static AppFetchState s_fetch_state;
|
||||
|
||||
//! Endpoint ID
|
||||
static const uint16_t APP_FETCH_ENDPOINT_ID = 6001;
|
||||
|
||||
////////////////////////////
|
||||
// Internal Helper Functions
|
||||
////////////////////////////
|
||||
|
||||
//! Puts an error event with the given error code
|
||||
static void prv_put_event_error(uint8_t error_code) {
|
||||
s_fetch_state.prev_error = error_code;
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_FETCH_EVENT,
|
||||
.app_fetch = {
|
||||
.type = AppFetchEventTypeError,
|
||||
.id = s_fetch_state.app_id,
|
||||
.error_code = error_code,
|
||||
}
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
//! Puts an event with the given progress
|
||||
static void prv_put_event_progress(uint8_t percent) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_FETCH_EVENT,
|
||||
.app_fetch = {
|
||||
.type = AppFetchEventTypeProgress,
|
||||
.id = s_fetch_state.app_id,
|
||||
.progress_percent = percent,
|
||||
}
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
//! Simply posts the type of event given.
|
||||
static void prv_put_event_simple(AppFetchEventType type) {
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_FETCH_EVENT,
|
||||
.app_fetch = {
|
||||
.type = type,
|
||||
.id = s_fetch_state.app_id,
|
||||
}
|
||||
};
|
||||
event_put(&event);
|
||||
}
|
||||
|
||||
|
||||
//! Recomputes and saves the progress percent for the current application fetch session
|
||||
static uint8_t prv_compute_progress_percent(PutBytesObjectType type, unsigned int type_percent) {
|
||||
// Add 33(34) percent for each piece that has finished (or is unneeded)
|
||||
uint8_t percent = 0;
|
||||
if (s_fetch_state.app) {
|
||||
percent += 30;
|
||||
}
|
||||
if (s_fetch_state.worker) {
|
||||
percent += 10;
|
||||
}
|
||||
if (s_fetch_state.resources) {
|
||||
percent += 60;
|
||||
}
|
||||
|
||||
// add in the progress for the currently transferring piece.
|
||||
percent += (type_percent / 3);
|
||||
|
||||
// store value
|
||||
return MIN(100, percent);
|
||||
}
|
||||
|
||||
//! Cleans up the state of the app fetch endpoint. Always called from the system task
|
||||
static void prv_cleanup(AppFetchResult result) {
|
||||
if (result != AppFetchResultSuccess) {
|
||||
put_bytes_cancel();
|
||||
app_cache_remove_entry(s_fetch_state.app_id);
|
||||
prv_put_event_error(result);
|
||||
}
|
||||
|
||||
s_fetch_state.in_progress = false;
|
||||
|
||||
PBL_LOG(LOG_LEVEL_INFO, "App fetch cleanup with result %d", result);
|
||||
}
|
||||
|
||||
//! System task callback triggered by app_fetch_put_bytes_event_handler() when we are receiving
|
||||
//! put_bytes messages in reponse to a fetch request to the phone.
|
||||
void prv_put_bytes_event_system_task_cb(void *data) {
|
||||
PebblePutBytesEvent *pb_event = (PebblePutBytesEvent *)data;
|
||||
|
||||
if (!s_fetch_state.in_progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If put_bytes has failed, let's just say fail and stop everything.
|
||||
if (pb_event->failed == true) {
|
||||
AppFetchResult error;
|
||||
if (s_fetch_state.cancelling) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Put bytes cancelled by user");
|
||||
error = AppFetchResultUserCancelled;
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Put bytes failure");
|
||||
error = AppFetchResultPutBytesFailure;
|
||||
}
|
||||
|
||||
prv_cleanup(error);
|
||||
goto finally;
|
||||
}
|
||||
|
||||
if (pb_event->type == PebblePutBytesEventTypeInitTimeout) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Timed out waiting for putbytes request from phone");
|
||||
prv_cleanup(AppFetchResultTimeoutError);
|
||||
}
|
||||
|
||||
// If this is an object that doesn't have a cookie, then we won't care about it.
|
||||
if (pb_event->has_cookie == false) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Ignoring non cookie put_bytes event");
|
||||
goto finally;
|
||||
}
|
||||
|
||||
// Check for the different types of PutBytes events.
|
||||
if (pb_event->type == PebblePutBytesEventTypeProgress) {
|
||||
// compute and save the new progress, then show it on the progress bar
|
||||
uint8_t percent =
|
||||
prv_compute_progress_percent(pb_event->object_type, pb_event->progress_percent);
|
||||
prv_put_event_progress(percent);
|
||||
} else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
|
||||
// Mark off each finishing put_bytes transaction in our progress struct
|
||||
|
||||
switch (pb_event->object_type) {
|
||||
case ObjectWatchApp:
|
||||
s_fetch_state.app = true;
|
||||
break;
|
||||
case ObjectWatchWorker:
|
||||
s_fetch_state.worker = true;
|
||||
break;
|
||||
case ObjectAppResources:
|
||||
s_fetch_state.resources = true;
|
||||
break;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a PutBytes Object that we shouldn't have gotten");
|
||||
prv_cleanup(AppFetchResultGeneralFailure);
|
||||
goto finally;
|
||||
}
|
||||
|
||||
// add the size of the finished PutBytes transaction to the total size.
|
||||
s_fetch_state.total_size += pb_event->total_size;
|
||||
}
|
||||
|
||||
if (s_fetch_state.app && s_fetch_state.worker && s_fetch_state.resources) {
|
||||
// if everything has finished being transferred
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "All pieces (%"PRIu32" bytes) have been sent over put_bytes",
|
||||
s_fetch_state.total_size);
|
||||
|
||||
// signify in the app cache that the app binaries are now loaded
|
||||
status_t added = app_cache_add_entry(s_fetch_state.app_id, s_fetch_state.total_size);
|
||||
if (added == S_SUCCESS) {
|
||||
const PebbleProcessMd *md = app_install_get_md(s_fetch_state.app_id, false);
|
||||
if (rocky_app_validate_resources(md) == RockyResourceValidation_Invalid) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Received app contains invalid JS bytecode");
|
||||
prv_put_event_error(AppFetchResultIncompatibleJSFailure);
|
||||
} else {
|
||||
// Set prev_error as a Success.
|
||||
s_fetch_state.prev_error = AppFetchResultSuccess;
|
||||
prv_put_event_simple(AppFetchEventTypeFinish);
|
||||
}
|
||||
app_install_release_md(md);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to insert into app cache: %"PRId32, added);
|
||||
prv_put_event_error(AppFetchResultGeneralFailure);
|
||||
}
|
||||
prv_cleanup(AppFetchResultSuccess);
|
||||
|
||||
} else if (pb_event->type == PebblePutBytesEventTypeCleanup) {
|
||||
// Start the timeout watchdog again so we can tell if things get hung up
|
||||
// before the phone starts sending the next putbytes object.
|
||||
// This will only trigger if we've completed a piece and are still
|
||||
// waiting for another one.
|
||||
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
finally:
|
||||
kernel_free(pb_event);
|
||||
}
|
||||
|
||||
//! Put Bytes handler. Used for keeping track of progress and cleanup events. This is called
|
||||
//! from KernelMain's event handler when it receives a PEBBLE_PUT_BYTES_EVENT event. put_bytes
|
||||
//! posts these events to inform clients of progress.
|
||||
void app_fetch_put_bytes_event_handler(PebblePutBytesEvent *pb_event) {
|
||||
// If an app fetch isn't in progress, ignore it.
|
||||
if (!s_fetch_state.in_progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
PebblePutBytesEvent *pb_event_copy = kernel_malloc_check(sizeof(PebblePutBytesEvent));
|
||||
memcpy(pb_event_copy, pb_event, sizeof(PebblePutBytesEvent));
|
||||
system_task_add_callback(prv_put_bytes_event_system_task_cb, pb_event_copy);
|
||||
}
|
||||
|
||||
//! Callback for the system task to fire off the fetch request. Triggered by a call to
|
||||
//! app_fetch_binaries().
|
||||
static void prv_app_fetch_binaries_system_task_cb(void *data) {
|
||||
AppFetchInstallRequest *request = (AppFetchInstallRequest *)data;
|
||||
|
||||
// check if Bluetooth is active. If so, this will send.
|
||||
bool successful = comm_session_send_data(comm_session_get_system_session(), APP_FETCH_ENDPOINT_ID,
|
||||
(uint8_t*)request, sizeof(AppFetchInstallRequest), COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
|
||||
// log it
|
||||
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(&request->uuid, uuid_buffer);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "%s request for app with uuid: %s and app_id: %"PRIu32"",
|
||||
successful ? "Sent" : "Failed to send", uuid_buffer, request->app_id);
|
||||
|
||||
// free before error checking
|
||||
kernel_free(request);
|
||||
|
||||
// If Bluetooth wasn't active, then post the error and cleanup.
|
||||
if (!successful) {
|
||||
prv_cleanup(AppFetchResultNoBluetooth);
|
||||
return;
|
||||
}
|
||||
|
||||
// We next expect app_fetch_put_bytes_event_handler() to be called when the phone
|
||||
// gets our fetch request and issues a putbytes request.
|
||||
// Start the timeout watchdog to catch us in case the phone never issues the putbytes request.
|
||||
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
//! Called from the system task. Translates an Endpoint error to an event error and sends
|
||||
//! off the appropriate event.
|
||||
void prv_handle_app_fetch_install_response(uint8_t result_code) {
|
||||
switch (result_code) {
|
||||
case APP_FETCH_RESPONSE_STARTING:
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Phone confirmed it will start sending data");
|
||||
prv_put_event_simple(AppFetchEventTypeStart);
|
||||
put_bytes_expect_init(FETCH_TIMEOUT_MS);
|
||||
break;
|
||||
case APP_FETCH_RESPONSE_BUSY:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Error: Phone is currently busy");
|
||||
prv_cleanup(AppFetchResultPhoneBusy);
|
||||
break;
|
||||
case APP_FETCH_RESPONSE_UUID_INVALID:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Error: UUID Invalid");
|
||||
prv_cleanup(AppFetchResultUUIDInvalid);
|
||||
break;
|
||||
case APP_FETCH_RESPONSE_NO_DATA:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Error: No data on phone");
|
||||
prv_cleanup(AppFetchResultNoData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Exported App Fetch API
|
||||
/////////////////////////
|
||||
|
||||
//! Called by the system that triggers an app fetch install request
|
||||
void app_fetch_binaries(const Uuid *uuid, AppInstallId app_id, bool has_worker) {
|
||||
if (s_fetch_state.in_progress) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Already an app fetch in progress. Ignoring request");
|
||||
return;
|
||||
}
|
||||
|
||||
AppFetchInstallRequest *request = kernel_malloc_check(sizeof(AppFetchInstallRequest));
|
||||
|
||||
// reset all state
|
||||
s_fetch_state = (AppFetchState){};
|
||||
|
||||
// Mark whether the worker needs to be sent over.
|
||||
s_fetch_state.worker = !has_worker;
|
||||
s_fetch_state.app_id = app_id;
|
||||
s_fetch_state.in_progress = true;
|
||||
|
||||
// populate fields
|
||||
request->command = APP_FETCH_INSTALL_COMMAND;
|
||||
request->uuid = *uuid;
|
||||
request->app_id = app_id;
|
||||
|
||||
// Start "warming up" the connection, this will cause the low-latency period to start ~1s sooner.
|
||||
// Put bytes will extend the low-latency period after this:
|
||||
comm_session_set_responsiveness(comm_session_get_system_session(), BtConsumerPpAppFetch,
|
||||
ResponseTimeMin, MIN_LATENCY_MODE_TIMEOUT_APP_FETCH_SECS);
|
||||
|
||||
system_task_add_callback(prv_app_fetch_binaries_system_task_cb, request);
|
||||
}
|
||||
|
||||
AppFetchError app_fetch_get_previous_error(void) {
|
||||
AppFetchError error = {
|
||||
.error = s_fetch_state.prev_error,
|
||||
.id = s_fetch_state.app_id,
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static void prv_cancel_fetch_from_system_task(void *data) {
|
||||
AppInstallId app_id = (AppInstallId)data;
|
||||
|
||||
if ((!s_fetch_state.in_progress) ||
|
||||
((s_fetch_state.app_id != app_id) && (app_id != INSTALL_ID_INVALID))) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Attempted to cancel an app that is currently not being"
|
||||
" fetched: %"PRId32, app_id);
|
||||
return;
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Cancelling app fetch from system task");
|
||||
s_fetch_state.cancelling = true;
|
||||
put_bytes_cancel();
|
||||
}
|
||||
|
||||
void app_fetch_cancel_from_system_task(AppInstallId app_id) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
prv_cancel_fetch_from_system_task((void *)(uintptr_t)app_id);
|
||||
}
|
||||
|
||||
void app_fetch_cancel(AppInstallId app_id) {
|
||||
// Everything within app fetch happens on the background task
|
||||
system_task_add_callback(prv_cancel_fetch_from_system_task, (void *)(uintptr_t)app_id);
|
||||
}
|
||||
|
||||
bool app_fetch_in_progress(void) {
|
||||
return s_fetch_state.in_progress;
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// Exported Callbacks
|
||||
////////////////////////////
|
||||
|
||||
typedef struct __attribute__((__packed__)) {
|
||||
uint8_t command;
|
||||
uint8_t result_code;
|
||||
} AppFetchResponseData;
|
||||
|
||||
//! System task callback triggered by app_fetch_protocol_msg_callback().
|
||||
static void prv_app_fetch_protocol_handle_msg(AppFetchResponseData *response_data) {
|
||||
|
||||
switch (response_data->command) {
|
||||
case APP_FETCH_INSTALL_RESPONSE:
|
||||
prv_handle_app_fetch_install_response(response_data->result_code);
|
||||
break;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid message received, command: %u result: %u",
|
||||
response_data->command, response_data->result_code);
|
||||
prv_cleanup(AppFetchResultGeneralFailure);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//! Callback that is placed in the endpoints table. As of now, only responses will come through this
|
||||
//! callback as all commands are originally sent to the phone.
|
||||
void app_fetch_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) {
|
||||
if (length < sizeof(AppFetchResponseData)) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid message length %"PRIu32"", (uint32_t)length);
|
||||
prv_cleanup(AppFetchResultGeneralFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!s_fetch_state.in_progress) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Got a message but app fetch not in progress. Ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
prv_app_fetch_protocol_handle_msg((AppFetchResponseData *)data);
|
||||
}
|
||||
57
src/fw/services/normal/app_fetch_endpoint.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/uuid.h"
|
||||
#include "kernel/events.h"
|
||||
#include "process_management/app_install_types.h"
|
||||
|
||||
typedef enum {
|
||||
AppFetchResultSuccess,
|
||||
AppFetchResultTimeoutError,
|
||||
AppFetchResultGeneralFailure,
|
||||
AppFetchResultPhoneBusy,
|
||||
AppFetchResultUUIDInvalid,
|
||||
AppFetchResultNoBluetooth,
|
||||
AppFetchResultPutBytesFailure,
|
||||
AppFetchResultNoData,
|
||||
AppFetchResultUserCancelled,
|
||||
AppFetchResultIncompatibleJSFailure,
|
||||
} AppFetchResult;
|
||||
|
||||
typedef struct {
|
||||
AppFetchResult error;
|
||||
AppInstallId id;
|
||||
} AppFetchError;
|
||||
|
||||
void app_fetch_binaries(const Uuid *uuid, AppInstallId app_id, bool has_worker);
|
||||
|
||||
//! @param app_id The AppInstallId of the fetch to be cancelled.
|
||||
//! NOTE: If `app_id` is INSTALL_ID_INVALID, it will cancel the fetch regardless of AppInstallId
|
||||
void app_fetch_cancel(AppInstallId app_id);
|
||||
|
||||
//! @param app_id The AppInstallId of the fetch to be cancelled.
|
||||
//! NOTE: If `app_id` is INSTALL_ID_INVALID, it will cancel the fetch regardless of AppInstallId
|
||||
//! NOTE: Must be called from PebbleTask_KernelBackground
|
||||
void app_fetch_cancel_from_system_task(AppInstallId app_id);
|
||||
|
||||
bool app_fetch_in_progress(void);
|
||||
|
||||
//! Put Bytes handler. Used for keeping track of progress and cleanup events
|
||||
void app_fetch_put_bytes_event_handler(PebblePutBytesEvent *pb_event);
|
||||
|
||||
AppFetchError app_fetch_get_previous_error(void);
|
||||
212
src/fw/services/normal/app_glances/app_glance_service.c
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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 "app_glance_service.h"
|
||||
|
||||
#include "applib/app_glance.h"
|
||||
#include "applib/event_service_client.h"
|
||||
#include "drivers/rtc.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "services/normal/app_cache.h"
|
||||
#include "services/normal/blob_db/app_glance_db.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/math.h"
|
||||
|
||||
//! Return true to continue iteration and false to stop it.
|
||||
typedef bool (*SliceForEachCb)(AppGlanceSliceInternal *slice, void *context);
|
||||
|
||||
static void prv_slice_for_each(AppGlance *glance, SliceForEachCb cb, void *context) {
|
||||
if (!glance || !cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (unsigned int slice_index = 0;
|
||||
slice_index < MIN(glance->num_slices, APP_GLANCE_DB_MAX_SLICES_PER_GLANCE); slice_index++) {
|
||||
AppGlanceSliceInternal *current_slice = &glance->slices[slice_index];
|
||||
// Stop iterating if the client's callback function returns false
|
||||
if (!cb(current_slice, context)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct FindCurrentSliceData {
|
||||
time_t current_time;
|
||||
AppGlanceSliceInternal *current_slice;
|
||||
} FindCurrentSliceData;
|
||||
|
||||
//! The "current" slice is the slice with an expiration_time closest to the current time while
|
||||
//! still being after the current time
|
||||
static bool prv_find_current_glance(AppGlanceSliceInternal *slice, void *context) {
|
||||
FindCurrentSliceData *data = context;
|
||||
PBL_ASSERTN(data);
|
||||
|
||||
// First check if this slice never expires; the zero value of APP_GLANCE_SLICE_NO_EXPIRATION
|
||||
// won't work with the comparisons we perform below
|
||||
if (slice->expiration_time == APP_GLANCE_SLICE_NO_EXPIRATION) {
|
||||
// We'll only use a never-expiring slice if we haven't set a slice yet
|
||||
if (!data->current_slice) {
|
||||
data->current_slice = slice;
|
||||
}
|
||||
// Continue iterating through the slices
|
||||
return true;
|
||||
}
|
||||
|
||||
const int time_until_slice_expires = slice->expiration_time - data->current_time;
|
||||
|
||||
// Continue iterating through the slices if this slice expires in the past
|
||||
if (time_until_slice_expires <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't have a current slice or the current slice we have is a never-expiring slice, we can
|
||||
// go ahead and use this slice now but continue iterating to try to find an earlier expiring slice
|
||||
// in the list
|
||||
if (!data->current_slice ||
|
||||
(data->current_slice->expiration_time == APP_GLANCE_SLICE_NO_EXPIRATION)) {
|
||||
data->current_slice = slice;
|
||||
return true;
|
||||
}
|
||||
|
||||
const int time_until_current_slice_expires =
|
||||
data->current_slice->expiration_time - data->current_time;
|
||||
|
||||
// If this slice expires earlier than our current slice, use this slice as the new current slice
|
||||
if (time_until_slice_expires < time_until_current_slice_expires) {
|
||||
data->current_slice = slice;
|
||||
}
|
||||
|
||||
// Continue iterating to try to find an earlier slice
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prv_glance_event_put(const Uuid *app_uuid) {
|
||||
Uuid *app_uuid_copy = kernel_zalloc_check(sizeof(Uuid));
|
||||
*app_uuid_copy = *app_uuid;
|
||||
|
||||
PebbleEvent e = (PebbleEvent) {
|
||||
.type = PEBBLE_APP_GLANCE_EVENT,
|
||||
.app_glance = (PebbleAppGlanceEvent) {
|
||||
.app_uuid = app_uuid_copy,
|
||||
},
|
||||
};
|
||||
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Event handlers
|
||||
// NOTE: These events are handled on KernelMain (app_glance_service_init called from
|
||||
// services_normal_init)
|
||||
//////////////////////
|
||||
|
||||
static void prv_blob_db_event_handler(PebbleEvent *event, void *context) {
|
||||
const PebbleBlobDBEvent *blob_db_event = &event->blob_db;
|
||||
const BlobDBId blob_db_id = blob_db_event->db_id;
|
||||
|
||||
if (blob_db_id != BlobDBIdAppGlance) {
|
||||
// We only care about app glance changes
|
||||
return;
|
||||
}
|
||||
|
||||
prv_glance_event_put((Uuid *)blob_db_event->key);
|
||||
}
|
||||
|
||||
static void prv_handle_app_cache_event(PebbleEvent *e, void *context) {
|
||||
if (e->app_cache_event.cache_event_type == PebbleAppCacheEvent_Removed) {
|
||||
Uuid app_uuid;
|
||||
app_install_get_uuid_for_install_id(e->app_cache_event.install_id, &app_uuid);
|
||||
app_glance_db_delete_glance(&app_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Public API
|
||||
//////////////////////
|
||||
|
||||
void app_glance_service_init_glance(AppGlance *glance) {
|
||||
if (!glance) {
|
||||
return;
|
||||
}
|
||||
*glance = (AppGlance) {};
|
||||
}
|
||||
|
||||
void app_glance_service_init(void) {
|
||||
|
||||
static EventServiceInfo s_blob_db_event_info = {
|
||||
.type = PEBBLE_BLOBDB_EVENT,
|
||||
.handler = prv_blob_db_event_handler,
|
||||
};
|
||||
event_service_client_subscribe(&s_blob_db_event_info);
|
||||
|
||||
static EventServiceInfo s_app_cache_event_info = {
|
||||
.type = PEBBLE_APP_CACHE_EVENT,
|
||||
.handler = prv_handle_app_cache_event,
|
||||
};
|
||||
event_service_client_subscribe(&s_app_cache_event_info);
|
||||
}
|
||||
|
||||
bool app_glance_service_get_current_slice(const Uuid *app_uuid, AppGlanceSliceInternal *slice_out) {
|
||||
if (!slice_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success;
|
||||
// Try to read the app's glance, first checking the cache
|
||||
AppGlance *app_glance = kernel_zalloc_check(sizeof(*app_glance));
|
||||
const status_t rv = app_glance_db_read_glance(app_uuid, app_glance);
|
||||
if (rv != S_SUCCESS) {
|
||||
success = false;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Iterate over the slices to find the current slice (which might be NULL if there aren't any
|
||||
// slices or if all of the slices have expired)
|
||||
FindCurrentSliceData find_current_slice_data = (FindCurrentSliceData) {
|
||||
.current_time = rtc_get_time(),
|
||||
};
|
||||
prv_slice_for_each(app_glance, prv_find_current_glance, &find_current_slice_data);
|
||||
if (!find_current_slice_data.current_slice) {
|
||||
success = false;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Copy the current slice data to slice_out
|
||||
*slice_out = *find_current_slice_data.current_slice;
|
||||
success = true;
|
||||
|
||||
cleanup:
|
||||
kernel_free(app_glance);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_app_glance_update, const Uuid *uuid, const AppGlance *glance) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(uuid, sizeof(*uuid));
|
||||
syscall_assert_userspace_buffer(glance, sizeof(*glance));
|
||||
}
|
||||
const bool success = (app_glance_db_insert_glance(uuid, glance) == S_SUCCESS);
|
||||
if (success) {
|
||||
prv_glance_event_put(uuid);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
62
src/fw/services/normal/app_glances/app_glance_service.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/normal/blob_db/app_glance_db_private.h"
|
||||
#include "services/normal/timeline/attribute.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
typedef enum AppGlanceSliceType {
|
||||
AppGlanceSliceType_IconAndSubtitle = 0,
|
||||
|
||||
AppGlanceSliceTypeCount
|
||||
} AppGlanceSliceType;
|
||||
|
||||
//! We name this "internal" so it won't conflict with the AppGlanceSlice struct we export in the SDK
|
||||
#if UNITTEST
|
||||
// Memory comparisons in unit tests won't work unless we pack the struct
|
||||
typedef struct PACKED AppGlanceSliceInternal {
|
||||
#else
|
||||
typedef struct AppGlanceSliceInternal {
|
||||
#endif
|
||||
AppGlanceSliceType type;
|
||||
time_t expiration_time;
|
||||
union {
|
||||
//! Add more structs to this union as we introduce new app glance slice types
|
||||
struct {
|
||||
uint32_t icon_resource_id;
|
||||
char template_string[ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1];
|
||||
} icon_and_subtitle;
|
||||
};
|
||||
} AppGlanceSliceInternal;
|
||||
|
||||
typedef struct AppGlance {
|
||||
size_t num_slices;
|
||||
AppGlanceSliceInternal slices[APP_GLANCE_DB_MAX_SLICES_PER_GLANCE];
|
||||
} AppGlance;
|
||||
|
||||
//! Initializes an AppGlance.
|
||||
void app_glance_service_init_glance(AppGlance *glance);
|
||||
|
||||
//! Initializes the app glance service.
|
||||
void app_glance_service_init(void);
|
||||
|
||||
//! Returns true if the current slice was successfully copied to slice_out.
|
||||
//! Returns false if all slices in the glance have expired or if an error occurred.
|
||||
bool app_glance_service_get_current_slice(const Uuid *app_uuid, AppGlanceSliceInternal *slice_out);
|
||||
617
src/fw/services/normal/app_inbox_service.c
Normal file
@@ -0,0 +1,617 @@
|
||||
/*
|
||||
* 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 "app_inbox_service.h"
|
||||
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "os/mutex.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/buffer.h"
|
||||
#include "util/list.h"
|
||||
|
||||
typedef struct AppInboxNode {
|
||||
ListNode node;
|
||||
AppInboxServiceTag tag;
|
||||
AppInboxMessageHandler message_handler;
|
||||
AppInboxDroppedHandler dropped_handler;
|
||||
PebbleTask event_handler_task;
|
||||
|
||||
//! Indicates whether there is a writer.
|
||||
//! The writer can set it to anything they want, mostly for debugging purposes.
|
||||
void *writer;
|
||||
bool write_failed;
|
||||
bool has_pending_event;
|
||||
|
||||
uint32_t num_failed;
|
||||
uint32_t num_success;
|
||||
|
||||
struct {
|
||||
//! The size of `storage`.
|
||||
size_t size;
|
||||
|
||||
//! The positive offset relative relative to write_index, up until which the current
|
||||
//! (incomplete) message has been written.
|
||||
size_t current_offset;
|
||||
|
||||
//! Index after which the current message should get written.
|
||||
//! If this index is non-zero, there are completed message(s) in the buffer.
|
||||
size_t write_index;
|
||||
|
||||
///! Pointer to the beginning of the storage.
|
||||
uint8_t *storage;
|
||||
} buffer;
|
||||
} AppInboxNode;
|
||||
|
||||
typedef struct AppInboxConsumerInfo {
|
||||
AppInboxServiceTag tag;
|
||||
AppInboxMessageHandler message_handler;
|
||||
AppInboxDroppedHandler dropped_handler;
|
||||
uint32_t num_failed;
|
||||
uint32_t num_success;
|
||||
uint8_t *it;
|
||||
uint8_t *end;
|
||||
} AppInboxConsumerInfo;
|
||||
|
||||
|
||||
_Static_assert(sizeof(AppInboxServiceTag) <= sizeof(void *),
|
||||
"AppInboxServiceTag should fit inside a void *");
|
||||
|
||||
static AppInboxNode *s_app_inbox_head;
|
||||
|
||||
static PebbleRecursiveMutex *s_app_inbox_mutex;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Declarations of permitted handlers:
|
||||
|
||||
extern void app_message_receiver_message_handler(const uint8_t *data, size_t length,
|
||||
AppInboxConsumerInfo *consumer_info);
|
||||
extern void app_message_receiver_dropped_handler(uint32_t num_dropped_messages);
|
||||
|
||||
#ifdef UNITTEST
|
||||
extern void test_message_handler(const uint8_t *data, size_t length,
|
||||
AppInboxConsumerInfo *consumer_info);
|
||||
extern void test_dropped_handler(uint32_t num_dropped_messages);
|
||||
extern void test_alt_message_handler(const uint8_t *data, size_t length,
|
||||
AppInboxConsumerInfo *consumer_info);
|
||||
extern void test_alt_dropped_handler(uint32_t num_dropped_messages);
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Syscalls
|
||||
|
||||
static AppInboxServiceTag prv_tag_for_event_handlers(const AppInboxMessageHandler message_handler,
|
||||
const AppInboxDroppedHandler dropped_handler) {
|
||||
static const struct {
|
||||
AppInboxMessageHandler message_handler;
|
||||
AppInboxDroppedHandler dropped_handler;
|
||||
} s_event_handler_map[] = {
|
||||
[AppInboxServiceTagAppMessageReceiver] = {
|
||||
.message_handler = app_message_receiver_message_handler,
|
||||
.dropped_handler = app_message_receiver_dropped_handler,
|
||||
},
|
||||
#ifdef UNITTEST
|
||||
[AppInboxServiceTagUnitTest] = {
|
||||
.message_handler = test_message_handler,
|
||||
.dropped_handler = test_dropped_handler,
|
||||
},
|
||||
[AppInboxServiceTagUnitTestAlt] = {
|
||||
.message_handler = test_alt_message_handler,
|
||||
.dropped_handler = test_alt_dropped_handler,
|
||||
}
|
||||
#endif
|
||||
};
|
||||
for (AppInboxServiceTag tag = 0; tag < NumAppInboxServiceTag; ++tag) {
|
||||
if (s_event_handler_map[tag].message_handler == message_handler &&
|
||||
s_event_handler_map[tag].dropped_handler == dropped_handler) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
return AppInboxServiceTagInvalid;
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_app_inbox_service_register, uint8_t *storage, size_t storage_size,
|
||||
AppInboxMessageHandler message_handler, AppInboxDroppedHandler dropped_handler) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(storage, storage_size);
|
||||
}
|
||||
const AppInboxServiceTag service_tag = prv_tag_for_event_handlers(message_handler,
|
||||
dropped_handler);
|
||||
if (AppInboxServiceTagInvalid == service_tag) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "AppInbox event handlers not allowed <0x%"PRIx32", 0x%"PRIx32">",
|
||||
// Ugh.. no more format signature slots free for %p %p...
|
||||
(uint32_t)(uintptr_t)message_handler, (uint32_t)(uintptr_t)dropped_handler);
|
||||
syscall_failed();
|
||||
}
|
||||
|
||||
return app_inbox_service_register(storage, storage_size,
|
||||
message_handler, dropped_handler, service_tag);
|
||||
}
|
||||
|
||||
DEFINE_SYSCALL(uint32_t, sys_app_inbox_service_unregister, uint8_t *storage) {
|
||||
// No check is needed on the value of `storage `, we're not going to derefence it.
|
||||
return app_inbox_service_unregister_by_storage(storage);
|
||||
}
|
||||
|
||||
static bool prv_get_consumer_info(AppInboxServiceTag tag, AppInboxConsumerInfo *info_in_out);
|
||||
|
||||
DEFINE_SYSCALL(bool, sys_app_inbox_service_get_consumer_info,
|
||||
AppInboxServiceTag tag, AppInboxConsumerInfo *info_out) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
if (info_out) {
|
||||
syscall_assert_userspace_buffer(info_out, sizeof(*info_out));
|
||||
}
|
||||
}
|
||||
return prv_get_consumer_info(tag, info_out);
|
||||
}
|
||||
|
||||
static void prv_consume(AppInboxConsumerInfo *consumer_info);
|
||||
|
||||
DEFINE_SYSCALL(void, sys_app_inbox_service_consume, AppInboxConsumerInfo *consumer_info) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
syscall_assert_userspace_buffer(consumer_info, sizeof(*consumer_info));
|
||||
}
|
||||
prv_consume(consumer_info);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void prv_lock(void) {
|
||||
// Using one "global" lock for all app inboxes.
|
||||
// If needed, we could easily give each app inbox its own mutex, but it seems overkill right now.
|
||||
mutex_lock_recursive(s_app_inbox_mutex);
|
||||
}
|
||||
|
||||
static void prv_unlock(void) {
|
||||
mutex_unlock_recursive(s_app_inbox_mutex);
|
||||
}
|
||||
|
||||
static bool prv_list_filter_by_storage(ListNode *found_node, void *data) {
|
||||
return ((AppInboxNode *)found_node)->buffer.storage == (uint8_t *)data;
|
||||
}
|
||||
|
||||
static AppInboxNode *prv_find_inbox_by_storage(uint8_t *storage) {
|
||||
return (AppInboxNode *) list_find((ListNode *)s_app_inbox_head,
|
||||
prv_list_filter_by_storage, storage);
|
||||
}
|
||||
|
||||
static bool prv_list_filter_by_tag(ListNode *found_node, void *data) {
|
||||
return ((AppInboxNode *)found_node)->tag == (AppInboxServiceTag)(uintptr_t)data;
|
||||
}
|
||||
|
||||
static AppInboxNode *prv_find_inbox_by_tag(AppInboxServiceTag tag) {
|
||||
return (AppInboxNode *) list_find((ListNode *)s_app_inbox_head,
|
||||
prv_list_filter_by_tag, (void *)(uintptr_t)tag);
|
||||
}
|
||||
|
||||
static AppInboxNode *prv_find_inbox_by_tag_and_log_if_not_found(AppInboxServiceTag tag) {
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag(tag);
|
||||
if (!inbox) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "No AppInbox for tag <%d>", tag);
|
||||
}
|
||||
return inbox;
|
||||
}
|
||||
|
||||
//! We don't report "number of messages consumed", because that would force the system to parse
|
||||
//! the contents of the (app space) buffer, which might have been corrupted by the app.
|
||||
//! Note that it's in theory possible for a misbehaving app to pass in a consumed_up_to_ptr that is
|
||||
//! mid-way in a message. If it does so, it won't crash the kernel, but it will result in delivery
|
||||
//! of broken messages to the app, but it won't be our fault...
|
||||
static void prv_consume(AppInboxConsumerInfo *consumer_info) {
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(consumer_info->tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
uint8_t *const consumed_up_to_ptr = consumer_info->it;
|
||||
uint8_t * const completed_messages_end = (inbox->buffer.storage + inbox->buffer.write_index);
|
||||
if (consumed_up_to_ptr < inbox->buffer.storage ||
|
||||
consumed_up_to_ptr > completed_messages_end) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Out of bounds");
|
||||
goto unlock;
|
||||
}
|
||||
const size_t bytes_consumed = (consumed_up_to_ptr - inbox->buffer.storage);
|
||||
if (0 == bytes_consumed) {
|
||||
goto unlock;
|
||||
}
|
||||
uint8_t * const partial_message_end = completed_messages_end + inbox->buffer.current_offset;
|
||||
const size_t remaining_size = partial_message_end - consumed_up_to_ptr;
|
||||
consumer_info->it = inbox->buffer.storage;
|
||||
consumer_info->end = inbox->buffer.storage + remaining_size;
|
||||
if (remaining_size) {
|
||||
// New data has been written in the mean-time, move it all to the front of the buffer:
|
||||
memmove(inbox->buffer.storage, consumed_up_to_ptr, remaining_size);
|
||||
}
|
||||
inbox->buffer.write_index -= bytes_consumed;
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
static bool prv_get_consumer_info(AppInboxServiceTag tag, AppInboxConsumerInfo *info_out) {
|
||||
if (!info_out) {
|
||||
return false;
|
||||
}
|
||||
bool success = false;
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
*info_out = (const AppInboxConsumerInfo) {
|
||||
.tag = tag,
|
||||
.message_handler = inbox->message_handler,
|
||||
.dropped_handler = inbox->dropped_handler,
|
||||
.num_failed = inbox->num_failed,
|
||||
.num_success = inbox->num_success,
|
||||
.it = inbox->buffer.storage,
|
||||
.end = inbox->buffer.storage + inbox->buffer.write_index,
|
||||
};
|
||||
|
||||
// Also mark that there is no event pending any more:
|
||||
inbox->has_pending_event = false;
|
||||
|
||||
// Reset counters because the info is communicated to app and it's about to consume the data.
|
||||
inbox->num_failed = 0;
|
||||
inbox->num_success = 0;
|
||||
|
||||
success = true;
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
return success;
|
||||
}
|
||||
|
||||
//! @note Executes on app task, therefore we need to go through syscalls to access AppInbox!
|
||||
static void prv_callback_event_handler(void *ctx) {
|
||||
AppInboxServiceTag tag = (AppInboxServiceTag)(uintptr_t)ctx;
|
||||
AppInboxConsumerInfo info = {};
|
||||
size_t num_message_consumed = 0;
|
||||
if (!sys_app_inbox_service_get_consumer_info(tag, &info)) {
|
||||
// Inbox wasn't there any more
|
||||
return;
|
||||
}
|
||||
if (!info.message_handler) {
|
||||
// Shouldn't ever happen, but better not PBL_ASSERTN on app task
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "No AppInbox message handler!");
|
||||
return;
|
||||
}
|
||||
if (!info.num_success && !info.num_failed) {
|
||||
// Shouldn't ever happen, but better not PBL_ASSERTN on app task
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got callback, but zero messages!?");
|
||||
// fall-through
|
||||
}
|
||||
|
||||
// These conditions are redundant, just for safety:
|
||||
while ((num_message_consumed < info.num_success) && (info.it < info.end)) {
|
||||
AppInboxMessageHeader *msg = (AppInboxMessageHeader *)info.it;
|
||||
|
||||
// Increment now so that if the message_handler calls into sys_app_inbox_service_consume(),
|
||||
// it will be pointing *after* the message that is just handled:
|
||||
info.it += (sizeof(AppInboxMessageHeader) + msg->length);
|
||||
|
||||
// Check for safety, just in case the app has corrupted the buffer in the mean time:
|
||||
if (msg->data + msg->length <= info.end) {
|
||||
info.message_handler(msg->data, msg->length, &info);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Corrupted AppInbox message!");
|
||||
}
|
||||
++num_message_consumed;
|
||||
}
|
||||
|
||||
if (info.num_failed) {
|
||||
if (info.dropped_handler) {
|
||||
info.dropped_handler(info.num_failed);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Dropped %"PRIu32" messages but no dropped_handler",
|
||||
info.num_failed);
|
||||
}
|
||||
}
|
||||
|
||||
// Report back up to which byte we've consumed the data.
|
||||
sys_app_inbox_service_consume(&info);
|
||||
}
|
||||
|
||||
bool app_inbox_service_register(uint8_t *storage, size_t storage_size,
|
||||
AppInboxMessageHandler message_handler,
|
||||
AppInboxDroppedHandler dropped_handler, AppInboxServiceTag tag) {
|
||||
AppInboxNode *new_node = (AppInboxNode *)kernel_zalloc(sizeof(AppInboxNode));
|
||||
if (!new_node) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Not enough memory to allocate AppInboxNode");
|
||||
return false;
|
||||
}
|
||||
|
||||
prv_lock();
|
||||
{
|
||||
bool has_error = false;
|
||||
|
||||
if (prv_find_inbox_by_storage(storage)) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "AppInbox already registered for storage <%p>", storage);
|
||||
has_error = true;
|
||||
}
|
||||
|
||||
// This check effectively caps the kernel RAM impact of this service,
|
||||
// so it's not possible to abuse the syscall and cause kernel OOM.
|
||||
if (prv_find_inbox_by_tag(tag)) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "AppInbox already registered for tag <%d>", tag);
|
||||
has_error = true;
|
||||
}
|
||||
|
||||
if (has_error) {
|
||||
kernel_free(new_node);
|
||||
new_node = NULL;
|
||||
} else {
|
||||
new_node->tag = tag;
|
||||
new_node->message_handler = message_handler;
|
||||
new_node->dropped_handler = dropped_handler;
|
||||
new_node->event_handler_task = pebble_task_get_current();
|
||||
new_node->buffer.storage = storage;
|
||||
new_node->buffer.size = storage_size;
|
||||
s_app_inbox_head = (AppInboxNode *)list_prepend((ListNode *)s_app_inbox_head,
|
||||
(ListNode *)new_node);
|
||||
}
|
||||
}
|
||||
prv_unlock();
|
||||
|
||||
return (new_node != NULL);
|
||||
}
|
||||
|
||||
uint32_t app_inbox_service_unregister_by_storage(uint8_t *storage) {
|
||||
uint32_t num_messages_lost = 0;
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *node = prv_find_inbox_by_storage(storage);
|
||||
if (node) {
|
||||
list_remove((ListNode *)node, (ListNode **)&s_app_inbox_head, NULL);
|
||||
num_messages_lost = node->num_failed + node->num_success + (node->writer ? 1 : 0);
|
||||
kernel_free(node);
|
||||
}
|
||||
}
|
||||
prv_unlock();
|
||||
return num_messages_lost;
|
||||
}
|
||||
|
||||
void app_inbox_service_unregister_all(void) {
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *node = s_app_inbox_head;
|
||||
while (node) {
|
||||
AppInboxNode *next = (AppInboxNode *) node->node.next;
|
||||
kernel_free(node);
|
||||
node = next;
|
||||
}
|
||||
s_app_inbox_head = NULL;
|
||||
}
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
static bool prv_is_inbox_being_written(AppInboxNode *inbox) {
|
||||
return (inbox->writer != NULL);
|
||||
}
|
||||
|
||||
static size_t prv_get_space_remaining(AppInboxNode *inbox) {
|
||||
return (inbox->buffer.size - inbox->buffer.write_index - inbox->buffer.current_offset);
|
||||
}
|
||||
|
||||
bool prv_check_space_remaining(AppInboxNode *inbox, size_t required_free_length) {
|
||||
const size_t space_remaining = prv_get_space_remaining(inbox);
|
||||
if (required_free_length > space_remaining) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Dropping data, not enough space %"PRIu32" vs %"PRIu32,
|
||||
(uint32_t)required_free_length, (uint32_t)space_remaining);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prv_send_event_if_needed(AppInboxNode *inbox) {
|
||||
if (!inbox || inbox->has_pending_event) {
|
||||
return;
|
||||
}
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_CALLBACK_EVENT,
|
||||
.callback = {
|
||||
.callback = prv_callback_event_handler,
|
||||
.data = (void *)(uintptr_t) inbox->tag,
|
||||
},
|
||||
};
|
||||
const bool is_event_enqueued = process_manager_send_event_to_process(inbox->event_handler_task,
|
||||
&event);
|
||||
if (!is_event_enqueued) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Event queue full");
|
||||
}
|
||||
inbox->has_pending_event = is_event_enqueued;
|
||||
}
|
||||
|
||||
static void prv_mark_failed_if_no_writer(AppInboxNode *inbox) {
|
||||
if (!inbox->writer) {
|
||||
// See PBL-41464
|
||||
// App message has been reset (closed and opened again) while a message was being received.
|
||||
// Fail it because our state got lost.
|
||||
inbox->write_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool app_inbox_service_begin(AppInboxServiceTag tag, size_t required_free_length, void *writer) {
|
||||
if (!writer) {
|
||||
return false;
|
||||
}
|
||||
bool success = false;
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
if (prv_is_inbox_being_written(inbox)) {
|
||||
++inbox->num_failed;
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Dropping data, already written by <%p>", inbox->writer);
|
||||
// Don't send event here, when the current write finishes, the drop(s) will be reported too.
|
||||
goto unlock;
|
||||
}
|
||||
if (!prv_check_space_remaining(inbox, required_free_length + sizeof(AppInboxMessageHeader))) {
|
||||
++inbox->num_failed;
|
||||
// If it doesn't fit, send event immediately, we don't know when the next write will happen.
|
||||
prv_send_event_if_needed(inbox);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
inbox->writer = writer;
|
||||
inbox->write_failed = false;
|
||||
// Leave space at the beginning for the header, which we'll write in the end
|
||||
inbox->buffer.current_offset = sizeof(AppInboxMessageHeader);
|
||||
success = true;
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
return success;
|
||||
}
|
||||
|
||||
bool app_inbox_service_write(AppInboxServiceTag tag, const uint8_t *data, size_t length) {
|
||||
bool success = false;
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
prv_mark_failed_if_no_writer(inbox);
|
||||
if (inbox->write_failed) {
|
||||
goto unlock;
|
||||
}
|
||||
if (!prv_check_space_remaining(inbox, length)) {
|
||||
inbox->write_failed = true;
|
||||
goto unlock;
|
||||
}
|
||||
memcpy(inbox->buffer.storage + inbox->buffer.write_index + inbox->buffer.current_offset,
|
||||
data, length);
|
||||
inbox->buffer.current_offset += length;
|
||||
success = true;
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
return success;
|
||||
}
|
||||
|
||||
static void prv_finish(AppInboxNode *inbox) {
|
||||
inbox->writer = NULL;
|
||||
inbox->buffer.current_offset = 0;
|
||||
}
|
||||
|
||||
void app_inbox_service_init(void) {
|
||||
s_app_inbox_mutex = mutex_create_recursive();
|
||||
}
|
||||
|
||||
bool app_inbox_service_end(AppInboxServiceTag tag) {
|
||||
bool success = false;
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
prv_mark_failed_if_no_writer(inbox);
|
||||
if (inbox->write_failed) {
|
||||
++inbox->num_failed;
|
||||
} else {
|
||||
const AppInboxMessageHeader header = (const AppInboxMessageHeader) {
|
||||
.length = inbox->buffer.current_offset - sizeof(AppInboxMessageHeader),
|
||||
// Fill with something that might aid debugging one day:
|
||||
.padding = { 0xaa, 0xaa, 0xaa, 0xaa },
|
||||
};
|
||||
memcpy(inbox->buffer.storage + inbox->buffer.write_index, &header, sizeof(header));
|
||||
inbox->buffer.write_index += inbox->buffer.current_offset;
|
||||
++inbox->num_success;
|
||||
success = true;
|
||||
}
|
||||
prv_finish(inbox);
|
||||
|
||||
prv_send_event_if_needed(inbox);
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
return success;
|
||||
}
|
||||
|
||||
void app_inbox_service_cancel(AppInboxServiceTag tag) {
|
||||
prv_lock();
|
||||
{
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag_and_log_if_not_found(tag);
|
||||
if (!inbox) {
|
||||
goto unlock;
|
||||
}
|
||||
prv_finish(inbox);
|
||||
}
|
||||
unlock:
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Unit Test Interfaces
|
||||
|
||||
bool app_inbox_service_has_inbox_for_tag(AppInboxServiceTag tag) {
|
||||
bool has_inbox;
|
||||
prv_lock();
|
||||
has_inbox = (prv_find_inbox_by_tag(tag) != NULL);
|
||||
prv_unlock();
|
||||
return has_inbox;
|
||||
}
|
||||
|
||||
bool app_inbox_service_has_inbox_for_storage(uint8_t *storage) {
|
||||
bool has_inbox;
|
||||
prv_lock();
|
||||
has_inbox = (prv_find_inbox_by_storage(storage) != NULL);
|
||||
prv_unlock();
|
||||
return has_inbox;
|
||||
}
|
||||
|
||||
bool app_inbox_service_is_being_written_for_tag(AppInboxServiceTag tag) {
|
||||
bool is_written = false;
|
||||
prv_lock();
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag(tag);
|
||||
if (inbox) {
|
||||
is_written = (inbox->writer != NULL);
|
||||
}
|
||||
prv_unlock();
|
||||
return is_written;
|
||||
}
|
||||
|
||||
uint32_t app_inbox_service_num_failed_for_tag(AppInboxServiceTag tag) {
|
||||
uint32_t num_failed = 0;
|
||||
prv_lock();
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag(tag);
|
||||
if (inbox) {
|
||||
num_failed = inbox->num_failed;
|
||||
}
|
||||
prv_unlock();
|
||||
return num_failed;
|
||||
}
|
||||
|
||||
uint32_t app_inbox_service_num_success_for_tag(AppInboxServiceTag tag) {
|
||||
uint32_t num_success = 0;
|
||||
prv_lock();
|
||||
AppInboxNode *inbox = prv_find_inbox_by_tag(tag);
|
||||
if (inbox) {
|
||||
num_success = inbox->num_success;
|
||||
}
|
||||
prv_unlock();
|
||||
return num_success;
|
||||
}
|
||||
114
src/fw/services/normal/app_inbox_service.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "applib/app_inbox.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
// Design goals of this module:
|
||||
//
|
||||
// - Provide a generic mechanism to pass variable-length data from a kernel service to app.
|
||||
// - Have the data be written directly into an app-provided buffer (in app space).
|
||||
// - Data is chunked up in "messages".
|
||||
// - Data must be contiguously stored for easy parsing (no circular buffer wrap-arounds).
|
||||
// - Support writing a message, while having pending, unconsumed message(s) in the buffer.
|
||||
// - Support starting to write a partial message, write some more and finally decide to cancel it.
|
||||
// The partial message should not get delivered.
|
||||
// - No race conditions can exist that could cause reading of an incomplete message.
|
||||
// - Support for notifying the app when data has been dropped (not enough buffer space) and
|
||||
// report the number of dropped messages.
|
||||
//
|
||||
// Non-goals:
|
||||
// - Sharing the same buffer between multiple kernel services (1:1 service to buffer relation is OK)
|
||||
// - Concurrently writing to the inbox from multiple tasks (failing the write up front when another
|
||||
// task is currently in the process of writing a message is OK)
|
||||
// - Preserve the ordering of when the dropped messages happened vs the received messages (it's OK
|
||||
// to only report the number of dropped messages)
|
||||
|
||||
typedef enum {
|
||||
AppInboxServiceTagInvalid = -1,
|
||||
AppInboxServiceTagAppMessageReceiver,
|
||||
#ifdef UNITTEST
|
||||
AppInboxServiceTagUnitTest,
|
||||
AppInboxServiceTagUnitTestAlt,
|
||||
#endif
|
||||
NumAppInboxServiceTag,
|
||||
} AppInboxServiceTag;
|
||||
|
||||
typedef struct PACKED {
|
||||
// Length of `data` payload (excluding the size of this header)
|
||||
size_t length;
|
||||
//! To give us some room for future changes. This structure ends up in a buffer that is sized by
|
||||
//! the app, so we can't easily increase the size of this once shipped.
|
||||
uint8_t padding[4];
|
||||
uint8_t data[];
|
||||
} AppInboxMessageHeader;
|
||||
|
||||
#ifndef UNITTEST
|
||||
_Static_assert(sizeof(AppInboxMessageHeader) == 8,
|
||||
"The size of AppInboxMessageHeader cannot grow beyond 8 bytes!");
|
||||
#endif
|
||||
|
||||
//! To be called once at boot.
|
||||
void app_inbox_service_init(void);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Owner / Receiver (App) API
|
||||
|
||||
//! @param storage_size The size of the buffer (in app space). Note that a header will be appended
|
||||
//! to the data of sizeof(AppInboxMessageHeader) bytes.
|
||||
//! @note The event handler will be executed on the task that called this function.
|
||||
//! @see app_inbox_create_and_register() for the applib invocation.
|
||||
bool app_inbox_service_register(uint8_t *storage, size_t storage_size,
|
||||
AppInboxMessageHandler message_handler,
|
||||
AppInboxDroppedHandler dropped_handler, AppInboxServiceTag tag);
|
||||
|
||||
//! @return The number of messages that were dropped, plus the ones that were still waiting
|
||||
//! to be consumed.
|
||||
//! @see app_inbox_destroy_and_deregister() for the applib invocation.
|
||||
uint32_t app_inbox_service_unregister_by_storage(uint8_t *storage);
|
||||
|
||||
void app_inbox_service_unregister_all(void);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Sender (Kernel) API
|
||||
|
||||
//! @param required_free_length The length in bytes of the data that needs to be written. Note that
|
||||
//! this should not include the size of the AppInboxMessageHeader. However, there must be at least
|
||||
//! (required_free_length + sizeof(AppInboxMessageHeader)) bytes free in the buffer in order to
|
||||
//! be able to write the message.
|
||||
//! @param writer Reference to the writer, just for debugging.
|
||||
//! @return True if the buffer is claimed successfully, false if not. If this function returns
|
||||
//! true, you MUST call app_inbox_service_end() at some point. Inversely, if this functions returns
|
||||
//! false, you MUST NOT call app_inbox_service_write() nor app_inbox_service_end() nor
|
||||
//! app_inbox_service_cancel().
|
||||
bool app_inbox_service_begin(AppInboxServiceTag tag, size_t required_free_length, void *writer);
|
||||
|
||||
//! @return True if the write was successful, false if not. If one write failed, successive writes
|
||||
//! will also fail and `app_inbox_service_end` will not actually dispatch the (broken) message,
|
||||
//! but instead just dispatch an event that data got dropped.
|
||||
bool app_inbox_service_write(AppInboxServiceTag tag, const uint8_t *data, size_t length);
|
||||
|
||||
//! @return True is the entire message was written successfully, false if not. If a partial write
|
||||
//! failed, the "dropped handler" will be invoked.
|
||||
bool app_inbox_service_end(AppInboxServiceTag tag);
|
||||
|
||||
void app_inbox_service_cancel(AppInboxServiceTag tag);
|
||||
197
src/fw/services/normal/app_message/app_message_receiver.c
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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/app_message/app_message_internal.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/comm_session/session_receive_router.h"
|
||||
#include "services/normal/app_inbox_service.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/math.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern const ReceiverImplementation g_default_kernel_receiver_implementation;
|
||||
extern const ReceiverImplementation g_app_message_receiver_implementation;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ReceiverImplementation that writes App Message PP messages to the app's memory space using
|
||||
// app_inbox_service. It also forwards a copy of the header to the default system receiver, but
|
||||
// with a special handler that will always send a nack reply. If all goes well, this forward is
|
||||
// cancelled in the end and the nack does not get sent.
|
||||
|
||||
//! The maximum amount of header bytes that is needed in order to let the system nack it.
|
||||
//! To nack an App Message push, only the transaction ID is needed. Therefore, only buffer the
|
||||
//! AppMessageHeader of the incoming push:
|
||||
#define MAX_HEADER_SIZE (sizeof(AppMessageHeader))
|
||||
|
||||
typedef struct {
|
||||
bool is_writing_to_app_inbox;
|
||||
|
||||
CommSession *session;
|
||||
|
||||
//! Used to keep track of how many header bytes are remaining to either forward to the default
|
||||
//! system receiver or to save them in the event the app inbox write fails in the end.
|
||||
//! We only want to write up to MAX_HEADER_SIZE, to keep the kernel heap impact to a minimum.
|
||||
size_t header_bytes_remaining;
|
||||
|
||||
//! Pointer to the default system receiver context, to which we want to forward the header data.
|
||||
Receiver *kernel_receiver;
|
||||
} AppMessageReceiver;
|
||||
|
||||
static bool prv_fwd_prepare(AppMessageReceiver *rcv, CommSession *session,
|
||||
size_t header_bytes_remaining) {
|
||||
// Try to set up a forward to the default system receiver that will send a nack back, based
|
||||
// on the header of the message:
|
||||
static const PebbleProtocolEndpoint kernel_nack_endpoint = {
|
||||
.endpoint_id = APP_MESSAGE_ENDPOINT_ID,
|
||||
.handler = app_message_app_protocol_system_nack_callback,
|
||||
.access_mask = PebbleProtocolAccessAny,
|
||||
.receiver_imp = &g_default_kernel_receiver_implementation,
|
||||
.receiver_opt = NULL,
|
||||
};
|
||||
Receiver *kernel_receiver = g_default_kernel_receiver_implementation.prepare(session,
|
||||
&kernel_nack_endpoint,
|
||||
header_bytes_remaining);
|
||||
if (!kernel_receiver) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "System receiver wasn't able to prepare");
|
||||
return false;
|
||||
}
|
||||
rcv->kernel_receiver = kernel_receiver;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prv_write(const uint8_t *data, size_t length) {
|
||||
app_inbox_service_write(AppInboxServiceTagAppMessageReceiver, data, length);
|
||||
}
|
||||
|
||||
static Receiver *prv_app_message_receiver_prepare(CommSession *session,
|
||||
const PebbleProtocolEndpoint *endpoint,
|
||||
size_t total_payload_size) {
|
||||
analytics_inc(ANALYTICS_APP_METRIC_MSG_IN_COUNT, AnalyticsClient_App);
|
||||
|
||||
// FIXME: Find a better solution for this.
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-21538
|
||||
if (total_payload_size > 500) {
|
||||
comm_session_set_responsiveness(session, BtConsumerPpAppMessage, ResponseTimeMin,
|
||||
MIN_LATENCY_MODE_TIMEOUT_APP_MESSAGE_SECS);
|
||||
}
|
||||
|
||||
AppMessageReceiver *rcv = (AppMessageReceiver *)kernel_zalloc(sizeof(AppMessageReceiver));
|
||||
if (!rcv) {
|
||||
return NULL;
|
||||
}
|
||||
rcv->session = session;
|
||||
|
||||
const size_t header_bytes_remaining = MIN(MAX_HEADER_SIZE, total_payload_size);
|
||||
rcv->header_bytes_remaining = header_bytes_remaining;
|
||||
|
||||
// Always forward the header to default system receiver as well, we'll cancel it later on if the
|
||||
// message was written succesfully to the app inbox.
|
||||
if (!prv_fwd_prepare(rcv, session, header_bytes_remaining)) {
|
||||
kernel_free(rcv);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const size_t total_size = sizeof(AppMessageReceiverHeader) + total_payload_size;
|
||||
|
||||
// Reasons why app_inbox_service_begin() might fail:
|
||||
// - the watchapp does not have App Message context opened
|
||||
// - there is no more space in the buffer that the app had allocated for it,
|
||||
// - the inbox is already being written to (by another CommSession) -- should be very rare
|
||||
if (app_inbox_service_begin(AppInboxServiceTagAppMessageReceiver, total_size, session)) {
|
||||
rcv->is_writing_to_app_inbox = true;
|
||||
|
||||
// Log most recent communication timestamp
|
||||
const AppInstallId app_id = app_manager_get_current_app_id();
|
||||
app_install_mark_prioritized(app_id, true /* can_expire */);
|
||||
|
||||
// Write the header, this info is needed for the app to handle the message and reply:
|
||||
const AppMessageReceiverHeader header = (const AppMessageReceiverHeader) {
|
||||
.session = session,
|
||||
};
|
||||
prv_write((const uint8_t *)&header, sizeof(header));
|
||||
}
|
||||
|
||||
return (Receiver *)rcv;
|
||||
}
|
||||
|
||||
static void prv_app_message_receiver_write(Receiver *receiver, const uint8_t *data, size_t length) {
|
||||
AppMessageReceiver *rcv = (AppMessageReceiver *)receiver;
|
||||
|
||||
// FIXME: Find a better solution for this.
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-21538
|
||||
comm_session_set_responsiveness(rcv->session, BtConsumerPpAppMessage, ResponseTimeMin,
|
||||
MIN_LATENCY_MODE_TIMEOUT_APP_MESSAGE_SECS);
|
||||
|
||||
analytics_add(ANALYTICS_APP_METRIC_MSG_BYTE_IN_COUNT, length, AnalyticsClient_App);
|
||||
|
||||
if (rcv->header_bytes_remaining > 0) {
|
||||
const size_t header_bytes_to_write = MIN(rcv->header_bytes_remaining, length);
|
||||
g_default_kernel_receiver_implementation.write(rcv->kernel_receiver,
|
||||
data, header_bytes_to_write);
|
||||
rcv->header_bytes_remaining -= header_bytes_to_write;
|
||||
}
|
||||
|
||||
if (rcv->is_writing_to_app_inbox) {
|
||||
prv_write(data, length);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_finally(AppMessageReceiver *receiver,
|
||||
void (*kernel_receiver_finally_cb)(Receiver *)) {
|
||||
kernel_receiver_finally_cb(receiver->kernel_receiver);
|
||||
kernel_free(receiver);
|
||||
}
|
||||
|
||||
static void prv_app_message_receiver_finish(Receiver *receiver) {
|
||||
AppMessageReceiver *rcv = (AppMessageReceiver *)receiver;
|
||||
|
||||
// Default to letting the system receiver process the message and thus nack it:
|
||||
void (*kernel_receiver_finally_cb)(Receiver *) = g_default_kernel_receiver_implementation.finish;
|
||||
|
||||
if (rcv->is_writing_to_app_inbox) {
|
||||
if (app_inbox_service_end(AppInboxServiceTagAppMessageReceiver)) {
|
||||
// The write was successful, cancel processing the header for nacking:
|
||||
kernel_receiver_finally_cb = g_default_kernel_receiver_implementation.cleanup;
|
||||
} else {
|
||||
analytics_inc(ANALYTICS_APP_METRIC_MSG_DROP_COUNT, AnalyticsClient_App);
|
||||
}
|
||||
}
|
||||
|
||||
prv_finally(rcv, kernel_receiver_finally_cb);
|
||||
}
|
||||
|
||||
static void prv_app_message_receiver_cleanup(Receiver *receiver) {
|
||||
AppMessageReceiver *rcv = (AppMessageReceiver *)receiver;
|
||||
|
||||
if (rcv->is_writing_to_app_inbox) {
|
||||
// Cancel the write, we don't want to deliver a broken message to the watchapp:
|
||||
app_inbox_service_cancel(AppInboxServiceTagAppMessageReceiver);
|
||||
}
|
||||
|
||||
prv_finally(rcv, g_default_kernel_receiver_implementation.cleanup);
|
||||
}
|
||||
|
||||
const ReceiverImplementation g_app_message_receiver_implementation = {
|
||||
.prepare = prv_app_message_receiver_prepare,
|
||||
.write = prv_app_message_receiver_write,
|
||||
.finish = prv_app_message_receiver_finish,
|
||||
.cleanup = prv_app_message_receiver_cleanup,
|
||||
};
|
||||
235
src/fw/services/normal/app_message/app_message_sender.c
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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/app_message/app_message_internal.h"
|
||||
#include "applib/app_outbox.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/app_manager.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/normal/app_message/app_message_sender.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/math.h"
|
||||
#include "util/net.h"
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Misc helpers:
|
||||
|
||||
static void prv_request_fast_connection(CommSession *session) {
|
||||
// TODO: apply some heuristic to decide whether to put connection in fast mode or not:
|
||||
// https://pebbletechnology.atlassian.net/browse/PBL-21538
|
||||
comm_session_set_responsiveness(session, BtConsumerPpAppMessage, ResponseTimeMin,
|
||||
MIN_LATENCY_MODE_TIMEOUT_APP_MESSAGE_SECS);
|
||||
}
|
||||
|
||||
static AppOutboxMessage *prv_outbox_message_from_app_message_send_job(
|
||||
AppMessageSendJob *app_message_send_job) {
|
||||
const size_t offset = offsetof(AppOutboxMessage, consumer_data);
|
||||
return (AppOutboxMessage *)(((uint8_t *)app_message_send_job) - offset);
|
||||
}
|
||||
|
||||
static AppOutboxMessage *prv_outbox_message_from_send_job(SessionSendQueueJob *send_job) {
|
||||
return prv_outbox_message_from_app_message_send_job((AppMessageSendJob *)send_job);
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interfaces towards Send Queue:
|
||||
|
||||
static size_t prv_get_length(AppMessageSendJob *app_message_send_job) {
|
||||
AppOutboxMessage *outbox_message =
|
||||
prv_outbox_message_from_app_message_send_job(app_message_send_job);
|
||||
return (outbox_message->length - offsetof(AppMessageAppOutboxData, payload) +
|
||||
sizeof(PebbleProtocolHeader) - app_message_send_job->consumed_length);
|
||||
}
|
||||
|
||||
static bool prv_is_header_consumed_for_offset(uint32_t offset) {
|
||||
return (offset >= sizeof(PebbleProtocolHeader));
|
||||
}
|
||||
|
||||
static size_t prv_get_read_pointer(AppMessageSendJob *app_message_send_job,
|
||||
uint32_t offset, const uint8_t **data_out) {
|
||||
const uint8_t *read_pointer;
|
||||
size_t num_bytes_available;
|
||||
if (prv_is_header_consumed_for_offset(offset)) {
|
||||
AppOutboxMessage *outbox_message =
|
||||
prv_outbox_message_from_app_message_send_job(app_message_send_job);
|
||||
|
||||
// Avoid reading from the buffer in app space if the message was cancelled,
|
||||
// just read zeroes instead.
|
||||
// Note: we could consider removing messages from the send queue that have not been started to
|
||||
// get sent out at all. This requires the send queue to keep track of what has started and
|
||||
// what not, and requires transports to tell the send queue what it has in flight so far.
|
||||
const bool is_cancelled = app_outbox_service_is_message_cancelled(outbox_message);
|
||||
if (is_cancelled) {
|
||||
static const uint32_t s_zeroes = 0;
|
||||
*data_out = (const uint8_t *)&s_zeroes;
|
||||
return sizeof(s_zeroes);
|
||||
}
|
||||
|
||||
const AppMessageAppOutboxData *outbox_data =
|
||||
(const AppMessageAppOutboxData *)outbox_message->data;
|
||||
read_pointer = (outbox_data->payload - sizeof(PebbleProtocolHeader));
|
||||
num_bytes_available = prv_get_length(app_message_send_job);
|
||||
} else {
|
||||
read_pointer = (const uint8_t *)&app_message_send_job->header;
|
||||
num_bytes_available = (sizeof(PebbleProtocolHeader) - offset);
|
||||
}
|
||||
read_pointer += offset;
|
||||
*data_out = read_pointer;
|
||||
|
||||
return num_bytes_available;
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_get_length(const SessionSendQueueJob *send_job) {
|
||||
return prv_get_length((AppMessageSendJob *)send_job);
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_copy(const SessionSendQueueJob *send_job, int start_offset,
|
||||
size_t length, uint8_t *data_out) {
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)send_job;
|
||||
prv_request_fast_connection(app_message_send_job->session);
|
||||
|
||||
const size_t length_available = prv_get_length(app_message_send_job);
|
||||
const size_t length_after_offset = (length_available - start_offset);
|
||||
const size_t length_to_copy = MIN(length_after_offset, length);
|
||||
|
||||
size_t length_remaining = length_to_copy;
|
||||
while (length_remaining) {
|
||||
const uint8_t *part_data;
|
||||
uint32_t data_out_pos = (length_to_copy - length_remaining);
|
||||
size_t part_length =
|
||||
prv_get_read_pointer(app_message_send_job,
|
||||
app_message_send_job->consumed_length + start_offset + data_out_pos,
|
||||
&part_data);
|
||||
part_length = MIN(part_length, length_remaining);
|
||||
memcpy(data_out + data_out_pos, part_data, part_length);
|
||||
length_remaining -= part_length;
|
||||
}
|
||||
|
||||
return length_to_copy;
|
||||
}
|
||||
|
||||
static size_t prv_send_job_impl_get_read_pointer(const SessionSendQueueJob *send_job,
|
||||
const uint8_t **data_out) {
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)send_job;
|
||||
prv_request_fast_connection(app_message_send_job->session);
|
||||
|
||||
return prv_get_read_pointer(app_message_send_job,
|
||||
app_message_send_job->consumed_length, data_out);
|
||||
}
|
||||
|
||||
static void prv_send_job_impl_consume(const SessionSendQueueJob *send_job, size_t length) {
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)send_job;
|
||||
app_message_send_job->consumed_length += length;
|
||||
|
||||
analytics_add(ANALYTICS_APP_METRIC_MSG_BYTE_OUT_COUNT, length, AnalyticsClient_App);
|
||||
}
|
||||
|
||||
static void prv_send_job_impl_free(SessionSendQueueJob *send_job) {
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)send_job;
|
||||
AppOutboxMessage *outbox_message = prv_outbox_message_from_send_job(send_job);
|
||||
const bool is_completed = (0 == prv_get_length(app_message_send_job));
|
||||
if (is_completed) {
|
||||
const AppInstallId app_id = app_manager_get_current_app_id();
|
||||
app_install_mark_prioritized(app_id, true /* can_expire */);
|
||||
|
||||
analytics_inc(ANALYTICS_APP_METRIC_MSG_OUT_COUNT, AnalyticsClient_App);
|
||||
}
|
||||
// The outbox_message is owned by app_outbox_service, calling consume will free it as well:
|
||||
const AppOutboxStatus status =
|
||||
(const AppOutboxStatus) (is_completed ? AppMessageSenderErrorSuccess :
|
||||
AppMessageSenderErrorDisconnected);
|
||||
app_outbox_service_consume_message(outbox_message, status);
|
||||
}
|
||||
|
||||
T_STATIC const SessionSendJobImpl s_app_message_send_job_impl = {
|
||||
.get_length = prv_send_job_impl_get_length,
|
||||
.copy = prv_send_job_impl_copy,
|
||||
.get_read_pointer = prv_send_job_impl_get_read_pointer,
|
||||
.consume = prv_send_job_impl_consume,
|
||||
.free = prv_send_job_impl_free,
|
||||
};
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Interfaces towards App Outbox service:
|
||||
|
||||
static bool prv_is_endpoint_allowed(uint16_t endpoint_id) {
|
||||
return (endpoint_id == APP_MESSAGE_ENDPOINT_ID);
|
||||
}
|
||||
|
||||
static AppMessageSenderError prv_sanity_check_msg_and_fill_header(const AppOutboxMessage *message) {
|
||||
if (message->length < (sizeof(AppMessageAppOutboxData) + 1 /* Prohibit zero length PP msg */)) {
|
||||
return AppMessageSenderErrorDataTooShort;
|
||||
}
|
||||
|
||||
const AppMessageAppOutboxData *outbox_data = (const AppMessageAppOutboxData *)message->data;
|
||||
|
||||
const uint16_t endpoint_id = outbox_data->endpoint_id;
|
||||
if (!prv_is_endpoint_allowed(endpoint_id)) {
|
||||
return AppMessageSenderErrorEndpointDisallowed;
|
||||
}
|
||||
|
||||
const size_t pp_payload_length = (message->length - offsetof(AppMessageAppOutboxData, payload));
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)message->consumer_data;
|
||||
app_message_send_job->header = (const PebbleProtocolHeader) {
|
||||
.endpoint_id = htons(endpoint_id),
|
||||
.length = htons(pp_payload_length),
|
||||
};
|
||||
|
||||
return AppMessageSenderErrorSuccess;
|
||||
}
|
||||
|
||||
static void prv_handle_outbox_message(AppOutboxMessage *message) {
|
||||
AppMessageSendJob *app_message_send_job = (AppMessageSendJob *)message->consumer_data;
|
||||
*app_message_send_job = (const AppMessageSendJob) {
|
||||
.send_queue_job = {
|
||||
.impl = &s_app_message_send_job_impl,
|
||||
},
|
||||
.consumed_length = 0,
|
||||
};
|
||||
|
||||
const AppMessageSenderError err = prv_sanity_check_msg_and_fill_header(message);
|
||||
if (AppMessageSenderErrorSuccess != err) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Outbound app message corrupted %u", err);
|
||||
app_outbox_service_consume_message(message, (AppOutboxStatus)err);
|
||||
return;
|
||||
}
|
||||
|
||||
const AppMessageAppOutboxData *outbox_data =
|
||||
(const AppMessageAppOutboxData *)message->data;
|
||||
|
||||
app_message_send_job->session = outbox_data->session;
|
||||
comm_session_sanitize_app_session(&app_message_send_job->session);
|
||||
if (!app_message_send_job->session) {
|
||||
// Most likely disconnected in the mean time, don't spam our logs about this
|
||||
app_outbox_service_consume_message(message, (AppOutboxStatus)AppMessageSenderErrorDisconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_request_fast_connection(app_message_send_job->session);
|
||||
comm_session_send_queue_add_job(app_message_send_job->session,
|
||||
(SessionSendQueueJob **)&app_message_send_job);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
void app_message_sender_init(void) {
|
||||
const size_t consumer_data_size = sizeof(AppMessageSendJob);
|
||||
// Make prv_handle_outbox_message() execute on KernelMain:
|
||||
app_outbox_service_register(AppOutboxServiceTagAppMessageSender,
|
||||
prv_handle_outbox_message, PebbleTask_KernelMain, consumer_data_size);
|
||||
}
|
||||
79
src/fw/services/normal/app_message/app_message_sender.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/normal/app_outbox_service.h"
|
||||
#include "services/common/comm_session/protocol.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/comm_session/session_send_queue.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
//! This module uses AppOutbox to get Pebble Protocol outbound messages from the app.
|
||||
//! It does not keep any static state inside this module, all the state is stored by the app outbox
|
||||
//! service. It's really just a piece of glue code between app_outbox.c and session_send_queue.c
|
||||
|
||||
|
||||
//! Enum that "inherits" from AppOutboxStatus and defines app-message-sender-specific status
|
||||
//! values in the user range:
|
||||
typedef enum {
|
||||
AppMessageSenderErrorSuccess = AppOutboxStatusSuccess,
|
||||
AppMessageSenderErrorDisconnected = AppOutboxStatusConsumerDoesNotExist,
|
||||
AppMessageSenderErrorDataTooShort = AppOutboxStatusUserRangeStart,
|
||||
AppMessageSenderErrorEndpointDisallowed,
|
||||
|
||||
NumAppMessageSenderError,
|
||||
} AppMessageSenderError;
|
||||
|
||||
_Static_assert((NumAppMessageSenderError - 1) <= AppOutboxStatusUserRangeEnd,
|
||||
"AppMessageSenderError value can't be bigger than AppOutboxStatusUserRangeEnd");
|
||||
|
||||
//! @note This is the data structure for the `consumer_data` of the AppOutboxMessage.
|
||||
//! app_message_sender.c assumes this struct is always contained within the AppOutboxMessage
|
||||
//! struct.
|
||||
typedef struct {
|
||||
SessionSendQueueJob send_queue_job;
|
||||
|
||||
CommSession *session;
|
||||
PebbleProtocolHeader header;
|
||||
|
||||
size_t consumed_length;
|
||||
} AppMessageSendJob;
|
||||
|
||||
_Static_assert(offsetof(AppMessageSendJob, send_queue_job) == 0,
|
||||
"send_queue_job must be first member, due to the way session_send_queue.c works");
|
||||
|
||||
//! Structure of `data` in outbox_message (in app's memory space)
|
||||
//! @note None of these fields can be trusted / used as is, they need to be sanitized.
|
||||
typedef struct {
|
||||
//! Can be NULL to "auto select" the session based on the UUID of the running app.
|
||||
CommSession *session;
|
||||
|
||||
//! Padding for future use
|
||||
uint8_t padding[6];
|
||||
|
||||
uint16_t endpoint_id;
|
||||
uint8_t payload[];
|
||||
} AppMessageAppOutboxData;
|
||||
|
||||
#if !UNITTEST
|
||||
_Static_assert(sizeof(AppMessageAppOutboxData) <= 12,
|
||||
"Can't grow AppMessageAppOutboxData beyond 12 bytes, can break apps!");
|
||||
#endif
|
||||
|
||||
//! To be called once during boot. This registers this module with app_outbox_service.
|
||||
void app_message_sender_init(void);
|
||||
101
src/fw/services/normal/app_order_endpoint.c
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 "process_management/app_install_manager_private.h"
|
||||
#include "process_management/app_order_storage.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
//! @file app_order_endpoint.c
|
||||
//! App Order Endpoint
|
||||
//!
|
||||
//! There is only 1 way to use this endpoint
|
||||
|
||||
//! \code{.c}
|
||||
//! 0x01 <uint8_t num_uuids>
|
||||
//! <16-byte UUID_1>
|
||||
//! ...
|
||||
//! <16-byte UUID_N>
|
||||
//! \endcode
|
||||
|
||||
//! AppOrder Endpoint ID
|
||||
static const uint16_t APP_ORDER_ENDPOINT_ID = 0xabcd;
|
||||
|
||||
typedef enum {
|
||||
APP_ORDER_CMD = 0x01,
|
||||
} AppOrderCommand;
|
||||
|
||||
typedef enum {
|
||||
APP_ORDER_RES_SUCCESS = 0x01,
|
||||
APP_ORDER_RES_FAILURE = 0x02,
|
||||
APP_ORDER_RES_INVALID = 0x03,
|
||||
APP_ORDER_RES_RETRY_LATER = 0x04,
|
||||
} AppOrderResponse;
|
||||
|
||||
typedef struct {
|
||||
CommSession *session;
|
||||
uint8_t result;
|
||||
} ResponseInfo;
|
||||
|
||||
static void prv_send_result(CommSession *session, uint8_t result) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Sending result of %d", result);
|
||||
comm_session_send_data(session, APP_ORDER_ENDPOINT_ID, (uint8_t*)&result, sizeof(result),
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
static void prv_handle_app_order_msg(CommSession *session, const uint8_t *data, uint32_t length) {
|
||||
// call write order function, then fire an event for app install manager to tell the launcher
|
||||
// to throw everything away or at least don't overwrite the data please.
|
||||
uint8_t num_uuids = data[0];
|
||||
|
||||
if (num_uuids != (length / UUID_SIZE)) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "invalid length, num_uuids does not match with the length of message");
|
||||
prv_send_result(session, APP_ORDER_RES_INVALID);
|
||||
}
|
||||
|
||||
write_uuid_list_to_file((const Uuid *)&data[1], num_uuids);
|
||||
prv_send_result(session, APP_ORDER_RES_SUCCESS);
|
||||
}
|
||||
|
||||
void app_order_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
|
||||
// header includes APP_ORDER_CMD and a num_uuids uint8_t
|
||||
const uint8_t header_len = sizeof(AppOrderCommand) + sizeof(uint8_t);
|
||||
|
||||
// Ensure it is a valid message. There is a list of UUID's after the header.
|
||||
if ((length % UUID_SIZE) != header_len) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "invalid length, (length - header_len) not multiple of 16");
|
||||
prv_send_result(session, APP_ORDER_RES_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data[0]) {
|
||||
case APP_ORDER_CMD:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got APP_ORDER message");
|
||||
prv_handle_app_order_msg(session, &data[1], length - 1);
|
||||
break;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid message received, first byte is %u", data[0]);
|
||||
prv_send_result(session, APP_ORDER_RES_FAILURE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
338
src/fw/services/normal/app_outbox_service.c
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* 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/app_message/app_message_internal.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "process_management/process_manager.h"
|
||||
#include "services/normal/app_message/app_message_sender.h"
|
||||
#include "services/normal/app_outbox_service.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "syscall/syscall_internal.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/list.h"
|
||||
|
||||
static PebbleRecursiveMutex *s_app_outbox_mutex;
|
||||
|
||||
typedef struct {
|
||||
AppOutboxMessage *head;
|
||||
AppOutboxMessageHandler message_handler;
|
||||
size_t consumer_data_length;
|
||||
PebbleTask consumer_task;
|
||||
} AppOutboxConsumer;
|
||||
|
||||
//! Array with the consuming kernel services that have been registered at run-time:
|
||||
static AppOutboxConsumer s_app_outbox_consumer[NumAppOutboxServiceTag];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Declarations of permitted senders:
|
||||
|
||||
typedef struct {
|
||||
AppOutboxSentHandler sent_handler;
|
||||
size_t max_length;
|
||||
uint32_t max_pending_messages;
|
||||
} AppOutboxSenderDef;
|
||||
|
||||
extern void app_message_outbox_handle_app_outbox_message_sent(AppOutboxStatus status, void *cb_ctx);
|
||||
|
||||
#ifdef UNITTEST
|
||||
extern void test_app_outbox_sent_handler(AppOutboxStatus status, void *cb_ctx);
|
||||
#endif
|
||||
|
||||
//! Constant array defining the allowed handlers and their restrictions:
|
||||
static const AppOutboxSenderDef s_app_outbox_sender_defs[] = {
|
||||
[AppOutboxServiceTagAppMessageSender] = {
|
||||
.sent_handler = app_message_outbox_handle_app_outbox_message_sent,
|
||||
.max_length = (sizeof(AppMessageAppOutboxData) + APP_MSG_HDR_OVRHD_SIZE + APP_MSG_8K_DICT_SIZE),
|
||||
.max_pending_messages = 1,
|
||||
},
|
||||
#ifdef UNITTEST
|
||||
[AppOutboxServiceTagUnitTest] = {
|
||||
.sent_handler = test_app_outbox_sent_handler,
|
||||
.max_length = 1,
|
||||
.max_pending_messages = 2,
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Syscalls
|
||||
|
||||
static const AppOutboxSenderDef *prv_find_def_and_tag_by_handler(AppOutboxSentHandler sent_handler,
|
||||
AppOutboxServiceTag *tag_out) {
|
||||
for (AppOutboxServiceTag tag = 0; tag < NumAppOutboxServiceTag; ++tag) {
|
||||
if (s_app_outbox_sender_defs[tag].sent_handler == sent_handler) {
|
||||
if (tag_out) {
|
||||
*tag_out = tag;
|
||||
}
|
||||
return &s_app_outbox_sender_defs[tag];
|
||||
}
|
||||
}
|
||||
if (tag_out) {
|
||||
*tag_out = AppOutboxServiceTagInvalid;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void app_outbox_service_send(const uint8_t *data, size_t length,
|
||||
AppOutboxSentHandler sent_handler, void *cb_ctx);
|
||||
|
||||
DEFINE_SYSCALL(void, sys_app_outbox_send, const uint8_t *data, size_t length,
|
||||
AppOutboxSentHandler sent_handler, void *cb_ctx) {
|
||||
if (PRIVILEGE_WAS_ELEVATED) {
|
||||
// Check that data is in app space:
|
||||
syscall_assert_userspace_buffer(data, length);
|
||||
}
|
||||
|
||||
const AppOutboxSenderDef *def = prv_find_def_and_tag_by_handler(sent_handler, NULL);
|
||||
if (!def) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "AppOutbox sent_handler not allowed <%p>", sent_handler);
|
||||
syscall_failed();
|
||||
}
|
||||
|
||||
const size_t max_length = def->max_length;
|
||||
if (length > max_length) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "AppOutbox max_length exceeded %"PRIu32" vs %"PRIu32,
|
||||
(uint32_t)length, (uint32_t)max_length);
|
||||
syscall_failed();
|
||||
}
|
||||
app_outbox_service_send(data, length, sent_handler, cb_ctx);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Helpers
|
||||
|
||||
static void prv_lock(void) {
|
||||
// Using one "global" lock for all app outboxes.
|
||||
// If needed, we could easily give each app outbox its own mutex, but it seems overkill right now.
|
||||
mutex_lock_recursive(s_app_outbox_mutex);
|
||||
}
|
||||
|
||||
static void prv_unlock(void) {
|
||||
mutex_unlock_recursive(s_app_outbox_mutex);
|
||||
}
|
||||
|
||||
static AppOutboxConsumer *prv_consumer_for_tag(AppOutboxServiceTag tag) {
|
||||
if (tag == AppOutboxServiceTagInvalid) {
|
||||
return NULL;
|
||||
}
|
||||
AppOutboxConsumer *consumer = &s_app_outbox_consumer[tag];
|
||||
if (consumer->message_handler == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return consumer;
|
||||
}
|
||||
|
||||
static void prv_schedule_sent_handler(AppOutboxSentHandler sent_handler,
|
||||
void *cb_ctx, AppOutboxStatus status) {
|
||||
if (!sent_handler) {
|
||||
return;
|
||||
}
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_OUTBOX_SENT_EVENT,
|
||||
.app_outbox_sent = {
|
||||
.sent_handler = sent_handler,
|
||||
.cb_ctx = cb_ctx,
|
||||
.status = status,
|
||||
},
|
||||
};
|
||||
process_manager_send_event_to_process(PebbleTask_App, &event);
|
||||
}
|
||||
|
||||
//! @note This executes on App Task
|
||||
static void prv_schedule_consumer_message_handler(AppOutboxConsumer *consumer,
|
||||
AppOutboxMessage *message) {
|
||||
void (*callback)(void *) = (__typeof__(callback))consumer->message_handler;
|
||||
PebbleEvent event = {
|
||||
.type = PEBBLE_APP_OUTBOX_MSG_EVENT,
|
||||
.app_outbox_msg = {
|
||||
.callback = callback,
|
||||
.data = message,
|
||||
},
|
||||
};
|
||||
sys_send_pebble_event_to_kernel(&event);
|
||||
}
|
||||
|
||||
static uint32_t prv_num_pending_messages(const AppOutboxConsumer *consumer) {
|
||||
return list_count((ListNode *)consumer->head);
|
||||
}
|
||||
|
||||
static AppOutboxConsumer *prv_find_consumer_with_message(const AppOutboxMessage *message) {
|
||||
AppOutboxMessage *head = (AppOutboxMessage *)list_get_head((ListNode *)message);
|
||||
for (AppOutboxServiceTag tag = 0; tag < NumAppOutboxServiceTag; ++tag) {
|
||||
if (s_app_outbox_consumer[tag].head == head) {
|
||||
return &s_app_outbox_consumer[tag];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void prv_cleanup_pending_messages(AppOutboxConsumer *consumer, bool should_call_sent_handler) {
|
||||
AppOutboxMessage *message = consumer->head;
|
||||
consumer->head = NULL;
|
||||
while (message) {
|
||||
if (should_call_sent_handler) {
|
||||
prv_schedule_sent_handler(message->sent_handler, message->cb_ctx,
|
||||
AppOutboxStatusConsumerDoesNotExist);
|
||||
}
|
||||
|
||||
AppOutboxMessage *next = (AppOutboxMessage *)message->node.next;
|
||||
message->node = (ListNode) {};
|
||||
// Don't free it, it's the responsibility of the consumer to eventually call
|
||||
// app_outbox_service_consume_message(), which will free the message!
|
||||
message = next;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Exported functions
|
||||
|
||||
void app_outbox_service_register(AppOutboxServiceTag tag,
|
||||
AppOutboxMessageHandler message_handler,
|
||||
PebbleTask consumer_task,
|
||||
size_t consumer_data_length) {
|
||||
prv_lock();
|
||||
{
|
||||
PBL_ASSERTN(!prv_consumer_for_tag(tag));
|
||||
AppOutboxConsumer *consumer = &s_app_outbox_consumer[tag];
|
||||
consumer->message_handler = message_handler;
|
||||
consumer->consumer_data_length = consumer_data_length;
|
||||
consumer->consumer_task = consumer_task;
|
||||
}
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
void app_outbox_service_unregister(AppOutboxServiceTag service_tag) {
|
||||
prv_lock();
|
||||
{
|
||||
prv_cleanup_pending_messages(&s_app_outbox_consumer[service_tag],
|
||||
true /* should_call_sent_handler */);
|
||||
s_app_outbox_consumer[service_tag].message_handler = NULL;
|
||||
}
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
//! @note This executes on App Task
|
||||
//! Should only get called through the syscall, sys_app_outbox_send
|
||||
static void app_outbox_service_send(const uint8_t *data, size_t length,
|
||||
AppOutboxSentHandler sent_handler, void *cb_ctx) {
|
||||
AppOutboxStatus status = AppOutboxStatusSuccess;
|
||||
prv_lock();
|
||||
{
|
||||
AppOutboxServiceTag tag;
|
||||
const AppOutboxSenderDef *def = prv_find_def_and_tag_by_handler(sent_handler, &tag);
|
||||
AppOutboxConsumer *consumer = prv_consumer_for_tag(tag);
|
||||
if (!consumer) {
|
||||
status = AppOutboxStatusConsumerDoesNotExist;
|
||||
goto finally;
|
||||
}
|
||||
|
||||
if (prv_num_pending_messages(consumer) >= def->max_pending_messages) {
|
||||
status = AppOutboxStatusOutOfResources;
|
||||
goto finally;
|
||||
}
|
||||
|
||||
const size_t consumer_data_length = consumer->consumer_data_length;
|
||||
AppOutboxMessage *message =
|
||||
(AppOutboxMessage *)kernel_zalloc(sizeof(AppOutboxMessage) + consumer_data_length);
|
||||
if (!message) {
|
||||
status = AppOutboxStatusOutOfMemory;
|
||||
goto finally;
|
||||
}
|
||||
|
||||
*message = (AppOutboxMessage) {
|
||||
.data = data,
|
||||
.length = length,
|
||||
.sent_handler = sent_handler,
|
||||
.cb_ctx = cb_ctx,
|
||||
};
|
||||
|
||||
consumer->head = (AppOutboxMessage *)list_prepend((ListNode *)consumer->head,
|
||||
(ListNode *)message);
|
||||
|
||||
prv_schedule_consumer_message_handler(consumer, message);
|
||||
}
|
||||
finally:
|
||||
if (AppOutboxStatusSuccess != status) {
|
||||
prv_schedule_sent_handler(sent_handler, cb_ctx, status);
|
||||
}
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
bool app_outbox_service_is_message_cancelled(AppOutboxMessage *message) {
|
||||
prv_lock();
|
||||
bool cancelled = !prv_find_consumer_with_message(message);
|
||||
prv_unlock();
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
void app_outbox_service_consume_message(AppOutboxMessage *message, AppOutboxStatus status) {
|
||||
prv_lock();
|
||||
{
|
||||
if (app_outbox_service_is_message_cancelled(message)) {
|
||||
// Don't call the sent_handler
|
||||
goto finally;
|
||||
}
|
||||
AppOutboxConsumer *consumer = prv_find_consumer_with_message(message);
|
||||
PBL_ASSERTN(consumer);
|
||||
list_remove(&message->node, (ListNode **)&consumer->head, NULL);
|
||||
prv_schedule_sent_handler(message->sent_handler, message->cb_ctx, status);
|
||||
}
|
||||
finally:
|
||||
kernel_free(message);
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
void app_outbox_service_cleanup_all_pending_messages(void) {
|
||||
prv_lock();
|
||||
for (AppOutboxServiceTag tag = 0; tag < NumAppOutboxServiceTag; ++tag) {
|
||||
AppOutboxConsumer *consumer = &s_app_outbox_consumer[tag];
|
||||
prv_cleanup_pending_messages(consumer, false /* should_call_sent_handler */);
|
||||
}
|
||||
prv_unlock();
|
||||
}
|
||||
|
||||
void app_outbox_service_cleanup_event(PebbleEvent *event) {
|
||||
if (event->type != PEBBLE_APP_OUTBOX_MSG_EVENT) {
|
||||
return;
|
||||
}
|
||||
// Call consume directly to clean up the message, it's not valid anyway:
|
||||
app_outbox_service_consume_message((AppOutboxMessage *)event->app_outbox_msg.data,
|
||||
AppOutboxStatusSuccess /* ignored */);
|
||||
}
|
||||
|
||||
void app_outbox_service_init(void) {
|
||||
s_app_outbox_mutex = mutex_create_recursive();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Unit Test Interfaces
|
||||
|
||||
void app_outbox_service_deinit(void) {
|
||||
app_outbox_service_cleanup_all_pending_messages();
|
||||
memset(&s_app_outbox_consumer, 0, sizeof(s_app_outbox_consumer));
|
||||
mutex_destroy((PebbleMutex *)s_app_outbox_mutex);
|
||||
s_app_outbox_mutex = NULL;
|
||||
}
|
||||
|
||||
uint32_t app_outbox_service_max_pending_messages(AppOutboxServiceTag tag) {
|
||||
return s_app_outbox_sender_defs[tag].max_pending_messages;
|
||||
}
|
||||
|
||||
uint32_t app_outbox_service_max_message_length(AppOutboxServiceTag tag) {
|
||||
return s_app_outbox_sender_defs[tag].max_length;
|
||||
}
|
||||
122
src/fw/services/normal/app_outbox_service.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/app_outbox.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "util/list.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Design goals of this module:
|
||||
//
|
||||
// - Provide a generic mechanism to pass variable-length data from app to kernel service.
|
||||
// - Have the data be read directly from an app-provided buffer (in app space).
|
||||
// - Asynchronous: a "sent" callback should execute on the (app) task that created the outbox, when
|
||||
// the transfer is completed.
|
||||
// - Simple status results: the "sent" callback should be called with a simple status code that
|
||||
// indicates whether the transfer was successful or not.
|
||||
// - Use is limited only to the hard-coded set of permitted use cases and their handlers, to avoid
|
||||
// abuse of the API by misbehaving apps.
|
||||
// - The kernel manages the existence of service instances. If data is sent while the service is not
|
||||
// registered, the sent_handler should be called right away with a failure.
|
||||
// - Allow adding a message while there is already one or more waiting in the outbox.
|
||||
//
|
||||
// Non-goals:
|
||||
//
|
||||
// - Ability to cancel messages that have already been added to the outbox (could be added easily
|
||||
// in the future)
|
||||
|
||||
typedef enum {
|
||||
AppOutboxServiceTagInvalid = -1,
|
||||
AppOutboxServiceTagAppMessageSender,
|
||||
#ifdef UNITTEST
|
||||
AppOutboxServiceTagUnitTest,
|
||||
#endif
|
||||
NumAppOutboxServiceTag,
|
||||
} AppOutboxServiceTag;
|
||||
|
||||
//! To be called once at boot.
|
||||
void app_outbox_service_init(void);
|
||||
|
||||
//! Cleans up all pending messages. To be called by the app manager when an app is terminated.
|
||||
//! @note This will *NOT* invoke the `sent_handler`s of the pending messages.
|
||||
void app_outbox_service_cleanup_all_pending_messages(void);
|
||||
|
||||
//! Cleans up any pending app outbox events in the queue towards the kernel, that have not been
|
||||
//! processed.
|
||||
void app_outbox_service_cleanup_event(PebbleEvent *event);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Sender (App) API
|
||||
|
||||
//! @see app_outbox_send for documentation
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Owner / Receiver (Kernel) API
|
||||
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
|
||||
//! Pointer to message data
|
||||
//! @note This will reside in app's memory space and never in kernel memory space. Therefore the
|
||||
//! contents should be sanity checked carefully.
|
||||
const uint8_t *data;
|
||||
|
||||
//! The length of `data` in bytes
|
||||
size_t length;
|
||||
|
||||
//! Callback to execute on app task, when the data is consumed by the receiver.
|
||||
AppOutboxSentHandler sent_handler;
|
||||
//! User context to pass into the `sent_handler` callback.
|
||||
void *cb_ctx;
|
||||
|
||||
//! Additional user data that will be allocated by app_outbox_service, on behalf of the receiving
|
||||
//! kernel service. This can be used to store any state needed to parse and process the message.
|
||||
//! The buffer will be zeroed out just before the message handler gets called.
|
||||
uint8_t consumer_data[];
|
||||
} AppOutboxMessage;
|
||||
|
||||
//! Callback to indicate there is a message added.
|
||||
//! @note Only `consumer_data` is allowed to be mutated by the client!
|
||||
typedef void (*AppOutboxMessageHandler)(AppOutboxMessage *message);
|
||||
|
||||
//! Can be used by the receiving kernel service to check whether message has been cancelled in the
|
||||
//! mean time. Note that app_outbox_service_consume_message() still MUST be called with a cancelled
|
||||
//! message at some point in time, to clean up the resources associated with it.
|
||||
bool app_outbox_service_is_message_cancelled(AppOutboxMessage *message);
|
||||
|
||||
//! Registers a consumer for a specific app outbox service tag.
|
||||
//! @param consumer_data_size The additional space that will be allocated for context by the app
|
||||
//! outbox service, on behalf of the consumer. The extra space will be appended to the message that
|
||||
//! gets passed into `message_handler`.
|
||||
void app_outbox_service_register(AppOutboxServiceTag service_tag,
|
||||
AppOutboxMessageHandler message_handler,
|
||||
PebbleTask consumer_task,
|
||||
size_t consumer_data_size);
|
||||
|
||||
//! Will invoke the sender's `sent_handler` with the status on the app task.
|
||||
//! @param message Pointer to the message to be consumed. Note that this message will have been
|
||||
//! free'd after this function returns and should not be used thereafter.
|
||||
void app_outbox_service_consume_message(AppOutboxMessage *message, AppOutboxStatus status);
|
||||
|
||||
//! Closes the outbox.
|
||||
//! This will call the `sent_handler` callback for all messages in the outbox with
|
||||
//! AppOutboxStatusConsumerDoesNotExist.
|
||||
void app_outbox_service_unregister(AppOutboxServiceTag service_tag);
|
||||
204
src/fw/services/normal/audio_endpoint.c
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 "audio_endpoint.h"
|
||||
#include "audio_endpoint_private.h"
|
||||
|
||||
#include "comm/bt_lock.h"
|
||||
#include "services/common/comm_session/session_send_buffer.h"
|
||||
#include "services/common/new_timer/new_timer.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/circular_buffer.h"
|
||||
|
||||
#define AUDIO_ENDPOINT (10000)
|
||||
|
||||
#define ACTIVE_MODE_TIMEOUT (10000)
|
||||
#define ACTIVE_MODE_START_BUFFER (100)
|
||||
|
||||
_Static_assert(ACTIVE_MODE_TIMEOUT > ACTIVE_MODE_START_BUFFER,
|
||||
"ACTIVE_MODE_TIMEOUT must be greater than ACTIVE_MODE_START_BUFFER");
|
||||
|
||||
typedef struct {
|
||||
AudioEndpointSessionId id;
|
||||
AudioEndpointSetupCompleteCallback setup_completed;
|
||||
AudioEndpointStopTransferCallback stop_transfer;
|
||||
TimerID active_mode_trigger;
|
||||
} AudioEndpointSession;
|
||||
|
||||
static AudioEndpointSessionId s_session_id = AUDIO_ENDPOINT_SESSION_INVALID_ID;
|
||||
static AudioEndpointSession s_session;
|
||||
static uint32_t s_dropped_frames;
|
||||
|
||||
static void prv_session_deinit(bool call_stop_handler) {
|
||||
bt_lock();
|
||||
if (call_stop_handler && s_session.stop_transfer) {
|
||||
s_session.stop_transfer(s_session.id);
|
||||
}
|
||||
|
||||
if (s_session.active_mode_trigger != TIMER_INVALID_ID) {
|
||||
new_timer_delete(s_session.active_mode_trigger);
|
||||
s_session.active_mode_trigger = TIMER_INVALID_ID;
|
||||
CommSession *comm_session = comm_session_get_system_session();
|
||||
comm_session_set_responsiveness(
|
||||
comm_session, BtConsumerPpAudioEndpoint, ResponseTimeMax, 0);
|
||||
}
|
||||
|
||||
s_session.id = AUDIO_ENDPOINT_SESSION_INVALID_ID;
|
||||
s_session.setup_completed = NULL;
|
||||
s_session.stop_transfer = NULL;
|
||||
bt_unlock();
|
||||
|
||||
if (s_dropped_frames > 0) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Dropped %"PRIu32" frames during audio transfer", s_dropped_frames);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef PLATFORM_TINTIN
|
||||
void audio_endpoint_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t size) {
|
||||
MsgId msg_id = data[0];
|
||||
if (size >= sizeof(StopTransferMsg) && msg_id == MsgIdStopTransfer) {
|
||||
StopTransferMsg *msg = (StopTransferMsg *)data;
|
||||
|
||||
if (msg->session_id == s_session.id) {
|
||||
prv_session_deinit(true /* call_stop_handler */);
|
||||
} else {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Received mismatching session id: %u vs %u",
|
||||
msg->session_id, s_session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
void audio_endpoint_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
static void prv_responsiveness_granted_handler(void) {
|
||||
if (s_session.id == AUDIO_ENDPOINT_SESSION_INVALID_ID) {
|
||||
return; // Party's over
|
||||
}
|
||||
|
||||
AudioEndpointSetupCompleteCallback cb = NULL;
|
||||
AudioEndpointSessionId id = AUDIO_ENDPOINT_SESSION_INVALID_ID;
|
||||
|
||||
bt_lock();
|
||||
// We're repeatedly calling comm_session_set_responsiveness_ext, but we only need to call the
|
||||
// completed handler the first time the requested responsiveness takes effect:
|
||||
if (s_session.setup_completed) {
|
||||
cb = s_session.setup_completed;
|
||||
id = s_session.id;
|
||||
s_session.setup_completed = NULL;
|
||||
}
|
||||
bt_unlock();
|
||||
|
||||
if (cb) {
|
||||
cb(id);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_start_active_mode(void *data) {
|
||||
CommSession *comm_session = comm_session_get_system_session();
|
||||
comm_session_set_responsiveness_ext(comm_session, BtConsumerPpAudioEndpoint, ResponseTimeMin,
|
||||
MIN_LATENCY_MODE_TIMEOUT_AUDIO_SECS,
|
||||
prv_responsiveness_granted_handler);
|
||||
}
|
||||
|
||||
AudioEndpointSessionId audio_endpoint_setup_transfer(
|
||||
AudioEndpointSetupCompleteCallback setup_completed,
|
||||
AudioEndpointStopTransferCallback stop_transfer) {
|
||||
|
||||
if (s_session.id != AUDIO_ENDPOINT_SESSION_INVALID_ID) {
|
||||
return AUDIO_ENDPOINT_SESSION_INVALID_ID;
|
||||
}
|
||||
|
||||
bt_lock();
|
||||
|
||||
s_session.id = ++s_session_id;
|
||||
s_session.setup_completed = setup_completed;
|
||||
s_session.stop_transfer = stop_transfer;
|
||||
s_session.active_mode_trigger = new_timer_create();
|
||||
s_dropped_frames = 0;
|
||||
|
||||
// restart active mode before it expires, this way it will never be off during the transfer
|
||||
new_timer_start(s_session.active_mode_trigger, ACTIVE_MODE_TIMEOUT - ACTIVE_MODE_START_BUFFER,
|
||||
prv_start_active_mode, NULL, TIMER_START_FLAG_REPEATING);
|
||||
|
||||
bt_unlock();
|
||||
|
||||
prv_start_active_mode(NULL);
|
||||
|
||||
return s_session.id;
|
||||
}
|
||||
|
||||
void audio_endpoint_add_frame(AudioEndpointSessionId session_id, uint8_t *frame,
|
||||
uint8_t frame_size) {
|
||||
PBL_ASSERTN(session_id != AUDIO_ENDPOINT_SESSION_INVALID_ID);
|
||||
|
||||
if (s_session.id != session_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
CommSession *comm_session = comm_session_get_system_session();
|
||||
SendBuffer *sb = comm_session_send_buffer_begin_write(comm_session, AUDIO_ENDPOINT,
|
||||
sizeof(DataTransferMsg) + frame_size + 1,
|
||||
0 /* timeout_ms, never block */);
|
||||
if (!sb) {
|
||||
s_dropped_frames++;
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Dropping a frame...");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t header[sizeof(DataTransferMsg) + sizeof(uint8_t) /* frame_size */];
|
||||
DataTransferMsg *msg = (DataTransferMsg *) header;
|
||||
*msg = (const DataTransferMsg) {
|
||||
.msg_id = MsgIdDataTransfer,
|
||||
.session_id = session_id,
|
||||
.frame_count = 1,
|
||||
};
|
||||
msg->frames[0] = frame_size;
|
||||
|
||||
comm_session_send_buffer_write(sb, header, sizeof(header));
|
||||
comm_session_send_buffer_write(sb, frame, frame_size);
|
||||
comm_session_send_buffer_end_write(sb);
|
||||
}
|
||||
|
||||
void audio_endpoint_cancel_transfer(AudioEndpointSessionId session_id) {
|
||||
PBL_ASSERTN(session_id != AUDIO_ENDPOINT_SESSION_INVALID_ID);
|
||||
|
||||
if (s_session.id != session_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
prv_session_deinit(false /* call_stop_handler */);
|
||||
}
|
||||
|
||||
void audio_endpoint_stop_transfer(AudioEndpointSessionId session_id) {
|
||||
PBL_ASSERTN(session_id != AUDIO_ENDPOINT_SESSION_INVALID_ID);
|
||||
|
||||
if (s_session.id != session_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
StopTransferMsg msg = (const StopTransferMsg) {
|
||||
.msg_id = MsgIdStopTransfer,
|
||||
.session_id = session_id,
|
||||
};
|
||||
|
||||
prv_session_deinit(false /* call_stop_handler */);
|
||||
|
||||
comm_session_send_data(comm_session_get_system_session(), AUDIO_ENDPOINT, (const uint8_t *) &msg,
|
||||
sizeof(msg), COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
57
src/fw/services/normal/audio_endpoint.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
//! Endpoint for transferring audio data between the watch and phone
|
||||
//! https://pebbletechnology.atlassian.net/wiki/pages/viewpage.action?pageId=491698
|
||||
|
||||
//! Session identifier passed to endpoint functions
|
||||
typedef uint16_t AudioEndpointSessionId;
|
||||
#define AUDIO_ENDPOINT_SESSION_INVALID_ID (0)
|
||||
|
||||
//! Function signature of the callback to handle stop transfer message received from phone
|
||||
typedef void (*AudioEndpointStopTransferCallback)(AudioEndpointSessionId session_id);
|
||||
|
||||
//! Function signature of the callback to handle the completion of the setup process.
|
||||
//! After this point, the client may start adding audio frames using audio_endpoint_add_frame.
|
||||
typedef void (*AudioEndpointSetupCompleteCallback)(AudioEndpointSessionId session_id);
|
||||
|
||||
//! Create a session for transferring audio data from watch to phone
|
||||
//! @param setup_completed Callback to handle the completion of the audio endpoint setup process.
|
||||
//! @param stop_transfer Callback to handle stop transfer message received from phone.
|
||||
//! @return Session identifier to pass to other endpoint functions
|
||||
AudioEndpointSessionId audio_endpoint_setup_transfer(
|
||||
AudioEndpointSetupCompleteCallback setup_completed,
|
||||
AudioEndpointStopTransferCallback stop_transfer);
|
||||
|
||||
//! Add a frame of audio data to session's internal buffer
|
||||
//! @param session_id Session identifier returned by audio_endpoint_start_transfer
|
||||
//! @param frame Pointer to frame of encoded audio data
|
||||
//! @param frame_size Size of frame of encoded audio data in bytes
|
||||
void audio_endpoint_add_frame(AudioEndpointSessionId session_id, uint8_t *frame,
|
||||
uint8_t frame_size);
|
||||
|
||||
//! Stop transferring audio data from watch to phone
|
||||
//! @param session_id Session identifier returned by audio_endpoint_setup_transfer
|
||||
void audio_endpoint_stop_transfer(AudioEndpointSessionId session_id);
|
||||
|
||||
//! Cancel a transfer session without sending a stop transfer message
|
||||
//! @param session_id Session identifier returned by audio_endpoint_setup_transfer
|
||||
void audio_endpoint_cancel_transfer(AudioEndpointSessionId session_id);
|
||||
39
src/fw/services/normal/audio_endpoint_private.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/attributes.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
MsgIdDataTransfer = 0x02,
|
||||
MsgIdStopTransfer = 0x03,
|
||||
} MsgId;
|
||||
|
||||
typedef struct PACKED {
|
||||
MsgId msg_id;
|
||||
AudioEndpointSessionId session_id;
|
||||
uint8_t frame_count;
|
||||
uint8_t frames[];
|
||||
} DataTransferMsg;
|
||||
|
||||
typedef struct PACKED {
|
||||
MsgId msg_id;
|
||||
AudioEndpointSessionId session_id;
|
||||
} StopTransferMsg;
|
||||
328
src/fw/services/normal/blob_db/api.c
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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 "api.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "app_db.h"
|
||||
#include "app_glance_db.h"
|
||||
#include "contacts_db.h"
|
||||
#include "health_db.h"
|
||||
#include "ios_notif_pref_db.h"
|
||||
#include "notif_db.h"
|
||||
#include "pin_db.h"
|
||||
#include "prefs_db.h"
|
||||
#include "reminder_db.h"
|
||||
#include "watch_app_prefs_db.h"
|
||||
#include "weather_db.h"
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
typedef struct {
|
||||
BlobDBInitImpl init;
|
||||
BlobDBInsertImpl insert;
|
||||
BlobDBGetLenImpl get_len;
|
||||
BlobDBReadImpl read;
|
||||
BlobDBDeleteImpl del;
|
||||
BlobDBFlushImpl flush;
|
||||
BlobDBIsDirtyImpl is_dirty;
|
||||
BlobDBGetDirtyListImpl get_dirty_list;
|
||||
BlobDBMarkSyncedImpl mark_synced;
|
||||
bool disabled;
|
||||
} BlobDB;
|
||||
|
||||
static const BlobDB s_blob_dbs[NumBlobDBs] = {
|
||||
[BlobDBIdPins] = {
|
||||
.init = pin_db_init,
|
||||
.insert = pin_db_insert,
|
||||
.get_len = pin_db_get_len,
|
||||
.read = pin_db_read,
|
||||
.del = pin_db_delete,
|
||||
.flush = pin_db_flush,
|
||||
.is_dirty = pin_db_is_dirty,
|
||||
.get_dirty_list = pin_db_get_dirty_list,
|
||||
.mark_synced = pin_db_mark_synced,
|
||||
},
|
||||
[BlobDBIdApps] = {
|
||||
.init = app_db_init,
|
||||
.insert = app_db_insert,
|
||||
.get_len = app_db_get_len,
|
||||
.read = app_db_read,
|
||||
.del = app_db_delete,
|
||||
.flush = app_db_flush,
|
||||
},
|
||||
[BlobDBIdReminders] = {
|
||||
.init = reminder_db_init,
|
||||
.insert = reminder_db_insert,
|
||||
.get_len = reminder_db_get_len,
|
||||
.read = reminder_db_read,
|
||||
.del = reminder_db_delete,
|
||||
.flush = reminder_db_flush,
|
||||
.is_dirty = reminder_db_is_dirty,
|
||||
.get_dirty_list = reminder_db_get_dirty_list,
|
||||
.mark_synced = reminder_db_mark_synced,
|
||||
},
|
||||
[BlobDBIdNotifs] = {
|
||||
.init = notif_db_init,
|
||||
.insert = notif_db_insert,
|
||||
.get_len = notif_db_get_len,
|
||||
.read = notif_db_read,
|
||||
.del = notif_db_delete,
|
||||
.flush = notif_db_flush,
|
||||
},
|
||||
[BlobDBIdWeather] = {
|
||||
#if CAPABILITY_HAS_WEATHER
|
||||
.init = weather_db_init,
|
||||
.insert = weather_db_insert,
|
||||
.get_len = weather_db_get_len,
|
||||
.read = weather_db_read,
|
||||
.del = weather_db_delete,
|
||||
.flush = weather_db_flush,
|
||||
#else
|
||||
.disabled = true,
|
||||
#endif
|
||||
},
|
||||
[BlobDBIdiOSNotifPref] = {
|
||||
.init = ios_notif_pref_db_init,
|
||||
.insert = ios_notif_pref_db_insert,
|
||||
.get_len = ios_notif_pref_db_get_len,
|
||||
.read = ios_notif_pref_db_read,
|
||||
.del = ios_notif_pref_db_delete,
|
||||
.flush = ios_notif_pref_db_flush,
|
||||
.is_dirty = ios_notif_pref_db_is_dirty,
|
||||
.get_dirty_list = ios_notif_pref_db_get_dirty_list,
|
||||
.mark_synced = ios_notif_pref_db_mark_synced,
|
||||
},
|
||||
[BlobDBIdPrefs] = {
|
||||
.init = prefs_db_init,
|
||||
.insert = prefs_db_insert,
|
||||
.get_len = prefs_db_get_len,
|
||||
.read = prefs_db_read,
|
||||
.del = prefs_db_delete,
|
||||
.flush = prefs_db_flush,
|
||||
},
|
||||
[BlobDBIdContacts] = {
|
||||
#if !PLATFORM_TINTIN
|
||||
.init = contacts_db_init,
|
||||
.insert = contacts_db_insert,
|
||||
.get_len = contacts_db_get_len,
|
||||
.read = contacts_db_read,
|
||||
.del = contacts_db_delete,
|
||||
.flush = contacts_db_flush,
|
||||
#else
|
||||
// Disabled on tintin for code savings
|
||||
.disabled = true,
|
||||
#endif
|
||||
},
|
||||
[BlobDBIdWatchAppPrefs] = {
|
||||
#if !PLATFORM_TINTIN
|
||||
.init = watch_app_prefs_db_init,
|
||||
.insert = watch_app_prefs_db_insert,
|
||||
.get_len = watch_app_prefs_db_get_len,
|
||||
.read = watch_app_prefs_db_read,
|
||||
.del = watch_app_prefs_db_delete,
|
||||
.flush = watch_app_prefs_db_flush,
|
||||
#else
|
||||
// Disabled on tintin for code savings
|
||||
.disabled = true,
|
||||
#endif
|
||||
},
|
||||
[BlobDBIdHealth] = {
|
||||
#if CAPABILITY_HAS_HEALTH_TRACKING
|
||||
.init = health_db_init,
|
||||
.insert = health_db_insert,
|
||||
.get_len = health_db_get_len,
|
||||
.read = health_db_read,
|
||||
.del = health_db_delete,
|
||||
.flush = health_db_flush,
|
||||
#else
|
||||
.disabled = true,
|
||||
#endif
|
||||
},
|
||||
[BlobDBIdAppGlance] = {
|
||||
#if CAPABILITY_HAS_APP_GLANCES
|
||||
.init = app_glance_db_init,
|
||||
.insert = app_glance_db_insert,
|
||||
.get_len = app_glance_db_get_len,
|
||||
.read = app_glance_db_read,
|
||||
.del = app_glance_db_delete,
|
||||
.flush = app_glance_db_flush,
|
||||
#else
|
||||
.disabled = true,
|
||||
#endif
|
||||
},
|
||||
};
|
||||
|
||||
static bool prv_db_valid(BlobDBId db_id) {
|
||||
return (db_id < NumBlobDBs) && (!s_blob_dbs[db_id].disabled);
|
||||
|
||||
}
|
||||
|
||||
void blob_db_event_put(BlobDBEventType type, BlobDBId db_id, const uint8_t *key, int key_len) {
|
||||
// copy key for event
|
||||
uint8_t *key_bytes = NULL;
|
||||
if (key_len > 0) {
|
||||
key_bytes = kernel_malloc(key_len);
|
||||
memcpy(key_bytes, key, key_len);
|
||||
}
|
||||
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_BLOBDB_EVENT,
|
||||
.blob_db = {
|
||||
.db_id = db_id,
|
||||
.type = type,
|
||||
.key = key_bytes,
|
||||
.key_len = (uint8_t)key_len,
|
||||
}
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
void blob_db_init_dbs(void) {
|
||||
const BlobDB *db = s_blob_dbs;
|
||||
for (int i = 0; i < NumBlobDBs; ++i, ++db) {
|
||||
if (db->init) {
|
||||
db->init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void blob_db_get_dirty_dbs(uint8_t *ids, uint8_t *num_ids) {
|
||||
const BlobDB *db = s_blob_dbs;
|
||||
*num_ids = 0;
|
||||
for (uint8_t i = 0; i < NumBlobDBs; ++i, ++db) {
|
||||
bool is_dirty = false;
|
||||
if (db->is_dirty && (db->is_dirty(&is_dirty) == S_SUCCESS) && is_dirty) {
|
||||
ids[*num_ids] = i;
|
||||
*num_ids += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status_t blob_db_insert(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->insert) {
|
||||
status_t rv = db->insert(key, key_len, val, val_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
blob_db_event_put(BlobDBEventTypeInsert, db_id, key, key_len);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
int blob_db_get_len(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->get_len) {
|
||||
return db->get_len(key, key_len);
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
status_t blob_db_read(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->read) {
|
||||
return db->read(key, key_len, val_out, val_len);
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
status_t blob_db_delete(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->del) {
|
||||
status_t rv = db->del(key, key_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
blob_db_event_put(BlobDBEventTypeDelete, db_id, key, key_len);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
status_t blob_db_flush(BlobDBId db_id) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->flush) {
|
||||
status_t rv = db->flush();
|
||||
if (rv == S_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Flushing BlobDB with Id %d", db_id);
|
||||
blob_db_event_put(BlobDBEventTypeFlush, db_id, NULL, 0);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
BlobDBDirtyItem *blob_db_get_dirty_list(BlobDBId db_id) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->get_dirty_list) {
|
||||
return db->get_dirty_list();
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
status_t blob_db_mark_synced(BlobDBId db_id, uint8_t *key, int key_len) {
|
||||
if (!prv_db_valid(db_id)) {
|
||||
return E_RANGE;
|
||||
}
|
||||
|
||||
const BlobDB *db = &s_blob_dbs[db_id];
|
||||
if (db->mark_synced) {
|
||||
status_t rv = db->mark_synced(key, key_len);
|
||||
// TODO event?
|
||||
return rv;
|
||||
}
|
||||
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
186
src/fw/services/normal/blob_db/api.h
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api_types.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "system/status_codes.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/list.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
//! The BlobDB API is a single consistent API to a number of key/value stores on the watch.
|
||||
//! It is used in conjunction with the BlobDB endpoint.
|
||||
//! Key/Value stores that are meant to be used with the endpoint need to implement this API
|
||||
//! by implementing each of the Impl functions (see below).
|
||||
//! A BlobDB is not guaranteed to persist across reboots, but it is guaranteed to
|
||||
//! have executed a command when it returns a success code.
|
||||
//! If you want to route commands to your BlobDB implementation API, you need
|
||||
//! to add it to the \ref BlobDBId enum and to the BlobDBs list (\ref s_blob_dbs) in api.c
|
||||
|
||||
typedef enum PACKED {
|
||||
BlobDBIdTest = 0x00,
|
||||
BlobDBIdPins = 0x01,
|
||||
BlobDBIdApps = 0x02,
|
||||
BlobDBIdReminders = 0x03,
|
||||
BlobDBIdNotifs = 0x04,
|
||||
BlobDBIdWeather = 0x05,
|
||||
BlobDBIdiOSNotifPref = 0x06,
|
||||
BlobDBIdPrefs = 0x07,
|
||||
BlobDBIdContacts = 0x08,
|
||||
BlobDBIdWatchAppPrefs = 0x09,
|
||||
BlobDBIdHealth = 0x0A,
|
||||
BlobDBIdAppGlance = 0x0B,
|
||||
NumBlobDBs,
|
||||
} BlobDBId;
|
||||
_Static_assert(sizeof(BlobDBId) == 1, "BlobDBId is larger than 1 byte");
|
||||
|
||||
//! A linked list of blob DB items that need to be synced
|
||||
typedef struct {
|
||||
ListNode node;
|
||||
time_t last_updated;
|
||||
int key_len; //!< length of the key, in bytes
|
||||
uint8_t key[]; //!< key_len-size byte array of key data
|
||||
} BlobDBDirtyItem;
|
||||
|
||||
//! A Blob DB's initialization routine.
|
||||
//! This function will be called at boot when all blob dbs are init-ed
|
||||
typedef void (*BlobDBInitImpl)(void);
|
||||
|
||||
//! Implements the insert API. Note that this function should be blocking.
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
//! \param val a pointer to the value data
|
||||
//! \param val_len the length of the value, in bytes
|
||||
//! \returns S_SUCCESS if the key/val pair was succesfully inserted
|
||||
//! and an error code otherwise (See \ref StatusCode)
|
||||
typedef status_t (*BlobDBInsertImpl)
|
||||
(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
//! Implements the get length API.
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
//! \returns the length in bytes of the value for key on success
|
||||
//! and an error code otherwise (See \ref StatusCode)
|
||||
typedef int (*BlobDBGetLenImpl)
|
||||
(const uint8_t *key, int key_len);
|
||||
|
||||
//! Implements the read API. Note that this function should be blocking.
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
//! \param[out] val_out a pointer to a buffer of size val_len
|
||||
//! \param val_len the length of the value to be copied, in bytes
|
||||
//! \returns S_SUCCESS if the value for key was succesfully read,
|
||||
//! and an error code otherwise (See \ref StatusCode)
|
||||
typedef status_t (*BlobDBReadImpl)
|
||||
(const uint8_t *key, int key_len, uint8_t *val_out, int val_len);
|
||||
|
||||
//! Implements the delete API. Note that this function should be blocking.
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
//! \returns S_SUCCESS if the key/val pair was succesfully deleted
|
||||
//! and an error code otherwise (See \ref StatusCode)
|
||||
typedef status_t (*BlobDBDeleteImpl)
|
||||
(const uint8_t *key, int key_len);
|
||||
|
||||
//! Implements the flush API. Note that this function should be blocking.
|
||||
//! \returns S_SUCCESS if all key/val pairs were succesfully deleted
|
||||
//! and an error code otherwise (See \ref StatusCode)
|
||||
typedef status_t (*BlobDBFlushImpl)(void);
|
||||
|
||||
//! Implements the IsDirty API.
|
||||
//! \param[out] is_dirty_out reference to a boolean that will be set depending on the DB state
|
||||
//! \note if the function does not return S_SUCCESS, the state of the boolean is undefined.
|
||||
//! \returns S_SUCCESS if the query succeeded, an error code otherwise
|
||||
typedef status_t (*BlobDBIsDirtyImpl)(bool *is_dirty_out);
|
||||
|
||||
//! Implements the GetDirtyList API.
|
||||
//! \return a linked list of \ref BlobDBDirtyItem with a node per out-of-sync item.
|
||||
//! \note There is no limit how large this list can get. Handle OOM scenarios gracefully!
|
||||
typedef BlobDBDirtyItem *(*BlobDBGetDirtyListImpl)(void);
|
||||
|
||||
//! Implements the MarkSynced API.
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
//! \returns S_SUCCESS if the item was marked synced, an error code otherwise
|
||||
typedef status_t (*BlobDBMarkSyncedImpl)(const uint8_t *key, int key_len);
|
||||
|
||||
//! Emits a Blob DB event.
|
||||
//! \param type The type of event to emit
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the length of the key, in bytes
|
||||
void blob_db_event_put(BlobDBEventType type, BlobDBId db_id, const uint8_t *key, int key_len);
|
||||
|
||||
//! Call the BlobDBInitImpl for all the databases
|
||||
void blob_db_init_dbs(void);
|
||||
|
||||
//! Call the BlobDBIsDirtyImpl for each database, and fill the 'ids' list
|
||||
//! with all the dirty DB ids
|
||||
//! \param[out] ids an array of BlobDbIds of size NumBlobDBs or more.
|
||||
//! \param[out] num_ids an array of BlobDbIds of size NumBlobDBs or more.
|
||||
//! \note The unused entries will be set to 0.
|
||||
void blob_db_get_dirty_dbs(uint8_t *ids, uint8_t *num_ids);
|
||||
|
||||
//! Insert a key/val pair in a blob DB.
|
||||
//! See \ref BlobDBReadImpl
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
status_t blob_db_insert(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
//! Get the length of the value in a blob DB for a given key.
|
||||
//! See \ref BlobDBGetLenImpl
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! \param key a pointer to the key data
|
||||
//! \param key_len the lenght of the key, in bytes
|
||||
int blob_db_get_len(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len);
|
||||
|
||||
//! Get the value of length val_len for a given key
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! See \ref BlobDBReadImpl
|
||||
status_t blob_db_read(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len, uint8_t *val_out, int val_len);
|
||||
|
||||
//! Delete the key/val pair in a blob DB for a given key
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! See \ref BlobDBDeleteImpl
|
||||
status_t blob_db_delete(BlobDBId db_id,
|
||||
const uint8_t *key, int key_len);
|
||||
|
||||
//! Delete all key/val pairs in a blob DB.
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! See \ref BlobDBFlushImpl
|
||||
status_t blob_db_flush(BlobDBId db_id);
|
||||
|
||||
//! Get the list of items in a given blob DB that have yet to be synced.
|
||||
//! Items originating from the phone are always marked as synced.
|
||||
//! \note Use the APIs in sync.h to initiate a sync.
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! \see BlobDBGetDirtyListImpl
|
||||
BlobDBDirtyItem *blob_db_get_dirty_list(BlobDBId db_id);
|
||||
|
||||
//! Mark an item in a blob DB as having been synced
|
||||
//! \note This API is used upon receiving an ACK from the phone during sync
|
||||
//! \param db_id the ID of the blob DB
|
||||
//! \see BlobDBMarkSyncedImpl
|
||||
status_t blob_db_mark_synced(BlobDBId db_id, uint8_t *key, int key_len);
|
||||
23
src/fw/services/normal/blob_db/api_types.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
typedef enum BlobDBEventType {
|
||||
BlobDBEventTypeInsert,
|
||||
BlobDBEventTypeDelete,
|
||||
BlobDBEventTypeFlush,
|
||||
} BlobDBEventType;
|
||||
424
src/fw/services/normal/blob_db/app_db.c
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* 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 "app_db.h"
|
||||
|
||||
#include "util/uuid.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_install_manager_private.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "services/normal/app_fetch_endpoint.h"
|
||||
#include "os/mutex.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/math.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#define SETTINGS_FILE_NAME "appdb"
|
||||
// Holds about ~150 app metadata blobs
|
||||
#define SETTINGS_FILE_SIZE KiBYTES(20)
|
||||
|
||||
#define FIRST_VALID_INSTALL_ID (INSTALL_ID_INVALID + 1)
|
||||
|
||||
static AppInstallId s_next_unique_flash_app_id;
|
||||
|
||||
static struct {
|
||||
SettingsFile settings_file;
|
||||
PebbleMutex *mutex;
|
||||
} s_app_db;
|
||||
|
||||
//////////////////////
|
||||
// Settings helpers
|
||||
//////////////////////
|
||||
|
||||
struct AppDBInitData {
|
||||
AppInstallId max_id;
|
||||
uint32_t num_apps;
|
||||
};
|
||||
|
||||
static status_t prv_lock_mutex_and_open_file(void) {
|
||||
mutex_lock(s_app_db.mutex);
|
||||
status_t rv = settings_file_open(&s_app_db.settings_file,
|
||||
SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
mutex_unlock(s_app_db.mutex);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void prv_close_file_and_unlock_mutex(void) {
|
||||
settings_file_close(&s_app_db.settings_file);
|
||||
mutex_unlock(s_app_db.mutex);
|
||||
}
|
||||
|
||||
static status_t prv_cancel_app_fetch(AppInstallId app_id) {
|
||||
if (pebble_task_get_current() == PebbleTask_KernelBackground) {
|
||||
// if we are on kernel_bg, we can go ahead and cancel the app fetch instantly
|
||||
app_fetch_cancel_from_system_task(app_id);
|
||||
return S_SUCCESS;
|
||||
} else {
|
||||
// ignore the deletion and send back a failure message. The phone will retry later.
|
||||
return E_BUSY;
|
||||
}
|
||||
}
|
||||
|
||||
//! SettingsFileEachCallback function is used to iterate over all keys and find the largest
|
||||
//! AppInstallId currently being using.
|
||||
static bool prv_each_inspect_ids(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
// check entry is valid
|
||||
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
struct AppDBInitData *data = context;
|
||||
|
||||
AppInstallId app_id;
|
||||
info->get_key(file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
|
||||
data->max_id = MAX(data->max_id, app_id);
|
||||
data->num_apps++;
|
||||
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
struct UuidFilterData {
|
||||
Uuid uuid;
|
||||
AppInstallId found_id;
|
||||
};
|
||||
|
||||
//! SettingsFileEachCallback function is used to iterate over all entries and search for
|
||||
//! the particular entry with the given UUID. If one is found, it will set the uuid_data->found_id
|
||||
//! to a value other than INSTALL_ID_INVALID
|
||||
static bool prv_db_filter_app_id(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
// check entry is valid
|
||||
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
struct UuidFilterData *uuid_data = (struct UuidFilterData *)context;
|
||||
|
||||
AppInstallId app_id;
|
||||
AppDBEntry entry;
|
||||
info->get_key(file, (uint8_t *)&app_id, info->key_len);
|
||||
info->get_val(file, (uint8_t *)&entry, info->val_len);
|
||||
|
||||
if (uuid_equal(&uuid_data->uuid, &entry.uuid)) {
|
||||
uuid_data->found_id = app_id;
|
||||
return false; // stop iterating
|
||||
}
|
||||
return true; // continue iterating
|
||||
}
|
||||
|
||||
//! Retrieves the AppInstallId for a given UUID using the SettingsFile that is already open.
|
||||
//! @note Requires holding the lock already
|
||||
static AppInstallId prv_find_install_id_for_uuid(SettingsFile *file, const Uuid *uuid) {
|
||||
// used when iterating through all entries in our database.
|
||||
struct UuidFilterData filter_data = {
|
||||
.found_id = INSTALL_ID_INVALID,
|
||||
.uuid = *uuid,
|
||||
};
|
||||
|
||||
settings_file_each(file, prv_db_filter_app_id, (void *)&filter_data);
|
||||
return filter_data.found_id;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// App DB Specific API
|
||||
/////////////////////////
|
||||
|
||||
AppInstallId app_db_get_install_id_for_uuid(const Uuid *uuid) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, uuid);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return app_id;
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
// App DB API
|
||||
///////////////////////////
|
||||
|
||||
|
||||
//! Fills an AppDBEntry for a given UUID. This is a wrapper around app_db_read to keep it uniform
|
||||
//! with `app_db_get_app_entry_for_install_id`
|
||||
status_t app_db_get_app_entry_for_uuid(const Uuid *uuid, AppDBEntry *entry) {
|
||||
return app_db_read((uint8_t *)uuid, sizeof(Uuid), (uint8_t *)entry, sizeof(AppDBEntry));
|
||||
}
|
||||
|
||||
status_t app_db_get_app_entry_for_install_id(AppInstallId app_id, AppDBEntry *entry) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_get(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId),
|
||||
(uint8_t *)entry, sizeof(AppDBEntry));
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool app_db_exists_install_id(AppInstallId app_id) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool exists = settings_file_exists(&s_app_db.settings_file, (uint8_t *)&app_id,
|
||||
sizeof(AppInstallId));
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return exists;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
AppDBEnumerateCb cb;
|
||||
void *data;
|
||||
AppDBEntry *entry_buf;
|
||||
} EnumerateData;
|
||||
|
||||
static bool prv_enumerate_entries(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
// check entry is valid
|
||||
if ((info->val_len == 0) || (info->key_len != sizeof(AppInstallId))) {
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
EnumerateData *cb_data = (EnumerateData *)context;
|
||||
|
||||
AppInstallId id;
|
||||
info->get_key(file, (uint8_t *)&id, info->key_len);
|
||||
info->get_val(file, (uint8_t *)cb_data->entry_buf, info->val_len);
|
||||
|
||||
// check return value
|
||||
cb_data->cb(id, cb_data->entry_buf, cb_data->data);
|
||||
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
void app_db_enumerate_entries(AppDBEnumerateCb cb, void *data) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppDBEntry *db_entry = kernel_malloc_check(sizeof(AppDBEntry));
|
||||
|
||||
EnumerateData cb_data = {
|
||||
.cb = cb,
|
||||
.data = data,
|
||||
.entry_buf = db_entry,
|
||||
};
|
||||
settings_file_each(&s_app_db.settings_file, prv_enumerate_entries, &cb_data);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
kernel_free(db_entry);
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void app_db_init(void) {
|
||||
memset(&s_app_db, 0, sizeof(s_app_db));
|
||||
s_app_db.mutex = mutex_create();
|
||||
|
||||
// set to zero to reset unit test static variable.
|
||||
s_next_unique_flash_app_id = INSTALL_ID_INVALID;
|
||||
|
||||
// Iterate through all entires and find the one with the highest AppInstallId. The next unique
|
||||
// is then one greater than the largest found.
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
WTF;
|
||||
}
|
||||
|
||||
struct AppDBInitData data = { 0 };
|
||||
|
||||
settings_file_each(&s_app_db.settings_file, prv_each_inspect_ids, &data);
|
||||
|
||||
if (data.max_id == INSTALL_ID_INVALID) {
|
||||
s_next_unique_flash_app_id = (INSTALL_ID_INVALID + 1);
|
||||
} else {
|
||||
s_next_unique_flash_app_id = (data.max_id + 1);
|
||||
}
|
||||
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Found %"PRIu32" apps. Next ID: %"PRIu32" ", data.num_apps,
|
||||
s_next_unique_flash_app_id);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
}
|
||||
|
||||
status_t app_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if (key_len != UUID_SIZE ||
|
||||
val_len != sizeof(AppDBEntry)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
PBL_ASSERTN(key_len == 16);
|
||||
PBL_ASSERTN(val_len > 0);
|
||||
|
||||
bool new_install = false;
|
||||
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (const Uuid *)key);
|
||||
if (app_id == INSTALL_ID_INVALID) {
|
||||
new_install = true;
|
||||
app_id = s_next_unique_flash_app_id++;
|
||||
} else if (app_fetch_in_progress()) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Got an insert for an app that is currently being fetched, %"PRId32,
|
||||
app_id);
|
||||
rv = prv_cancel_app_fetch(app_id);
|
||||
}
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
rv = settings_file_set(&s_app_db.settings_file, (uint8_t *)&app_id,
|
||||
sizeof(AppInstallId), val, val_len);
|
||||
}
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
// app install something
|
||||
app_install_do_callbacks(new_install ? APP_AVAILABLE : APP_UPGRADED, app_id, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
int app_db_get_len(const uint8_t *key, int key_len) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
PBL_ASSERTN(key_len == 16);
|
||||
|
||||
// should not increment !!!!
|
||||
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
|
||||
|
||||
if (app_id == INSTALL_ID_INVALID) {
|
||||
rv = 0;
|
||||
} else {
|
||||
rv = settings_file_get_len(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
}
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
PBL_ASSERTN(key_len == 16);
|
||||
|
||||
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
|
||||
if (app_id == INSTALL_ID_INVALID) {
|
||||
rv = E_DOES_NOT_EXIST;
|
||||
} else {
|
||||
rv = settings_file_get(&s_app_db.settings_file, (uint8_t *)&app_id,
|
||||
sizeof(AppInstallId), val_out, val_len);
|
||||
}
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_db_delete(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
PBL_ASSERTN(key_len == 16);
|
||||
|
||||
AppInstallId app_id = prv_find_install_id_for_uuid(&s_app_db.settings_file, (Uuid *)key);
|
||||
|
||||
if (app_id == INSTALL_ID_INVALID) {
|
||||
rv = E_DOES_NOT_EXIST;
|
||||
} else if (app_fetch_in_progress()) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Tried to delete an app that is currently being fetched, %"PRId32,
|
||||
app_id);
|
||||
rv = prv_cancel_app_fetch(app_id);
|
||||
}
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
rv = settings_file_delete(&s_app_db.settings_file, (uint8_t *)&app_id, sizeof(AppInstallId));
|
||||
}
|
||||
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
// uuid will be free'd by app_install_manager
|
||||
Uuid *uuid_copy = kernel_malloc_check(sizeof(Uuid));
|
||||
memcpy(uuid_copy, key, sizeof(Uuid));
|
||||
app_install_do_callbacks(APP_REMOVED, app_id, uuid_copy, NULL, NULL);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_db_flush(void) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "AppDB Flush initiated");
|
||||
|
||||
if (app_fetch_in_progress()) {
|
||||
// cancels any app fetch
|
||||
status_t rv = prv_cancel_app_fetch(INSTALL_ID_INVALID);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
||||
app_install_do_callbacks(APP_DB_CLEARED, INSTALL_ID_INVALID, NULL, NULL, NULL);
|
||||
|
||||
// let app install manager deal with deleting the cache and removing related timeline pins
|
||||
app_install_clear_app_db();
|
||||
|
||||
// remove the settings file
|
||||
mutex_lock(s_app_db.mutex);
|
||||
pfs_remove(SETTINGS_FILE_NAME);
|
||||
|
||||
mutex_unlock(s_app_db.mutex);
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "AppDB Flush finished");
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Test functions
|
||||
//////////////////////
|
||||
|
||||
// automated testing and app_install_manager prompt commands
|
||||
int32_t app_db_check_next_unique_id(void) {
|
||||
return s_next_unique_flash_app_id;
|
||||
}
|
||||
76
src/fw/services/normal/blob_db/app_db.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "util/uuid.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "process_management/pebble_process_info.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/list.h"
|
||||
|
||||
|
||||
//! App database entry for BlobDB. First pass is very basic. The list will expand as more features
|
||||
//! and requirements are implemented.
|
||||
typedef struct PACKED {
|
||||
Uuid uuid;
|
||||
uint32_t info_flags;
|
||||
uint32_t icon_resource_id;
|
||||
Version app_version;
|
||||
Version sdk_version;
|
||||
GColor8 app_face_bg_color;
|
||||
uint8_t template_id;
|
||||
char name[APP_NAME_SIZE_BYTES];
|
||||
} AppDBEntry;
|
||||
|
||||
//! Used in app_db_enumerate_entries
|
||||
typedef void(*AppDBEnumerateCb)(AppInstallId install_id, AppDBEntry *entry, void *data);
|
||||
|
||||
/* AppDB Functions */
|
||||
|
||||
int32_t app_db_get_next_unique_id(void);
|
||||
|
||||
AppInstallId app_db_get_install_id_for_uuid(const Uuid *uuid);
|
||||
|
||||
status_t app_db_get_app_entry_for_uuid(const Uuid *uuid, AppDBEntry *entry);
|
||||
|
||||
status_t app_db_get_app_entry_for_install_id(AppInstallId app_id, AppDBEntry *entry);
|
||||
|
||||
void app_db_enumerate_entries(AppDBEnumerateCb cb, void *data);
|
||||
|
||||
/* AppDB AppInstallId Implementation */
|
||||
|
||||
bool app_db_exists_install_id(AppInstallId app_id);
|
||||
|
||||
/* BlobDB Implementation */
|
||||
|
||||
void app_db_init(void);
|
||||
|
||||
status_t app_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int app_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t app_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t app_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t app_db_flush(void);
|
||||
|
||||
/* TEST */
|
||||
AppInstallId app_db_check_next_unique_id(void);
|
||||
860
src/fw/services/normal/blob_db/app_glance_db.c
Normal file
@@ -0,0 +1,860 @@
|
||||
/*
|
||||
* 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 "app_glance_db.h"
|
||||
|
||||
#include "app_glance_db_private.h"
|
||||
|
||||
#include "applib/app_glance.h"
|
||||
#include "drivers/rtc.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "services/normal/app_cache.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/math.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#define SETTINGS_FILE_NAME "appglancedb"
|
||||
//! The defines below calculate `APP_GLANCE_DB_MAX_USED_SIZE` which is the actual minimum space we
|
||||
//! need to guarantee all of the apps's glances on the watch can have the same number of slices,
|
||||
//! and that number currently evaluates to 69050 bytes. We provide some additional space beyond that
|
||||
//! for some safety margin and easy future expansion, and thus use 80KB for the settings file size.
|
||||
#define SETTINGS_FILE_SIZE (KiBYTES(80))
|
||||
|
||||
#define APP_GLANCE_DB_GLANCE_MAX_SIZE \
|
||||
(sizeof(SerializedAppGlanceHeader) + \
|
||||
(APP_GLANCE_DB_SLICE_MAX_SIZE * APP_GLANCE_DB_MAX_SLICES_PER_GLANCE))
|
||||
#define APP_GLANCE_DB_MAX_USED_SIZE \
|
||||
(APP_GLANCE_DB_GLANCE_MAX_SIZE * APP_GLANCE_DB_MAX_NUM_APP_GLANCES)
|
||||
|
||||
_Static_assert(APP_GLANCE_DB_MAX_USED_SIZE <= SETTINGS_FILE_SIZE, "AppGlanceDB is too small!");
|
||||
|
||||
static struct {
|
||||
SettingsFile settings_file;
|
||||
PebbleMutex *mutex;
|
||||
} s_app_glance_db;
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Slice Type Implementation Definition
|
||||
//////////////////////////////////////////
|
||||
|
||||
//! Return true if the type-specific serialized slice's attribute list is valid. You don't have to
|
||||
//! check the attribute list pointer (we check it before calling this callback).
|
||||
typedef bool (*AttributeListValidationFunc)(const AttributeList *attr_list);
|
||||
|
||||
//! Callback for copying the type-specific attributes from a serialized slice's attribute list
|
||||
//! to the provided slice. You can assume that the attribute list and the slice_out pointers are
|
||||
//! valid because we check them before calling this callback.
|
||||
typedef void (*InitSliceFromAttributeListFunc)(const AttributeList *attr_list,
|
||||
AppGlanceSliceInternal *slice_out);
|
||||
|
||||
//! Callback for adding the type-specific fields from a slice to the provided attribute list.
|
||||
//! You can assume that the slice and attribute list pointers are valid because we check them
|
||||
//! before calling this callback.
|
||||
typedef void (*InitAttributeListFromSliceFunc)(const AppGlanceSliceInternal *slice,
|
||||
AttributeList *attr_list_to_init);
|
||||
|
||||
typedef struct SliceTypeImplementation {
|
||||
AttributeListValidationFunc is_attr_list_valid;
|
||||
InitSliceFromAttributeListFunc init_slice_from_attr_list;
|
||||
InitAttributeListFromSliceFunc init_attr_list_from_slice;
|
||||
} SliceTypeImplementation;
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// AppGlanceSliceType_IconAndSubtitle Implementation
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
static bool prv_is_icon_and_subtitle_slice_attribute_list_valid(const AttributeList *attr_list) {
|
||||
// The icon and subtitle are optional.
|
||||
return true;
|
||||
}
|
||||
|
||||
static void prv_init_icon_and_subtitle_slice_from_attr_list(const AttributeList *attr_list,
|
||||
AppGlanceSliceInternal *slice_out) {
|
||||
slice_out->icon_and_subtitle.icon_resource_id = attribute_get_uint32(attr_list,
|
||||
AttributeIdIcon,
|
||||
INVALID_RESOURCE);
|
||||
strncpy(slice_out->icon_and_subtitle.template_string,
|
||||
attribute_get_string(attr_list, AttributeIdSubtitleTemplateString, NULL),
|
||||
ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1);
|
||||
}
|
||||
|
||||
static void prv_init_attribute_list_from_icon_and_subtitle_slice(
|
||||
const AppGlanceSliceInternal *slice, AttributeList *attr_list_to_init) {
|
||||
attribute_list_add_cstring(attr_list_to_init, AttributeIdSubtitleTemplateString,
|
||||
slice->icon_and_subtitle.template_string);
|
||||
attribute_list_add_resource_id(attr_list_to_init, AttributeIdIcon,
|
||||
slice->icon_and_subtitle.icon_resource_id);
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Slice Type Implementations
|
||||
//////////////////////////////////
|
||||
|
||||
//! Add new entries to this array as we introduce new slice types
|
||||
static const SliceTypeImplementation s_slice_type_impls[AppGlanceSliceTypeCount] = {
|
||||
[AppGlanceSliceType_IconAndSubtitle] = {
|
||||
.is_attr_list_valid = prv_is_icon_and_subtitle_slice_attribute_list_valid,
|
||||
.init_slice_from_attr_list = prv_init_icon_and_subtitle_slice_from_attr_list,
|
||||
.init_attr_list_from_slice = prv_init_attribute_list_from_icon_and_subtitle_slice,
|
||||
},
|
||||
};
|
||||
|
||||
//////////////////////////////////
|
||||
// Serialized Slice Iteration
|
||||
//////////////////////////////////
|
||||
|
||||
//! Return true to continue iteration and false to stop it.
|
||||
typedef bool (*SliceForEachCb)(SerializedAppGlanceSliceHeader *serialized_slice, void *context);
|
||||
|
||||
//! Returns true if iteration completed successfully, either due to reaching the end of the slices
|
||||
//! or if the client's callback returns false to stop iteration early.
|
||||
//! Returns false if an error occurred during iteration due to the slices' `.total_size` values
|
||||
//! not being consistent with the provided `serialized_glance_size` argument.
|
||||
static bool prv_slice_for_each(SerializedAppGlanceHeader *serialized_glance,
|
||||
size_t serialized_glance_size, SliceForEachCb cb, void *context) {
|
||||
if (!serialized_glance || !cb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SerializedAppGlanceSliceHeader *current_slice =
|
||||
(SerializedAppGlanceSliceHeader *)serialized_glance->data;
|
||||
|
||||
size_t glance_size_processed = sizeof(SerializedAppGlanceHeader);
|
||||
|
||||
// Note that we'll stop iterating after reading the max supported number of slices per glance
|
||||
for (unsigned int i = 0; i < APP_GLANCE_DB_MAX_SLICES_PER_GLANCE; i++) {
|
||||
// Stop iterating if we've read all of the slices by hitting the end of the glance data
|
||||
if (glance_size_processed == serialized_glance_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop iterating and report an error if we've somehow gone beyond the end of the glance data
|
||||
if (glance_size_processed > serialized_glance_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop iterating if the client's callback function returns false
|
||||
if (!cb(current_slice, context)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance to the next slice
|
||||
glance_size_processed += current_slice->total_size;
|
||||
current_slice =
|
||||
(SerializedAppGlanceSliceHeader *)(((uint8_t *)current_slice) + current_slice->total_size);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// Serialized Slice Validation Helpers
|
||||
/////////////////////////////////////////
|
||||
|
||||
static bool prv_is_slice_type_valid(uint8_t type) {
|
||||
return (type < AppGlanceSliceTypeCount);
|
||||
}
|
||||
|
||||
//! Returns true if the provided AttributeList is valid for the specified AppGlanceSliceType,
|
||||
//! false otherwise.
|
||||
static bool prv_is_slice_attribute_list_valid(uint8_t type, const AttributeList *attr_list) {
|
||||
// Check if the slice type is valid before we plug it into the validation func array below
|
||||
if (!prv_is_slice_type_valid(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the AttributeList has the attributes required for this specific slice type
|
||||
return s_slice_type_impls[type].is_attr_list_valid(attr_list);
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Slice Deserialization
|
||||
//////////////////////////////////
|
||||
|
||||
//! Returns true if a non-empty AttributeList was successfully deserialized from `serialized_slice`,
|
||||
//! `attr_list` was filled with the result, and `attr_list_data_buffer_out` was filled with the data
|
||||
//! buffer for `attr_list_out`. Returns false otherwise.
|
||||
//! @note If function returns true, client must call `attribute_list_destroy_list()`
|
||||
//! on `attr_list_out` and `kernel_free()` on `attr_list_data_buffer_out`.
|
||||
static bool prv_deserialize_attribute_list(const SerializedAppGlanceSliceHeader *serialized_slice,
|
||||
AttributeList *attr_list_out,
|
||||
char **attr_list_data_buffer_out) {
|
||||
if (!serialized_slice || !attr_list_out || !attr_list_data_buffer_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t num_attributes = serialized_slice->num_attributes;
|
||||
// If there aren't any attributes, set `attr_list_out` to be an empty AttributeList and return
|
||||
// true because technically we did successfully deserialize the AttributeList
|
||||
if (!num_attributes) {
|
||||
*attr_list_out = (AttributeList) {};
|
||||
*attr_list_data_buffer_out = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t * const serialized_attr_list_start = serialized_slice->data;
|
||||
const uint8_t * const serialized_attr_list_end =
|
||||
serialized_attr_list_start + serialized_slice->total_size;
|
||||
|
||||
// Get the buffer size needed for the attributes we're going to deserialize
|
||||
const uint8_t *buffer_size_cursor = serialized_attr_list_start;
|
||||
const int32_t buffer_size =
|
||||
attribute_get_buffer_size_for_serialized_attributes(num_attributes, &buffer_size_cursor,
|
||||
serialized_attr_list_end);
|
||||
if (buffer_size < 0) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Failed to measure the buffer size required for deserializing an AttributeList from a "
|
||||
"serialized slice");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer_size) {
|
||||
// Allocate buffer for the data attached to the attributes
|
||||
*attr_list_data_buffer_out = kernel_zalloc(buffer_size);
|
||||
if (!*attr_list_data_buffer_out) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR,
|
||||
"Failed to alloc memory for the Attributes' data buffer while deserializing an "
|
||||
"AttributeList from a serialized slice");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// No buffer needed, but set the output pointer to NULL because we might blindly free it below
|
||||
// if we fail to alloc memory for the attribute_buffer
|
||||
*attr_list_data_buffer_out = NULL;
|
||||
}
|
||||
|
||||
// Allocate buffer for the Attribute's
|
||||
// Note that this doesn't need to be passed back as an output because it gets freed as part of the
|
||||
// client calling `attribute_list_destroy_list()` on `attr_list_out`
|
||||
Attribute *attribute_buffer = kernel_zalloc(num_attributes * sizeof(*attribute_buffer));
|
||||
if (!attribute_buffer) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR,
|
||||
"Failed to alloc memory for the buffer of Attribute's while deserializing an "
|
||||
"AttributeList from a serialized slice");
|
||||
// Free the `*attr_list_data_buffer_out` we might have allocated above
|
||||
kernel_free(*attr_list_data_buffer_out);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup the arguments for `attribute_deserialize_list()`
|
||||
char *attribute_data_buffer_pointer = *attr_list_data_buffer_out;
|
||||
char * const attribute_data_buffer_end = attribute_data_buffer_pointer + buffer_size;
|
||||
*attr_list_out = (AttributeList) {
|
||||
.num_attributes = num_attributes,
|
||||
.attributes = attribute_buffer,
|
||||
};
|
||||
const uint8_t *deserialization_cursor = serialized_attr_list_start;
|
||||
|
||||
// Try to deserialize the AttributeList
|
||||
const bool was_attr_list_deserialized = attribute_deserialize_list(&attribute_data_buffer_pointer,
|
||||
attribute_data_buffer_end,
|
||||
&deserialization_cursor,
|
||||
serialized_attr_list_end,
|
||||
*attr_list_out);
|
||||
if (!was_attr_list_deserialized) {
|
||||
kernel_free(attribute_buffer);
|
||||
kernel_free(*attr_list_data_buffer_out);
|
||||
}
|
||||
|
||||
return was_attr_list_deserialized;
|
||||
}
|
||||
|
||||
typedef struct SliceDeserializationIteratorContext {
|
||||
AppGlance *glance_out;
|
||||
bool deserialization_failed;
|
||||
} SliceDeserializationIteratorContext;
|
||||
|
||||
static bool prv_deserialize_slice(SerializedAppGlanceSliceHeader *serialized_slice, void *context) {
|
||||
SliceDeserializationIteratorContext *deserialization_context = context;
|
||||
|
||||
// Deserialize the serialized slice's attribute list
|
||||
AttributeList attr_list = {};
|
||||
char *attr_list_data_buffer = NULL;
|
||||
if (!prv_deserialize_attribute_list(serialized_slice, &attr_list, &attr_list_data_buffer)) {
|
||||
deserialization_context->deserialization_failed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the deserialized attribute list is valid
|
||||
const bool success = prv_is_slice_attribute_list_valid(serialized_slice->type, &attr_list);
|
||||
if (!success) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
AppGlance *glance_out = deserialization_context->glance_out;
|
||||
|
||||
// Copy the common serialized slice fields to the output glance's slice
|
||||
const unsigned int current_slice_index = glance_out->num_slices;
|
||||
AppGlanceSliceInternal *current_slice_out = &glance_out->slices[current_slice_index];
|
||||
// Note that we default the expiration time to "never expire" if one was not provided
|
||||
*current_slice_out = (AppGlanceSliceInternal) {
|
||||
.expiration_time = attribute_get_uint32(&attr_list, AttributeIdTimestamp,
|
||||
APP_GLANCE_SLICE_NO_EXPIRATION),
|
||||
.type = (AppGlanceSliceType)serialized_slice->type,
|
||||
};
|
||||
// Copy type-specific fields from the serialized slice to the output glance's slice
|
||||
s_slice_type_impls[serialized_slice->type].init_slice_from_attr_list(&attr_list,
|
||||
current_slice_out);
|
||||
|
||||
// Increment the number of slices in the glance
|
||||
glance_out->num_slices++;
|
||||
|
||||
cleanup:
|
||||
attribute_list_destroy_list(&attr_list);
|
||||
kernel_free(attr_list_data_buffer);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
static status_t prv_deserialize_glance(SerializedAppGlanceHeader *serialized_glance,
|
||||
size_t serialized_glance_size, AppGlance *glance_out) {
|
||||
if (!serialized_glance || !glance_out) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Zero out the output glance
|
||||
*glance_out = (AppGlance) {};
|
||||
|
||||
// Iterate over the slices to deserialize them
|
||||
SliceDeserializationIteratorContext context = (SliceDeserializationIteratorContext) {
|
||||
.glance_out = glance_out,
|
||||
};
|
||||
if (!prv_slice_for_each(serialized_glance, serialized_glance_size,
|
||||
prv_deserialize_slice, &context) ||
|
||||
context.deserialization_failed) {
|
||||
return E_ERROR;
|
||||
}
|
||||
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Slice Serialization
|
||||
//////////////////////////////////
|
||||
|
||||
typedef struct SliceSerializationAttributeListData {
|
||||
AttributeList attr_list;
|
||||
size_t attr_list_size;
|
||||
} SliceSerializationAttributeListData;
|
||||
|
||||
//! Returns S_SUCCESS if the provided glance was successfully serialized into serialized_glance_out
|
||||
//! and its serialized size copied to serialized_glance_size_out.
|
||||
//! @note If function returns S_SUCCESS, client must call `kernel_free()` on the pointer provided
|
||||
//! for `serialized_glance_out`.
|
||||
static status_t prv_serialize_glance(const AppGlance *glance,
|
||||
SerializedAppGlanceHeader **serialized_glance_out,
|
||||
size_t *serialized_glance_size_out) {
|
||||
if (!glance || (glance->num_slices > APP_GLANCE_DB_MAX_SLICES_PER_GLANCE) ||
|
||||
!serialized_glance_out || !serialized_glance_size_out) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Allocate a buffer for data about each slice's attribute list, but only if we have at least
|
||||
// one slice because allocating 0 bytes would return NULL and that is a return value we want to
|
||||
// reserve for the case when we've run out of memory
|
||||
SliceSerializationAttributeListData *attr_lists = NULL;
|
||||
if (glance->num_slices > 0) {
|
||||
attr_lists = kernel_zalloc(sizeof(SliceSerializationAttributeListData) * glance->num_slices);
|
||||
if (!attr_lists) {
|
||||
return E_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
status_t rv;
|
||||
|
||||
// Iterate over the glance slices, creating attribute lists and summing the size we need for the
|
||||
// overall serialized slice
|
||||
size_t serialized_glance_size = sizeof(SerializedAppGlanceHeader);
|
||||
for (unsigned int slice_index = 0; slice_index < glance->num_slices; slice_index++) {
|
||||
SliceSerializationAttributeListData *current_attr_list_data = &attr_lists[slice_index];
|
||||
const AppGlanceSliceInternal *current_slice = &glance->slices[slice_index];
|
||||
// Check the slice's type, fail the entire serialization if it's invalid
|
||||
if (!prv_is_slice_type_valid(current_slice->type)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Tried to serialize a glance containing a slice with invalid type: %d",
|
||||
current_slice->type);
|
||||
rv = E_INVALID_ARGUMENT;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
serialized_glance_size += sizeof(SerializedAppGlanceSliceHeader);
|
||||
|
||||
AttributeList *attr_list = ¤t_attr_list_data->attr_list;
|
||||
// Initialize the attributes common to all slice types in the attribute list
|
||||
attribute_list_add_uint32(attr_list, AttributeIdTimestamp,
|
||||
(uint32_t)current_slice->expiration_time);
|
||||
// Initialize the type-specific attributes in the attribute list
|
||||
s_slice_type_impls[current_slice->type].init_attr_list_from_slice(current_slice, attr_list);
|
||||
|
||||
// Record size of the attribute list in the data struct as well as the overall size accumulator
|
||||
current_attr_list_data->attr_list_size = attribute_list_get_serialized_size(attr_list);
|
||||
serialized_glance_size += current_attr_list_data->attr_list_size;
|
||||
}
|
||||
|
||||
// Allocate a buffer for the serialized glance
|
||||
SerializedAppGlanceHeader *serialized_glance = kernel_zalloc(serialized_glance_size);
|
||||
if (!serialized_glance) {
|
||||
rv = E_OUT_OF_MEMORY;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Populate the header of the serialized glance
|
||||
*serialized_glance = (SerializedAppGlanceHeader) {
|
||||
.version = APP_GLANCE_DB_CURRENT_VERSION,
|
||||
.creation_time = (uint32_t)rtc_get_time(),
|
||||
};
|
||||
|
||||
uint8_t *glance_buffer_start = (uint8_t *)serialized_glance;
|
||||
uint8_t *glance_buffer_end = glance_buffer_start + serialized_glance_size;
|
||||
// Start the cursor where the serialized slices go
|
||||
uint8_t *glance_buffer_cursor = serialized_glance->data;
|
||||
|
||||
// Serialize each slice into the serialized glance buffer
|
||||
for (unsigned int slice_index = 0; slice_index < glance->num_slices; slice_index++) {
|
||||
const AppGlanceSliceInternal *current_slice = &glance->slices[slice_index];
|
||||
|
||||
SliceSerializationAttributeListData *current_attr_list_data = &attr_lists[slice_index];
|
||||
AttributeList *attr_list = ¤t_attr_list_data->attr_list;
|
||||
const size_t attr_list_size = current_attr_list_data->attr_list_size;
|
||||
|
||||
// Calculate the total size of this serialized slice
|
||||
const uint16_t serialized_slice_total_size =
|
||||
sizeof(SerializedAppGlanceSliceHeader) + attr_list_size;
|
||||
|
||||
// Populate the serialized slice header
|
||||
SerializedAppGlanceSliceHeader *serialized_slice_header =
|
||||
(SerializedAppGlanceSliceHeader *)glance_buffer_cursor;
|
||||
*serialized_slice_header = (SerializedAppGlanceSliceHeader) {
|
||||
.type = current_slice->type,
|
||||
.total_size = serialized_slice_total_size,
|
||||
.num_attributes = attr_list->num_attributes,
|
||||
};
|
||||
|
||||
// Serialize the slice's attribute list
|
||||
attribute_list_serialize(attr_list, serialized_slice_header->data, glance_buffer_end);
|
||||
|
||||
// Note that we'll destroy the attribute list's attributes below in the cleanup section
|
||||
|
||||
// Advance the cursor by the serialized slice's total size
|
||||
glance_buffer_cursor += serialized_slice_total_size;
|
||||
}
|
||||
|
||||
// Check that we fully populated the serialized glance buffer
|
||||
rv = (glance_buffer_cursor == glance_buffer_end) ? S_SUCCESS : E_ERROR;
|
||||
if (rv == S_SUCCESS) {
|
||||
*serialized_glance_out = serialized_glance;
|
||||
*serialized_glance_size_out = serialized_glance_size;
|
||||
} else {
|
||||
kernel_free(serialized_glance);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
// Destroy the attributes of each of the attribute lists in attr_lists
|
||||
for (unsigned int i = 0; i < glance->num_slices; i++) {
|
||||
attribute_list_destroy_list(&attr_lists[i].attr_list);
|
||||
}
|
||||
kernel_free(attr_lists);
|
||||
return rv;
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Serialized Slice Validation
|
||||
//////////////////////////////////
|
||||
|
||||
static bool prv_is_serialized_slice_valid(const SerializedAppGlanceSliceHeader *serialized_slice) {
|
||||
if (!serialized_slice ||
|
||||
!prv_is_slice_type_valid(serialized_slice->type) ||
|
||||
!WITHIN(serialized_slice->total_size, APP_GLANCE_DB_SLICE_MIN_SIZE,
|
||||
APP_GLANCE_DB_SLICE_MAX_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deserialize the AttributeList from `serialized_slice`
|
||||
AttributeList attr_list = {};
|
||||
char *attr_list_data_buffer = NULL;
|
||||
if (!prv_deserialize_attribute_list(serialized_slice, &attr_list, &attr_list_data_buffer)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Failed to deserialize an AttributeList from a serialized slice");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the AttributeList has the attributes required for the slice
|
||||
const bool is_attr_list_valid = prv_is_slice_attribute_list_valid(serialized_slice->type,
|
||||
&attr_list);
|
||||
if (!is_attr_list_valid) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Serialized slice AttributeList is invalid");
|
||||
}
|
||||
|
||||
attribute_list_destroy_list(&attr_list);
|
||||
kernel_free(attr_list_data_buffer);
|
||||
|
||||
return is_attr_list_valid;
|
||||
}
|
||||
|
||||
typedef struct SliceValidationIteratorContext {
|
||||
bool is_at_least_one_slice_invalid;
|
||||
size_t validated_size;
|
||||
} SliceValidationIteratorContext;
|
||||
|
||||
//! If any slices are invalid, context.is_at_least_one_slice_invalid will be set to true.
|
||||
//! If all the slices are valid, context.validated_size will hold the size of the entire serialized
|
||||
//! glance after trimming any slices that go beyond the APP_GLANCE_DB_MAX_SLICES_PER_GLANCE limit.
|
||||
//! @note This assumes that context.validated_size has been initialized to take into account
|
||||
//! the serialized app glance's header.
|
||||
static bool prv_validate_slice(SerializedAppGlanceSliceHeader *serialized_slice, void *context) {
|
||||
SliceValidationIteratorContext *validation_context = context;
|
||||
PBL_ASSERTN(validation_context);
|
||||
|
||||
if (!prv_is_serialized_slice_valid(serialized_slice)) {
|
||||
*validation_context = (SliceValidationIteratorContext) {
|
||||
.is_at_least_one_slice_invalid = true,
|
||||
.validated_size = 0,
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
validation_context->validated_size += serialized_slice->total_size;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// AppGlanceDB API
|
||||
/////////////////////////
|
||||
|
||||
status_t app_glance_db_insert_glance(const Uuid *uuid, const AppGlance *glance) {
|
||||
if (!uuid || !glance) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SerializedAppGlanceHeader *serialized_glance = NULL;
|
||||
size_t serialized_glance_size = 0;
|
||||
status_t rv = prv_serialize_glance(glance, &serialized_glance, &serialized_glance_size);
|
||||
if (rv == S_SUCCESS) {
|
||||
rv = app_glance_db_insert((uint8_t *)uuid, UUID_SIZE, (uint8_t *)serialized_glance,
|
||||
serialized_glance_size);
|
||||
}
|
||||
|
||||
kernel_free(serialized_glance);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_glance_db_read_glance(const Uuid *uuid, AppGlance *glance_out) {
|
||||
if (!uuid || !glance_out) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const uint8_t *key = (uint8_t *)uuid;
|
||||
const int key_size = UUID_SIZE;
|
||||
|
||||
const int serialized_glance_size = app_glance_db_get_len(key, key_size);
|
||||
if (!serialized_glance_size) {
|
||||
return E_DOES_NOT_EXIST;
|
||||
} else if (serialized_glance_size < 0) {
|
||||
WTF;
|
||||
}
|
||||
|
||||
uint8_t *serialized_glance = kernel_zalloc((size_t)serialized_glance_size);
|
||||
if (!serialized_glance) {
|
||||
return E_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
status_t rv = app_glance_db_read(key, key_size, serialized_glance, serialized_glance_size);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rv = prv_deserialize_glance((SerializedAppGlanceHeader *)serialized_glance,
|
||||
(size_t)serialized_glance_size, glance_out);
|
||||
|
||||
cleanup:
|
||||
kernel_free(serialized_glance);
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_glance_db_read_creation_time(const Uuid *uuid, time_t *time_out) {
|
||||
if (!uuid || !time_out) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SerializedAppGlanceHeader serialized_glance_header = {};
|
||||
const status_t rv = app_glance_db_read((uint8_t *)uuid, UUID_SIZE,
|
||||
(uint8_t *)&serialized_glance_header,
|
||||
sizeof(serialized_glance_header));
|
||||
if (rv == S_SUCCESS) {
|
||||
*time_out = serialized_glance_header.creation_time;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_glance_db_delete_glance(const Uuid *uuid) {
|
||||
return app_glance_db_delete((uint8_t *)uuid, UUID_SIZE);
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Settings helpers
|
||||
//////////////////////
|
||||
|
||||
// TODO PBL-38080: Extract out settings file opening/closing and mutex locking/unlocking for BlobDB
|
||||
|
||||
static status_t prv_lock_mutex_and_open_file(void) {
|
||||
mutex_lock(s_app_glance_db.mutex);
|
||||
const status_t rv = settings_file_open(&s_app_glance_db.settings_file, SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
mutex_unlock(s_app_glance_db.mutex);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void prv_close_file_and_unlock_mutex(void) {
|
||||
settings_file_close(&s_app_glance_db.settings_file);
|
||||
mutex_unlock(s_app_glance_db.mutex);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void app_glance_db_init(void) {
|
||||
s_app_glance_db.mutex = mutex_create();
|
||||
}
|
||||
|
||||
status_t app_glance_db_flush(void) {
|
||||
mutex_lock(s_app_glance_db.mutex);
|
||||
pfs_remove(SETTINGS_FILE_NAME);
|
||||
mutex_unlock(s_app_glance_db.mutex);
|
||||
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
static status_t prv_validate_glance(const Uuid *app_uuid,
|
||||
const SerializedAppGlanceHeader *serialized_glance, size_t *len) {
|
||||
// Change this block if we support multiple app glance versions in the future
|
||||
// For now report an error if the glance's version isn't the current database version
|
||||
if (serialized_glance->version != APP_GLANCE_DB_CURRENT_VERSION) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Tried to insert AppGlanceDB entry with invalid version!"
|
||||
" Entry version: %"PRIu8", AppGlanceDB version: %u",
|
||||
serialized_glance->version, APP_GLANCE_DB_CURRENT_VERSION);
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Check that the creation_time of this new glance value is newer than any existing glance value
|
||||
SerializedAppGlanceHeader existing_glance = {};
|
||||
status_t rv = app_glance_db_read((uint8_t *)app_uuid, UUID_SIZE, (uint8_t *)&existing_glance,
|
||||
sizeof(existing_glance));
|
||||
if ((rv == S_SUCCESS) && (serialized_glance->creation_time <= existing_glance.creation_time)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Tried to insert AppGlanceDB entry with older creation_time (%"PRIu32")"
|
||||
" than existing entry (%"PRIu32")", serialized_glance->creation_time,
|
||||
existing_glance.creation_time);
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Validate the slices (which also records a `validated_size` we'll use to trim excess slices)
|
||||
SliceValidationIteratorContext validation_context = {
|
||||
// Start by taking into account the header of the serialized glance
|
||||
.validated_size = sizeof(SerializedAppGlanceHeader),
|
||||
};
|
||||
// Iteration will fail if the slices report `total_size` values that
|
||||
const bool iteration_succeeded =
|
||||
prv_slice_for_each((SerializedAppGlanceHeader *)serialized_glance, *len,
|
||||
prv_validate_slice, &validation_context);
|
||||
if (!iteration_succeeded) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Tried to insert AppGlanceDB entry but failed to iterate over the serialized slices");
|
||||
return E_INVALID_ARGUMENT;
|
||||
} else if (validation_context.is_at_least_one_slice_invalid) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Tried to insert AppGlanceDB entry with at least one invalid slice");
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Trim the serialized glance of excess slices by shrinking `val_len` to `validated_size`
|
||||
// We do this if the glance entry has more slices than the max number of slices per glance.
|
||||
// This can happen for glance entries sent to us by the mobile apps because they don't have a way
|
||||
// of knowing the max number of slices supported by the firmware, and so they send us as many
|
||||
// slices as they can fit in a BlobDB packet. We just take as many slices as we support and trim
|
||||
// the excess.
|
||||
if (validation_context.validated_size < *len) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Trimming AppGlanceDB entry of excess slices before insertion");
|
||||
*len = validation_context.validated_size;
|
||||
}
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
status_t app_glance_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if ((key_len != UUID_SIZE) || (val_len < (int)sizeof(SerializedAppGlanceHeader))) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const Uuid *app_uuid = (const Uuid *)key;
|
||||
const SerializedAppGlanceHeader *serialized_glance = (const SerializedAppGlanceHeader *)val;
|
||||
size_t len = val_len;
|
||||
status_t rv = prv_validate_glance(app_uuid, serialized_glance, &len);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Fetch app if it's in the app DB, but not cached. If it's not in the app db and not a system app
|
||||
// reject the glance insert
|
||||
|
||||
AppInstallId app_id = app_install_get_id_for_uuid(app_uuid);
|
||||
if (app_install_id_from_app_db(app_id)) {
|
||||
// Bump the app's priority by telling the cache we're using it
|
||||
if (app_cache_entry_exists(app_id)) {
|
||||
app_cache_app_launched(app_id);
|
||||
} else {
|
||||
// The app isn't cached. Fetch it!
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_APP_FETCH_REQUEST_EVENT,
|
||||
.app_fetch_request = {
|
||||
.id = app_id,
|
||||
.with_ui = false,
|
||||
.fetch_args = NULL,
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
} else if (!app_install_id_from_system(app_id)) {
|
||||
// App is not installed (not in app db and not a system app). Do not insert the glance
|
||||
|
||||
// String initialized on the heap to reduce stack usage
|
||||
char *app_uuid_string = kernel_malloc_check(UUID_STRING_BUFFER_LENGTH);
|
||||
uuid_to_string(app_uuid, app_uuid_string);
|
||||
PBL_LOG(LOG_LEVEL_WARNING,
|
||||
"Attempted app glance insert for an app that's not installed. UUID: %s",
|
||||
app_uuid_string);
|
||||
kernel_free(app_uuid_string);
|
||||
return E_DOES_NOT_EXIST;
|
||||
}
|
||||
|
||||
rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_set(&s_app_glance_db.settings_file, key, (size_t)key_len, serialized_glance,
|
||||
len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
int app_glance_db_get_len(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int length = settings_file_get_len(&s_app_glance_db.settings_file, key, (size_t)key_len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
status_t app_glance_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
|
||||
if ((key_len != UUID_SIZE) || val_out == NULL) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_get(&s_app_glance_db.settings_file, key, (size_t)key_len, val_out,
|
||||
(size_t)val_out_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
SerializedAppGlanceHeader *serialized_app_glance = (SerializedAppGlanceHeader *)val_out;
|
||||
|
||||
// Change this block if we support multiple app glance versions in the future
|
||||
if (serialized_app_glance->version != APP_GLANCE_DB_CURRENT_VERSION) {
|
||||
// Clear out the stale entry
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Read a AppGlanceDB entry with an outdated version; deleting it");
|
||||
settings_file_delete(&s_app_glance_db.settings_file, key, (size_t)key_len);
|
||||
rv = E_DOES_NOT_EXIST;
|
||||
}
|
||||
}
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t app_glance_db_delete(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (settings_file_exists(&s_app_glance_db.settings_file, key, (size_t)key_len)) {
|
||||
rv = settings_file_delete(&s_app_glance_db.settings_file, key, (size_t)key_len);
|
||||
} else {
|
||||
rv = S_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Testing code
|
||||
/////////////////////////
|
||||
|
||||
#if UNITTEST
|
||||
void app_glance_db_deinit(void) {
|
||||
app_glance_db_flush();
|
||||
mutex_destroy(s_app_glance_db.mutex);
|
||||
}
|
||||
|
||||
status_t app_glance_db_insert_stale(const uint8_t *key, int key_len, const uint8_t *val,
|
||||
int val_len) {
|
||||
// Quick and dirty insert which doesn't do any error checking. Used to insert stale entries
|
||||
// for testing
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_set(&s_app_glance_db.settings_file, key, key_len, val, val_len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
#endif
|
||||
51
src/fw/services/normal/blob_db/app_glance_db.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/normal/app_glances/app_glance_service.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// AppGlanceDB Implementation
|
||||
|
||||
status_t app_glance_db_insert_glance(const Uuid *uuid, const AppGlance *glance);
|
||||
|
||||
status_t app_glance_db_read_glance(const Uuid *uuid, AppGlance *glance_out);
|
||||
|
||||
status_t app_glance_db_read_creation_time(const Uuid *uuid, time_t *time_out);
|
||||
|
||||
status_t app_glance_db_delete_glance(const Uuid *uuid);
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// BlobDB API Implementation
|
||||
|
||||
void app_glance_db_init(void);
|
||||
|
||||
status_t app_glance_db_flush(void);
|
||||
|
||||
status_t app_glance_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int app_glance_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t app_glance_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t app_glance_db_delete(const uint8_t *key, int key_len);
|
||||
58
src/fw/services/normal/blob_db/app_glance_db_private.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/normal/timeline/attribute.h"
|
||||
#include "services/normal/timeline/attribute_private.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
#define APP_GLANCE_DB_CURRENT_VERSION (1)
|
||||
|
||||
//! This number is reduced for unit tests to avoid creating large glance payloads in the unit tests
|
||||
#if UNITTEST
|
||||
#define APP_GLANCE_DB_MAX_SLICES_PER_GLANCE (2)
|
||||
#else
|
||||
#define APP_GLANCE_DB_MAX_SLICES_PER_GLANCE (8)
|
||||
#endif
|
||||
|
||||
#define APP_GLANCE_DB_MAX_NUM_APP_GLANCES (50)
|
||||
|
||||
typedef struct PACKED SerializedAppGlanceHeader {
|
||||
uint8_t version;
|
||||
uint32_t creation_time;
|
||||
uint8_t data[]; // Serialized slices
|
||||
} SerializedAppGlanceHeader;
|
||||
|
||||
typedef struct PACKED SerializedAppGlanceSliceHeader {
|
||||
uint16_t total_size;
|
||||
uint8_t type;
|
||||
uint8_t num_attributes;
|
||||
uint8_t data[]; // Serialized attributes
|
||||
} SerializedAppGlanceSliceHeader;
|
||||
|
||||
//! The minimum size of an AppGlanceSliceType_IconAndSubtitle slice is the size of the header plus
|
||||
//! the expiration_time because the icon and subtitle are optional
|
||||
#define APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MIN_SIZE \
|
||||
(sizeof(SerializedAppGlanceSliceHeader) + sizeof(SerializedAttributeHeader) + sizeof(uint32_t))
|
||||
//! The maximum size of an AppGlanceSliceType_IconAndSubtitle slice is the size of the header plus
|
||||
//! the expiration_time, icon resource ID, and subtitle string attributes (+1 added for null char)
|
||||
#define APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MAX_SIZE \
|
||||
(sizeof(SerializedAppGlanceSliceHeader) + (sizeof(SerializedAttributeHeader) * 3) + \
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + ATTRIBUTE_APP_GLANCE_SUBTITLE_MAX_LEN + 1)
|
||||
|
||||
#define APP_GLANCE_DB_SLICE_MIN_SIZE (APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MIN_SIZE)
|
||||
#define APP_GLANCE_DB_SLICE_MAX_SIZE (APP_GLANCE_DB_ICON_AND_SUBTITLE_SLICE_MAX_SIZE)
|
||||
183
src/fw/services/normal/blob_db/contacts_db.c
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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 "contacts_db.h"
|
||||
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "services/normal/contacts/attributes_address.h"
|
||||
#include "services/normal/contacts/contacts.h"
|
||||
#include "os/mutex.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/units.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
#define SETTINGS_FILE_NAME "contactsdb"
|
||||
#define SETTINGS_FILE_SIZE (KiBYTES(30))
|
||||
|
||||
static struct {
|
||||
SettingsFile settings_file;
|
||||
PebbleMutex *mutex;
|
||||
} s_contacts_db;
|
||||
|
||||
//////////////////////
|
||||
// Settings helpers
|
||||
//////////////////////
|
||||
|
||||
static status_t prv_lock_mutex_and_open_file(void) {
|
||||
mutex_lock(s_contacts_db.mutex);
|
||||
status_t rv = settings_file_open(&s_contacts_db.settings_file,
|
||||
SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
mutex_unlock(s_contacts_db.mutex);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void prv_close_file_and_unlock_mutex(void) {
|
||||
settings_file_close(&s_contacts_db.settings_file);
|
||||
mutex_unlock(s_contacts_db.mutex);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// Contacts DB API
|
||||
//////////////////////////////
|
||||
|
||||
int contacts_db_get_serialized_contact(const Uuid *uuid, SerializedContact **contact_out) {
|
||||
*contact_out = NULL;
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const unsigned contact_len = settings_file_get_len(&s_contacts_db.settings_file,
|
||||
(uint8_t *)uuid, UUID_SIZE);
|
||||
if (contact_len < sizeof(SerializedContact)) {
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return 0;
|
||||
}
|
||||
|
||||
*contact_out = task_zalloc(contact_len);
|
||||
if (!*contact_out) {
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return 0;
|
||||
}
|
||||
|
||||
rv = settings_file_get(&s_contacts_db.settings_file, (uint8_t *)uuid, UUID_SIZE,
|
||||
(void *) *contact_out, contact_len);
|
||||
prv_close_file_and_unlock_mutex();
|
||||
if (rv != S_SUCCESS) {
|
||||
task_free(*contact_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SerializedContact *serialized_contact = (SerializedContact *)*contact_out;
|
||||
|
||||
return (contact_len - sizeof(SerializedContact));
|
||||
}
|
||||
|
||||
void contacts_db_free_serialized_contact(SerializedContact *contact) {
|
||||
task_free(contact);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void contacts_db_init(void) {
|
||||
memset(&s_contacts_db, 0, sizeof(s_contacts_db));
|
||||
s_contacts_db.mutex = mutex_create();
|
||||
}
|
||||
|
||||
status_t contacts_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if (key_len != UUID_SIZE || val_len < (int) sizeof(SerializedContact)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// TODO: Verify the serialized_contact data before storing it
|
||||
SerializedContact *serialized_contact = (SerializedContact *)val;
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_set(&s_contacts_db.settings_file, key, key_len, val, val_len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
int contacts_db_get_len(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_get_len(&s_contacts_db.settings_file, key, key_len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t contacts_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
|
||||
if (key_len != UUID_SIZE || val_out == NULL) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_get(&s_contacts_db.settings_file, key, key_len, val_out, val_out_len);
|
||||
prv_close_file_and_unlock_mutex();
|
||||
|
||||
SerializedContact *serialized_contact = (SerializedContact *)val_out;
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t contacts_db_delete(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = prv_lock_mutex_and_open_file();
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_delete(&s_contacts_db.settings_file, key, key_len);
|
||||
|
||||
prv_close_file_and_unlock_mutex();
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t contacts_db_flush(void) {
|
||||
mutex_lock(s_contacts_db.mutex);
|
||||
status_t rv = pfs_remove(SETTINGS_FILE_NAME);
|
||||
mutex_unlock(s_contacts_db.mutex);
|
||||
return rv;
|
||||
}
|
||||
57
src/fw/services/normal/blob_db/contacts_db.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "system/status_codes.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
typedef struct PACKED {
|
||||
Uuid uuid;
|
||||
uint32_t flags;
|
||||
uint8_t num_attributes;
|
||||
uint8_t num_addresses;
|
||||
uint8_t data[]; // Serialized attributes followed by serialized addresses
|
||||
} SerializedContact;
|
||||
|
||||
//! Given a contact's uuid, return the serialized data for that contact. This should probably only
|
||||
//! be called by the contacts service. You probably want contacts_get_contact_by_uuid() instead
|
||||
//! @param uuid The contact's uuid.
|
||||
//! @param contact_out A pointer to the serialized contact data, NULL if the contact isn't found.
|
||||
//! @return The length of the data[] field.
|
||||
//! @note The caller must cleanup with contacts_db_free_serialized_contact().
|
||||
int contacts_db_get_serialized_contact(const Uuid *uuid, SerializedContact **contact_out);
|
||||
|
||||
//! Frees the serialized contact data returned by contacts_db_get_serialized_contact().
|
||||
void contacts_db_free_serialized_contact(SerializedContact *contact);
|
||||
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void contacts_db_init(void);
|
||||
|
||||
status_t contacts_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int contacts_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t contacts_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t contacts_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t contacts_db_flush(void);
|
||||
299
src/fw/services/normal/blob_db/endpoint.c
Normal file
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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 "sync.h"
|
||||
#include "endpoint_private.h"
|
||||
|
||||
#include "services/common/bluetooth/bluetooth_persistent_storage.h"
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/net.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
//! @file endpoint.c
|
||||
//! BlobDB Endpoint
|
||||
//!
|
||||
//! There are 3 commands implemented in this endpoint: INSERT, DELETE, and CLEAR
|
||||
//!
|
||||
//! <b>INSERT:</b> This command will insert a key and value into the database specified.
|
||||
//!
|
||||
//! \code{.c}
|
||||
//! 0x01 <uint16_t token> <uint8_t DatabaseId>
|
||||
//! <uint8_t key_size M> <uint8_t[M]> key_bytes>
|
||||
//! <uint16_t value_size N> <uint8_t[N]> value_bytes>
|
||||
//! \endcode
|
||||
//!
|
||||
//! <b>DELETE:</b> This command will delete an entry with the key in the database specified.
|
||||
//!
|
||||
//! \code{.c}
|
||||
//! 0x04 <uint16_t token> <uint8_t DatabaseId>
|
||||
//! <uint8_t key_size M> <uint8_t[M]> key_bytes>
|
||||
//! \endcode
|
||||
//!
|
||||
//! <b>CLEAR:</b> This command will clear all entries in the database specified.
|
||||
//!
|
||||
//! \code{.c}
|
||||
//! 0x05 <uint16_t token> <uint8_t DatabaseId>
|
||||
//! \endcode
|
||||
|
||||
//! BlobDB Endpoint ID
|
||||
static const uint16_t BLOB_DB_ENDPOINT_ID = 0xb1db;
|
||||
|
||||
static const uint8_t KEY_DATA_LENGTH = (sizeof(uint8_t) + sizeof(uint8_t));
|
||||
static const uint8_t VALUE_DATA_LENGTH = (sizeof(uint16_t) + sizeof(uint8_t));
|
||||
|
||||
//! Message Length Constants
|
||||
static const uint8_t MIN_INSERT_LENGTH = 8;
|
||||
static const uint8_t MIN_DELETE_LENGTH = 6;
|
||||
static const uint8_t MIN_CLEAR_LENGTH = 3;
|
||||
|
||||
static bool s_bdb_accepting_messages;
|
||||
|
||||
static void prv_send_response(CommSession *session, BlobDBToken token, BlobDBResponse result) {
|
||||
struct PACKED BlobDBResponseMsg {
|
||||
BlobDBToken token;
|
||||
BlobDBResponse result;
|
||||
} response = {
|
||||
.token = token,
|
||||
.result = result,
|
||||
};
|
||||
|
||||
comm_session_send_data(session, BLOB_DB_ENDPOINT_ID, (uint8_t*)&response, sizeof(response),
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
static BlobDBResponse prv_interpret_db_ret_val(status_t ret_val) {
|
||||
switch (ret_val) {
|
||||
case S_SUCCESS:
|
||||
return BLOB_DB_SUCCESS;
|
||||
case E_DOES_NOT_EXIST:
|
||||
return BLOB_DB_KEY_DOES_NOT_EXIST;
|
||||
case E_RANGE:
|
||||
return BLOB_DB_INVALID_DATABASE_ID;
|
||||
case E_INVALID_ARGUMENT:
|
||||
return BLOB_DB_INVALID_DATA;
|
||||
case E_OUT_OF_STORAGE:
|
||||
return BLOB_DB_DATABASE_FULL;
|
||||
case E_INVALID_OPERATION:
|
||||
return BLOB_DB_DATA_STALE;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "BlobDB return value caught by default case");
|
||||
return BLOB_DB_GENERAL_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
static const uint8_t *prv_read_ptr(const uint8_t *iter, const uint8_t *iter_end,
|
||||
const uint8_t **out_buf, uint16_t buf_len) {
|
||||
|
||||
// >= because we will be reading more bytes after this point
|
||||
if ((buf_len == 0) || ((iter + buf_len) > iter_end)) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "BlobDB: read invalid length");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// grab pointer to start of buf
|
||||
*out_buf = (uint8_t*)iter;
|
||||
iter += buf_len;
|
||||
|
||||
return iter;
|
||||
}
|
||||
|
||||
static const uint8_t *prv_read_key_size(const uint8_t *iter, const uint8_t *iter_end,
|
||||
uint8_t *out_int) {
|
||||
|
||||
// copy length from iter to out_len, then save a local copy of the length
|
||||
*out_int = *iter++;
|
||||
return iter;
|
||||
}
|
||||
|
||||
static const uint8_t *prv_read_value_size(const uint8_t *iter, const uint8_t *iter_end,
|
||||
uint16_t *out_int) {
|
||||
|
||||
// copy length from iter to out_len, then save a local copy of the length
|
||||
*out_int = *(uint16_t*)iter;
|
||||
iter += sizeof(uint16_t);
|
||||
return iter;
|
||||
}
|
||||
|
||||
static BlobDBToken prv_try_read_token(const uint8_t *data, uint32_t length) {
|
||||
if (length < sizeof(BlobDBToken)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return *(BlobDBToken*)data;
|
||||
}
|
||||
|
||||
static void prv_handle_database_insert(CommSession *session, const uint8_t *data, uint32_t length) {
|
||||
if (length < MIN_INSERT_LENGTH) {
|
||||
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *iter = data;
|
||||
BlobDBToken token;
|
||||
BlobDBId db_id;
|
||||
|
||||
// Read token and db_id
|
||||
iter = endpoint_private_read_token_db_id(iter, &token, &db_id);
|
||||
|
||||
// read key length and key bytes ptr
|
||||
uint8_t key_size;
|
||||
const uint8_t *key_bytes = NULL;
|
||||
iter = prv_read_key_size(iter, data + length, &key_size);
|
||||
iter = prv_read_ptr(iter, data + length, &key_bytes, key_size);
|
||||
|
||||
// If read past end or there is not enough data left in buffer for a value size and data to exist
|
||||
if (!iter || (iter > (data + length - VALUE_DATA_LENGTH))) {
|
||||
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
// read value length and value bytes ptr
|
||||
uint16_t value_size;
|
||||
const uint8_t *value_bytes = NULL;
|
||||
iter = prv_read_value_size(iter, data + length, &value_size);
|
||||
iter = prv_read_ptr(iter, data + length, &value_bytes, value_size);
|
||||
|
||||
// If we read too many bytes or didn't read all the bytes (2nd test)
|
||||
if (!iter || (iter != (data + length))) {
|
||||
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
// perform action on database and return result
|
||||
status_t ret = blob_db_insert(db_id, key_bytes, key_size, value_bytes, value_size);
|
||||
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
|
||||
}
|
||||
|
||||
|
||||
static void prv_handle_database_delete(CommSession *session, const uint8_t *data, uint32_t length) {
|
||||
if (length < MIN_DELETE_LENGTH) {
|
||||
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *iter = data;
|
||||
BlobDBToken token;
|
||||
BlobDBId db_id;
|
||||
|
||||
// Read token and db_id
|
||||
iter = endpoint_private_read_token_db_id(iter, &token, &db_id);
|
||||
|
||||
// Read key length and key bytes
|
||||
uint8_t key_size;
|
||||
const uint8_t *key_bytes = NULL;
|
||||
iter = prv_read_key_size(iter, data + length, &key_size);
|
||||
iter = prv_read_ptr(iter, data + length, &key_bytes, key_size);
|
||||
|
||||
// If we read too many bytes or key_size is 0 or didn't read all the bytes
|
||||
if (!iter || (iter != (data + length))) {
|
||||
prv_send_response(session, token, BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
// perform action on database and return result
|
||||
status_t ret = blob_db_delete(db_id, key_bytes, key_size);
|
||||
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
|
||||
}
|
||||
|
||||
|
||||
static void prv_handle_database_clear(CommSession *session, const uint8_t *data, uint32_t length) {
|
||||
if (length < MIN_CLEAR_LENGTH) {
|
||||
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
BlobDBToken token;
|
||||
BlobDBId db_id;
|
||||
|
||||
// Read token and db_id
|
||||
endpoint_private_read_token_db_id(data, &token, &db_id);
|
||||
|
||||
// perform action on database and return result
|
||||
status_t ret = blob_db_flush(db_id);
|
||||
prv_send_response(session, token, prv_interpret_db_ret_val(ret));
|
||||
|
||||
// Mark the device as faithful after successfully flushing
|
||||
if (ret == S_SUCCESS) {
|
||||
bt_persistent_storage_set_unfaithful(false /* We are now faithful */);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_blob_db_msg_decode_and_handle(
|
||||
CommSession *session, BlobDBCommand cmd, const uint8_t *data, size_t data_length) {
|
||||
switch (cmd) {
|
||||
case BLOB_DB_COMMAND_INSERT:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got INSERT");
|
||||
prv_handle_database_insert(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_DELETE:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got DELETE");
|
||||
prv_handle_database_delete(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_CLEAR:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got CLEAR");
|
||||
prv_handle_database_clear(session, data, data_length);
|
||||
break;
|
||||
// Commands not implemented.
|
||||
case BLOB_DB_COMMAND_READ:
|
||||
case BLOB_DB_COMMAND_UPDATE:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "BlobDB Command not implemented");
|
||||
// Fallthrough
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid BlobDB message received, cmd is %u", cmd);
|
||||
prv_send_response(session, prv_try_read_token(data, data_length), BLOB_DB_INVALID_OPERATION);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void blob_db_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BLOB_DB_EVENT_COUNT, AnalyticsClient_System);
|
||||
|
||||
PBL_HEXDUMP_D(LOG_DOMAIN_BLOBDB, LOG_LEVEL_DEBUG, data, length);
|
||||
|
||||
// Each BlobDB message is required to have at least a Command and a Token
|
||||
static const uint8_t MIN_RAW_DATA_LEN = sizeof(BlobDBCommand) + sizeof(BlobDBToken);
|
||||
if (length < MIN_RAW_DATA_LEN) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a blob_db message that was too short, len: %zu", length);
|
||||
prv_send_response(session, 0, BLOB_DB_INVALID_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
const BlobDBCommand cmd = *data;
|
||||
data += sizeof(BlobDBCommand); // fwd to message contents
|
||||
const size_t data_length = length - sizeof(BlobDBCommand);
|
||||
|
||||
if (!s_bdb_accepting_messages) {
|
||||
prv_send_response(session, prv_try_read_token(data, length), BLOB_DB_TRY_LATER);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_blob_db_msg_decode_and_handle(session, cmd, data, data_length);
|
||||
}
|
||||
|
||||
void blob_db_set_accepting_messages(bool enabled) {
|
||||
s_bdb_accepting_messages = enabled;
|
||||
}
|
||||
40
src/fw/services/normal/blob_db/endpoint.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "endpoint_private.h"
|
||||
|
||||
//! Send a write message for the given blob db item.
|
||||
//! @returns the blob db transaction token
|
||||
BlobDBToken blob_db_endpoint_send_write(BlobDBId db_id,
|
||||
time_t last_updated,
|
||||
const void *key,
|
||||
int key_len,
|
||||
const void *val,
|
||||
int val_len);
|
||||
|
||||
//! Send a WB message for the given blob db item.
|
||||
//! @returns the blob db transaction token
|
||||
BlobDBToken blob_db_endpoint_send_writeback(BlobDBId db_id,
|
||||
time_t last_updated,
|
||||
const void *key,
|
||||
int key_len,
|
||||
const void *val,
|
||||
int val_len);
|
||||
|
||||
//! Indicate that blob db sync is done for a given db id
|
||||
void blob_db_endpoint_send_sync_done(BlobDBId db_id);
|
||||
354
src/fw/services/normal/blob_db/endpoint2.c
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* 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 "api.h"
|
||||
#include "sync.h"
|
||||
#include "endpoint_private.h"
|
||||
|
||||
#include "services/common/comm_session/session.h"
|
||||
#include "services/common/comm_session/session_send_buffer.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/net.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
//! BlobDB Endpoint ID
|
||||
static const uint16_t BLOB_DB2_ENDPOINT_ID = 0xb2db;
|
||||
|
||||
//! Message Length Constants
|
||||
static const uint8_t DIRTY_DATABASES_LENGTH = 2;
|
||||
static const uint8_t START_SYNC_LENGTH = 3;
|
||||
static const uint8_t WRITE_RESPONSE_LENGTH = 3;
|
||||
static const uint8_t WRITEBACK_RESPONSE_LENGTH = 3;
|
||||
static const uint8_t SYNC_DONE_RESPONSE_LENGTH = 3;
|
||||
|
||||
static bool s_b2db_accepting_messages;
|
||||
|
||||
T_STATIC BlobDBToken prv_new_token(void) {
|
||||
static BlobDBToken next_token = 1; // 0 token should be avoided
|
||||
return next_token++;
|
||||
}
|
||||
|
||||
static const uint8_t *prv_read_token_and_response(const uint8_t *iter, BlobDBToken *out_token,
|
||||
BlobDBResponse *out_response) {
|
||||
*out_token = *(BlobDBToken *)iter;
|
||||
iter += sizeof(BlobDBToken);
|
||||
*out_response = *(BlobDBResponse *)iter;
|
||||
iter += sizeof(BlobDBResponse);
|
||||
|
||||
return iter;
|
||||
}
|
||||
|
||||
T_STATIC void prv_send_response(CommSession *session, uint8_t *response,
|
||||
uint8_t response_length) {
|
||||
comm_session_send_data(session, BLOB_DB2_ENDPOINT_ID, response, response_length,
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
static void prv_handle_get_dirty_databases(CommSession *session,
|
||||
const uint8_t *data,
|
||||
uint32_t length) {
|
||||
if (length < DIRTY_DATABASES_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a dirty databases with an invalid length: %"PRIu32"", length);
|
||||
return;
|
||||
}
|
||||
|
||||
struct PACKED DirtyDatabasesResponseMsg {
|
||||
BlobDBCommand cmd;
|
||||
BlobDBToken token;
|
||||
BlobDBResponse result;
|
||||
uint8_t num_ids;
|
||||
BlobDBId db_ids[NumBlobDBs];
|
||||
} response = {
|
||||
.cmd = BLOB_DB_COMMAND_DIRTY_DBS_RESPONSE,
|
||||
.token = *(BlobDBToken *)data,
|
||||
.result = BLOB_DB_SUCCESS,
|
||||
};
|
||||
|
||||
|
||||
blob_db_get_dirty_dbs(response.db_ids, &response.num_ids);
|
||||
// we don't want to send the extra bytes in response.db_ids
|
||||
int num_empty_ids = NumBlobDBs - response.num_ids;
|
||||
|
||||
prv_send_response(session, (uint8_t *) &response, sizeof(response) - num_empty_ids);
|
||||
}
|
||||
|
||||
static void prv_handle_start_sync(CommSession *session,
|
||||
const uint8_t *data,
|
||||
uint32_t length) {
|
||||
if (length < START_SYNC_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a start sync with an invalid length: %"PRIu32"", length);
|
||||
return;
|
||||
}
|
||||
|
||||
struct PACKED StartSyncResponseMsg {
|
||||
BlobDBCommand cmd;
|
||||
BlobDBToken token;
|
||||
BlobDBResponse result;
|
||||
} response = {
|
||||
.cmd = BLOB_DB_COMMAND_START_SYNC_RESPONSE,
|
||||
};
|
||||
|
||||
BlobDBId db_id;
|
||||
endpoint_private_read_token_db_id(data, &response.token, &db_id);
|
||||
|
||||
status_t rv = blob_db_sync_db(db_id);
|
||||
switch (rv) {
|
||||
case S_SUCCESS:
|
||||
case S_NO_ACTION_REQUIRED:
|
||||
response.result = BLOB_DB_SUCCESS;
|
||||
break;
|
||||
case E_INVALID_ARGUMENT:
|
||||
response.result = BLOB_DB_INVALID_DATABASE_ID;
|
||||
break;
|
||||
case E_BUSY:
|
||||
response.result = BLOB_DB_TRY_LATER;
|
||||
break;
|
||||
default:
|
||||
response.result = BLOB_DB_GENERAL_FAILURE;
|
||||
break;
|
||||
}
|
||||
|
||||
prv_send_response(session, (uint8_t *)&response, sizeof(response));
|
||||
}
|
||||
|
||||
static void prv_handle_wb_write_response(const uint8_t *data,
|
||||
uint32_t length) {
|
||||
// read token and response code
|
||||
BlobDBToken token;
|
||||
BlobDBResponse response_code;
|
||||
prv_read_token_and_response(data, &token, &response_code);
|
||||
|
||||
BlobDBSyncSession *sync_session = blob_db_sync_get_session_for_token(token);
|
||||
if (sync_session) {
|
||||
if (response_code == BLOB_DB_SUCCESS) {
|
||||
blob_db_sync_next(sync_session);
|
||||
} else {
|
||||
blob_db_sync_cancel(sync_session);
|
||||
}
|
||||
} else {
|
||||
// No session
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "received blob db wb response with an invalid token: %d", token);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_handle_write_response(CommSession *session,
|
||||
const uint8_t *data,
|
||||
uint32_t length) {
|
||||
if (length < WRITE_RESPONSE_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a write response with an invalid length: %"PRIu32"", length);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_handle_wb_write_response(data, length);
|
||||
}
|
||||
|
||||
static void prv_handle_wb_response(CommSession *session,
|
||||
const uint8_t *data,
|
||||
uint32_t length) {
|
||||
if (length < WRITEBACK_RESPONSE_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a writeback response with an invalid length: %"PRIu32"", length);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_handle_wb_write_response(data, length);
|
||||
}
|
||||
|
||||
static void prv_handle_sync_done_response(CommSession *session,
|
||||
const uint8_t *data,
|
||||
uint32_t length) {
|
||||
if (length < SYNC_DONE_RESPONSE_LENGTH) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a sync done response with an invalid length: %"PRIu32"", length);
|
||||
return;
|
||||
}
|
||||
|
||||
// read token and response code
|
||||
BlobDBToken token;
|
||||
BlobDBResponse response_code;
|
||||
prv_read_token_and_response(data, &token, &response_code);
|
||||
|
||||
if (response_code != BLOB_DB_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Sync Done response error: %d", response_code);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_send_error_response(CommSession *session,
|
||||
BlobDBCommand cmd,
|
||||
const uint8_t *data,
|
||||
BlobDBResponse response_code) {
|
||||
struct PACKED ErrorResponseMsg {
|
||||
BlobDBCommand cmd;
|
||||
BlobDBToken token;
|
||||
BlobDBResponse result;
|
||||
} response = {
|
||||
.cmd = cmd | RESPONSE_MASK,
|
||||
.token = *(BlobDBToken *)data,
|
||||
.result = response_code,
|
||||
};
|
||||
|
||||
prv_send_response(session, (uint8_t *)&response, sizeof(response));
|
||||
}
|
||||
|
||||
static void prv_blob_db_msg_decode_and_handle(
|
||||
CommSession *session, BlobDBCommand cmd, const uint8_t *data, size_t data_length) {
|
||||
switch (cmd) {
|
||||
case BLOB_DB_COMMAND_DIRTY_DBS:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got DIRTY DBs");
|
||||
prv_handle_get_dirty_databases(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_START_SYNC:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Got SYNC");
|
||||
prv_handle_start_sync(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_WRITE_RESPONSE:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "WRITE Response");
|
||||
prv_handle_write_response(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_WRITEBACK_RESPONSE:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "WRITEBACK Response");
|
||||
prv_handle_wb_response(session, data, data_length);
|
||||
break;
|
||||
case BLOB_DB_COMMAND_SYNC_DONE_RESPONSE:
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "SYNC DONE Response");
|
||||
prv_handle_sync_done_response(session, data, data_length);
|
||||
break;
|
||||
default:
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid BlobDB2 message received, cmd is %u", cmd);
|
||||
prv_send_error_response(session, cmd, data, BLOB_DB_INVALID_OPERATION);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t prv_send_write_writeback(BlobDBCommand cmd,
|
||||
BlobDBId db_id,
|
||||
time_t last_updated,
|
||||
const uint8_t *key,
|
||||
int key_len,
|
||||
const uint8_t *val,
|
||||
int val_len) {
|
||||
struct PACKED WritebackMetadata {
|
||||
BlobDBCommand cmd;
|
||||
BlobDBToken token;
|
||||
BlobDBId db_id;
|
||||
uint32_t last_updated;
|
||||
} writeback_metadata = {
|
||||
.cmd = cmd,
|
||||
.token = prv_new_token(),
|
||||
.db_id = db_id,
|
||||
.last_updated = last_updated,
|
||||
};
|
||||
|
||||
size_t writeback_length = sizeof(writeback_metadata) +
|
||||
sizeof(uint8_t) /* key length size*/ +
|
||||
key_len +
|
||||
sizeof(uint16_t) /* val length size */ +
|
||||
val_len;
|
||||
|
||||
SendBuffer *sb = comm_session_send_buffer_begin_write(comm_session_get_system_session(),
|
||||
BLOB_DB2_ENDPOINT_ID,
|
||||
writeback_length,
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
if (sb) {
|
||||
comm_session_send_buffer_write(sb, (uint8_t *)&writeback_metadata, sizeof(writeback_metadata));
|
||||
comm_session_send_buffer_write(sb, (uint8_t *)&key_len, sizeof(uint8_t));
|
||||
comm_session_send_buffer_write(sb, key, key_len);
|
||||
comm_session_send_buffer_write(sb, (uint8_t *)&val_len, sizeof(uint16_t));
|
||||
comm_session_send_buffer_write(sb, val, val_len);
|
||||
comm_session_send_buffer_end_write(sb);
|
||||
}
|
||||
|
||||
return writeback_metadata.token;
|
||||
}
|
||||
|
||||
BlobDBToken blob_db_endpoint_send_write(BlobDBId db_id,
|
||||
time_t last_updated,
|
||||
const void *key,
|
||||
int key_len,
|
||||
const void *val,
|
||||
int val_len) {
|
||||
BlobDBToken token = prv_send_write_writeback(BLOB_DB_COMMAND_WRITE, db_id, last_updated,
|
||||
key, key_len, val, val_len);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
BlobDBToken blob_db_endpoint_send_writeback(BlobDBId db_id,
|
||||
time_t last_updated,
|
||||
const void *key,
|
||||
int key_len,
|
||||
const void *val,
|
||||
int val_len) {
|
||||
BlobDBToken token = prv_send_write_writeback(BLOB_DB_COMMAND_WRITEBACK, db_id, last_updated,
|
||||
key, key_len, val, val_len);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
void blob_db_endpoint_send_sync_done(BlobDBId db_id) {
|
||||
struct PACKED SyncDoneMsg {
|
||||
BlobDBCommand cmd;
|
||||
BlobDBToken token;
|
||||
BlobDBId db_id;
|
||||
} msg = {
|
||||
.cmd = BLOB_DB_COMMAND_SYNC_DONE,
|
||||
.token = prv_new_token(),
|
||||
.db_id = db_id,
|
||||
};
|
||||
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Sending sync done for db: %d", db_id);
|
||||
|
||||
comm_session_send_data(comm_session_get_system_session(),
|
||||
BLOB_DB2_ENDPOINT_ID,
|
||||
(uint8_t *)&msg,
|
||||
sizeof(msg),
|
||||
COMM_SESSION_DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
void blob_db2_protocol_msg_callback(CommSession *session, const uint8_t* data, size_t length) {
|
||||
PBL_ASSERT_TASK(PebbleTask_KernelBackground);
|
||||
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_BLOB_DB_EVENT_COUNT, AnalyticsClient_System);
|
||||
|
||||
// Each BlobDB message is required to have at least a Command and a Token
|
||||
static const uint8_t MIN_RAW_DATA_LEN = sizeof(BlobDBCommand) + sizeof(BlobDBToken);
|
||||
if (length < MIN_RAW_DATA_LEN) {
|
||||
// We don't send failure responses for too short messages in endpoint2
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Got a blob_db2 message that was too short, len: %zu", length);
|
||||
return;
|
||||
}
|
||||
|
||||
const BlobDBCommand cmd = *data;
|
||||
data += sizeof(BlobDBCommand); // fwd to message contents
|
||||
const size_t data_length = length - sizeof(BlobDBCommand);
|
||||
|
||||
if (!s_b2db_accepting_messages) {
|
||||
prv_send_error_response(session, cmd, data, BLOB_DB_TRY_LATER);
|
||||
return;
|
||||
}
|
||||
|
||||
prv_blob_db_msg_decode_and_handle(session, cmd, data, data_length);
|
||||
}
|
||||
|
||||
void blob_db2_set_accepting_messages(bool enabled) {
|
||||
s_b2db_accepting_messages = enabled;
|
||||
}
|
||||
39
src/fw/services/normal/blob_db/endpoint_private.c
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 "endpoint_private.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
extern void blob_db_set_accepting_messages(bool enabled);
|
||||
extern void blob_db2_set_accepting_messages(bool enabled);
|
||||
|
||||
void blob_db_enabled(bool enabled) {
|
||||
blob_db_set_accepting_messages(enabled);
|
||||
blob_db2_set_accepting_messages(enabled);
|
||||
}
|
||||
|
||||
const uint8_t *endpoint_private_read_token_db_id(const uint8_t *iter, BlobDBToken *out_token,
|
||||
BlobDBId *out_db_id) {
|
||||
// read token
|
||||
*out_token = *(BlobDBToken*)iter;
|
||||
iter += sizeof(BlobDBToken);
|
||||
// read database id
|
||||
*out_db_id = *iter;
|
||||
iter += sizeof(BlobDBId);
|
||||
|
||||
return iter;
|
||||
}
|
||||
72
src/fw/services/normal/blob_db/endpoint_private.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "util/attributes.h"
|
||||
|
||||
typedef uint16_t BlobDBToken;
|
||||
|
||||
//! Response / result values
|
||||
typedef enum PACKED {
|
||||
BLOB_DB_SUCCESS = 0x01,
|
||||
BLOB_DB_GENERAL_FAILURE = 0x02,
|
||||
BLOB_DB_INVALID_OPERATION = 0x03,
|
||||
BLOB_DB_INVALID_DATABASE_ID = 0x04,
|
||||
BLOB_DB_INVALID_DATA = 0x05,
|
||||
BLOB_DB_KEY_DOES_NOT_EXIST = 0x06,
|
||||
BLOB_DB_DATABASE_FULL = 0x07,
|
||||
BLOB_DB_DATA_STALE = 0x08,
|
||||
BLOB_DB_DB_NOT_SUPPORTED = 0x09,
|
||||
BLOB_DB_DB_LOCKED = 0x0A,
|
||||
BLOB_DB_TRY_LATER = 0x0B,
|
||||
} BlobDBResponse;
|
||||
_Static_assert(sizeof(BlobDBResponse) == 1, "BlobDBResponse is larger than 1 byte");
|
||||
|
||||
#define RESPONSE_MASK (1 << 7)
|
||||
|
||||
typedef enum PACKED {
|
||||
BLOB_DB_COMMAND_INSERT = 0x01,
|
||||
BLOB_DB_COMMAND_READ = 0x02, // Not implemented yet
|
||||
BLOB_DB_COMMAND_UPDATE = 0x03, // Not implemented yet
|
||||
BLOB_DB_COMMAND_DELETE = 0x04,
|
||||
BLOB_DB_COMMAND_CLEAR = 0x05,
|
||||
|
||||
// Commands below were added as part of sync and may not all be supported by the phone
|
||||
BLOB_DB_COMMAND_DIRTY_DBS = 0x06,
|
||||
BLOB_DB_COMMAND_START_SYNC = 0x07,
|
||||
BLOB_DB_COMMAND_WRITE = 0x08,
|
||||
BLOB_DB_COMMAND_WRITEBACK = 0x09,
|
||||
BLOB_DB_COMMAND_SYNC_DONE = 0x0A,
|
||||
// Response commands
|
||||
BLOB_DB_COMMAND_DIRTY_DBS_RESPONSE = BLOB_DB_COMMAND_DIRTY_DBS | RESPONSE_MASK,
|
||||
BLOB_DB_COMMAND_START_SYNC_RESPONSE = BLOB_DB_COMMAND_START_SYNC | RESPONSE_MASK,
|
||||
BLOB_DB_COMMAND_WRITE_RESPONSE = BLOB_DB_COMMAND_WRITE | RESPONSE_MASK,
|
||||
BLOB_DB_COMMAND_WRITEBACK_RESPONSE = BLOB_DB_COMMAND_WRITEBACK | RESPONSE_MASK,
|
||||
BLOB_DB_COMMAND_SYNC_DONE_RESPONSE = BLOB_DB_COMMAND_SYNC_DONE | RESPONSE_MASK,
|
||||
} BlobDBCommand;
|
||||
_Static_assert(sizeof(BlobDBCommand) == 1, "BlobDBCommand is larger than 1 byte");
|
||||
|
||||
|
||||
const uint8_t *endpoint_private_read_token_db_id(const uint8_t *iter, BlobDBToken *out_token,
|
||||
BlobDBId *out_db_id);
|
||||
|
||||
void blob_db_enabled(bool enabled);
|
||||
437
src/fw/services/normal/blob_db/health_db.c
Normal file
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* 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 "health_db.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "services/normal/activity/activity_private.h"
|
||||
#include "services/normal/activity/hr_util.h"
|
||||
#include "services/normal/blob_db/api.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "system/hexdump.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define HEALTH_DB_DEBUG 0
|
||||
#define HEALTH_DB_MAX_KEY_LEN 30
|
||||
|
||||
static const char *HEALTH_DB_FILE_NAME = "healthdb";
|
||||
static const int HEALTH_DB_MAX_SIZE = KiBYTES(12);
|
||||
static PebbleMutex *s_mutex;
|
||||
|
||||
#define MOVEMENT_DATA_KEY_SUFFIX "_movementData"
|
||||
#define SLEEP_DATA_KEY_SUFFIX "_sleepData"
|
||||
#define STEP_TYPICALS_KEY_SUFFIX "_steps" // Not the best suffix, but we are stuck with it now...
|
||||
#define STEP_AVERAGE_KEY_SUFFIX "_dailySteps"
|
||||
#define SLEEP_AVERAGE_KEY_SUFFIX "_sleepDuration"
|
||||
#define HR_ZONE_DATA_KEY_SUFFIX "_heartRateZoneData"
|
||||
|
||||
static const char *WEEKDAY_NAMES[] = {
|
||||
[Sunday] = "sunday",
|
||||
[Monday] = "monday",
|
||||
[Tuesday] = "tuesday",
|
||||
[Wednesday] = "wednesday",
|
||||
[Thursday] = "thursday",
|
||||
[Friday] = "friday",
|
||||
[Saturday] = "saturday",
|
||||
};
|
||||
|
||||
#define CURRENT_MOVEMENT_DATA_VERSION 1
|
||||
#define CURRENT_SLEEP_DATA_VERSION 1
|
||||
#define CURRENT_HR_ZONE_DATA_VERSION 1
|
||||
|
||||
typedef struct PACKED MovementData {
|
||||
uint32_t version;
|
||||
uint32_t last_processed_timestamp;
|
||||
uint32_t steps;
|
||||
uint32_t active_kcalories;
|
||||
uint32_t resting_kcalories;
|
||||
uint32_t distance;
|
||||
uint32_t active_seconds;
|
||||
} MovementData;
|
||||
_Static_assert(offsetof(MovementData, version) == 0, "Version not at the start of MovementData");
|
||||
_Static_assert(sizeof(MovementData) % sizeof(uint32_t) == 0, "MovementData size is invalid");
|
||||
|
||||
typedef struct PACKED SleepData {
|
||||
uint32_t version;
|
||||
uint32_t last_processed_timestamp;
|
||||
uint32_t sleep_duration;
|
||||
uint32_t deep_sleep_duration;
|
||||
uint32_t fall_asleep_time;
|
||||
uint32_t wakeup_time;
|
||||
uint32_t typical_sleep_duration;
|
||||
uint32_t typical_deep_sleep_duration;
|
||||
uint32_t typical_fall_asleep_time;
|
||||
uint32_t typical_wakeup_time;
|
||||
} SleepData;
|
||||
_Static_assert(offsetof(SleepData, version) == 0, "Version not at the start of SleepData");
|
||||
_Static_assert(sizeof(SleepData) % sizeof(uint32_t) == 0, "SleepData size is invalid");
|
||||
|
||||
// The phone doesn't send us Zone0 minutes
|
||||
typedef struct PACKED HeartRateZoneData {
|
||||
uint32_t version;
|
||||
uint32_t last_processed_timestamp;
|
||||
uint32_t num_zones;
|
||||
uint32_t minutes_in_zone[HRZone_Max];
|
||||
} HeartRateZoneData;
|
||||
_Static_assert(offsetof(HeartRateZoneData, version) == 0,
|
||||
"Version not at the start of HeartRateZoneData");
|
||||
_Static_assert(sizeof(HeartRateZoneData) % sizeof(uint32_t) == 0,
|
||||
"HeartRateZoneData size is invalid");
|
||||
|
||||
|
||||
static status_t prv_file_open_and_lock(SettingsFile *file) {
|
||||
mutex_lock(s_mutex);
|
||||
|
||||
status_t rv = settings_file_open(file, HEALTH_DB_FILE_NAME, HEALTH_DB_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Failed to open settings file");
|
||||
mutex_unlock(s_mutex);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void prv_file_close_and_unlock(SettingsFile *file) {
|
||||
settings_file_close(file);
|
||||
mutex_unlock(s_mutex);
|
||||
}
|
||||
|
||||
static bool prv_key_is_valid(const uint8_t *key, int key_len) {
|
||||
return key_len != 0 && // invalid length
|
||||
strchr((const char *)key, '_') != NULL; // invalid key
|
||||
}
|
||||
|
||||
static bool prv_value_is_valid(const uint8_t *key,
|
||||
int key_len,
|
||||
const uint8_t *val,
|
||||
int val_len) {
|
||||
return val_len && val_len % sizeof(uint32_t) == 0;
|
||||
}
|
||||
|
||||
static bool prv_is_last_processed_timestamp_valid(time_t timestamp) {
|
||||
// We only store today + the last 6 days. Anything older than that should be ignored
|
||||
const time_t start_of_today = time_start_of_today();
|
||||
// This might not handle DST perfectly, but it should be good enough
|
||||
const time_t oldest_valid_timestamp = start_of_today - (SECONDS_PER_DAY * 6);
|
||||
|
||||
if (timestamp < oldest_valid_timestamp || timestamp > start_of_today + SECONDS_PER_DAY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//! Tell the activity service that it needs to update its "current" values (non typicals / averages)
|
||||
static void prv_notify_health_listeners(const char *key,
|
||||
int key_len,
|
||||
const uint8_t *val,
|
||||
int val_len) {
|
||||
DayInWeek wday;
|
||||
for (wday = 0; wday < DAYS_PER_WEEK; wday++) {
|
||||
if (strstr(key, WEEKDAY_NAMES[wday])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// For logging
|
||||
const DayInWeek cur_wday = time_util_get_day_in_week(rtc_get_time());
|
||||
|
||||
if (strstr(key, MOVEMENT_DATA_KEY_SUFFIX)) {
|
||||
MovementData *data = (MovementData *)val;
|
||||
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
|
||||
return;
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Got MovementData for wday: %d, cur_wday: %d, steps: %"PRIu32"",
|
||||
wday, cur_wday, data->steps);
|
||||
activity_metrics_prv_set_metric(ActivityMetricStepCount, wday, data->steps);
|
||||
activity_metrics_prv_set_metric(ActivityMetricActiveSeconds, wday, data->active_seconds);
|
||||
activity_metrics_prv_set_metric(ActivityMetricRestingKCalories, wday, data->resting_kcalories);
|
||||
activity_metrics_prv_set_metric(ActivityMetricActiveKCalories, wday, data->active_kcalories);
|
||||
activity_metrics_prv_set_metric(ActivityMetricDistanceMeters, wday, data->distance);
|
||||
} else if (strstr(key, SLEEP_DATA_KEY_SUFFIX)) {
|
||||
SleepData *data = (SleepData *)val;
|
||||
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
|
||||
return;
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Got SleepData for wday: %d, cur_wday: %d, sleep: %"PRIu32"",
|
||||
wday, cur_wday, data->sleep_duration);
|
||||
activity_metrics_prv_set_metric(ActivityMetricSleepTotalSeconds, wday, data->sleep_duration);
|
||||
activity_metrics_prv_set_metric(ActivityMetricSleepRestfulSeconds, wday,
|
||||
data->deep_sleep_duration);
|
||||
activity_metrics_prv_set_metric(ActivityMetricSleepEnterAtSeconds, wday,
|
||||
data->fall_asleep_time);
|
||||
activity_metrics_prv_set_metric(ActivityMetricSleepExitAtSeconds, wday, data->wakeup_time);
|
||||
} else if (strstr(key, HR_ZONE_DATA_KEY_SUFFIX)) {
|
||||
HeartRateZoneData *data = (HeartRateZoneData *)val;
|
||||
if (!prv_is_last_processed_timestamp_valid(data->last_processed_timestamp)) {
|
||||
return;
|
||||
}
|
||||
if (data->num_zones != HRZone_Max) {
|
||||
return;
|
||||
}
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Got HeartRateZoneData for wday: %d, cur_wday: %d, zone1: %"PRIu32"",
|
||||
wday, cur_wday, data->minutes_in_zone[0]);
|
||||
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone1Minutes, wday,
|
||||
data->minutes_in_zone[0]);
|
||||
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone2Minutes, wday,
|
||||
data->minutes_in_zone[1]);
|
||||
activity_metrics_prv_set_metric(ActivityMetricHeartRateZone3Minutes, wday,
|
||||
data->minutes_in_zone[2]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////
|
||||
// Public API
|
||||
/////////////////////////
|
||||
|
||||
bool health_db_get_typical_value(ActivityMetric metric,
|
||||
DayInWeek day,
|
||||
int32_t *value_out) {
|
||||
char key[HEALTH_DB_MAX_KEY_LEN];
|
||||
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], SLEEP_DATA_KEY_SUFFIX);
|
||||
const int key_len = strlen(key);
|
||||
|
||||
|
||||
SettingsFile file;
|
||||
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We cheat a bit here because the only typical values we store are sleep related
|
||||
SleepData data;
|
||||
status_t s = settings_file_get(&file, key, key_len, (uint8_t *)&data, sizeof(data));
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
if (s != S_SUCCESS || data.version != CURRENT_SLEEP_DATA_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (metric) {
|
||||
case ActivityMetricSleepTotalSeconds:
|
||||
*value_out = data.typical_sleep_duration;
|
||||
break;
|
||||
case ActivityMetricSleepRestfulSeconds:
|
||||
*value_out = data.typical_deep_sleep_duration;
|
||||
break;
|
||||
case ActivityMetricSleepEnterAtSeconds:
|
||||
*value_out = data.typical_fall_asleep_time;
|
||||
break;
|
||||
case ActivityMetricSleepExitAtSeconds:
|
||||
*value_out = data.typical_wakeup_time;
|
||||
break;
|
||||
case ActivityMetricStepCount:
|
||||
case ActivityMetricActiveSeconds:
|
||||
case ActivityMetricRestingKCalories:
|
||||
case ActivityMetricActiveKCalories:
|
||||
case ActivityMetricDistanceMeters:
|
||||
case ActivityMetricSleepStateSeconds:
|
||||
case ActivityMetricLastVMC:
|
||||
case ActivityMetricHeartRateRawBPM:
|
||||
case ActivityMetricHeartRateRawQuality:
|
||||
case ActivityMetricHeartRateRawUpdatedTimeUTC:
|
||||
case ActivityMetricHeartRateFilteredBPM:
|
||||
case ActivityMetricHeartRateFilteredUpdatedTimeUTC:
|
||||
case ActivityMetricNumMetrics:
|
||||
case ActivityMetricSleepState:
|
||||
case ActivityMetricHeartRateZone1Minutes:
|
||||
case ActivityMetricHeartRateZone2Minutes:
|
||||
case ActivityMetricHeartRateZone3Minutes:
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Health DB doesn't know about typical metric %d", metric);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool health_db_get_monthly_average_value(ActivityMetric metric,
|
||||
int32_t *value_out) {
|
||||
if (metric != ActivityMetricStepCount && metric != ActivityMetricSleepTotalSeconds) {
|
||||
PBL_LOG(LOG_LEVEL_WARNING, "Health DB doesn't store an average for metric %d", metric);
|
||||
return false;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char key[HEALTH_DB_MAX_KEY_LEN];
|
||||
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "average%s", (metric == ActivityMetricStepCount) ?
|
||||
STEP_AVERAGE_KEY_SUFFIX : SLEEP_AVERAGE_KEY_SUFFIX);
|
||||
const int key_len = strlen(key);
|
||||
|
||||
status_t s = settings_file_get(&file, key, key_len, value_out, sizeof(uint32_t));
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
return (s == S_SUCCESS);
|
||||
}
|
||||
|
||||
bool health_db_get_typical_step_averages(DayInWeek day, ActivityMetricAverages *averages) {
|
||||
if (!averages) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default results
|
||||
_Static_assert(((ACTIVITY_METRIC_AVERAGES_UNKNOWN >> 8) & 0xFF)
|
||||
== (ACTIVITY_METRIC_AVERAGES_UNKNOWN & 0xFF), "Cannot use memset");
|
||||
memset(averages->average, ACTIVITY_METRIC_AVERAGES_UNKNOWN & 0xFF, sizeof(averages->average));
|
||||
|
||||
SettingsFile file;
|
||||
if (prv_file_open_and_lock(&file) != S_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char key[HEALTH_DB_MAX_KEY_LEN];
|
||||
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], STEP_TYPICALS_KEY_SUFFIX);
|
||||
const int key_len = strlen(key);
|
||||
|
||||
status_t s = settings_file_get(&file, key, key_len, averages->average, sizeof(averages->average));
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
return (s == S_SUCCESS);
|
||||
}
|
||||
|
||||
//! For test / debug purposes only
|
||||
bool health_db_set_typical_values(ActivityMetric metric,
|
||||
DayInWeek day,
|
||||
uint16_t *values,
|
||||
int num_values) {
|
||||
char key[HEALTH_DB_MAX_KEY_LEN];
|
||||
snprintf(key, HEALTH_DB_MAX_KEY_LEN, "%s%s", WEEKDAY_NAMES[day], STEP_TYPICALS_KEY_SUFFIX);
|
||||
const int key_len = strlen(key);
|
||||
|
||||
return health_db_insert((uint8_t *)key, key_len, (uint8_t*)values, num_values * sizeof(uint16_t));
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void health_db_init(void) {
|
||||
s_mutex = mutex_create();
|
||||
PBL_ASSERTN(s_mutex != NULL);
|
||||
}
|
||||
|
||||
status_t health_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if (!prv_key_is_valid(key, key_len)) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid health db key");
|
||||
PBL_HEXDUMP(LOG_LEVEL_ERROR, key, key_len);
|
||||
return E_INVALID_ARGUMENT;
|
||||
} else if (!prv_value_is_valid(key, key_len, val, val_len)) {
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Invalid health db value. Length %d", val_len);
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
#if HEALTH_DB_DEBUG
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "New health db entry key:");
|
||||
PBL_HEXDUMP(LOG_LEVEL_DEBUG, key, key_len);
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "val: ");
|
||||
PBL_HEXDUMP(LOG_LEVEL_DEBUG, val, val_len);
|
||||
#endif
|
||||
|
||||
// Only store typicals / averages in this settings file. "Current" values are stored in the
|
||||
// activity settings file.
|
||||
// Sleep data contains a mix of current and typical values. The current values are just stored
|
||||
// for convience and can't be accessed from this settings file.
|
||||
status_t rv = S_SUCCESS;
|
||||
if (!strstr((char *)key, MOVEMENT_DATA_KEY_SUFFIX)) {
|
||||
SettingsFile file;
|
||||
rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_set(&file, key, key_len, val, val_len);
|
||||
prv_file_close_and_unlock(&file);
|
||||
}
|
||||
|
||||
prv_notify_health_listeners((const char *)key, key_len, val, val_len);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
int health_db_get_len(const uint8_t *key, int key_len) {
|
||||
if (!prv_key_is_valid(key, key_len)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int length = settings_file_get_len(&file, key, key_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
status_t health_db_read(const uint8_t *key, int key_len, uint8_t *value_out, int value_out_len) {
|
||||
if (!prv_key_is_valid(key, key_len)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_get(&file, key, key_len, value_out, value_out_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t health_db_delete(const uint8_t *key, int key_len) {
|
||||
if (!prv_key_is_valid(key, key_len)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_delete(&file, key, key_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t health_db_flush(void) {
|
||||
mutex_lock(s_mutex);
|
||||
status_t rv = pfs_remove(HEALTH_DB_FILE_NAME);
|
||||
mutex_unlock(s_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
60
src/fw/services/normal/blob_db/health_db.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "services/normal/activity/activity.h"
|
||||
#include "system/status_codes.h"
|
||||
#include "util/attributes.h"
|
||||
|
||||
|
||||
//! Get the typical metric value for a given day.
|
||||
//! If you want "typical steps" you probably want health_db_get_typical_step_averages
|
||||
bool health_db_get_typical_value(ActivityMetric metric,
|
||||
DayInWeek day,
|
||||
int32_t *value_out);
|
||||
|
||||
//! Get the average metric value over the last month
|
||||
bool health_db_get_monthly_average_value(ActivityMetric metric,
|
||||
int32_t *value_out);
|
||||
|
||||
//! Often referred to as "typical steps"
|
||||
bool health_db_get_typical_step_averages(DayInWeek day,
|
||||
ActivityMetricAverages *averages);
|
||||
|
||||
|
||||
|
||||
//! For test / debug purposes only
|
||||
bool health_db_set_typical_values(ActivityMetric metric,
|
||||
DayInWeek day,
|
||||
uint16_t *values,
|
||||
int num_values);
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void health_db_init(void);
|
||||
|
||||
status_t health_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int health_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t health_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t health_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t health_db_flush(void);
|
||||
409
src/fw/services/normal/blob_db/ios_notif_pref_db.c
Normal file
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* 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 "ios_notif_pref_db.h"
|
||||
|
||||
#include "sync.h"
|
||||
#include "sync_util.h"
|
||||
|
||||
#include "console/prompt.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "os/mutex.h"
|
||||
#include "services/normal/filesystem/pfs.h"
|
||||
#include "services/normal/settings/settings_file.h"
|
||||
#include "services/normal/timeline/attributes_actions.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
T_STATIC const char *iOS_NOTIF_PREF_DB_FILE_NAME = "iosnotifprefdb";
|
||||
T_STATIC const int iOS_NOTIF_PREF_MAX_SIZE = KiBYTES(10);
|
||||
|
||||
|
||||
typedef struct PACKED {
|
||||
uint32_t flags;
|
||||
uint8_t num_attributes;
|
||||
uint8_t num_actions;
|
||||
uint8_t data[]; // Serialized attributes followed by serialized actions
|
||||
} SerializedNotifPrefs;
|
||||
|
||||
static PebbleMutex *s_mutex;
|
||||
|
||||
static status_t prv_file_open_and_lock(SettingsFile *file) {
|
||||
mutex_lock(s_mutex);
|
||||
|
||||
status_t rv = settings_file_open(file, iOS_NOTIF_PREF_DB_FILE_NAME, iOS_NOTIF_PREF_MAX_SIZE);
|
||||
if (rv != S_SUCCESS) {
|
||||
mutex_unlock(s_mutex);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
static void prv_file_close_and_unlock(SettingsFile *file) {
|
||||
settings_file_close(file);
|
||||
mutex_unlock(s_mutex);
|
||||
}
|
||||
|
||||
//! Assumes the file is opened and locked
|
||||
static status_t prv_save_serialized_prefs(SettingsFile *file, const void *key, size_t key_len,
|
||||
const void *val, size_t val_len) {
|
||||
// Invert flags before writing to flash
|
||||
((SerializedNotifPrefs *)val)->flags = ~(((SerializedNotifPrefs *)val)->flags);
|
||||
|
||||
return settings_file_set(file, key, key_len, val, val_len);
|
||||
}
|
||||
|
||||
//! Assumes the file is opened and locked
|
||||
static status_t prv_read_serialized_prefs(SettingsFile *file, const void *key, size_t key_len,
|
||||
void *val_out, size_t val_out_len) {
|
||||
|
||||
status_t rv = settings_file_get(file, key, key_len, val_out, val_out_len);
|
||||
|
||||
// The flags for inverted before writing, revert them back
|
||||
((SerializedNotifPrefs *)val_out)->flags = ~(((SerializedNotifPrefs *)val_out)->flags);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
//! Returns the length of the data
|
||||
//! When done with the prefs, call prv_free_serialzed_prefs()
|
||||
static int prv_get_serialized_prefs(SettingsFile *file, const uint8_t *app_id, int key_len,
|
||||
SerializedNotifPrefs **prefs_out) {
|
||||
const unsigned prefs_len = settings_file_get_len(file, app_id, key_len);
|
||||
if (prefs_len < sizeof(SerializedNotifPrefs)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*prefs_out = kernel_zalloc(prefs_len);
|
||||
if (!*prefs_out) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
status_t rv = prv_read_serialized_prefs(file, app_id, key_len, (void *) *prefs_out, prefs_len);
|
||||
if (rv != S_SUCCESS) {
|
||||
kernel_free(*prefs_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (prefs_len - sizeof(SerializedNotifPrefs));
|
||||
}
|
||||
|
||||
static void prv_free_serialzed_prefs(SerializedNotifPrefs *prefs) {
|
||||
kernel_free(prefs);
|
||||
}
|
||||
|
||||
iOSNotifPrefs* ios_notif_pref_db_get_prefs(const uint8_t *app_id, int key_len) {
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!settings_file_exists(&file, app_id, key_len)) {
|
||||
char buffer[key_len + 1];
|
||||
strncpy(buffer, (const char *)app_id, key_len);
|
||||
buffer[key_len] = '\0';
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "No prefs found for <%s>", buffer);
|
||||
prv_file_close_and_unlock(&file);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SerializedNotifPrefs *serialized_prefs = NULL;
|
||||
const int serialized_prefs_data_len = prv_get_serialized_prefs(&file, app_id, key_len,
|
||||
&serialized_prefs);
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
size_t string_alloc_size;
|
||||
uint8_t attributes_per_action[serialized_prefs->num_actions];
|
||||
bool r = attributes_actions_parse_serial_data(serialized_prefs->num_attributes,
|
||||
serialized_prefs->num_actions,
|
||||
serialized_prefs->data,
|
||||
serialized_prefs_data_len,
|
||||
&string_alloc_size,
|
||||
attributes_per_action);
|
||||
if (!r) {
|
||||
char buffer[key_len + 1];
|
||||
strncpy(buffer, (const char *)app_id, key_len);
|
||||
buffer[key_len] = '\0';
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Could not parse serial data for <%s>", buffer);
|
||||
prv_free_serialzed_prefs(serialized_prefs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const size_t alloc_size =
|
||||
attributes_actions_get_required_buffer_size(serialized_prefs->num_attributes,
|
||||
serialized_prefs->num_actions,
|
||||
attributes_per_action,
|
||||
string_alloc_size);
|
||||
|
||||
iOSNotifPrefs *notif_prefs = kernel_zalloc_check(sizeof(iOSNotifPrefs) + alloc_size);
|
||||
|
||||
uint8_t *buffer = (uint8_t *)notif_prefs + sizeof(iOSNotifPrefs);
|
||||
uint8_t *const buf_end = buffer + alloc_size;
|
||||
|
||||
attributes_actions_init(¬if_prefs->attr_list, ¬if_prefs->action_group,
|
||||
&buffer, serialized_prefs->num_attributes, serialized_prefs->num_actions,
|
||||
attributes_per_action);
|
||||
|
||||
if (!attributes_actions_deserialize(¬if_prefs->attr_list,
|
||||
¬if_prefs->action_group,
|
||||
buffer,
|
||||
buf_end,
|
||||
serialized_prefs->data,
|
||||
serialized_prefs_data_len)) {
|
||||
char buffer[key_len + 1];
|
||||
strncpy(buffer, (const char *)app_id, key_len);
|
||||
buffer[key_len] = '\0';
|
||||
PBL_LOG(LOG_LEVEL_ERROR, "Could not deserialize data for <%s>", buffer);
|
||||
prv_free_serialzed_prefs(serialized_prefs);
|
||||
kernel_free(notif_prefs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
prv_free_serialzed_prefs(serialized_prefs);
|
||||
return notif_prefs;
|
||||
}
|
||||
|
||||
void ios_notif_pref_db_free_prefs(iOSNotifPrefs *prefs) {
|
||||
kernel_free(prefs);
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_store_prefs(const uint8_t *app_id, int length, AttributeList *attr_list,
|
||||
TimelineItemActionGroup *action_group) {
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
size_t payload_size = attributes_actions_get_serialized_payload_size(attr_list, action_group);
|
||||
size_t serialized_prefs_size = sizeof(SerializedNotifPrefs) + payload_size;
|
||||
SerializedNotifPrefs *new_prefs = kernel_zalloc_check(serialized_prefs_size);
|
||||
*new_prefs = (SerializedNotifPrefs) {
|
||||
.num_attributes = attr_list ? attr_list->num_attributes : 0,
|
||||
.num_actions = action_group ? action_group->num_actions : 0,
|
||||
};
|
||||
attributes_actions_serialize_payload(attr_list, action_group, new_prefs->data, payload_size);
|
||||
|
||||
// Add the new entry to the DB
|
||||
rv = prv_save_serialized_prefs(&file, app_id, length, new_prefs, serialized_prefs_size);
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
char buffer[length + 1];
|
||||
strncpy(buffer, (const char *)app_id, length);
|
||||
buffer[length] = '\0';
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Added <%s> to the notif pref db", buffer);
|
||||
|
||||
blob_db_sync_record(BlobDBIdiOSNotifPref, app_id, length, rtc_get_time());
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
void ios_notif_pref_db_init(void) {
|
||||
s_mutex = mutex_create();
|
||||
PBL_ASSERTN(s_mutex != NULL);
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_insert(const uint8_t *key, int key_len,
|
||||
const uint8_t *val, int val_len) {
|
||||
if (key_len == 0 || val_len == 0 || val_len < (int) sizeof(SerializedNotifPrefs)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = prv_save_serialized_prefs(&file, key, key_len, val, val_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
char buffer[key_len + 1];
|
||||
strncpy(buffer, (const char *)key, key_len);
|
||||
buffer[key_len] = '\0';
|
||||
PBL_LOG(LOG_LEVEL_INFO, "iOS notif pref insert <%s>", buffer);
|
||||
|
||||
// All records inserted from the phone are not dirty (the phone is the source of truth)
|
||||
rv = settings_file_mark_synced(&file, key, key_len);
|
||||
}
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
int ios_notif_pref_db_get_len(const uint8_t *key, int key_len) {
|
||||
if (key_len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int length = settings_file_get_len(&file, key, key_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_read(const uint8_t *key, int key_len,
|
||||
uint8_t *val_out, int val_out_len) {
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = prv_read_serialized_prefs(&file, key, key_len, val_out, val_out_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_delete(const uint8_t *key, int key_len) {
|
||||
if (key_len == 0) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_delete(&file, key, key_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_flush(void) {
|
||||
mutex_lock(s_mutex);
|
||||
status_t rv = pfs_remove(iOS_NOTIF_PREF_DB_FILE_NAME);
|
||||
mutex_unlock(s_mutex);
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_is_dirty(bool *is_dirty_out) {
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
*is_dirty_out = false;
|
||||
rv = settings_file_each(&file, sync_util_is_dirty_cb, is_dirty_out);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
BlobDBDirtyItem* ios_notif_pref_db_get_dirty_list(void) {
|
||||
SettingsFile file;
|
||||
if (S_SUCCESS != prv_file_open_and_lock(&file)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
BlobDBDirtyItem *dirty_list = NULL;
|
||||
settings_file_each(&file, sync_util_build_dirty_list_cb, &dirty_list);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return dirty_list;
|
||||
}
|
||||
|
||||
status_t ios_notif_pref_db_mark_synced(const uint8_t *key, int key_len) {
|
||||
if (key_len == 0) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
rv = settings_file_mark_synced(&file, key, key_len);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
#if UNITTEST
|
||||
uint32_t ios_notif_pref_db_get_flags(const uint8_t *app_id, int key_len) {
|
||||
SettingsFile file;
|
||||
status_t rv = prv_file_open_and_lock(&file);
|
||||
if (rv != S_SUCCESS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
SerializedNotifPrefs *prefs = NULL;
|
||||
prv_get_serialized_prefs(&file, app_id, key_len, &prefs);
|
||||
uint32_t flags = prefs->flags;
|
||||
prv_free_serialzed_prefs(prefs);
|
||||
prv_file_close_and_unlock(&file);
|
||||
return flags;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ----------------------------------------------------------------------------------------------
|
||||
static bool prv_print_notif_pref_db(SettingsFile *file, SettingsRecordInfo *info, void *context) {
|
||||
char app_id[64];
|
||||
info->get_key(file, app_id, info->key_len);
|
||||
app_id[info->key_len] = '\0';
|
||||
prompt_send_response(app_id);
|
||||
|
||||
char buffer[64];
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Dirty: %s", info->dirty ? "Yes" : "No");
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Last modified: %"PRIu32"", info->last_modified);
|
||||
|
||||
SerializedNotifPrefs *serialized_prefs = NULL;
|
||||
prv_get_serialized_prefs(file, (uint8_t *)app_id, info->key_len, &serialized_prefs);
|
||||
prompt_send_response_fmt(buffer, sizeof(buffer), "Attributes: %d, Actions: %d",
|
||||
serialized_prefs->num_attributes, serialized_prefs->num_actions);
|
||||
|
||||
// TODO: Print the attributes and actions
|
||||
|
||||
prv_free_serialzed_prefs(serialized_prefs);
|
||||
prompt_send_response("");
|
||||
return true;
|
||||
}
|
||||
|
||||
void command_dump_notif_pref_db(void) {
|
||||
SettingsFile file;
|
||||
if (S_SUCCESS != prv_file_open_and_lock(&file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
settings_file_each(&file, prv_print_notif_pref_db, NULL);
|
||||
|
||||
prv_file_close_and_unlock(&file);
|
||||
}
|
||||
81
src/fw/services/normal/blob_db/ios_notif_pref_db.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api.h"
|
||||
|
||||
#include "services/normal/timeline/attribute.h"
|
||||
#include "services/normal/timeline/item.h"
|
||||
|
||||
#include "system/status_codes.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
//! The iOS Pebble app doesn't have much control over the notification experience.
|
||||
//! The watch receives notifications directly from ANCS, so the iOS app doesn't get a
|
||||
//! chance to do any processing or filtering.
|
||||
//! This db stores preferences on different types of notifications so the FW can perform
|
||||
//! some processing / filtering.
|
||||
|
||||
|
||||
typedef struct {
|
||||
AttributeList attr_list;
|
||||
TimelineItemActionGroup action_group;
|
||||
} iOSNotifPrefs;
|
||||
|
||||
//! @param app_id The iOS app id to check. ex com.apple.MobileSMS
|
||||
//! @param length The length of the app_id
|
||||
//! @return A pointer to the prefs, NULL if none are available
|
||||
//! @note The caller must cleanup with ios_notif_pref_db_free_prefs()
|
||||
iOSNotifPrefs* ios_notif_pref_db_get_prefs(const uint8_t *app_id, int length);
|
||||
|
||||
//! @param prefs A pointer to prefs returned by ios_notif_pref_db_get_prefs()
|
||||
void ios_notif_pref_db_free_prefs(iOSNotifPrefs *prefs);
|
||||
|
||||
//! Adds or updates a record in the notif_pref_db.
|
||||
//! @param app_id The iOS app id to check. ex com.apple.MobileSMS
|
||||
//! @param length The length of the app_id
|
||||
//! @param attr_list AttributeList for the app
|
||||
//! @param attr_list ActionGroup for the app
|
||||
status_t ios_notif_pref_db_store_prefs(const uint8_t *app_id, int length, AttributeList *attr_list,
|
||||
TimelineItemActionGroup *action_group);
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void ios_notif_pref_db_init(void);
|
||||
|
||||
status_t ios_notif_pref_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int ios_notif_pref_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t ios_notif_pref_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t ios_notif_pref_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t ios_notif_pref_db_flush(void);
|
||||
|
||||
status_t ios_notif_pref_db_is_dirty(bool *is_dirty_out);
|
||||
|
||||
BlobDBDirtyItem* ios_notif_pref_db_get_dirty_list(void);
|
||||
|
||||
status_t ios_notif_pref_db_mark_synced(const uint8_t *key, int key_len);
|
||||
|
||||
#if UNITTEST
|
||||
uint32_t ios_notif_pref_db_get_flags(const uint8_t *app_id, int key_len);
|
||||
#endif
|
||||
93
src/fw/services/normal/blob_db/notif_db.c
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 "notif_db.h"
|
||||
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/normal/notifications/notification_storage.h"
|
||||
#include "system/logging.h"
|
||||
|
||||
void notif_db_init(void) {
|
||||
}
|
||||
|
||||
status_t notif_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
if (key_len != UUID_SIZE ||
|
||||
val_len < (int)sizeof(SerializedTimelineItemHeader)) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// [FBO] this is a little bit silly: we deserialize the item to then re-serialize it in
|
||||
// notification_storage_store. It has the advantage that it validates the payload
|
||||
// and works with the existing storage
|
||||
SerializedTimelineItemHeader *hdr = (SerializedTimelineItemHeader *)val;
|
||||
const uint8_t *payload = val + sizeof(SerializedTimelineItemHeader);
|
||||
|
||||
const bool has_status_bits = (hdr->common.status != 0);
|
||||
|
||||
TimelineItem notification = {};
|
||||
if (!timeline_item_deserialize_item(¬ification, hdr, payload)) {
|
||||
return E_INTERNAL;
|
||||
}
|
||||
|
||||
Uuid *id = kernel_malloc_check(sizeof(Uuid));
|
||||
*id = notification.header.id;
|
||||
|
||||
char uuid_string[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string(id, uuid_string);
|
||||
|
||||
// If the notification already exists, only update the status flags
|
||||
if (notification_storage_notification_exists(¬ification.header.id)) {
|
||||
notification_storage_set_status(¬ification.header.id, notification.header.status);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Notification modified: %s", uuid_string);
|
||||
notifications_handle_notification_acted_upon(id);
|
||||
} else if (!has_status_bits) {
|
||||
notification_storage_store(¬ification);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Notification added: %s", uuid_string);
|
||||
notifications_handle_notification_added(id);
|
||||
}
|
||||
|
||||
timeline_item_free_allocated_buffer(¬ification);
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
int notif_db_get_len(const uint8_t *key, int key_len) {
|
||||
if (key_len < UUID_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return notification_storage_get_len((Uuid *)key);
|
||||
}
|
||||
|
||||
status_t notif_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
|
||||
// NYI
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
status_t notif_db_delete(const uint8_t *key, int key_len) {
|
||||
if (key_len != UUID_SIZE) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
notification_storage_remove((Uuid *)key);
|
||||
notifications_handle_notification_removed((Uuid *)key);
|
||||
|
||||
return S_SUCCESS;
|
||||
}
|
||||
|
||||
status_t notif_db_flush(void) {
|
||||
notification_storage_reset_and_init();
|
||||
return S_SUCCESS;
|
||||
}
|
||||
36
src/fw/services/normal/blob_db/notif_db.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "system/status_codes.h"
|
||||
#include "services/normal/timeline/item.h"
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void notif_db_init(void);
|
||||
|
||||
status_t notif_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int notif_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t notif_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t notif_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t notif_db_flush(void);
|
||||
276
src/fw/services/normal/blob_db/pin_db.c
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 "api.h"
|
||||
#include "pin_db.h"
|
||||
#include "reminder_db.h"
|
||||
#include "sync.h"
|
||||
#include "sync_util.h"
|
||||
#include "timeline_item_storage.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "services/normal/app_cache.h"
|
||||
#include "services/normal/timeline/calendar.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/units.h"
|
||||
#include "util/uuid.h"
|
||||
|
||||
#define PIN_DB_MAX_AGE (3 * SECONDS_PER_DAY) // so we get at two full past days in there
|
||||
#define PIN_DB_FILE_NAME "pindb"
|
||||
#define PIN_DB_MAX_SIZE KiBYTES(40) // TODO [FBO] variable size / reasonable value
|
||||
|
||||
static TimelineItemStorage s_pin_db_storage;
|
||||
|
||||
/////////////////////////
|
||||
// Pin DB specific API
|
||||
/////////////////////////
|
||||
|
||||
status_t pin_db_delete_with_parent(const TimelineItemId *parent_id) {
|
||||
return (timeline_item_storage_delete_with_parent(&s_pin_db_storage, parent_id, NULL));
|
||||
}
|
||||
|
||||
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
|
||||
status_t pin_db_each(SettingsFileEachCallback each, void *data) {
|
||||
return timeline_item_storage_each(&s_pin_db_storage, each, data);
|
||||
}
|
||||
|
||||
static status_t prv_insert_serialized_item(const uint8_t *key, int key_len, const uint8_t *val,
|
||||
int val_len, bool mark_synced) {
|
||||
CommonTimelineItemHeader *hdr = (CommonTimelineItemHeader *)val;
|
||||
if (hdr->layout == LayoutIdNotification || hdr->layout == LayoutIdReminder) {
|
||||
// pins do not support these layouts
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
status_t rv = timeline_item_storage_insert(&s_pin_db_storage, key, key_len,
|
||||
val, val_len, mark_synced);
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
TimelineItemId parent_id = ((CommonTimelineItemHeader *)val)->parent_id;
|
||||
if (timeline_get_private_data_source(&parent_id)) {
|
||||
goto done;
|
||||
}
|
||||
// Not a private data source, must be a PBW
|
||||
AppInstallId install_id = app_install_get_id_for_uuid(&parent_id);
|
||||
// can't add a pin for a not installed app!
|
||||
if (install_id == INSTALL_ID_INVALID) {
|
||||
// String initialized on the heap to reduce stack usage
|
||||
char *parent_id_string = kernel_malloc_check(UUID_STRING_BUFFER_LENGTH);
|
||||
uuid_to_string(&parent_id, parent_id_string);
|
||||
PBL_LOG(LOG_LEVEL_ERROR,
|
||||
"Pin insert for a pin with no app installed, parent id: %s",
|
||||
parent_id_string);
|
||||
kernel_free(parent_id_string);
|
||||
goto done;
|
||||
}
|
||||
// Bump the app's priority by telling the cache we're using it
|
||||
if (app_cache_entry_exists(install_id)) {
|
||||
app_cache_app_launched(install_id);
|
||||
goto done;
|
||||
}
|
||||
// System apps don't need to be fetched / are always installed
|
||||
if (app_install_id_from_system(install_id)) {
|
||||
goto done;
|
||||
}
|
||||
// The app isn't cached. Fetch it!
|
||||
PebbleEvent e = {
|
||||
.type = PEBBLE_APP_FETCH_REQUEST_EVENT,
|
||||
.app_fetch_request = {
|
||||
.id = install_id,
|
||||
.with_ui = false,
|
||||
.fetch_args = NULL,
|
||||
},
|
||||
};
|
||||
event_put(&e);
|
||||
}
|
||||
|
||||
done:
|
||||
return rv;
|
||||
}
|
||||
|
||||
static status_t prv_insert_item(TimelineItem *item, bool emit_event) {
|
||||
if (item->header.type != TimelineItemTypePin) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// allocate a buffer big enough for serialized item
|
||||
size_t payload_size = timeline_item_get_serialized_payload_size(item);
|
||||
uint8_t *buffer = kernel_malloc_check(sizeof(SerializedTimelineItemHeader) + payload_size);
|
||||
uint8_t *write_ptr = buffer;
|
||||
|
||||
// serialize the header
|
||||
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *) write_ptr);
|
||||
write_ptr += sizeof(SerializedTimelineItemHeader);
|
||||
|
||||
// serialize the attributes / actions
|
||||
size_t bytes_serialized = timeline_item_serialize_payload(item, write_ptr, payload_size);
|
||||
status_t rv;
|
||||
if (bytes_serialized != payload_size) {
|
||||
rv = E_INVALID_ARGUMENT;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Only pins from the reminders app should be dirty and synced to the phone
|
||||
Uuid reminders_data_source_uuid = UUID_REMINDERS_DATA_SOURCE;
|
||||
const bool mark_synced = !uuid_equal(&item->header.parent_id, &reminders_data_source_uuid);
|
||||
rv = prv_insert_serialized_item(
|
||||
(uint8_t *)&item->header.id, sizeof(TimelineItemId),
|
||||
buffer, sizeof(SerializedTimelineItemHeader) + payload_size, mark_synced);
|
||||
if (rv == S_SUCCESS && emit_event) {
|
||||
blob_db_event_put(BlobDBEventTypeInsert, BlobDBIdPins, (uint8_t *)&item->header.id,
|
||||
sizeof(TimelineItemId));
|
||||
}
|
||||
|
||||
if (!mark_synced) {
|
||||
blob_db_sync_record(BlobDBIdPins, (uint8_t *)&item->header.id, sizeof(TimelineItemId),
|
||||
rtc_get_time());
|
||||
}
|
||||
|
||||
cleanup:
|
||||
kernel_free(buffer);
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t pin_db_insert_item(TimelineItem *item) {
|
||||
return prv_insert_item(item, true /* emit_event */);
|
||||
}
|
||||
|
||||
status_t pin_db_insert_item_without_event(TimelineItem *item) {
|
||||
return prv_insert_item(item, false /* emit_event */);
|
||||
}
|
||||
status_t pin_db_set_status_bits(const TimelineItemId *id, uint8_t status) {
|
||||
return timeline_item_storage_set_status_bits(&s_pin_db_storage, (uint8_t *)id, sizeof(*id),
|
||||
status);
|
||||
}
|
||||
|
||||
status_t pin_db_get(const TimelineItemId *id, TimelineItem *pin) {
|
||||
int size = pin_db_get_len((uint8_t *)id, UUID_SIZE);
|
||||
if (size <= 0) {
|
||||
return E_DOES_NOT_EXIST;
|
||||
}
|
||||
uint8_t *read_buf = task_malloc_check(size);
|
||||
status_t status = pin_db_read((uint8_t *)id, UUID_SIZE, read_buf, size);
|
||||
if (status != S_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
SerializedTimelineItemHeader *header = (SerializedTimelineItemHeader *)read_buf;
|
||||
uint8_t *payload = read_buf + sizeof(SerializedTimelineItemHeader);
|
||||
if (!timeline_item_deserialize_item(pin, header, payload)) {
|
||||
status = E_INTERNAL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
task_free(read_buf);
|
||||
return (S_SUCCESS);
|
||||
|
||||
cleanup:
|
||||
task_free(read_buf);
|
||||
return status;
|
||||
}
|
||||
|
||||
bool pin_db_exists_with_parent(const TimelineItemId *parent_id) {
|
||||
return timeline_item_storage_exists_with_parent(&s_pin_db_storage, parent_id);
|
||||
}
|
||||
|
||||
status_t pin_db_read_item_header(TimelineItem *item_out, TimelineItemId *id) {
|
||||
SerializedTimelineItemHeader hdr = {{{0}}};
|
||||
status_t rv = pin_db_read((uint8_t *)id, sizeof(TimelineItemId), (uint8_t *)&hdr,
|
||||
sizeof(SerializedTimelineItemHeader));
|
||||
timeline_item_deserialize_header(item_out, &hdr);
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t pin_db_next_item_header(TimelineItem *next_item_out,
|
||||
TimelineItemStorageFilterCallback filter) {
|
||||
TimelineItemId id;
|
||||
status_t rv = timeline_item_storage_next_item(&s_pin_db_storage, &id, filter);
|
||||
if (rv) {
|
||||
return rv;
|
||||
}
|
||||
rv = pin_db_read_item_header(next_item_out, &id);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void pin_db_init(void) {
|
||||
s_pin_db_storage = (TimelineItemStorage){};
|
||||
timeline_item_storage_init(&s_pin_db_storage,
|
||||
PIN_DB_FILE_NAME,
|
||||
PIN_DB_MAX_SIZE,
|
||||
PIN_DB_MAX_AGE);
|
||||
}
|
||||
|
||||
void pin_db_deinit(void) {
|
||||
timeline_item_storage_deinit(&s_pin_db_storage);
|
||||
}
|
||||
|
||||
bool pin_db_has_entry_expired(time_t pin_end_timestamp) {
|
||||
return (pin_end_timestamp < (rtc_get_time() - PIN_DB_MAX_AGE));
|
||||
}
|
||||
|
||||
status_t pin_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
// Records inserted from the phone are already synced
|
||||
const bool mark_synced = true;
|
||||
return prv_insert_serialized_item(key, key_len, val, val_len, mark_synced);
|
||||
}
|
||||
|
||||
int pin_db_get_len(const uint8_t *key, int key_len) {
|
||||
return timeline_item_storage_get_len(&s_pin_db_storage, key, key_len);
|
||||
}
|
||||
|
||||
status_t pin_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
|
||||
return timeline_item_storage_read(&s_pin_db_storage, key, key_len, val_out, val_len);
|
||||
}
|
||||
|
||||
status_t pin_db_delete(const uint8_t *key, int key_len) {
|
||||
status_t rv = timeline_item_storage_delete(&s_pin_db_storage, key, key_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
//! remove reminders that are children of this pin
|
||||
reminder_db_delete_with_parent((TimelineItemId *)key);
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t pin_db_flush(void) {
|
||||
return timeline_item_storage_flush(&s_pin_db_storage);
|
||||
}
|
||||
|
||||
status_t pin_db_is_dirty(bool *is_dirty_out) {
|
||||
*is_dirty_out = false;
|
||||
return timeline_item_storage_each(&s_pin_db_storage, sync_util_is_dirty_cb, is_dirty_out);
|
||||
}
|
||||
|
||||
BlobDBDirtyItem* pin_db_get_dirty_list(void) {
|
||||
BlobDBDirtyItem *dirty_list = NULL;
|
||||
timeline_item_storage_each(&s_pin_db_storage, sync_util_build_dirty_list_cb, &dirty_list);
|
||||
|
||||
return dirty_list;
|
||||
}
|
||||
|
||||
status_t pin_db_mark_synced(const uint8_t *key, int key_len) {
|
||||
return timeline_item_storage_mark_synced(&s_pin_db_storage, key, key_len);
|
||||
}
|
||||
78
src/fw/services/normal/blob_db/pin_db.h
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "api.h"
|
||||
#include "timeline_item_storage.h"
|
||||
|
||||
#include "system/status_codes.h"
|
||||
#include "services/normal/timeline/item.h"
|
||||
#include "util/iterator.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
status_t pin_db_get(const TimelineItemId *id, TimelineItem *pin);
|
||||
|
||||
status_t pin_db_insert_item(TimelineItem *item);
|
||||
|
||||
//! Inserts an item without emitting a BlobDB event.
|
||||
//! @note This is provided for testing automatically generated pins which would otherwise flood
|
||||
//! the event queue. Please use \ref pin_db_insert_item instead when possible.
|
||||
status_t pin_db_insert_item_without_event(TimelineItem *item);
|
||||
|
||||
status_t pin_db_set_status_bits(const TimelineItemId *id, uint8_t status);
|
||||
|
||||
//! Caution: CommonTimelineItemHeader .flags & .status are stored inverted and not auto-restored
|
||||
status_t pin_db_each(TimelineItemStorageEachCallback each, void *data);
|
||||
|
||||
status_t pin_db_delete_with_parent(const TimelineItemId *parent_id);
|
||||
|
||||
bool pin_db_exists_with_parent(const TimelineItemId *parent_id);
|
||||
|
||||
status_t pin_db_read_item_header(TimelineItem *item_out, TimelineItemId *id);
|
||||
|
||||
status_t pin_db_next_item_header(TimelineItem *next_item_out,
|
||||
TimelineItemStorageFilterCallback filter);
|
||||
|
||||
//! Determines whether or not the timeline entry has expired based on its age
|
||||
//! @param pin_timestamp - the timestamp of the pin being removed
|
||||
bool pin_db_has_entry_expired(time_t pin_end_timestamp);
|
||||
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void pin_db_init(void);
|
||||
|
||||
void pin_db_deinit(void);
|
||||
|
||||
status_t pin_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int pin_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t pin_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t pin_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t pin_db_flush(void);
|
||||
|
||||
status_t pin_db_is_dirty(bool *is_dirty_out);
|
||||
|
||||
BlobDBDirtyItem* pin_db_get_dirty_list(void);
|
||||
|
||||
status_t pin_db_mark_synced(const uint8_t *key, int key_len);
|
||||
60
src/fw/services/normal/blob_db/prefs_db.c
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 "prefs_db.h"
|
||||
|
||||
#include "process_management/app_install_manager.h"
|
||||
#include "shell/prefs_private.h"
|
||||
|
||||
// BlobDB APIs
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void prefs_db_init(void) {
|
||||
}
|
||||
|
||||
status_t prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
bool success = prefs_private_write_backing(key, key_len, val, val_len);
|
||||
if (success) {
|
||||
return S_SUCCESS;
|
||||
} else {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
int prefs_db_get_len(const uint8_t *key, int key_len) {
|
||||
size_t len = prefs_private_get_backing_len(key, key_len);
|
||||
if (len == 0) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
status_t prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
|
||||
bool success = prefs_private_read_backing(key, key_len, val_out, val_out_len);
|
||||
if (success) {
|
||||
return S_SUCCESS;
|
||||
} else {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
status_t prefs_db_delete(const uint8_t *key, int key_len) {
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
|
||||
status_t prefs_db_flush(void) {
|
||||
return E_INVALID_OPERATION;
|
||||
}
|
||||
35
src/fw/services/normal/blob_db/prefs_db.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "system/status_codes.h"
|
||||
|
||||
///////////////////////////////////////////
|
||||
// BlobDB Boilerplate (see blob_db/api.h)
|
||||
///////////////////////////////////////////
|
||||
|
||||
void prefs_db_init(void);
|
||||
|
||||
status_t prefs_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len);
|
||||
|
||||
int prefs_db_get_len(const uint8_t *key, int key_len);
|
||||
|
||||
status_t prefs_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len);
|
||||
|
||||
status_t prefs_db_delete(const uint8_t *key, int key_len);
|
||||
|
||||
status_t prefs_db_flush(void);
|
||||
284
src/fw/services/normal/blob_db/reminder_db.c
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* 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 "reminder_db.h"
|
||||
#include "sync.h"
|
||||
#include "sync_util.h"
|
||||
#include "timeline_item_storage.h"
|
||||
|
||||
#include "util/uuid.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "services/common/analytics/analytics.h"
|
||||
#include "services/normal/timeline/reminders.h"
|
||||
#include "system/passert.h"
|
||||
#include "system/logging.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#define REMINDER_DB_FILE_NAME "reminderdb"
|
||||
#define REMINDER_DB_MAX_SIZE KiBYTES(40)
|
||||
#define MAX_REMINDER_SIZE SETTINGS_VAL_MAX_LEN
|
||||
#define MAX_REMINDER_AGE (15 * SECONDS_PER_MINUTE)
|
||||
|
||||
typedef struct {
|
||||
TimelineItemStorageFilterCallback filter_cb;
|
||||
time_t timestamp;
|
||||
const char *title;
|
||||
TimelineItem *reminder_out;
|
||||
bool match;
|
||||
} ReminderInfo;
|
||||
|
||||
static TimelineItemStorage s_storage;
|
||||
|
||||
static status_t prv_read_item_header(TimelineItem *item_out, TimelineItemId *id) {
|
||||
SerializedTimelineItemHeader hdr = {{{0}}};
|
||||
status_t rv = reminder_db_read((uint8_t *)id, sizeof(TimelineItemId), (uint8_t *)&hdr,
|
||||
sizeof(SerializedTimelineItemHeader));
|
||||
timeline_item_deserialize_header(item_out, &hdr);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Reminder DB specific API
|
||||
/////////////////////////
|
||||
|
||||
status_t reminder_db_delete_with_parent(const TimelineItemId *parent_id) {
|
||||
return (timeline_item_storage_delete_with_parent(&s_storage, parent_id,
|
||||
reminders_handle_reminder_removed));
|
||||
}
|
||||
|
||||
status_t reminder_db_read_item(TimelineItem *item_out, TimelineItemId *id) {
|
||||
size_t size = reminder_db_get_len((uint8_t *)id, sizeof(TimelineItemId));
|
||||
if (size == 0) {
|
||||
return E_DOES_NOT_EXIST;
|
||||
}
|
||||
uint8_t *read_buf = kernel_malloc_check(size);
|
||||
status_t rv = reminder_db_read((uint8_t *)id, sizeof(TimelineItemId), read_buf, size);
|
||||
if (rv != S_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
SerializedTimelineItemHeader *header = (SerializedTimelineItemHeader *)read_buf;
|
||||
uint8_t *payload = read_buf + sizeof(SerializedTimelineItemHeader);
|
||||
if (!timeline_item_deserialize_item(item_out, header, payload)) {
|
||||
rv = E_INTERNAL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
kernel_free(read_buf);
|
||||
return S_SUCCESS;
|
||||
|
||||
cleanup:
|
||||
kernel_free(read_buf);
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Only keep reminders that have not been fired yet.
|
||||
static bool prv_reminder_filter(SerializedTimelineItemHeader *hdr, void *context) {
|
||||
return ((TimelineItemStatusReminded & hdr->common.status) == 0);
|
||||
}
|
||||
|
||||
status_t reminder_db_next_item_header(TimelineItem *next_item_out) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "Finding next item in queue.");
|
||||
TimelineItemId id;
|
||||
status_t rv = timeline_item_storage_next_item(&s_storage, &id, prv_reminder_filter);
|
||||
if (rv) {
|
||||
return rv;
|
||||
}
|
||||
rv = prv_read_item_header(next_item_out, &id);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static bool prv_timestamp_title_compare_func(SettingsFile *file, SettingsRecordInfo *info,
|
||||
void *context) {
|
||||
// Check entry is valid
|
||||
if (info->key_len != UUID_SIZE || info->val_len == 0) {
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
// Compare timestamps (this should omit most reminders)
|
||||
ReminderInfo *reminder_info = (ReminderInfo *)context;
|
||||
SerializedTimelineItemHeader header;
|
||||
info->get_val(file, (uint8_t *)&header, sizeof(SerializedTimelineItemHeader));
|
||||
if (reminder_info->timestamp != header.common.timestamp) {
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
// Read the full reminder to compare text
|
||||
TimelineItem *reminder = reminder_info->reminder_out;
|
||||
if (timeline_item_storage_get_from_settings_record(file, info, reminder) != S_SUCCESS) {
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
const char *title = attribute_get_string(&reminder->attr_list, AttributeIdTitle, "");
|
||||
if (strcmp(title, reminder_info->title) != 0) {
|
||||
timeline_item_free_allocated_buffer(reminder);
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
if (reminder_info->filter_cb && !reminder_info->filter_cb(&header, context)) {
|
||||
timeline_item_free_allocated_buffer(reminder);
|
||||
return true; // continue iteration
|
||||
}
|
||||
|
||||
reminder_info->match = true;
|
||||
return false; // stop iteration
|
||||
}
|
||||
|
||||
bool reminder_db_find_by_timestamp_title(time_t timestamp, const char *title,
|
||||
TimelineItemStorageFilterCallback filter_cb,
|
||||
TimelineItem *reminder_out) {
|
||||
PBL_ASSERTN(reminder_out);
|
||||
|
||||
ReminderInfo reminder_info = {
|
||||
.filter_cb = filter_cb,
|
||||
.timestamp = timestamp,
|
||||
.title = title,
|
||||
.reminder_out = reminder_out,
|
||||
.match = false
|
||||
};
|
||||
|
||||
timeline_item_storage_each(&s_storage, prv_timestamp_title_compare_func, &reminder_info);
|
||||
|
||||
return reminder_info.match;
|
||||
}
|
||||
|
||||
static status_t prv_insert_reminder(const uint8_t *key, int key_len,
|
||||
const uint8_t *val, int val_len, bool mark_synced) {
|
||||
const SerializedTimelineItemHeader *hdr = (const SerializedTimelineItemHeader *)val;
|
||||
const bool has_reminded = hdr->common.reminded;
|
||||
|
||||
status_t rv = timeline_item_storage_insert(&s_storage, key, key_len, val, val_len, mark_synced);
|
||||
|
||||
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
|
||||
uuid_to_string((Uuid *)key, uuid_buffer);
|
||||
PBL_LOG(LOG_LEVEL_INFO, "Reminder added: %s", uuid_buffer);
|
||||
|
||||
if (rv == S_SUCCESS) {
|
||||
if (has_reminded) {
|
||||
reminders_handle_reminder_updated(&hdr->common.id);
|
||||
} else {
|
||||
rv = reminders_update_timer();
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t reminder_db_insert_item(TimelineItem *item) {
|
||||
if (item->header.type != TimelineItemTypeReminder) {
|
||||
return E_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
size_t payload_size = timeline_item_get_serialized_payload_size(item);
|
||||
uint8_t *buffer = kernel_malloc_check(sizeof(SerializedTimelineItemHeader) + payload_size);
|
||||
timeline_item_serialize_header(item, (SerializedTimelineItemHeader *) buffer);
|
||||
timeline_item_serialize_payload(item, buffer + sizeof(SerializedTimelineItemHeader),
|
||||
payload_size);
|
||||
|
||||
// only for items without attributes as of right now
|
||||
// Records inserted by the watch are dirty and need to be synced to the phone
|
||||
const bool mark_synced = false;
|
||||
status_t rv = prv_insert_reminder((uint8_t *)&item->header.id, sizeof(TimelineItemId),
|
||||
buffer, sizeof(SerializedTimelineItemHeader) + payload_size, mark_synced);
|
||||
|
||||
blob_db_sync_record(BlobDBIdReminders, (uint8_t *)&item->header.id, sizeof(TimelineItemId),
|
||||
rtc_get_time());
|
||||
|
||||
kernel_free(buffer);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static status_t prv_reminder_db_delete_common(const uint8_t *key, int key_len) {
|
||||
status_t rv = timeline_item_storage_delete(&s_storage, key, key_len);
|
||||
if (rv == S_SUCCESS) {
|
||||
reminders_update_timer();
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t reminder_db_delete_item(const TimelineItemId *id, bool send_event) {
|
||||
return (send_event ? reminder_db_delete :
|
||||
prv_reminder_db_delete_common)((uint8_t *)id, sizeof(TimelineItemId));
|
||||
}
|
||||
|
||||
bool reminder_db_is_empty(void) {
|
||||
return timeline_item_storage_is_empty(&s_storage);
|
||||
}
|
||||
|
||||
status_t reminder_db_set_status_bits(const TimelineItemId *id, uint8_t status) {
|
||||
return timeline_item_storage_set_status_bits(&s_storage, (uint8_t *)id,
|
||||
sizeof(ReminderId), status);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// Blob DB API
|
||||
/////////////////////////
|
||||
|
||||
void reminder_db_init(void) {
|
||||
timeline_item_storage_init(&s_storage,
|
||||
REMINDER_DB_FILE_NAME,
|
||||
REMINDER_DB_MAX_SIZE,
|
||||
MAX_REMINDER_AGE);
|
||||
reminders_init();
|
||||
}
|
||||
|
||||
void reminder_db_deinit(void) {
|
||||
timeline_item_storage_deinit(&s_storage);
|
||||
}
|
||||
|
||||
status_t reminder_db_insert(const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
|
||||
analytics_inc(ANALYTICS_DEVICE_METRIC_REMINDER_RECEIVED_COUNT, AnalyticsClient_System);
|
||||
|
||||
// Records inserted from the phone are synced
|
||||
const bool mark_synced = true;
|
||||
return prv_insert_reminder(key, key_len, val, val_len, mark_synced);
|
||||
}
|
||||
|
||||
int reminder_db_get_len(const uint8_t *key, int key_len) {
|
||||
return timeline_item_storage_get_len(&s_storage, key, key_len);
|
||||
}
|
||||
|
||||
status_t reminder_db_read(const uint8_t *key, int key_len, uint8_t *val_out, int val_out_len) {
|
||||
return timeline_item_storage_read(&s_storage, key, key_len, val_out, val_out_len);
|
||||
}
|
||||
|
||||
status_t reminder_db_delete(const uint8_t *key, int key_len) {
|
||||
status_t rv = prv_reminder_db_delete_common(key, key_len);
|
||||
reminders_handle_reminder_removed((Uuid *) key);
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
status_t reminder_db_flush(void) {
|
||||
return timeline_item_storage_flush(&s_storage);
|
||||
}
|
||||
|
||||
status_t reminder_db_is_dirty(bool *is_dirty_out) {
|
||||
*is_dirty_out = false;
|
||||
return timeline_item_storage_each(&s_storage, sync_util_is_dirty_cb, is_dirty_out);
|
||||
}
|
||||
|
||||
BlobDBDirtyItem* reminder_db_get_dirty_list(void) {
|
||||
BlobDBDirtyItem *dirty_list = NULL;
|
||||
timeline_item_storage_each(&s_storage, sync_util_build_dirty_list_cb, &dirty_list);
|
||||
|
||||
return dirty_list;
|
||||
}
|
||||
|
||||
status_t reminder_db_mark_synced(const uint8_t *key, int key_len) {
|
||||
PBL_LOG(LOG_LEVEL_DEBUG, "reminder_db_mark_synced");
|
||||
return timeline_item_storage_mark_synced(&s_storage, key, key_len);
|
||||
}
|
||||