Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson
2024-12-12 16:43:03 -08:00
committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View 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;
}

View 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);

View 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;
}

View 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);

View 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);
}

View 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);

View 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();
}

View 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);

View 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;
}

View 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;
}

View 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);

View File

@@ -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

View 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;
}

View 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);

View 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;
}

View 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);
}

View 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);

File diff suppressed because it is too large Load Diff

View 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();

View 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);

View 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;
}

View 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);

File diff suppressed because it is too large Load Diff

View 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);

View 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);
}

View 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);

View 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, &params->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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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 0Gs 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 watchs 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 users 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.
![Raw accelerometer data](raw_accel_5s.png)
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._
![Spectral Density](spectial_density.png)
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.
![](vmc_formula.png)
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 seconds 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):
![FFT of stepping epoch, 9 steps](fft_walking.png)
#### 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.
![Stepping epoch with large arm-swing component](fft_arm_swing.png "FFT of arm-swing walk")
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.
![Non-stepping epoch](fft_non_walk.png)
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.
![Driving epoch](fft_driving.png)
#### 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.cs` accel service callback is called, it simply passes the raw accel data onto the underlying algorithms 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View 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);
}

View 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);

View 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();
}

View 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);

View 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);
}

View 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);

File diff suppressed because it is too large Load Diff

View 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/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

View 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

View 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);

File diff suppressed because it is too large Load Diff

View 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

View 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));
}

View 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);

View 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;
}
}

View 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);
}

View 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);
}

View 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();
}

View 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

View 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);
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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);

View 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);
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View 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,
};

View 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);
}

View 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);

View 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;
}
}

View 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;
}

View 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);

View 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);
}

View 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);

View 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;

View 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;
}

View 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);

View 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;

View 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;
}

View 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);

View 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 = &current_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 = &current_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

View 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);

View 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)

View 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;
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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;
}

View 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);

View 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;
}

View 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);

View 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(&notif_prefs->attr_list, &notif_prefs->action_group,
&buffer, serialized_prefs->num_attributes, serialized_prefs->num_actions,
attributes_per_action);
if (!attributes_actions_deserialize(&notif_prefs->attr_list,
&notif_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);
}

View 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

View 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(&notification, 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(&notification.header.id)) {
notification_storage_set_status(&notification.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(&notification);
PBL_LOG(LOG_LEVEL_INFO, "Notification added: %s", uuid_string);
notifications_handle_notification_added(id);
}
timeline_item_free_allocated_buffer(&notification);
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;
}

View 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);

View 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);
}

View 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);

View 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;
}

View 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);

View 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);
}

Some files were not shown because too many files have changed in this diff Show More